/***************************************************************************
 *
 * Copyright (c) 2000 BalaBit IT Ltd, Budapest, Hungary
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: finger.c,v 1.3.2.1.2.2 2001/09/19 19:28:47 bazsi Exp $
 *
 ***************************************************************************/
 
#include <zorp/zorp.h>
#include <zorp/readline.h>
#include <zorp/proxy.h>
#include <zorp/thread.h>
#include <zorp/registry.h>
#include <zorp/log.h>
#include <zorp/policy.h>

#include <ctype.h>

/* log classes used by this module */

#define FINGER_DEBUG "finger.debug"
#define FINGER_ERROR "finger.error"
#define FINGER_POLICY "finger.policy"
#define FINGER_REQUEST "finger.request"

/*+

  State information of the Finger proxy.

  +*/
typedef struct _FingerProxy
{
  ZProxy super;
  ZReadLine *readline[EP_MAX];
  gint timeout;
  gboolean long_req;
  gint max_hop_count;
  gint max_line_length;
  gint max_username_length;
  gint max_hostname_length;
  gboolean strict_username_check;
  GString *username;
  GString *hostnames;
  GString *response_header;
  GString *response_footer;
} FingerProxy;

/*+

  Fill in our state with default values.

  +*/
static void
finger_config_set_defaults(FingerProxy *self)
{
  z_proxy_enter(self);
  
  self->max_line_length = 132;
  self->max_username_length = 8;
  self->max_hostname_length = 30;
  self->max_hop_count = 0;
  self->strict_username_check = TRUE;
  self->username = g_string_sized_new(32);
  self->hostnames = g_string_sized_new(0);
  self->response_header = g_string_sized_new(0);
  self->response_footer = g_string_sized_new(0);

  z_proxy_leave(self);
}

/*+

  Register variables exported to the policy layer.

  +*/
static void
finger_register_vars(FingerProxy *self)
{
  z_proxy_enter(self);
  
  z_proxy_var_new(&self->super, "timeout",
                  Z_VAR_GET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_INT,
                  &self->timeout);
                  
  z_proxy_var_new(&self->super, "max_line_length",
                  Z_VAR_GET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_INT,
                  &self->max_line_length);
                  
  z_proxy_var_new(&self->super, "max_username_length",
                  Z_VAR_GET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_INT,
                  &self->max_username_length);
                  
  z_proxy_var_new(&self->super, "max_hostname_length",
                  Z_VAR_GET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_INT,
                  &self->max_hostname_length);
                  
  z_proxy_var_new(&self->super, "max_hop_count",
                  Z_VAR_GET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_INT,
                  &self->max_hop_count);
                  
  z_proxy_var_new(&self->super, "request_detailed",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_TYPE_INT,
                  &self->long_req);                  
  z_proxy_var_new(&self->super, "long_request",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_TYPE_ALIAS,
                  "request_detailed");
                  
  z_proxy_var_new(&self->super, "request_username",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_TYPE_STRING,
                  self->username);
  z_proxy_var_new(&self->super, "username",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_TYPE_ALIAS,
                  "request_username");
                  
  z_proxy_var_new(&self->super, "request_hostnames",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_TYPE_STRING,
                  self->hostnames);
  z_proxy_var_new(&self->super, "hostnames",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_TYPE_ALIAS,
                  "request_hostnames");
                  
  z_proxy_var_new(&self->super, "response_header",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_STRING,
                  self->response_header);
                  
  z_proxy_var_new(&self->super, "response_footer",
                  Z_VAR_GET | Z_VAR_SET | Z_VAR_SET_CONFIG | Z_VAR_TYPE_STRING,
                  self->response_footer);

  z_proxy_leave(self);
}

/*+

  Initialize config that python gave us. For now it's empty.

  +*/
static void
finger_config_init(FingerProxy *self)
{
  /* should initialize self based on settings previously set by the config event handler */
}

/*+

  Initialize our client stream. We allocate a readline instance so 
  that we can fetch input line by line.

  +*/
static gboolean
finger_init_client_stream(FingerProxy *self)
{
  z_proxy_enter(self);
  self->readline[EP_CLIENT] = z_read_line_new(self->super.endpoints[EP_CLIENT], self->max_line_length, ZRL_EOL_CRLF);
  z_proxy_leave(self);
  return TRUE;
}

/*+

  Initialize our server stream. Exit with an error if our server side
  is not connected. (ie NULL)

  +*/
static gboolean
finger_init_server_stream(FingerProxy *self)
{
  z_proxy_enter(self);
  if (!self->super.endpoints[EP_SERVER])
    {
      z_proxy_leave(self);
      return FALSE;
    }
  self->readline[EP_SERVER] = z_read_line_new(self->super.endpoints[EP_SERVER], self->max_line_length, ZRL_EOL_CRLF);
  z_proxy_leave(self);
  return TRUE;
}

/*+ 

  Read and process a request.
  
  +*/
static gboolean
finger_fetch_request(FingerProxy *self)
{
  gchar *p, *line, *user;
  gint left, line_length, hop_count, userlen;
  gint res;
  gboolean fetch_user = TRUE;
  gint hostlen;
  
  z_proxy_enter(self);
  res = z_read_line_get(self->readline[EP_CLIENT], &line, &line_length);
  if (ST_ERROR(res) != ST_OK)
    {
      z_proxy_log(self, FINGER_ERROR, 1, "NULL request");
      z_proxy_leave(self);
      return FALSE;
    }
    
  z_proxy_log(self, FINGER_REQUEST, 7, "request, line=%.*s", line_length, line);
  
  p = line;
  left = line_length;
  
  self->long_req = FALSE;
  
  while (*p == ' ' && left)
    {
      p++;
      left--;
    }
  if (*p == '/')
    {
      p++;
      left--;
      if (*p == 'W')
        {
          self->long_req = TRUE;
          p++;
          left--;
        }
      else
        {
          /* bad request */
          z_proxy_log(self, FINGER_REQUEST, 3, "parse error, dropping request, line=%.*s", line_length, line);
          z_proxy_leave(self);
          return FALSE;
        }
    }
  while (*p == ' ' && left)
    {
      p++;
      left--;
    }
  hop_count = 0;
  user = p;
  userlen = left;
  g_string_truncate(self->username, 0);
  g_string_truncate(self->hostnames, 0);
  while (*p && left)
    {
      if (*p == '@')
        {
          if (self->max_hop_count != -1)
            {
              hop_count++;
              if (hop_count > self->max_hop_count)
                break;
            }
          fetch_user = FALSE;
          hostlen = 0;
        }
      if (self->strict_username_check &&
          !(isalnum(*p) || *p == '_' || *p == '@' || *p == '.' || *p == '-'))
        {
          /* invalid character found */
          z_proxy_log(self, FINGER_REQUEST, 3, "invalid character, dropping request, line=%.*s", line_length, line);
          z_proxy_leave(self);
          return FALSE;
        }
      if (fetch_user)
        {
          g_string_append_c(self->username, *p);
          if (self->username->len > self->max_username_length)
            {
              /* too long username */
              z_proxy_log(self, FINGER_REQUEST, 3, "Username too long, dropping request; line=%.*s", line_length, line);
              z_proxy_leave(self);
              return FALSE;
            }
        }
      else
        {
          g_string_append_c(self->hostnames, *p);
          if (hostlen > self->max_hostname_length)
            {
              /* too long hostname in hostname chain */
              z_proxy_log(self, FINGER_REQUEST, 3, "One hostname is too long in hostname chain, dropping request; line=%.*s", line_length, line);
              z_proxy_leave(self);
              return FALSE;
            }
          hostlen++;
        }
      p++;
      left--;
    }
  
  z_proxy_leave(self);
  return TRUE;
}

/*+

  Construct and send a request to the server based on the state
  stored by finger_fetch_request().

  +*/
static gboolean
finger_send_request(FingerProxy *self)
{
  gchar request[self->username->len + self->hostnames->len + 6];
  gint bytes_written;
  
  z_proxy_enter(self);
  
  if (self->long_req)
    {
      if (self->username->len >0)
        {
          if (self->hostnames->len > 0)
            g_snprintf(request, sizeof(request), "/W %s%s\r\n",
                       self->username->str,
                       self->hostnames->str);
          else
            g_snprintf(request, sizeof(request), "/W %s\r\n",
                       self->username->str);
        }
      else
        {
          if (self->hostnames->len > 0)
            g_snprintf(request, sizeof(request), "/W %s\r\n",
                       self->hostnames->str);
          else
            g_snprintf(request, sizeof(request), "/W\r\n");
        }
    }
  else
    {
      if (self->username->len >0)
        {
          if (self->hostnames->len > 0)
            g_snprintf(request, sizeof(request), "%s%s\r\n",
                       self->username->str,
                       self->hostnames->str);
          else
            g_snprintf(request, sizeof(request), "%s\r\n",
                       self->username->str);
        }
      else
        {
          if (self->hostnames->len > 0)
            g_snprintf(request, sizeof(request), "%s\r\n",
                       self->hostnames->str);
          else
            g_snprintf(request, sizeof(request), "\r\n");
        }
    }
  
  if (z_stream_write(self->super.endpoints[EP_SERVER], request, strlen(request), &bytes_written) != G_IO_ERROR_NONE)
    {
      z_proxy_leave(self);
      return FALSE;
    }
  z_proxy_leave(self);
  return TRUE;
}

/*+
 
  Copy server's response to the client.

  +*/
static gboolean
finger_copy_response(FingerProxy *self)
{
  gint bytes_written;
  
  z_proxy_enter(self);
  if (self->response_header->len &&
      z_stream_write(self->super.endpoints[EP_CLIENT], self->response_header->str, self->response_header->len, &bytes_written) != G_IO_ERROR_NONE)
    {
      z_proxy_leave(self);
      return FALSE;
    }

  while (1)
    {
      gchar *line;
      gint line_len, res;
      gchar *response;
      
      res = z_read_line_get(self->readline[EP_SERVER], &line, &line_len);
      if (ST_ERROR(res) != ST_OK)
        /* EOF or read error */
        break;

      response = alloca(line_len + 3);
      memcpy(response, line, line_len);
      strcpy(response + line_len, "\r\n");

      if (z_stream_write(self->super.endpoints[EP_CLIENT], response, line_len + 2, &bytes_written) != G_IO_ERROR_NONE)
        {
          z_proxy_leave(self);
          return FALSE;
        }

    }
  if (self->response_footer->len &&
      z_stream_write(self->super.endpoints[EP_CLIENT], self->response_footer->str, self->response_footer->len, &bytes_written) != G_IO_ERROR_NONE)
    {
      z_proxy_leave(self);
      return FALSE;
    }

  z_proxy_leave(self);
  return TRUE;
}

static gboolean
finger_query_policy(FingerProxy *self)
{
  char *errmsg = "Policy violation, request denied.\r\n";
  gint bytes_written;
  gint res;
  
  z_proxy_enter(self);
  
  z_policy_lock(self->super.thread);
  res = z_policy_event(self->super.handler, "fingerRequest", z_policy_var_build("(ss)", self->username->str, self->hostnames->str));
  switch (res)
    {
    case Z_DROP:
    case Z_REJECT:
    case Z_ABORT:
      
      z_stream_write(self->super.endpoints[EP_CLIENT], errmsg, strlen(errmsg), &bytes_written);
      /* fallthrough */
      
    case Z_DENY:
      z_policy_unlock(self->super.thread);
      z_proxy_leave(self);
      return FALSE;
    case Z_UNSPEC:
    case Z_ACCEPT:
    default:
      break;
    }
  z_policy_unlock(self->super.thread);
  z_proxy_leave(self);
  return TRUE;
}

/*+

  main proxy routine. called by finger_thread.

  +*/
static void
finger_main(FingerProxy *self)
{
  z_proxy_enter(self);
    
  if (!finger_init_client_stream(self))
    {
      z_proxy_leave(self);
      return;
    }

  z_proxy_log(self, FINGER_DEBUG, 6, "fetching request");
  if (!finger_fetch_request(self))
    {
      char *errmsg = "Finger protocol or disallowed protocol element, request denied.\r\n";
      gint bytes_written;
      
      z_stream_write(self->super.endpoints[EP_CLIENT], errmsg, strlen(errmsg), &bytes_written);
      z_proxy_leave(self);
      return;
    }
  
  z_proxy_log(self, FINGER_DEBUG, 6, "asking policy");
  
  if (!finger_query_policy(self))
    {
      z_proxy_leave(self);
      return;
    }
  
  z_proxy_log(self, FINGER_DEBUG, 6, "connecting server");
  
  /* this sets the server side endpoint if successful */
  if (!z_proxy_connect_server_event(&self->super, NULL, 0))
    {
      z_proxy_leave(self);
      return;
    }

  if (!finger_init_server_stream(self))
    {
      z_proxy_leave(self);
      return;
    }
    
  z_proxy_log(self, FINGER_DEBUG, 6, "sending request");
  if (!finger_send_request(self))
    {
      z_proxy_leave(self);
      return;
    }
    
  z_proxy_log(self, FINGER_DEBUG, 6, "copying response");
  if (!finger_copy_response(self))
    {
      z_proxy_leave(self);
      return;
    }

  z_proxy_log(self, FINGER_DEBUG, 6, "everything is done");
  z_proxy_leave(self);

}

/*+

  Thread function. finger allocates a new thread for each connection.

  +*/
static void
finger_thread(gpointer s)
{
  FingerProxy *self = (FingerProxy *) s;

  z_proxy_enter(self);
  
  finger_config_set_defaults(self);  
  finger_register_vars(self);
  
  ZPROXY_SET_STATE(self, PS_CONFIG);
  if(!z_proxy_config_event(&self->super))
    ZPROXY_SET_FLAG(self, PF_QUIT);
  else
    {
      finger_config_init(self);
      
      ZPROXY_SET_STATE(self, PS_STARTING_UP);
      if(!z_proxy_startup_event(&self->super))
        ZPROXY_SET_FLAG(self,PF_QUIT);
    }

  if (!(ZPROXY_GET_FLAG(self,PF_QUIT)))
    {
      ZPROXY_SET_STATE(self, PS_WORKING);

      finger_main(self);
    }
  ZPROXY_SET_STATE(self, PS_SHUTTING_DOWN);
  z_proxy_shutdown_event(&self->super);
  
  z_proxy_destroy(&self->super);
  z_leave();
}

/* forward declaration */
static void finger_proxy_free(ZProxy *self);

/*+

  Finger proxy constructor. Allocates and initializes a proxy instance,
  starts proxy thread.

  +*/
static ZProxy *
finger_proxy_new(gchar *session_id, ZStream *client, ZPolicyObj *handler)
{
  FingerProxy  *self = g_new0(FingerProxy, 1);
  
  z_enter();
  z_proxy_init(&self->super, session_id, client, handler);
  self->super.free = finger_proxy_free;
  
  z_thread_new(session_id, finger_thread, self);
  z_leave();
  return (ZProxy *) self;
}

static void
finger_proxy_free(ZProxy *s)
{
  FingerProxy *self = (FingerProxy *) s;  
  int i;
  
  z_proxy_enter(self);
  for (i = EP_CLIENT; i <= EP_SERVER; i++)
    {
      z_read_line_destroy(self->readline[i]);
      self->readline[i] = NULL;
    }
#if 0
  g_string_free(self->username, TRUE);
  g_string_free(self->hostnames, TRUE);
  g_string_free(self->response_header, TRUE);
  g_string_free(self->response_footer, TRUE);
#endif
  z_proxy_leave(self);
}

/*+

  Module initialization function. Registers a new proxy type.
  
  +*/
gint
zorp_module_init(void)
{
  z_registry_add("finger", ZR_SPROXY, finger_proxy_new);
  return TRUE;
}
