// Emacs style mode select -*- C++ -*-
//--------------------------------------------------------------------------
//
// Copyright(C) 2000 Simon Howard
//
// 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
//--------------------------------------------------------------------------
//
// Server Program
//
// Designed to run on linux or other *nix type system
// Also includes code to make it work on djgpp w/libsocket
// although it doesnt actually work properly on my ver of djgpp
//
// By Simon Howard
//
//--------------------------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/time.h>

#include "main.h"
#include "serv.h"
#include "cmds.h"

#define MAX_IDIOTCOUNT 3
#define DEFAULT_PORT 52638

// ? should work with inaddr_loopback but its the wrong way around
// (ie. 1.0.0.127) -- endianness?
#define LOCAL ((1 << 24) + 127)

char *motd = NULL;

static int port = DEFAULT_PORT;
static int listen_sock;

// no. of people who have connected since
// the server was started

int total_connections = 0;  

connection_t connections;     // root for connection freelist

// connection message: display debug message for a particular connection

void connection_message(connection_t *conn, char *s, ...)
{
  va_list args;
  char temp_buffer[BUFFER_SIZE];

  va_start(args, s);
  vsprintf(temp_buffer, s, args);
  va_end(args);

  LogMessage("%s: %s",
	     inet_ntoa(conn->remote_addr),
	     temp_buffer);

}


//--------------------------------------------------------------------------
//
// Rather than use free() and malloc() all the time, we
// store freed connections in a freelist -- this is faster
// as we do not have to malloc every time we want a new
// connection_t. It also means that we can disconnect in
// the middle of reading commands from a connection --
// the connection_t is still malloc'ed

connection_t *disconnected_connections;

// get a new connection_t, either from the freelist or
// from a malloc

static connection_t *get_connection_t()
{
  if(disconnected_connections)
    {
      connection_t *conn = disconnected_connections;

      disconnected_connections = disconnected_connections->next;

      return conn;
    }
  else
    return malloc(sizeof(connection_t));
}

static void free_connection_t(connection_t *conn)
{
  // add in this connection at the start of the freelist
  
  conn->next = disconnected_connections;
  disconnected_connections = conn;
}


//-------------------------------------------------------------------------
//
// Read data from sockets, Deal with as appropriate
//
// Once we have a connected socket, we wait for data. This is dealt
// with by ReadInput which is called whenever new data arrives at
// a socket. We have a number of commands in server_commands, a
// function is called according to what command is sent
//

// write a message to a connection

void WriteMessage(connection_t *conn, char *s, ...)
{
  char buffer[BUFFER_SIZE];
  va_list args;

  va_start(args, s);
  vsprintf(buffer, s, args);
  va_end(args);
  
  // add newlines to end

  s = buffer + strlen(buffer);
  *s++ = '\n';
  *s++ = '\r';
  *s++ = '\0';
  
  write(conn->sock, buffer, strlen(buffer)+1); 
}

// disconnect connected node with an error message

void KickConnection(connection_t *conn, char *reason)
{
  WriteMessage(conn, reason);

  if(debug)
    connection_message(conn, "kicking (%s)", reason);

  Disconnect(conn);
}

// read a new line from a connection

static char *ReadLine(connection_t *conn)
{
  char *read_buffer = conn->read_buffer;
  static char return_buffer[BUFFER_SIZE];
  char *nl;
  
  // find newline
  
  nl = strchr(read_buffer, '\n');
  
  if(!nl)
    return NULL;

  // copy new line data into return buffer
  
  strncpy(return_buffer, read_buffer, nl - read_buffer);
  return_buffer[nl - read_buffer] = '\0';

  // store remaining as-yet-unused data in the read_buffer
  
  strcpy(read_buffer, nl+1);

  return return_buffer;
}

// read new command lines from a connection

static void ReadCommands(connection_t *conn)
{
  char *input_line;
  int i;

  while(conn->connected)
    {
      input_line = ReadLine(conn);
      
      // no more lines to read ?
      if(!input_line)
	  return;

      if(!input_line[0])
	continue;

#ifdef LOGALL
      connection_message(conn, "'%s'", input_line);
#endif

      for(i=0; i<num_server_commands; i++)
	{
	  if(!strncasecmp(input_line, server_commands[i].name,
		      strlen(server_commands[i].name)))
	    {
	      // check for non-local clients trying to run
	      // restricted commands

	      // if they do it too many times, kick them

	      if(server_commands[i].local_only &&
		 conn->remote_addr.s_addr != LOCAL)
		{
		  conn->idiotcount++;
		  if(conn->idiotcount >= MAX_IDIOTCOUNT)
		    KickConnection(conn, "go away");
		  else
		    WriteMessage(conn, 
				 "only localhost may %s",
				 server_commands[i].name);
		}
	      else      // run command
		{
		  char *extra_data =
		    input_line + strlen(server_commands[i].name);
		  
		  // skip leading spaces

		  while(*extra_data && *extra_data==' ')
		    extra_data++;
		  
		  // call handler function
		  server_commands[i].handler(conn, extra_data);
		  WriteMessage(conn, "ok");
		}

	      break;
	    }
	}
      
      // invalid command?
      if(i == num_server_commands)
	WriteMessage(conn, "say what?");
    }
}

//-------------------------------------------------------------------------
//
// Get Connections, Wait for Data
//
// A single socket listen_sock is set up on startup to listen for
// new connections. This and other sockets are passed to select()
// which waits for new data to arrive. New data is dealt with by
// ReadInput() (above) which parses the data sent. Connections to
// listen_sock are accepted and added to connections[]
//

static void ShutdownSockets()
{
  // close all ports and shut down
  connection_t *conn;
  
  close(listen_sock);

  for(conn = connections.next; conn; conn = conn->next)
    {
      close(conn->sock);
    }  
}

// init listen socket

void CreateSocket()
{
  struct sockaddr_in in;

  listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if(listen_sock == -1)
    error("CreateSocket: socket");

  in.sin_family = AF_INET;
  in.sin_addr.s_addr = INADDR_ANY;
  in.sin_port = htons(port);
  if(bind(listen_sock, &in, sizeof(in)))
    error("CreateSocket: bind");

  if(listen(listen_sock, 10))
    error("CreateSocket: listen");

  atexit(ShutdownSockets);
}

// accept a new connection from listen_sock

static void AcceptConnection()
{
  struct sockaddr_in remote_addr;
  int remote_addr_size = sizeof(struct sockaddr_in);
  unsigned long remote_ip;       // remote ip addr
  connection_t *new_conn;

  // get a new connection_t and hook into freelist
  
  new_conn = get_connection_t();

  if(connections.next)
    connections.next->prev = new_conn;
  new_conn->next = connections.next;
  new_conn->prev = &connections;
  connections.next = new_conn;

  // accept connection from listen sock
  
  new_conn->sock = accept(listen_sock, &remote_addr, &remote_addr_size);

  if(new_conn->sock == -1)
    error("AcceptConnection: accept");

  // copy out remote address
  
  memcpy(&new_conn->remote_addr, &remote_addr.sin_addr,
	 sizeof(new_conn->remote_addr));

  // set up other data: clear read buffer etc.
  
  new_conn->connected = true;
  new_conn->read_buffer[0] = '\0';
  new_conn->idiotcount = 0;       // count for unauthorised command attempts
  
  connection_message(new_conn, "connected");

  // send motd to new client

  if(motd)
    WriteMessage(new_conn, motd);

  WriteMessage(new_conn, "ok");

  total_connections++;
}

void Disconnect(connection_t *conn)
{
  close(conn->sock);

  conn->connected = false;
  
#ifdef LOGALL
  connection_message(conn, "disconnected");
#endif

  // remove from freelist
  
  conn->prev->next = conn->next;
  if(conn->next)
    conn->next->prev = conn->prev;

  // return to disconnected freelist

  free_connection_t(conn);
}

// deal with input from a node

static void ReadInput(connection_t *conn)
{ 
  char *write_point = conn->read_buffer + strlen(conn->read_buffer);
  
  // read in new data

  int bytes = read(conn->sock,
	       write_point,
	       BUFFER_SIZE - 1 - strlen(conn->read_buffer));

  // end string properly
  
  write_point[bytes] = '\0'; 
  
  if(bytes == 0)
    {
      // disconnected
#ifdef LOGALL
      connection_message(conn, "remote disconnection");
#endif
      Disconnect(conn);
    }
}

// listen for new connections to listen_sock
// or data sent to existing connections

int html_time=0;   // check for html update: if time() > html_time, update

static void ListenForChange()
{
  fd_set fd_list;
  int num_changed;
  int max;
  connection_t *conn;
  struct timeval tv;

  //  printf("ListenForChange\n");
  
  FD_ZERO(&fd_list);
  FD_SET(listen_sock, &fd_list);  // add listening socket

  max = listen_sock;

  for(conn = connections.next; conn; conn = conn->next)
    {
      FD_SET(conn->sock, &fd_list);
      if(conn->sock > max)
        max = conn->sock;
    }

  // Wait up to a minute
  tv.tv_sec = 60;
  tv.tv_usec = 0;

  num_changed = select(max+1, &fd_list, NULL, NULL, &tv);

  if(num_changed == -1)
    error("ListenForInput:select");

  // check if time to update html page

  if(time(NULL) > html_time)
    {
      BuildHTML();
      html_time = time(NULL) + 5*60;   // wait 5 minutes before checking again
    }

  // only check sockets if any have changed

  if(num_changed)
    {
      // check listening socket
      
      if(FD_ISSET(listen_sock, &fd_list))
	{
	  AcceptConnection();
	}
      
      for(conn = connections.next; conn; conn = conn->next)
	{
	  if(FD_ISSET(conn->sock, &fd_list))
	    {
	      ReadInput(conn);
	      ReadCommands(conn);
	      return;
	    }
	}
    }
}

void Server()
{
server_num = 0;

  // loop forever
  // We are not continually calling ListenForChange,
  // but wait in ListenForChange and are alerted by
  // select() when input is received 

  while(true)
    ListenForChange();
}
