/*

$Id: talk.c,v 1.5 94/03/11 23:10:32 bert Exp $

*/

#include "extern.h"
#include "proto.h"
#include "version.h"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <ctype.h>
#include <string.h>
#include <stdio.h>
#include <stdarg.h>
#include <malloc.h>
#include "status.h"

/* status codes */
char S_ok[] =                "200 Document follows";
char S_bad_request[] =       "400 Bad request";
char S_unauthorized[] =      "401 Unauthorized";
char S_forbidden[] =         "403 Forbidden";
char S_not_found[] =         "404 Not Found";
char S_internal_error[] =    "500 Internal error";
char S_not_implemented[] =   "501 Not implemented";

/* encoding types */
char T_plain[] = "text/plain";
char T_html[] =  "text/html";

/* error reply formats */

char E_not_found_fmt[] = "document '%s' does not exist on this gateway.";
char E_bad_method_fmt[] = "Method '%s' isn't supported.";
char E_missing_method[] = "No method found in request.";

char error_title[] =
"<HEAD><TITLE>Server Error: %s</TITLE></HEAD>\r\n\
<H1>Server Error: %s</H1>\r\n";

char error_fmt[] = "<P>\r\n\
If you believe this is an error and wish to report it, please send a\r\n\
mail message including the URL, information about your browser and any\r\n\
other relevant details to<P>\r\n\
<ADDRESS>%s</ADDRESS>\r\n";

/* random other static strings */
char header_fmt[] = 
"HTTP/1.0 %s\r\n\
Server: diswww/%s\r\n\
MIME-version: 1.0\r\n\
Content-type: %s\r\n\r\n";

char w_buf[SRV_REPLYSIZE];     /* temporary buffer for random output */

/*
 *  fd_write is just a simple wrapper for write()
 */
int fd_write(int fd, char* text)
{
  return write(fd, text, strlen(text));
}

/*
 *  fd_print is similar to fprintf, but without buffering.
 *  The caller needs to make sure that the length of the output string
 *  won't be more than SRV_REPLYSIZE bytes long.
 */
int fd_print(int fd, const char* fmt, ...)
{
  va_list pvar;

  va_start(pvar, fmt);
  vsprintf(w_buf, fmt, pvar);
  va_end(pvar);
  return fd_write(fd, w_buf);
}

/*
 *  This copies the contents of the named file to the file descriptor.
 */
void fd_spew_file(int fd, char* fname)
{
  int in, rr, ww, wp;

  if ((in = open(fname, 0, O_RDONLY)) < 0) {
    SYSLOG_ERROR("open() failed");
    exit(10);
  }

  while ((rr = read(in, w_buf, SRV_REPLYSIZE)) > 0) {
    wp = 0;
    while (wp < rr) {
      if ((ww = write(fd, &w_buf[wp], rr-wp)) < 0) {
	SYSLOG_ERROR("write() failed");
	exit(10);
      }
      wp += ww;
    }
  }
  if (rr < 0) {
    SYSLOG_ERROR("read() failed");
    exit(10);
  }

  close(in);
}

/*
 *  read_line reads a line of text from the socket in structure rl.
 *  The line is supposed to be terminated by CRLF or LF.  The function
 *  returns status (<0 on error).  A pointer to the line inside a
 *  buffer (or NULL if stream is empty) is returned in variable line.
 *  Note that this means any strings we want to keep must be copied
 *  away before the next read.
 */
int read_line(struct clt_sock *rl, char **line)
{
  int i, idx, size;

/*
 *  The part that has been read but not processed lies between
 *  rl->begin and rl->size-1, inclusively.
 *  (If rl->begin == rl->size, all data has been processed.)
 */

  /* check if we already have a LF */
  for (i = rl->begin; ((i < rl->size) && (rl->buf[i] != LF_CHAR)); i++);

  /* if we don't, move data to the beginning of the buffer */
  if ((i >= rl->size) && (rl->begin <= rl->size) && (rl->begin > 0)) {
    rl->size -= rl->begin;               /* after this, i stays >= rl->size */
    memmove(&rl->buf[0], &rl->buf[rl->begin], (size_t)rl->size);
    rl->begin = 0;
  }

  if (i >= rl->size) {    /* get a complete line */

    if (rl->size >= SRV_BUFSIZE) {     /* out of buffer space? */
      SYSLOG_WARNING("line too long, ask a wizard to shrink the client. =)");
      *line = NULL;
      return -1;       /* drop the connection */
    }

    /* assume select() already said reading was OK */

    idx = rl->size;

    /* read data and adjust rl->size */
    if ((size = read(rl->fd, &rl->buf[rl->size], SRV_BUFSIZE-rl->size)) <= 0) {
      *line = NULL;
      if (errno == EWOULDBLOCK)
	return 0;
      else {
	SYSLOG_EWARN("read from socket failed");
	return -2;       /* drop the connection */
      }
    }
    rl->size += size;

    /* check only the newly read portion for LF's */
    for (i = idx; ((i < rl->size) && (rl->buf[i] != LF_CHAR)); i++);
  }

  if (i >= rl->size) {
    *line = NULL;
    return 0;
  }

  /* strip [CR]LF and make the line into a \0-terminated string */
  rl->buf[i] = 0;
  if ((i > 0) && (rl->buf[i-1] == CR_CHAR)) {
    rl->buf[i-1] = 0;
  } else {
    SYSLOG_NOTICE("broken client does not use CR.");
  }

  idx = rl->begin;    /* keep the value of rl->begin for return... */
  rl->begin = i+1;    /* ...and point rl->begin to next line */
  *line = (&rl->buf[idx]);
  return 0;
}

/*
 *  This function is used to notify the client on error.  The first
 * argument is the fd, the second is the status code string.  The rest
 * are the printf-style format and arguments to make the description
 * of error condition.
 */
void send_error_notice(struct clt_sock *clt, char *status, char *fmt, ...)
{
  va_list pvar;

  if (clt->ver)
    send_header(clt, status, T_html);
  fd_print(clt->fd, error_title, status, status);

  va_start(pvar, fmt);
  vsprintf(w_buf, fmt, pvar);
  va_end(pvar);
  fd_write(clt->fd, w_buf);

  fd_print(clt->fd, error_fmt, SRV_WEBMASTER);

#ifdef DEBUG
  SYSLOG(LOG_DEBUG, "%s just got a %s", clt->host, status);
#endif
}

/*
 *  This function is used to send the HTTP/1.0 reply header to the
 *  client.  The first argument is the status code string, and the
 *  second is the MIME content-type identifier.
 */
void send_header(struct clt_sock *clt, char *status, char *type)
{
  fd_print(clt->fd, header_fmt, status, DISWWW_VERSION, type);
}

/*
 *  perform a conversation with client (read request and output reply)
 */
int talk_to_client(struct clt_sock *clt)
{
  char* line;

  if (clt->reply) return 0;   /* already done? */

  if (! clt->method) {          /* is this the first line? */

    /* read request */
    if (read_line(clt, &line) < 0) return -1;  /* error? */
    if (line == NULL) return -1;               /* no data? */

    /* tokenize request into method, path and protocol version */
    if (! (clt->method = strtok(line, " "))) {      /* assume not HTTP/1.0 */
      send_error_notice(clt, S_bad_request, E_missing_method);
      return -1;
    }
    if (! (clt->path = strtok(NULL, " "))) {
      clt->path = "/";
      clt->ver = NULL;
    } else {
      clt->ver = strtok(NULL," ");
    }

    /* replicate strings so buffer can change */
    clt->method = strdup(clt->method);
    clt->path =   strdup(clt->path);
    clt->ver =    strdup(clt->ver);
  }

  if (clt->ver) {            /* if not HTTP/1.0, we're done */
    do {                /* otherwise, read headers */
      if (read_line(clt, &line) < 0) return -1;  /* error? */
      if (line == NULL) return 0;                /* no data? */
      if (! strncasecmp("User-Agent: ",line,12))
	clt->agent = strdup(line+12);
      /* processing of headers goes here */
    } while ((*line) != (char)0);
  }

  /* if we're here, we have a complete request */

  if (strcasecmp(clt->method,"GET")) {
    send_error_notice(clt, S_not_implemented, E_bad_method_fmt, clt->method);
    return -1;
  }

  return 0;
}
