/*
#  This is version 0.3 of absent
#
#               Copyright (c) 1999 by AT&T.
#  Permission to use, copy, and modify this software without fee is
#  hereby granted, provided that this entire notice is included in all
#  copies of any software which is or includes a copy or modification
#  of this software and in all copies of the supporting documentation
#  for such software.
#
#  SOME PARTS OF ABSENT MAY BE RESTRICTED UNDER UNITED STATES EXPORT
#  REGULATIONS.
#
#  THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
#  IMPLIED WARRANTY.  IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE
#  ANY REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE
#  MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR
#  PURPOSE.
*/
/* pushweb apache module.  */
/* the module's pretty simple right now, since all of the handlers are
   actually in the perl module.  eventually, though, we'll probably want
   to put those here, if we can figure out how to call them.  */
#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "util_script.h"

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "absent.h"

typedef struct pushweb_config {
  char key[8];                      /* MAC key */
  int *control;                     /* control fd (in shm) */
  struct sockaddr_in abs_addr;      /* resolved absent address */
  struct sockaddr_in pwb_addr;      /* resolved pushweb address */
  unsigned ControlPort;
  unsigned PushwebPort;
  unsigned atimeout;
  char *KeyFile;
  char *AbsentHost;
  char *PushwebHost;
  listen_rec *lr_absent;
} pushweb_config;

extern listen_rec *ap_listeners;

module mod_pushweb_module;

static absent_cmsg cmsg;

static pool *pushweb_pool=NULL;

#define our_sconfig(s) \
  (pushweb_config *)ap_get_module_config(s->module_config,&mod_pushweb_module);

int pushweb_control_socket(server_rec *s) {
  /* just an accessor because http_main will need to know this. */
  pushweb_config *p=our_sconfig(s);

  return p->control ? *(p->control) : -1; 
}

int set_pushweb_control_socket(pushweb_config *p, int s) {
  /* ditto, but for different reasons. */

  return p->control ? (*(p->control) = s) : -1;
}

/* pushweb server configuration stuff.  nothing exciting here. */
const char *set_absenthost(cmd_parms *cmd, void *unused, char *arg) {
  server_rec *s=cmd->server;
  pushweb_config *p=our_sconfig(s);

  p->AbsentHost=arg;
  return NULL;
}   

const char *set_pushwebport(cmd_parms *cmd, void *unused, char *arg) {
  /* probably should error-check this, but enh. */
  server_rec *s=cmd->server;
  pushweb_config *p=our_sconfig(s);

  p->PushwebPort=atoi(arg);
  return NULL;
}   

const char *set_controlport(cmd_parms *cmd, void *unused, char *arg) {
  /* ditto */
  server_rec *s=cmd->server;
  pushweb_config *p=our_sconfig(s);

  p->ControlPort=atoi(arg);
  return NULL;
}   

const char *set_keyfile(cmd_parms *cmd, void *unused, char *arg) {
  server_rec *s=cmd->server;
  pushweb_config *p=our_sconfig(s);

  p->KeyFile=arg;
  return NULL;
}   

const char *set_pushwebhost(cmd_parms *cmd, void *unused, char *arg) {
  server_rec *s=cmd->server;
  pushweb_config *p=our_sconfig(s);

  p->PushwebHost=arg;
  return NULL;
}   

void pushweb_shutdown(server_rec *s) {
  /* ungracefully slam shut the control to absent, either because it
     appears to be losing or because we were told to die.
     on a restart, this'll actually get called multiple times as
     different bits of the server discover that the control channel
     has gone away and decide to commit suicide or infanticide. this
     is perfectly safe and normal. */
  int c=pushweb_control_socket(s);
  struct pushweb_config *p=our_sconfig(s);

  if(c>=0) {
    ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, s,
		 "Pushweb shutdown (or restart) requested");
    close(c);
    set_pushweb_control_socket(p, -1);
  }
  if(p->lr_absent) {
    p->lr_absent->fd=-1;
    p->lr_absent->used=0;
  }
}

void pushweb_init(server_rec *s, pool *pl) {
  /* creative repurposing: this is now no longer a traditional
     module initializer; it's intended to be called just before
     setup_listeners in http_main.c (though it probably won't
     break if called as a module initializer) */
  struct pushweb_config *p;
  struct hostent *hp;
  int r, c, shm;

  p=our_sconfig(s);
  if(!p->KeyFile) {
    /* sigh.  we can't return anything, have to just exit the
       server if this or any other config verification fails. */
    fprintf(stderr, "mod_pushweb: Required KeyFile missing\n");
    exit(1);
  }
  if(!p->AbsentHost) {
    fprintf(stderr, "mod_pushweb: Required AbsentHost missing\n");
    exit(1);
  }
  bzero(&(p->abs_addr), sizeof(p->abs_addr));
  p->abs_addr.sin_family=AF_INET;
  p->abs_addr.sin_port=htons(p->ControlPort);
  if(!(hp=gethostbyname(p->AbsentHost))) {
    perror("mod_pushweb: AbsentHost not found");
    exit(1);
  }
  p->abs_addr.sin_family=hp->h_addrtype;
  bcopy((char *)hp->h_addr, (char *)&(p->abs_addr.sin_addr), hp->h_length);
  if(read_key(NULL, p->key, p->KeyFile) <= 0) {
    fprintf(stderr, "mod_pushweb: error reading key from %s", p->KeyFile);
    exit(1);
  }
  if(!p->control) {
    shm=shmget(IPC_PRIVATE, sizeof(int), 00600);
    p->control=(int *)shmat(shm, (void *)0, 0);
    shmctl(shm, IPC_RMID, NULL);
  }
  bzero(&(p->pwb_addr), sizeof(p->pwb_addr));
  p->abs_addr.sin_family=AF_INET;
  if(p->PushwebHost) {
    if(!(hp=gethostbyname(p->PushwebHost)))      
      ap_log_error(APLOG_MARK, APLOG_ERR, s,
		   "Warning: mod_pushweb: PushwebHost %s not found.",
		   p->PushwebHost);
    else {
      p->abs_addr.sin_family=hp->h_addrtype;
      bcopy((char *)hp->h_addr, (char *)&(p->pwb_addr.sin_addr), hp->h_length);
    }
  }
  /* we come through here again on a restart; we should do our bestest
     not to leak fds.  */
  if(pushweb_control_socket(s) >= 0)
    pushweb_shutdown(s);
  if(!pushweb_pool) pushweb_pool=ap_make_sub_pool(NULL);
  do {
    if((c=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
      perror("socket");
      exit(2);
    }
    /* if PushwebHost has been specified, bind the control socket locally
       to force it out a specified interface; otherwise, absent may
       have trouble accepting connections from multihomed hosts with
       different visible names for each interface */
    if(p->PushwebHost && (bind(c,(struct sockaddr *)&(p->pwb_addr),
			       sizeof(struct sockaddr_in)) < 0))
      ap_log_error(APLOG_MARK, APLOG_ERR, s,
		   "Warning: mod_pushweb: Could not bind to %s",
		   p->PushwebHost);
    ap_log_error(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO, s,
		 "Attempting to contact absent at %s:%d",
		 p->AbsentHost, p->ControlPort);
    if((r=connect(c,
		  (struct sockaddr *)&(p->abs_addr),
		  sizeof(p->abs_addr)))< 0) {
      ap_log_error(APLOG_MARK,APLOG_DEBUG, s,
		   "absentd connect failed.  sleeping for 5.");
      close (c);
      sleep(5);
    }
  } while(r < 0);
  cmsg.tstamp=htonl(time(NULL));
  hmac_md5((char *)&(cmsg.tstamp),
	   sizeof(cmsg.tstamp),p->key, sizeof(p->key), cmsg.mac);
  write(c, (char *)&cmsg, sizeof(cmsg));
  p->abs_addr.sin_port=p->PushwebPort;
  set_pushweb_control_socket(p, c);
  /* sigh, i think this leaks listener_recs on restart, but i can't
     be sure. */
  p->lr_absent = ap_pcalloc(pl, sizeof(listen_rec));
  /* this isn't quite right, but it should produce, well, _interesting_
     results. */
  bcopy(&(p->abs_addr), &(p->lr_absent->local_addr), sizeof(p->abs_addr));
  p->lr_absent->fd=c;
  p->lr_absent->used=1;
  p->lr_absent->next=ap_listeners;
  ap_listeners=p->lr_absent;
  fprintf(stderr, "Connected to absent on %d\n", p->lr_absent->fd);
}

/* the following two functions are the real guts of pushweb; note that
   they're not called in traditional apache module fashion; instead, they're
   called directly from our hacks to http_main.c (grep for PUSHWEB). */
int pushweb_cmsg_read(server_rec *s, pool *pl) {
  /* called when select unblocks and we're looking at I/O on the control
     channel.  returns the read message.  i'm aware that the use of the
     cmsg static to communicate state from the code inside the accept to
     the apache mainline is a little cheesy, but i also think it's probably
     the cleanest and most efficient solution.

     it turns out the best way to handle pings is just to cope with
     them here, since we need the mutex to protect us from interleaving
     response writes (which are INCREDIBLY unlikely, but still possible)
     
     this function is remarkable primarily in the ratio of possible
     errors to possible successful returns.  the only really successful
     path out of this function is with the arrival of a COPEN message. */
  int ctrl=pushweb_control_socket(s);
  pushweb_config *p=our_sconfig(s);

  while(read(ctrl, &cmsg, sizeof(cmsg)) == sizeof(cmsg)) {
    switch(cmsg.msg) {
    case ABSENT_COPEN:
      /* this'll be handled outside the mutex.  it doesn't do
	 anything else that needs protection. */
      return 1;
    case ABSENT_PING:
    case ABSENT_PONG:
      /* this is the case we care about */
      if(check_time(ntohl(cmsg.tstamp))) {
	if(check_MAC((char *)&cmsg.tstamp, sizeof(cmsg.tstamp),
		      p->key, sizeof(p->key), cmsg.mac)) {
	  if(cmsg.msg == ABSENT_PING) {
	    cmsg.msg=ABSENT_PONG;
	    cmsg.tstamp=htonl(time(NULL));
	    hmac_md5((char *)&(cmsg.tstamp), sizeof(cmsg.tstamp),
		     p->key, sizeof(p->key), cmsg.mac);
	    if(write(ctrl, (char *)&cmsg, sizeof(cmsg)) < sizeof(cmsg)) {
	      ap_log_error(APLOG_MARK, APLOG_ERR, s,
			   "mod_pushweb: Ping-response write failed.");
	      return -1;
	    }
	    ap_log_error(APLOG_MARK,APLOG_DEBUG|APLOG_NOERRNO, s,
			 "Ping received, response sent.");
	  }
	}
	else {
	  ap_log_error(APLOG_MARK, APLOG_ERR, s,
		       "mod_pushweb: keepalive received with incorrect MAC");
	  return -1;
	}
      }
      else {
	ap_log_error(APLOG_MARK, APLOG_ERR, s,
		     "mod_pushweb: untimely keepalive received");
	return -1;
      }
      /* if it succeeded, just go 'round again. */
      break;
    default:
      ap_log_error(APLOG_MARK, APLOG_ERR, s,
		   "unrecognized control message %d", cmsg.msg);
      return -1;
    }
  }
  ap_log_error(APLOG_MARK, APLOG_ERR, s,
	       "mod_pushweb: control message read failure");
  /* critical to note is that we're holding the accept lock when
     this happens, so strictly speaking we have a producer-consumer
     situation.  also, because of the way the parent runs, it'll notice
     the problem within one second.  if i keep telling myself this,
     i might just start to believe it. */
  pushweb_shutdown(s);
  return -1;
}

int pushweb_cmsg_process(server_rec *s, struct sockaddr_in *sa_client) {
  /* we're now out of the accept() mutex and have successfully read a
     COPEN cmsg. handle it.  once we're out of this code, the fd
     we return to apache can be treated just as any other client. */

  int csd, sz;
  pushweb_config *p=our_sconfig(s);

  if(!check_time(ntohl(cmsg.tstamp))) {
    ap_log_error(APLOG_MARK, APLOG_ERR, s,
		 "mod_pushweb: untimely open request.");
    return -1;
  }
  if(!check_MAC(&(cmsg.id),
		sizeof(cmsg.id)+sizeof(cmsg.tstamp)+sizeof(struct sockaddr_in),
		p->key, sizeof(p->key), cmsg.mac)) {
    ap_log_error(APLOG_MARK, APLOG_ERR, s,
		 "mod_pushweb: open request MAC verify failed.");
    return -1;
  }
  cmsg.msg=ABSENT_COPEN_R;
  cmsg.tstamp=htonl(time(NULL));
  hmac_md5((char *)&(cmsg.id), sizeof(cmsg.id) + sizeof(cmsg.tstamp),
	   p->key, sizeof(p->key), cmsg.mac);
  csd=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  /* see above; the returning connection must come from someplace which
     the absent access control brain damage thinks is ok. */
  
  if(p->PushwebHost) {
    p->pwb_addr.sin_port=0;
    if(bind(csd, (struct sockaddr *)&(p->pwb_addr),
	    sizeof(struct sockaddr_in)) <0 ) {
      ap_log_error(APLOG_MARK, APLOG_ERR, s,
		   "mod_pushweb: warning: failed to bind to %s",
		   p->PushwebHost);
    }
  }
  if(connect(csd, (struct sockaddr *)&(p->abs_addr),sizeof(p->abs_addr)) < 0) {
    ap_log_error(APLOG_MARK, APLOG_ERR, s,
		 "mod_pushweb: absent client callback connect failed.");
    return -1;
  }
  if((sz=write(csd, (char *)&cmsg, sizeof(cmsg))) < sizeof(cmsg)) {
    ap_log_error(APLOG_MARK, APLOG_ERR, s,
		 "mod_pushweb: open response write failed.");
    close(csd);
    return -1;
  }
  if(sa_client)
    bcopy(&(cmsg.sa_client), sa_client, sizeof(struct sockaddr_in));
  return csd;
}

static void *pushweb_server_create(pool *p, server_rec *s) {
  /* server create's really the only meaningful one here, since
     there's not much sense in having a per-directory pushweb. */
    pushweb_config *cfg=
      (pushweb_config *)ap_pcalloc(p,sizeof(pushweb_config));

    cfg->control=NULL;
    cfg->ControlPort=CONTROL_PORT;
    cfg->PushwebPort=PUSHWEB_PORT;
    cfg->AbsentHost=NULL;
    cfg->lr_absent=NULL;
    return (void *) cfg;
}

/* a basic set of commands for now.  still need ping support.  right
   now, pushweb never sends pings, just receives them. */
command_rec pushweb_commands[] = {
  {"AbsentHost", set_absenthost, NULL, RSRC_CONF, TAKE1,
   "The hostname of the host running the absent daemon"},
  {"ControlPort", set_controlport, NULL, RSRC_CONF, TAKE1,
   "The port on which to send control messages to AbsentHost"},
  {"PushwebPort", set_pushwebport, NULL, RSRC_CONF, TAKE1,
   "The data communication port on AbsentHost"},
  {"KeyFile", set_keyfile, NULL, RSRC_CONF, TAKE1,
   "The file containing the MAC key for control messages"},
  {"PushwebHost", set_pushwebhost, NULL, RSRC_CONF, TAKE1,
   "The host from which to establish connections to absent"},
  { NULL }
};

module mod_pushweb_module = {
    STANDARD_MODULE_STUFF,
    NULL,
    NULL, NULL,
    pushweb_server_create,	/* server config creator */
    NULL,
    pushweb_commands,		/* command table */
    NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL
};
