/*
 * remconn.CXX
 *
 * Remote network connection (ppp) class implementation
 *
 * Portable Windows Library
 *
 * Copyright (c) 1993-1998 Equivalence Pty. Ltd.
 *
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
 * the License for the specific language governing rights and limitations
 * under the License.
 *
 * The Original Code is Portable Windows Library.
 *
 * The Initial Developer of the Original Code is Equivalence Pty. Ltd.
 *
 * Portions are Copyright (C) 1993 Free Software Foundation, Inc.
 * All Rights Reserved.
 *
 * Contributor(s): ______________________________________.
 *
 * $Revision: 21137 $
 * $Author: ms30002000 $
 * $Date: 2008-09-22 11:57:19 -0500 (Mon, 22 Sep 2008) $
 */

#pragma implementation "remconn.h"

#include <ptlib.h>
#include <ptlib/pipechan.h>
#include <ptlib/remconn.h>
#include <ptlib/pprocess.h>

#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#ifdef P_VXWORKS
#include <socklib.h>
#endif // P_VXWORKS

#ifdef P_SOLARIS
#include <signal.h>
#endif

#ifdef P_BEOS
#include <signal.h>
#endif

#include "uerror.h"

static const PString RasStr      = "ras";
static const PString NumberStr   = "Number";
static const PCaselessString UsernameStr = "$USERID";
static const PCaselessString PasswordStr = "$PASSWORD";
static const PString AddressStr = "Address";
static const PString NameServerStr = "NameServer";

static const PString OptionsStr = "Options";

static const PString DeviceStr     = "Device";
static const PString DefaultDevice = "ppp0";

static const PString PPPDStr     = "PPPD";
static const PString DefaultPPPD = "pppd";

static const PString ChatStr     = "Chat";
static const PString DefaultChat = "chat";

static const PString PortStr     = "Port";
static const PString DefaultPort = "/dev/modem";

static const PString DialPrefixStr     = "DialPrefix";
static const PString DefaultDialPrefix = "ATDT";

static const PString LoginStr     = "Login";
static const PString DefaultLogin = "'' sername: $USERID assword: $PASSWORD";

static const PString TimeoutStr     = "TimeoutStr";
static const PString DefaultTimeout = "90";

static const PString PPPDOptsStr     = "PPPDOpts";
static const PString PPPDOpts        = "-detach";
static const PString DefaultPPPDOpts = "crtscts modem defaultroute lock";

static const PString BaudRateStr     = "BaudRate";
static const PString DefaultBaudRate = "57600";

static const PString ErrorsStr     = "Errors";
static const PString DefaultErrors = "ABORT 'NO CARRIER' ABORT BUSY ABORT 'NO DIALTONE'";

static const PString InitStr     = "Init";
static const PString DefaultInit = "'' ATE1Q0Z OK";


static const PXErrorStruct ErrorTable[] =
{
  // Remote connection errors
  { 1000, "Attempt to open remote connection with empty system name" },
  { 1001, "Attempt to open connection to unknown remote system"},
  { 1002, "pppd could not connect to remote system"},
};

static int PPPDeviceStatus(const char * devName);

PRemoteConnection::PRemoteConnection()
{
  Construct();
}

PRemoteConnection::PRemoteConnection(const PString & name)
  : remoteName(name)
{
  Construct();
}

PRemoteConnection::~PRemoteConnection()
{
  Close();
}


PBoolean PRemoteConnection::Open(const PString & name, PBoolean existing)
{
  return Open(name, "", "", existing);
}

PBoolean PRemoteConnection::Open(const PString & name,
                             const PString & user,
                             const PString & pword,
                             PBoolean existing)
{
  userName = user;
  password = pword;

  // cannot open remote connection with an empty name
  if (name.IsEmpty()) {
    status = NoNameOrNumber;
    PProcess::PXShowSystemWarning(1000, ErrorTable[0].str);
    return PFalse;
  }

  // cannot open remote connection not in config file
  PConfig config(0, RasStr);
  PString phoneNumber;
  if ((phoneNumber = config.GetString(name, NumberStr, "")).IsEmpty()) {
    status = NoNameOrNumber;
    PProcess::PXShowSystemWarning(1001, ErrorTable[1].str);
    return PFalse;
  }

  // if there is a connection active, check to see if it has the same name
  if (pipeChannel != NULL &&
      pipeChannel->IsRunning() &&
      name == remoteName &&
      PPPDeviceStatus(deviceStr) > 0) {
    osError = errno;
    status = Connected;
    return PTrue;
  }
  osError = errno;

  if (existing)
    return PFalse;

  Close();

  // name        = name of configuration
  // sectionName = name of config section
  remoteName = name;

  ///////////////////////////////////////////
  //
  // get global options
  //
  config.SetDefaultSection(OptionsStr);
  deviceStr          = config.GetString(DeviceStr,     DefaultDevice);
  PString pppdStr    = config.GetString(PPPDStr,       DefaultPPPD);
  PString chatStr    = config.GetString(ChatStr,       DefaultChat);
  PString baudRate   = config.GetString(BaudRateStr,   DefaultBaudRate);
  PString chatErrs   = config.GetString(ErrorsStr,     DefaultErrors);
  PString modemInit  = config.GetString(InitStr,       DefaultInit);
  PString dialPrefix = config.GetString(DialPrefixStr, DefaultDialPrefix);
  PString pppdOpts   = config.GetString(PPPDOptsStr,   DefaultPPPDOpts);

  ///////////////////////////////////////////
  //
  // get remote system parameters
  //
  config.SetDefaultSection(remoteName);
  PString portName   = config.GetString(PortStr,
				config.GetString(OptionsStr, PortStr, DefaultPort));
  PString loginStr   = config.GetString(LoginStr,    DefaultLogin);
  PString timeoutStr = config.GetString(TimeoutStr,  DefaultTimeout);
  PINDEX timeout = timeoutStr.AsInteger();
  PString addressStr = config.GetString(AddressStr, "");
  PString nameServerStr = config.GetString(NameServerStr, "");


  ///////////////////////////////////////////
  //
  // start constructing the command argument array
  //
  PStringArray argArray;
  PINDEX argCount = 0;
  argArray[argCount++] = portName;
  argArray[argCount++] = baudRate;

  PStringArray tokens = PPPDOpts.Tokenise(' ');
  PINDEX i;
  for (i = 0; i < tokens.GetSize(); i++)
    argArray[argCount++] = tokens[i];

  tokens = pppdOpts.Tokenise(' ');
  for (i = 0; i < tokens.GetSize(); i++)
    argArray[argCount++] = tokens[i];

  if (!nameServerStr.IsEmpty()) {
    argArray[argCount++] = "ipparam";
    argArray[argCount++] = nameServerStr;
  }

  ///////////////////////////////////////////
  //
  // replace metastrings in the login string
  //
  loginStr.Replace(UsernameStr, user);
  loginStr.Replace(PasswordStr, pword);

  ///////////////////////////////////////////
  //
  // setup the chat command
  //
  PString chatCmd = chatErrs & (modemInit & (dialPrefix + (phoneNumber & loginStr)));
  if (!chatCmd.IsEmpty()) {
    argArray[argCount++] = "connect";
    argArray[argCount++] = chatStr & ("-t" + (timeoutStr & chatCmd));
  }

  if (!addressStr)
    argArray[argCount++] = addressStr + ":";

  ///////////////////////////////////////////
  //
  // instigate a dial using pppd
  //
  pipeChannel  = PNEW PPipeChannel(pppdStr, argArray);
  osError = errno;

  ///////////////////////////////////////////
  //
  //  wait until the dial succeeds, or times out
  //
  PTimer timer(timeout*1000);
  for (;;) {
    if (pipeChannel == NULL || !pipeChannel->IsRunning()) 
      break;

    if (PPPDeviceStatus(deviceStr) > 0) {
      osError = errno;
      return PTrue;
    }

    if (!timer.IsRunning())
      break;

    PThread::Current()->Sleep(1000);
  }
  osError = errno;

  ///////////////////////////////////////////
  //
  //  dial failed
  //
  Close();

  return PFalse;
}


PObject::Comparison PRemoteConnection::Compare(const PObject & obj) const
{
  return remoteName.Compare(((const PRemoteConnection &)obj).remoteName);
}


PINDEX PRemoteConnection::HashFunction() const
{
  return remoteName.HashFunction();
}


void PRemoteConnection::Construct()
{
  pipeChannel  = NULL;
  status       = Idle;
  osError      = 0;
}


PBoolean PRemoteConnection::Open(PBoolean existing)
{
  return Open(remoteName, existing);
}


void PRemoteConnection::Close()
{
  if (pipeChannel != NULL) {

    // give pppd a chance to clean up
    pipeChannel->Kill(SIGINT);

    PTimer timer(10*1000);
    for (;;) {
      if (!pipeChannel->IsRunning() ||
          (PPPDeviceStatus(deviceStr) <= 0) ||
          !timer.IsRunning())
        break;
      PThread::Current()->Sleep(1000);
    }

    // kill the connection for real
    delete pipeChannel;
    pipeChannel = NULL;
  }
}

PRemoteConnection::Status PRemoteConnection::GetStatus() const
{
  if (pipeChannel != NULL &&
      pipeChannel->IsRunning() &&
      PPPDeviceStatus(deviceStr) > 0) 
    return Connected;

  return Idle;
}

PStringArray PRemoteConnection::GetAvailableNames() 
{
  PStringArray names;

  // get the list of remote system names from the system config file
  PConfig config(0, RasStr);

  // remotes have section names of the form "Remote-x" where X is some
  // unique identifier, usually a number or the system name
  PStringList sections = config.GetSections();
  for (PINDEX i = 0; i < sections.GetSize(); i++) {
    PString sectionName = sections[i];
    if (sectionName != OptionsStr)
      names[names.GetSize()] = sectionName;
  }

  return names;
}

//
//  <0 = does not exist
//   0 = exists, but is down
//  >0 = exists, is up
//
static int PPPDeviceStatus(const char * devName)
{
#if defined(HAS_IFREQ)
  int skfd;
  struct ifreq ifr;

  // Create a channel to the NET kernel. 
  if ((skfd = socket(AF_INET, SOCK_DGRAM,0)) < 0) 
    return -1;

  // attempt to get the status of the ppp connection
  int stat;
  strcpy(ifr.ifr_name, devName);
  if (ioctl(skfd, SIOCGIFFLAGS, &ifr) < 0)
    stat = -1;
  else 
    stat = (ifr.ifr_flags & IFF_UP) ? 1 : 0;

  // attempt to get the status of the ppp connection
  close(skfd);
  return stat;
#else
#warning "No PPPDeviceExists implementation defined"
  return PFalse;
#endif
}


PRemoteConnection::Status PRemoteConnection::GetConfiguration(
                 Configuration & config  // Configuration of remote connection
               )
{
  return GetConfiguration(remoteName, config);
}


PRemoteConnection::Status PRemoteConnection::GetConfiguration(
                 const PString & name,   // Remote connection name to get configuration
                 Configuration & config  // Configuration of remote connection
               )
{
  if (name.IsEmpty())
    return NoNameOrNumber;

  PConfig cfg(0, RasStr);
  if (cfg.GetString(name, NumberStr, "").IsEmpty())
    return NoNameOrNumber;

  cfg.SetDefaultSection(name);

  config.device = cfg.GetString(OptionsStr, PortStr, DefaultPort);
  config.phoneNumber = cfg.GetString(NumberStr);
  config.ipAddress = cfg.GetString(AddressStr);
  config.dnsAddress = cfg.GetString(NameServerStr);
  config.script = cfg.GetString(LoginStr, DefaultLogin);
  config.subEntries = 0;
  config.dialAllSubEntries = PFalse;

  return Connected;
}


PRemoteConnection::Status PRemoteConnection::SetConfiguration(
                 const Configuration & config,  // Configuration of remote connection
                 PBoolean create            // Flag to create connection if not present
               )
{
  return SetConfiguration(remoteName, config, create);
}


PRemoteConnection::Status PRemoteConnection::SetConfiguration(
                 const PString & name,          // Remote connection name to configure
                 const Configuration & config,  // Configuration of remote connection
                 PBoolean create            // Flag to create connection if not present
               )
{
  if (config.phoneNumber.IsEmpty())
    return GeneralFailure;

  PConfig cfg(0, RasStr);

  if (!create && cfg.GetString(name, NumberStr, "").IsEmpty())
    return NoNameOrNumber;

  cfg.SetDefaultSection(name);

  if (config.device.IsEmpty())
    cfg.DeleteKey(PortStr);
  else
    cfg.SetString(PortStr, config.device);

  cfg.SetString(NumberStr, config.phoneNumber);

  if (config.ipAddress.IsEmpty())
    cfg.DeleteKey(AddressStr);
  else
    cfg.SetString(AddressStr, config.ipAddress);

  if (config.dnsAddress.IsEmpty())
    cfg.DeleteKey(NameServerStr);
  else
    cfg.SetString(NameServerStr, config.dnsAddress);

  if (config.script.IsEmpty())
    cfg.DeleteKey(LoginStr);
  else
    cfg.SetString(LoginStr, config.script);

  return Connected;
}


PRemoteConnection::Status PRemoteConnection::RemoveConfiguration(
		const PString & name  // Remote connection to remove
		)
{
  PConfig cfg(0, RasStr);

  if (cfg.GetString(name, NumberStr, "").IsEmpty())
    return NoNameOrNumber;

  cfg.DeleteSection(name);
  return Connected;
}



// End of File ////////////////////////////////////////////////////////////////
