"Fossies" - the Fresh Open Source Software archive

Member "apcupsd-3.14.10/src/drivers/pcnet/pcnet.c" of archive apcupsd-3.14.10.tar.gz:


/*
 * pcnet.c
 *
 * Driver for PowerChute Network Shutdown protocol.
 */

/*
 * Copyright (C) 2006 Adam Kropelin
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * 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.
 */

#include "apc.h"
#include "md5.h"
#include <sys/socket.h>
#include <netinet/in.h>

/* UPS broadcasts status packets to UDP port 3052 */
#define PCNET_DEFAULT_PORT 3052

/*
 * Number of seconds with no data before we declare COMMLOST.
 * UPS should report in every 25 seconds. We allow 2 missing
 * reports plus a fudge factor.
 */
#define COMMLOST_TIMEOUT   55

/* Win32 needs a special close for sockets */
#ifdef HAVE_MINGW
#define close(fd) closesocket(fd)
#endif

typedef struct {
   char device[MAXSTRING];             /* Copy of ups->device */
   char *ipaddr;                       /* IP address of UPS */
   char *user;                         /* Username */
   char *pass;                         /* Pass phrase */
   bool auth;                          /* Authenticate? */
   unsigned long uptime;               /* UPS uptime counter */
   unsigned long reboots;              /* UPS reboot counter */
   time_t datatime;                    /* Last time we got valid data */
} PCNET_DATA;

/* Convert UPS response to enum and string */
static SelfTestResult decode_testresult(const char* str)
{
   /*
    * Responses are:
    * "OK" - good battery, 
    * "BT" - failed due to insufficient capacity, 
    * "NG" - failed due to overload, 
    * "NO" - no results available (no test performed in last 5 minutes) 
    */
   if (str[0] == 'O' && str[1] == 'K')
      return TEST_PASSED;
   else if (str[0] == 'B' && str[1] == 'T')
      return TEST_FAILCAP;
   else if (str[0] == 'N' && str[1] == 'G')
      return TEST_FAILLOAD;

   return TEST_NONE;
}

/* Convert UPS response to enum and string */
static LastXferCause decode_lastxfer(const char *str)
{
   Dmsg1(80, "Transfer reason: %c\n", *str);

   switch (*str) {
   case 'N':
      return XFER_NA;
   case 'R':
      return XFER_RIPPLE;
   case 'H':
      return XFER_OVERVOLT;
   case 'L':
      return XFER_UNDERVOLT;
   case 'T':
      return XFER_NOTCHSPIKE;
   case 'O':
      return XFER_NONE;
   case 'K':
      return XFER_FORCED;
   case 'S':
      return XFER_SELFTEST;
   default:
      return XFER_UNKNOWN;
   }
}

static bool pcnet_process_data(UPSINFO* ups, const char *key, const char *value)
{
   unsigned long cmd;
   int ci;
   bool ret;

   /* Make sure we have a value */
   if (*value == '\0')
      return false;

   /* Detect remote shutdown command */
   if (strcmp(key, "SD") == 0)
   {
      cmd = strtoul(value, NULL, 10);
      switch (cmd)
      {
      case 0:
         Dmsg0(80, "SD: The UPS is NOT shutting down\n");
         ups->clear_shut_remote();
         break;

      case 1:
         Dmsg0(80, "SD: The UPS is shutting down\n");
         ups->set_shut_remote();
         break;

      default:
         Dmsg1(80, "Unrecognized SD value %s!\n", value);
         break;
      }
 
      return true;
   }

   /* Key must be 2 hex digits */
   if (!isxdigit(key[0]) || !isxdigit(key[1]))
      return false;

   /* Convert command to CI */
   cmd = strtoul(key, NULL, 16);
   for (ci=0; ci<CI_MAXCI; ci++)
      if (ups->UPS_Cmd[ci] == cmd)
         break;

   /* No match? */
   if (ci == CI_MAXCI)
      return false;

   /* Mark this CI as available */
   ups->UPS_Cap[ci] = true;

   /* Handle the data */
   ret = true;
   switch (ci) {
      /*
       * VOLATILE DATA
       */
   case CI_STATUS:
      Dmsg1(80, "Got CI_STATUS: %s\n", value);
      ups->Status &= ~0xFF;        /* clear APC byte */
      ups->Status |= strtoul(value, NULL, 16) & 0xFF;  /* set APC byte */
      break;
   case CI_LQUAL:
      Dmsg1(80, "Got CI_LQUAL: %s\n", value);
      astrncpy(ups->linequal, value, sizeof(ups->linequal));
      break;
   case CI_WHY_BATT:
      Dmsg1(80, "Got CI_WHY_BATT: %s\n", value);
      ups->lastxfer = decode_lastxfer(value);
      break;
   case CI_ST_STAT:
      Dmsg1(80, "Got CI_ST_STAT: %s\n", value);
      ups->testresult = decode_testresult(value);
      break;
   case CI_VLINE:
      Dmsg1(80, "Got CI_VLINE: %s\n", value);
      ups->LineVoltage = atof(value);
      break;
   case CI_VMIN:
      Dmsg1(80, "Got CI_VMIN: %s\n", value);
      ups->LineMin = atof(value);
      break;
   case CI_VMAX:
      Dmsg1(80, "Got CI_VMAX: %s\n", value);
      ups->LineMax = atof(value);
      break;
   case CI_VOUT:
      Dmsg1(80, "Got CI_VOUT: %s\n", value);
      ups->OutputVoltage = atof(value);
      break;
   case CI_BATTLEV:
      Dmsg1(80, "Got CI_BATTLEV: %s\n", value);
      ups->BattChg = atof(value);
      break;
   case CI_VBATT:
      Dmsg1(80, "Got CI_VBATT: %s\n", value);
      ups->BattVoltage = atof(value);
      break;
   case CI_LOAD:
      Dmsg1(80, "Got CI_LOAD: %s\n", value);
      ups->UPSLoad = atof(value);
      break;
   case CI_FREQ:
      Dmsg1(80, "Got CI_FREQ: %s\n", value);
      ups->LineFreq = atof(value);
      break;
   case CI_RUNTIM:
      Dmsg1(80, "Got CI_RUNTIM: %s\n", value);
      ups->TimeLeft = atof(value);
      break;
   case CI_ITEMP:
      Dmsg1(80, "Got CI_ITEMP: %s\n", value);
      ups->UPSTemp = atof(value);
      break;
   case CI_DIPSW:
      Dmsg1(80, "Got CI_DIPSW: %s\n", value);
      ups->dipsw = strtoul(value, NULL, 16);
      break;
   case CI_REG1:
      Dmsg1(80, "Got CI_REG1: %s\n", value);
      ups->reg1 = strtoul(value, NULL, 16);
      break;
   case CI_REG2:
      ups->reg2 = strtoul(value, NULL, 16);
      ups->set_battpresent(!(ups->reg2 & 0x20));
      break;
   case CI_REG3:
      Dmsg1(80, "Got CI_REG3: %s\n", value);
      ups->reg3 = strtoul(value, NULL, 16);
      break;
   case CI_HUMID:
      Dmsg1(80, "Got CI_HUMID: %s\n", value);
      ups->humidity = atof(value);
      break;
   case CI_ATEMP:
      Dmsg1(80, "Got CI_ATEMP: %s\n", value);
      ups->ambtemp = atof(value);
      break;
   case CI_ST_TIME:
      Dmsg1(80, "Got CI_ST_TIME: %s\n", value);
      ups->LastSTTime = atof(value);
      break;
      
      /*
       * STATIC DATA
       */
   case CI_SENS:
      Dmsg1(80, "Got CI_SENS: %s\n", value);
      astrncpy(ups->sensitivity, value, sizeof(ups->sensitivity));
      break;
   case CI_DWAKE:
      Dmsg1(80, "Got CI_DWAKE: %s\n", value);
      ups->dwake = (int)atof(value);
      break;
   case CI_DSHUTD:
      Dmsg1(80, "Got CI_DSHUTD: %s\n", value);
      ups->dshutd = (int)atof(value);
      break;
   case CI_LTRANS:
      Dmsg1(80, "Got CI_LTRANS: %s\n", value);
      ups->lotrans = (int)atof(value);
      break;
   case CI_HTRANS:
      Dmsg1(80, "Got CI_HTRANS: %s\n", value);
      ups->hitrans = (int)atof(value);
      break;
   case CI_RETPCT:
      Dmsg1(80, "Got CI_RETPCT: %s\n", value);
      ups->rtnpct = (int)atof(value);
      break;
   case CI_DALARM:
      Dmsg1(80, "Got CI_DALARM: %s\n", value);
      astrncpy(ups->beepstate, value, sizeof(ups->beepstate));
      break;
   case CI_DLBATT:
      Dmsg1(80, "Got CI_DLBATT: %s\n", value);
      ups->dlowbatt = (int)atof(value);
      break;
   case CI_IDEN:
      Dmsg1(80, "Got CI_IDEN: %s\n", value);
      if (ups->upsname[0] == 0)
         astrncpy(ups->upsname, value, sizeof(ups->upsname));
      break;
   case CI_STESTI:
      Dmsg1(80, "Got CI_STESTI: %s\n", value);
      astrncpy(ups->selftest, value, sizeof(ups->selftest));
      break;
   case CI_MANDAT:
      Dmsg1(80, "Got CI_MANDAT: %s\n", value);
      astrncpy(ups->birth, value, sizeof(ups->birth));
      break;
   case CI_SERNO:
      Dmsg1(80, "Got CI_SERNO: %s\n", value);
      astrncpy(ups->serial, value, sizeof(ups->serial));
      break;
   case CI_BATTDAT:
      Dmsg1(80, "Got CI_BATTDAT: %s\n", value);
      astrncpy(ups->battdat, value, sizeof(ups->battdat));
      break;
   case CI_NOMOUTV:
      Dmsg1(80, "Got CI_NOMOUTV: %s\n", value);
      ups->NomOutputVoltage = (int)atof(value);
      break;
   case CI_NOMBATTV:
      Dmsg1(80, "Got CI_NOMBATTV: %s\n", value);
      ups->nombattv = atof(value);
      break;
   case CI_REVNO:
      Dmsg1(80, "Got CI_REVNO: %s\n", value);
      astrncpy(ups->firmrev, value, sizeof(ups->firmrev));
      break;
   case CI_EXTBATTS:
      Dmsg1(80, "Got CI_EXTBATTS: %s\n", value);
      ups->extbatts = (int)atof(value);
      break;
   case CI_BADBATTS:
      Dmsg1(80, "Got CI_BADBATTS: %s\n", value);
      ups->badbatts = (int)atof(value);
      break;
   case CI_UPSMODEL:
      Dmsg1(80, "Got CI_UPSMODEL: %s\n", value);
      astrncpy(ups->upsmodel, value, sizeof(ups->upsmodel));
      break;
   case CI_EPROM:
      Dmsg1(80, "Got CI_EPROM: %s\n", value);
      astrncpy(ups->eprom, value, sizeof(ups->eprom));
      break;
   default:
      Dmsg1(100, "Unknown CI (%d)\n", ci);
      ret = false;
      break;
   }
   
   return ret;
}

static char *digest2ascii(md5_byte_t *digest)
{
   static char ascii[33];
   char *ptr;
   int idx;

   /* Convert binary digest to ascii */
   ptr = ascii;
   for (idx=0; idx<16; idx++) {
      sprintf(ptr, "%02x", (unsigned char)digest[idx]);
      ptr += 2;
   }

   return ascii;
}

struct pair {
   const char* key;
   const char* value;
};

#define MAX_PAIRS 256

static const char *lookup_key(const char *key, struct pair table[])
{
   int idx;
   const char *ret = NULL;

   for (idx=0; table[idx].key; idx++) {
      if (strcmp(key, table[idx].key) == 0) {
         ret = table[idx].value;
         break;
      }
   }

   return ret;
}

static struct pair *auth_and_map_packet(UPSINFO* ups, char *buf, int len)
{
   PCNET_DATA *my_data = (PCNET_DATA *)ups->driver_internal_data;
   char *key, *end, *ptr, *value;
   const char *val, *hash=NULL;
   static struct pair pairs[MAX_PAIRS+1];
   md5_state_t ms;
   md5_byte_t digest[16];
   unsigned int idx;
   unsigned long uptime, reboots;

   /* If there's no MD= field, drop the packet */
   if ((ptr = strstr(buf, "MD=")) == NULL || ptr == buf)
      return NULL;

   if (my_data->auth) {
      /* Calculate the MD5 of the packet before messing with it */
      md5_init(&ms);
      md5_append(&ms, (md5_byte_t*)buf, ptr-buf);
      md5_append(&ms, (md5_byte_t*)my_data->user, strlen(my_data->user));
      md5_append(&ms, (md5_byte_t*)my_data->pass, strlen(my_data->pass));
      md5_finish(&ms, digest);

      /* Convert binary digest to ascii */
      hash = digest2ascii(digest);
   }

   /* Build a table of pointers to key/value pairs */
   memset(pairs, 0, sizeof(pairs));
   ptr = buf;
   idx = 0;
   while (*ptr && idx < MAX_PAIRS) {
      /* Find the beginning of the line */
      while (isspace(*ptr))
         ptr++;
      key = ptr;

      /* Find the end of the line */
      while (*ptr && *ptr != '\r' && *ptr != '\n')
         ptr++;
      end = ptr;
      if (*ptr != '\0')
         ptr++;

      /* Remove trailing whitespace */
      do {
         *end-- = '\0';
      } while (end >= key && isspace(*end));

      Dmsg1(300, "process_packet: line='%s'\n", key);

      /* Split the string */
      if ((value = strchr(key, '=')) == NULL)
         continue;
      *value++ = '\0';

      Dmsg2(300, "process_packet: key='%s' value='%s'\n",
         key, value);

      /* Save key/value in table */
      pairs[idx].key = key;
      pairs[idx].value = value;
      idx++;
   }

   if (my_data->auth) {
      /* Check calculated hash vs received */
      Dmsg1(200, "process_packet: calculated=%s\n", hash);
      val = lookup_key("MD", pairs);
      if (!val || strcmp(hash, val)) {
         Dmsg0(200, "process_packet: message hash failed\n");
         return NULL;
      }
      Dmsg1(200, "process_packet: message hash passed\n", val);

      /* Check management card IP address */
      val = lookup_key("PC", pairs);
      if (!val) {
         Dmsg0(200, "process_packet: Missing PC field\n");
         return NULL;
      }
      Dmsg1(200, "process_packet: Expected IP=%s\n", my_data->ipaddr);
      Dmsg1(200, "process_packet: Received IP=%s\n", val);
      if (strcmp(val, my_data->ipaddr)) {
         Dmsg2(200, "process_packet: IP address mismatch\n",
            my_data->ipaddr, val);
         return NULL;
      }
   }

   /*
    * Check that uptime and/or reboots have advanced. If not,
    * this packet could be out of order, or an attacker may
    * be trying to replay an old packet.
    */
   val = lookup_key("SR", pairs);
   if (!val) {
      Dmsg0(200, "process_packet: Missing SR field\n");
      return NULL;
   }
   reboots = strtoul(val, NULL, 16);

   val = lookup_key("SU", pairs);
   if (!val) {
      Dmsg0(200, "process_packet: Missing SU field\n");
      return NULL;
   }
   uptime = strtoul(val, NULL, 16);

   Dmsg1(200, "process_packet: Our reboots=%d\n", my_data->reboots);
   Dmsg1(200, "process_packet: UPS reboots=%d\n", reboots);
   Dmsg1(200, "process_packet: Our uptime=%d\n", my_data->uptime);
   Dmsg1(200, "process_packet: UPS uptime=%d\n", uptime);

   if ((reboots == my_data->reboots && uptime <= my_data->uptime) ||
       (reboots < my_data->reboots)) {
      Dmsg0(200, "process_packet: Packet is out of order or replayed\n");
      return NULL;
   }

   my_data->reboots = reboots;
   my_data->uptime = uptime;
   return pairs;
}

/*
 * Read UPS events. I.e. state changes.
 */
int pcnet_ups_check_state(UPSINFO *ups)
{
   PCNET_DATA *my_data = (PCNET_DATA *)ups->driver_internal_data;
   struct timeval tv, now, exit;
   fd_set rfds;
   bool done = false;
   struct sockaddr_in from;
   socklen_t fromlen;
   int retval;
   char buf[4096];
   struct pair *map;
   int idx;

   /* Figure out when we need to exit by */
   gettimeofday(&exit, NULL);
   exit.tv_sec += ups->wait_time;

   while (!done) {

      /* Figure out how long until we have to exit */
      gettimeofday(&now, NULL);

      if (now.tv_sec > exit.tv_sec ||
         (now.tv_sec == exit.tv_sec &&
            now.tv_usec >= exit.tv_usec)) {
         /* Done already? How time flies... */
         break;
      }

      tv.tv_sec = exit.tv_sec - now.tv_sec;
      tv.tv_usec = exit.tv_usec - now.tv_usec;
      if (tv.tv_usec < 0) {
         tv.tv_sec--;              /* Normalize */
         tv.tv_usec += 1000000;
      }

      Dmsg2(100, "Waiting for %d.%d\n", tv.tv_sec, tv.tv_usec);
      FD_ZERO(&rfds);
      FD_SET(ups->fd, &rfds);

      retval = select(ups->fd + 1, &rfds, NULL, NULL, &tv);

      if (retval == 0) {
         /* No chars available in TIMER seconds. */
         break;
      } else if (retval == -1) {
         if (errno == EINTR || errno == EAGAIN)         /* assume SIGCHLD */
            continue;
         Dmsg1(200, "select error: ERR=%s\n", strerror(errno));
         return 0;
      }

      do {
         fromlen = sizeof(from);
         retval = recvfrom(ups->fd, buf, sizeof(buf)-1, 0, (struct sockaddr*)&from, &fromlen);
      } while (retval == -1 && (errno == EAGAIN || errno == EINTR));

      if (retval < 0) {            /* error */
         Dmsg1(200, "recvfrom error: ERR=%s\n", strerror(errno));
//         usb_link_check(ups);      /* notify that link is down, wait */
         break;
      }

      Dmsg4(200, "Packet from: %d.%d.%d.%d\n",
         (ntohl(from.sin_addr.s_addr) >> 24) & 0xff,
         (ntohl(from.sin_addr.s_addr) >> 16) & 0xff,
         (ntohl(from.sin_addr.s_addr) >> 8) & 0xff,
         ntohl(from.sin_addr.s_addr) & 0xff);

      /* Ensure the packet is nul-terminated */
      buf[retval] = '\0';

      hex_dump(300, buf, retval);

      map = auth_and_map_packet(ups, buf, retval);
      if (map == NULL)
         continue;

      write_lock(ups);

      for (idx=0; map[idx].key; idx++)
         done |= pcnet_process_data(ups, map[idx].key, map[idx].value);

      write_unlock(ups);
   }

   /* If we successfully received a data packet, update timer. */
   if (done) {
      time(&my_data->datatime);
      Dmsg1(100, "Valid data at time_t=%d\n", my_data->datatime);
   }

   return done;
}

int pcnet_ups_open(UPSINFO *ups)
{
   struct sockaddr_in addr;
   PCNET_DATA *my_data = (PCNET_DATA *)ups->driver_internal_data;
   char *ptr;

   write_lock(ups);

   if (my_data == NULL) {
      my_data = (PCNET_DATA *)malloc(sizeof(*my_data));
      memset(my_data, 0, sizeof(*my_data));
      ups->driver_internal_data = my_data;
   }

   unsigned short port = PCNET_DEFAULT_PORT;
   if (ups->device[0] != '\0') {
      my_data->auth = true;

      astrncpy(my_data->device, ups->device, sizeof(my_data->device));
      ptr = my_data->device;

      my_data->ipaddr = ptr;
      ptr = strchr(ptr, ':');
      if (ptr == NULL)
         Error_abort0("Malformed DEVICE [ip:user:pass]\n");
      *ptr++ = '\0';
      
      my_data->user = ptr;
      ptr = strchr(ptr, ':');
      if (ptr == NULL)
         Error_abort0("Malformed DEVICE [ip:user:pass]\n");
      *ptr++ = '\0';

      my_data->pass = ptr;
      if (*ptr == '\0')
         Error_abort0("Malformed DEVICE [ip:user:pass]\n");

      // Last segment is optional port number
      ptr = strchr(ptr, ':');
      if (ptr)
      {
         *ptr++ = '\0';
         port = atoi(ptr);
         if (port == 0)
            port = PCNET_DEFAULT_PORT;
      }
   }

   ups->fd = socket(PF_INET, SOCK_DGRAM, 0);
   if (ups->fd == -1)
      Error_abort1("Cannot create socket (%d)\n", errno);

   int enable = 1;
   setsockopt(ups->fd, SOL_SOCKET, SO_BROADCAST, (const char*)&enable, sizeof(enable));

   memset(&addr, 0, sizeof(addr));
   addr.sin_family = AF_INET;
   addr.sin_port = htons(port);
   addr.sin_addr.s_addr = INADDR_ANY;
   if (bind(ups->fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
      close(ups->fd);
      Error_abort1("Cannot bind socket (%d)\n", errno);
   }

   /* Reset datatime to now */
   time(&my_data->datatime);

   write_unlock(ups);
   return 1;
}

int pcnet_ups_setup(UPSINFO *ups)
{
   /* Seems that there is nothing to do. */
   return 1;
}

int pcnet_ups_close(UPSINFO *ups)
{
   write_lock(ups);
   
   close(ups->fd);
   ups->fd = -1;

   write_unlock(ups);
   return 1;
}

/*
 * Setup capabilities structure for UPS
 */
int pcnet_ups_get_capabilities(UPSINFO *ups)
{
   /*
    * Unfortunately, we don't know capabilities until we
    * receive the first broadcast status message.
    */
   return 1;
}

/*
 * Read UPS info that remains unchanged -- e.g. transfer
 * voltages, shutdown delay, ...
 *
 * This routine is called once when apcupsd is starting
 */
int pcnet_ups_read_static_data(UPSINFO *ups)
{
   /* All our data gathering is done in pcnet_ups_check_state() */
   return 1;
}

/*
 * Read UPS info that changes -- e.g. Voltage, temperature, ...
 *
 * This routine is called once every N seconds to get
 * a current idea of what the UPS is doing.
 */
int pcnet_ups_read_volatile_data(UPSINFO *ups)
{
   PCNET_DATA *my_data = (PCNET_DATA *)ups->driver_internal_data;
   time_t now, diff;
   
   /*
    * All our data gathering is done in pcnet_ups_check_state().
    * But we do use this function to check our commlost state.
    */

   time(&now);
   diff = now - my_data->datatime;

   if (ups->is_commlost()) {
      if (diff < COMMLOST_TIMEOUT) {
         generate_event(ups, CMDCOMMOK);
         ups->clear_commlost();
      }
   } else {
      if (diff >= COMMLOST_TIMEOUT) {
         generate_event(ups, CMDCOMMFAILURE);
         ups->set_commlost();
      }
   }

   return 1;
}

int pcnet_ups_kill_power(UPSINFO *ups)
{
   PCNET_DATA *my_data = (PCNET_DATA *)ups->driver_internal_data;
   struct sockaddr_in addr;
   char data[1024];
   int s, len=0, temp=0;
   char *start;
   const char *cs, *hash;
   struct pair *map;
   md5_state_t ms;
   md5_byte_t digest[16];

   /* We cannot perform a killpower without authentication data */
   if (!my_data->auth) {
      Error_abort0("Cannot perform killpower without authentication "
                   "data. Please set ip:user:pass for DEVICE in "
                   "apcupsd.conf.\n");
   }

   /* Open a TCP stream to the UPS */
   s = socket(PF_INET, SOCK_STREAM, 0);
   if (s == -1) {
      Dmsg1(100, "pcnet_ups_kill_power: Unable to open socket: %s\n",
         strerror(errno));
      return 0;
   }

   memset(&addr, 0, sizeof(addr));
   addr.sin_family = AF_INET;
   addr.sin_port = htons(80);
   inet_pton(AF_INET, my_data->ipaddr, &addr.sin_addr.s_addr);

   if (connect(s, (sockaddr*)&addr, sizeof(addr))) {
      Dmsg3(100, "pcnet_ups_kill_power: Unable to connect to %s:%d: %s\n",
         my_data->ipaddr, 80, strerror(errno));
      close(s);
      return 0;
   }

   /* Send a simple HTTP request for "/macontrol.htm". */
   asnprintf(data, sizeof(data),
      "GET /macontrol.htm HTTP/1.1\r\n"
      "Host: %s\r\n"
      "\r\n",
      my_data->ipaddr);

   Dmsg1(200, "Request:\n---\n%s---\n", data);

   if (send(s, data, strlen(data), 0) != (int)strlen(data)) {
      Dmsg1(100, "pcnet_ups_kill_power: send failed: %s\n", strerror(errno));
      close(s);
      return 0;
   }

   /*
    * Clear buffer and read data until we find the 0-length
    * chunk. We know that AP9617 uses chunked encoding, so we
    * can count on the 0-length chunk at the end.
    */
   memset(data, 0, sizeof(data));
   do {
      len += temp;
      temp = recv(s, data+len, sizeof(data)-len, 0);
   } while(temp > 0 && strstr(data, "\r\n0\r\n") == NULL);

   Dmsg1(200, "Response:\n---\n%s---\n", data);

   if (temp < 0) {
      Dmsg1(100, "pcnet_ups_kill_power: recv failed: %s\n", strerror(errno));
      close(s);
      return 0;
   }

   /*
    * Find "<html>" since that's where the real authenticated
    * data begins. Everything before that is headers. 
    */
   start = strstr(data, "<html>");
   if (start == NULL) {
      Dmsg0(100, "pcnet_ups_kill_power: Malformed data\n");
      close(s);
      return 0;
   }

   /*
    * Authenticate and map the packet contents. This will
    * extract all key/value pairs and ensure the packet 
    * authentication hash is valid.
    */
   map = auth_and_map_packet(ups, start, strlen(start));
   if (map == NULL) {
      close(s);
      return 0;
   }

   /* Check that we got a challenge string. */
   cs = lookup_key("CS", map);
   if (cs == NULL) {
      Dmsg0(200, "pcnet_ups_kill_power: Missing CS field\n");
      close(s);
      return 0;
   }

   /*
    * Now construct the hash of the packet we're about to
    * send using the challenge string from the packet we
    * just received, plus our username and passphrase.
    */
   md5_init(&ms);
   md5_append(&ms, (md5_byte_t*)"macontrol1_control_shutdown_1=1,", 32);
   md5_append(&ms, (md5_byte_t*)cs, strlen(cs));
   md5_append(&ms, (md5_byte_t*)my_data->user, strlen(my_data->user));
   md5_append(&ms, (md5_byte_t*)my_data->pass, strlen(my_data->pass));
   md5_finish(&ms, digest);
   hash = digest2ascii(digest);

   /* Send the shutdown request */
   asnprintf(data, sizeof(data),
      "POST /Forms/macontrol1 HTTP/1.1\r\n"
      "Host: %s\r\n"
      "Content-Type: application/x-www-form-urlencoded\r\n"
      "Content-Length: 72\r\n"
      "\r\n"
      "macontrol1%%5fcontrol%%5fshutdown%%5f1=1%%2C%s",
      my_data->ipaddr, hash);

   Dmsg2(200, "Request: (strlen=%d)\n---\n%s---\n", strlen(data), data);

   if (send(s, data, strlen(data), 0) != (int)strlen(data)) {
      Dmsg1(100, "pcnet_ups_kill_power: send failed: %s\n", strerror(errno));
      close(s);
      return 0;
   }

   /* That's it, we're done. */
   close(s);

   return 1;
}

int pcnet_ups_program_eeprom(UPSINFO *ups, int command, const char *data)
{
   /* Unsupported */
   return 0;
}

int pcnet_ups_entry_point(UPSINFO *ups, int command, void *data)
{
   int temp;

   switch (command) {
   case DEVICE_CMD_CHECK_SELFTEST:
      Dmsg0(80, "Checking self test.\n");
      if (ups->UPS_Cap[CI_WHY_BATT] && ups->lastxfer == XFER_SELFTEST) {
         /*
          * set Self Test start time
          */
         ups->SelfTest = time(NULL);
         Dmsg1(80, "Self Test time: %s", ctime(&ups->SelfTest));
      }
      break;

   case DEVICE_CMD_GET_SELFTEST_MSG:
      /*
       * This is a bit kludgy. The selftest result isn't available from
       * the UPS for about 10 seconds after the selftest completes. So we
       * invoke pcnet_ups_check_state() with a 12 second timeout, 
       * expecting that it should get a status report before then.
       */

      /* Save current ups->wait_time and set it to 12 seconds */
      temp = ups->wait_time;
      ups->wait_time = 12;
      
      /* Let check_status wait for the result */
      write_unlock(ups);
      pcnet_ups_check_state(ups);
      write_lock(ups);

      /* Restore ups->wait_time */
      ups->wait_time = temp;
      break;

   default:
      return FAILURE;
   }

   return SUCCESS;
}