/* $Id: pxy.c,v 1.19 2004/05/27 14:18:43 mjt Exp $
 * open proxy checker, proxy protocol routines.
 * Michael Tokarev  <mjt@corpit.ru>.
 * This code may be freely used and distributed according to
 * the terms of General Public License (GPL) version 2 or later.
 */

#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include "event.h"
#include "pxy.h"

int pxytimeout = 30000; /* 30 sec */
char pxybuf[8193];

static void
timedout(pxyconn_t *c) {
  errno = ETIMEDOUT;
  pxyaction(c, -1);
}

int pxyrenew(pxyconn_t *c, int tmo, void (*tmfn)(pxyconn_t *)) {
  if (!tmfn) tmfn = timedout;
  ev_tm_del(0, &c->timer);
  if (ev_tm_add(0, &c->timer, tmo ? tmo : pxytimeout, (ev_tm_cbck_f*)tmfn, c) != NULL)
    return 1;
  else {
    pxyaction(c, -1);
    return 0;
  }
}

pxyconn_t *pxynew() {
  pxyconn_t *c = (pxyconn_t *)malloc(sizeof(pxyconn_t));
  if (c)
    memset(c, 0, sizeof(*c));
  return c;
}

void pxyfree(pxyconn_t *c) {
  ev_tm_del(0, &c->timer);
  if (c->fd >= 0)
    ev_io_del(0, c->fd);
  if (c->detail)
    free(c->detail);
  if (c->buf) {
    free(c->buf);
    c->buf = NULL;
  }
  free(c);
}

int
pxyreqio(pxyconn_t *c, int e, void(*iofn)(pxyconn_t*,int)) {
  if (ev_io_mod(0, c->fd, e, (ev_io_cbck_f*)iofn, c) == 0)
    return 1;
  else {
    pxyaction(c, -1);
    return 0;
  }
}

int
pxyreqiot(pxyconn_t *c, int e, void(*iofn)(pxyconn_t*,int),
          int tmo, void(*tmfn)(pxyconn_t*)) {
  return pxyreqio(c, e, iofn) && pxyrenew(c, tmo, tmfn);
}

int pxywrite(pxyconn_t *c, const char *buf, int len, int level) {
  int r;
  if ((r = write(c->fd, buf, len)) <= 0) {
    pxyaction(c, r < 0 ? -1 : 1);
    return 0;
  }
  else {
    pxyvio(c, level, 1, buf, len);
    return 1;
  }
}

int pxyprintf(pxyconn_t *c, int loglevel, const char *fmt, ...) {
  va_list ap;
  int l;
  va_start(ap, fmt);
  l = vsprintf(pxybuf, fmt, ap);
  va_end(ap);
  return pxywrite(c, pxybuf, l, loglevel);
}

int pxystart(pxyconn_t *c, int fd) {
  c->fd = fd;
  if (ev_io_add(0, fd, EV_OUT, (ev_io_cbck_f*)c->proto->handler, c) != 0) {
    pxyaction(c, -1);
    return 0;
  }
  else
    return pxyrenew(c, pxytimeout/2, NULL);
}

int pxyread(pxyconn_t *c, char *buf, int l, int minlen, int level) {
  l = read(c->fd, buf, l);
  if (l > 0) {
    pxyvio(c, level, 0, buf, l);
    c->nread += l;
  }
  else if (l < 0) {
    if (minlen || errno != EAGAIN) {
      pxyaction(c, -1);
      return -1;
    }
    else
      l = 0;
  }
  else {
    errno = 0;
    pxyaction(c, -1);
    return -1;
  }
  buf[l] = '\0';
  return l;
}

int pxyreadnext(pxyconn_t *c, int minlen, int *tlen, int loglevel) {
  int l, r;
  if (c->buf && (l = c->buf->len) != 0)
    memcpy(pxybuf, c->buf->buf, l);
  else
    l = 0;
  r = pxyread(c, pxybuf + l, sizeof(pxybuf) - l - 1, minlen, loglevel);
  if (r >= 0 && tlen)
    *tlen = r + l;
  return r;
}

int pxysave(pxyconn_t *c, char *buf, unsigned len, unsigned max) {
  if (c->nread > 2048) {
    pxyinfo(c, 2, "too much input");
    pxyaction(c, 2);
    return 0;
  }
  if (buf && len > 0) {
    if (!c->buf && !(c->buf = (pxybuf_t*)malloc(sizeof(pxybuf_t)))) {
      errno = ENOMEM;
      pxyaction(c, -1);
      return 0;
    }
    if (len > sizeof(c->buf->buf)) {
      buf += len - sizeof(c->buf->buf);
      len = sizeof(c->buf->buf);
    }
    if (max && len > max) {
      buf += len - max;
      len = max;
    }
    memcpy(c->buf->buf, buf, len);
    c->buf->len = len;
  }
  else if (c->buf)
    c->buf->len = 0;
  return 1;
}

/* find the string `str' in memory block, case-insensitive
 * (`str' should be lowercased) */
char *memcfind(const char *buf, int l, const char *str) {
  const char *s, *b, *e;
  for (e = buf + l - strlen(str); buf <= e; ++buf) {
#define lc(c) (((c) >= 'A' && (c) <= 'Z') ? (c) - 'A' + 'a' : (c))
    if (lc(*buf) != *str) continue;
    s = str + 1; b = buf + 1;
    for(;;) {
      if (!*s) return (char*)buf;
      if (*s != lc(*b)) break;
      ++s; ++b;
    }
  }
  return NULL;
}

/* parse HTTP headers */
static int httpheaders(pxyconn_t *c, char *buf, int l) {
  char *a;
  int r;
  if (memcmp(buf, "HTTP/", 5) != 0 ||
      !(a = strchr(buf, ' ')) ||
      a > buf + 8 ||
      a[1] < '1' || a[1] > '9' ||
      a[2] < '0' || a[2] > '9' ||
      a[3] < '0' || a[3] > '9' ||
      a[4] != ' ')
    return 0;
  r = (a[1] - '0') * 100 + (a[2] - '0') * 10 + (a[3] - '0');

  /* find Proxy-agent:, Via: headers */
  if ((a = memcfind(buf, l, "\nproxy-agent: ")) ||
      (a = memcfind(buf, l, "\nvia: "))) {
    int via;
    char *e;
    ++a;
    a += (via = *a == 'V' || *a == 'v') ? 5 : 13;
    while(*a == ' ') ++a;
    if (via) {
      while(*a > ' ') ++a;
      while(*a == ' ') ++a;
    }
    e = a;
    while((unsigned char)*e >= ' ' && *e != 0x7f) ++e;
    if (e != a) {
      if (e - a > 60) a[65] = '\0';
      else *e = '\0';
      c->detail = strdup(a);
      pxyinfo(c, 2, "Proxy-agent: %s", a);
    }
  }

  if (r / 100 != 2) {
    /*XX determine whenever this is a permanent or temp error */
    pxyinfo(c, 2, "HTTP request refused or failed (%d)", r);
    /* too bad some proxies return 403 or 404 errors on temp errors */
    if (r == 407 ||
        /* r == 404 || */
        /* r == 403 || */
        r == 400 ||
        r == 302)
      pxyaction(c, 2);
    else
      pxyaction(c, 1);
    return -1;
  }

  return r;
}

static int hcc(pxyconn_t *c, char *buf, int l) {
  int r;
  /* todo:
   *  - headers may come in separate packets;
   *  - detect e.g. "Out of buffers" errors seen from wingate-3.0 (1182)
   *  - detect redirects (status=3xx)
   */
  if (c->pxystate > 1)
    return 0;
  c->pxystate = 2;
  if (memcfind(buf, l, "hwarang/"))
    pxyinfo(c, 2, "probably HwaRang \"lazy\" proxy");
  r = httpheaders(c, buf, l);
  if (!r) return 0;
  if (r < 0) return 1;
  /* 2xx result */
  /*if (r / 100 == 2)*/ {
    if (memcfind(buf, l, "content-length:")
        /* some STUPID software... || memcfind(buf, l, "content-type:") */) {
      pxyinfo(c, 2, "HTTP CONNECT answered with a page (code %d)", r);
      pxyaction(c, 1); /* maybe temp. error message */
      return 1;
    }
    else {
      pxyinfo(c, 2, "HTTP request successeful (%d)", r);
      return 0;
    }
  }
}

static void hch(pxyconn_t *c, int UNUSED e) {
  /* HTTP CONNECT.
   * state:
   *  1 - request sent, want reply
   */
  if (c->pxystate == 0) {
    /* some proxies requires \r\n in separate packet - doh */
    if (pxyprintf(c, 3, "CONNECT %s:%d HTTP/1.0\r\n",
                  inet_ntoa(c->dstaddr), c->dstport) &&
        pxywrite(c, "\r\n", 2, 3) &&
        pxyreqiot(c, EV_IN, hch, 0, NULL))
      c->pxystate = 1;
  }
  else
  /* todo: read headers if any here.
   * requires some logic change: upper-level protocol
   * routines should deal with already read data */
    pxyaction(c, 0);
}

/* HTTP POST/PUT methods */

static int
hxc(pxyconn_t *c, char *buf, int l) {
  int r;
  if (c->pxystate > 2) return 0;
  c->pxystate = 2;
  r = httpheaders(c, buf, l);
  if (!r) return 0;
  if (r < 0) return 1;
  /*if (r / 100 == 2)*/ {
    pxyinfo(c, 2, "HTTP request successeful (%d)", r);
    return 0;
  }
}

static void hxh(pxyconn_t *c, int UNUSED e) {
  if (!c->pxystate) {
    int dl = pxygetdata(c);
    char *b = pxybuf + dl + 1;
    int l = sprintf(b,
"%s http://%s:%d/ HTTP/1.0\r\n"
"Content-length: %d\r\n"
"Connection: close\r\n"
"\r\n",
      strchr(c->proto->fullname, ' ') + 1,
      inet_ntoa(c->dstaddr), c->dstport, dl);
    if (pxywrite(c, b, l, 3)) {
      c->pxystate = 1;
      pxyinfo(c, 3, "sending data");
      if (pxywrite(c, pxybuf, dl, 6))
        pxyreqiot(c, EV_IN, hxh, 0, NULL);
    }
  }
  else
    pxycheckdata(c);
}

static void s5h(pxyconn_t *c, int UNUSED e) {
  /* SOCKS5
   * state:
   *  1 - auth request sent, want reply
   *  2 - connect request sent, want reply
   */
  int l;
  if (!c->pxystate) {
    pxybuf[0] = 5; /* socks version */
    pxybuf[1] = 1; /* nmethods */
    pxybuf[2] = 0; /* no auth reqd */
    if (pxywrite(c, pxybuf, 3, 4) &&
        pxyreqiot(c, EV_IN, s5h, pxytimeout / 2, NULL))
      c->pxystate = 1;
  }
  else if (c->pxystate == 1) {
    if ((l = pxyread(c, pxybuf, 4, 2, 4)) <= 0)
      ;
    else if (pxybuf[1] != 0) {
      pxyinfo(c, 2, "auth rejected (v=%d c=%d%s)",
              (unsigned char)pxybuf[0], (unsigned char)pxybuf[1],
              pxybuf[0] == 0 && pxybuf[1] == 91 ?
                ", socks4-only server" : "");
      pxyaction(c, 2);
    }
    else if (l > 2) {
      pxyinfo(c, 2, "too much socks5 data v=%d c=%d %d %d",
              (unsigned char)pxybuf[0], (unsigned char)pxybuf[1],
              (unsigned char)pxybuf[2], (unsigned char)pxybuf[3]);
      pxyaction(c, 2);
    }
    else {
      ipport_t p = htons(c->dstport);
      pxyinfo(c, 3, "auth accepted");
      pxybuf[0] = 5; /* version */
      pxybuf[1] = 1; /* connect cmd */
      pxybuf[2] = 0; /* reserved */
      pxybuf[3] = 1; /* ipv4 addrtype */
      memcpy(pxybuf+4, &c->dstaddr.s_addr, 4);
      memcpy(pxybuf+8, &p, 2);
      if (pxywrite(c, pxybuf, 10, 4) && pxyrenew(c, 0, NULL))
        c->pxystate = 2;
    }
  }
  else if ((l = pxyread(c, pxybuf, 10, 2, 4)) <= 0)
    ;
  else if (pxybuf[1] == 0) {
    pxyinfo(c, 2, "request granted (v=%d c=%d l=%d)",
            (unsigned char)pxybuf[0], (unsigned char)pxybuf[1], l);
    pxyaction(c, 0);
  }
  else {
    char *s;
    switch(pxybuf[1]) {
    case 1: s = "general server failure"; l = 1; break;
    case 2: s = "connection not allowed by ruleset"; l = 2; break;
    case 3: s = "network unreachable"; l = 1; break;
    case 4: s = "host unreachable"; l = 1; break;
    case 5: s = "connection refused"; l = 1; break;
    case 6: s = "TTL expired"; l = 1; break;
    case 7: s = "command not supported"; l = 2; break;
    case 8: s = "address type not supported"; l = 2; break;
    default: s = "unexpected response"; l = 1; break;
    }
    pxyinfo(c, 2, "%s (v=%d c=%d)", s,
            (unsigned char)pxybuf[0], (unsigned char)pxybuf[1]);
    pxyaction(c, l);
  }
}

static void s4h(pxyconn_t *c, int UNUSED e) {
  /* SOCKS4.
   * state:
   *  1 - connect request sent, want reply
   */
  int l;
  if (!c->pxystate) {
    ipport_t p = htons(c->dstport);
    pxybuf[0] = 4; /* socks version */
    pxybuf[1] = 1; /* request: connect */
    memcpy(pxybuf+2, &p, 2);
    memcpy(pxybuf+4, &c->dstaddr.s_addr, 4);
    pxybuf[8] = 0; /* empty/null username */
    if (pxywrite(c, pxybuf, 9, 4) && pxyreqiot(c, EV_IN, s4h, 0, NULL))
      c->pxystate = 1;
  }
  else if ((l = pxyread(c, pxybuf, 8, 8, 4)) <= 0)
    ;
  else if (pxybuf[1] == 90) {
    pxyinfo(c, 2, "request granted (v=%d c=%d)",
            (unsigned char)pxybuf[0], (unsigned char)pxybuf[1]);
    pxyaction(c, 0);
  }
  else {
    char *s;
    switch(pxybuf[1]) {
    case 91: s = "request rejected or failed"; break;
    case 92: s = "identd required"; break;
    case 93: s = "identd info mismatch"; break;
    case 1:
      if (pxybuf[0] == 5) {
        pxyinfo(c, 2, "request rejected (v=5 c=1, socks5-only server)");
        pxyaction(c, 2);
        return;
      }
    default: s = "unexpected response"; break;
    }
    pxyinfo(c, 2, "%s (v=%d c=%d)", s,
            (unsigned char)pxybuf[0], (unsigned char)pxybuf[1]);
    pxyaction(c, 1);
  }
}

static int
checklist(const char *buf, int l, const char *const lp[]) {
  const char *const *sp = lp;
  while(*sp)
    if (memmem(buf, l, *sp, strlen(*sp)))
      return sp - lp;
    else
      ++sp;
  return -1;
}

static int
wgc(pxyconn_t *c, char *buf, int l) {
  /* look at Password:/etc strings */
  static const char *const wgabort[] = {
   "sername",
   "ogin:",
   "assword:",
   NULL
  };
  if (checklist(buf, l, wgabort) < 0)
    return 0;
  pxyaction(c, 2);
  return 1;
}

static void wgh(pxyconn_t *c, int e);

static void wgh_tmo(pxyconn_t *c) {
  wgh(c, -1);
}

static void wgh(pxyconn_t *c, int e) {
  /* WINGATE/telnet.
   * state:
   *  1 - wait for a prompt, interpret Username:/Login: and similar;
   *   we're waiting for a small timeout here, if not found - go to
   *   next state
   *  2 - connection request sent, wait for reply
   */
  int l;
  if (!c->pxystate) {
    if (pxyreadnext(c, 0, &l, 3) < 0)
      return;
    c->pxystate = 1;
    if (!pxyreqiot(c, EV_IN, wgh, pxytimeout / 8, wgh_tmo) ||
        !l || !pxysave(c, pxybuf, l, 0))
      return;
  }
  if (c->pxystate == 1) {
    if (e < 0) {
      /*XX try to recognize whenever to use telnet or "plain" wingate */
      l = sprintf(pxybuf, "%s:%d\r\n", inet_ntoa(c->dstaddr), c->dstport);
      if (c->pxyport == 23)
        l += sprintf(pxybuf + l, "telnet %s %d\r\n",
                     inet_ntoa(c->dstaddr), c->dstport);
      if (pxywrite(c, pxybuf, l, 3) && pxyrenew(c, 0, NULL))
        c->pxystate = 2;
    }
    else if (pxyreadnext(c, 0, &l, 3) < 0 || wgc(c, pxybuf, l))
      return;
    else {
      static const char *const wggo[] = {
       "cisco>", /*0 wingate-3.0 1181 */
       "MNGTR>", /*1 wingate-3.0 1181 */
       "WinGate>", /*2 wingate 23 */
       "host[:port]:", /*3 telnet 23 */
       "host_name:port", /*4 TelNet Gateway 23*/
       "SpoonProxy>", /*5 host port 23 */
       "tn-gw",  /*6 telnet gateways 23 */
       "telnet>", /*7*/
       "CCProxy Telnet>", /*8 open host port 23 */
       NULL
      };
      const char *d;
      switch(checklist(pxybuf, l, wggo)) {
      case 0:
      case 1:
      case 2:
        if (!pxyprintf(c, 3, "%s:%d\r\n", inet_ntoa(c->dstaddr), c->dstport))
          return;
        d = "host:port (wingate)";
        break;
      case 3:
      case 4:
        if (!pxyprintf(c, 3, "%s:%d\r\n", inet_ntoa(c->dstaddr), c->dstport))
          return;
        d = "host:port (telnet gateway)";
        break;
      case 5:
        if (!pxyprintf(c, 3, "%s %d\r\n",
                       inet_ntoa(c->dstaddr), c->dstport))
          return;
        d = "host port (SpoonProxy)";
        break;
      case 6:
      case 7:
        if (!pxyprintf(c, 3, "telnet %s %d\r\n",
                       inet_ntoa(c->dstaddr), c->dstport))
          return;
        d = "telnet host port (telnet gateway)";
        break;
      case 8:
        if (!pxyprintf(c, 3, "open %s %d\r\n",
                       inet_ntoa(c->dstaddr), c->dstport))
          return;
        d = "open host port (CCProxy)";
        break;
      default:
        return;
      }
      if (pxysave(c, pxybuf, l, 0) && pxyrenew(c, 0, NULL))
        c->pxystate = 2;
      c->detail = strdup(d);
    }
  }
  else
    pxyaction(c, 0);
}

static void fuh(pxyconn_t *c, int UNUSED e) {
  /* FTP proxy.  Wait for initial FTP greething and
   * issue a command:
   *   USER dummy@targethost:targetport\r\n
   * After this, target receives
   *   USER dummy
   * and transparent connection begun.
   * There is no way to use such proxy with protocols
   * dependant on first line from client (e.g. http).
   */
  int l;
  if (!c->pxystate) {
    if (pxyreadnext(c, 0, &l, 3) < 0) /* check if established */
      return;
    c->pxystate = 1;
    if (!pxyreqiot(c, EV_IN, fuh, pxytimeout / 2, NULL) ||
        !l || !pxysave(c, pxybuf, l, 0))
      return;
  }
  if (c->pxystate == 1) {
    if (pxyreadnext(c, 1, &l, 3) > 0 && l > 4) {
      if (memcmp(pxybuf, "220 ", 4) == 0) {
        char *a, *e;
	pxybuf[l] = '\0';
	a = pxybuf + 4;
	if (!(*a >= 'A' && *a <= 'Z') && !(*a >= 'a' && *a <= 'z')) {
	  while(*a && *a != ' ' && *a != '\n' && *a != '\r') ++a;
	  while(*a == ' ') ++a;
	}
	e = a;
	while(*e && *e >= ' ' && *e != 0x7f) ++e;
	if (a != e) {
          if (e - a > 6 && memcmp(e - 6, " ready", 6) == 0)
	    e -= 6;
	  if (e - a > 70) a[65] = '\0';
	  else *e = '\0';
	  pxyinfo(c, 2, "Proxy-agent: %s", a);
	  c->detail = strdup(a);
	}
        pxysave(c, NULL, 0, 0);
        if (pxyprintf(c, 3, "USER dummy@%s:%d\r\n",
                      inet_ntoa(c->dstaddr), c->dstport))
          pxyaction(c, 0);
      }
      else {
        pxyinfo(c, 2, "seems not to be FTP proxy");
        /*XXX handle temp errors? */
        pxyaction(c, 2);
      }
    }
  }
}

enum pxyfamily { pfSOCKS, pfHTTP, pfWG, pfFTP };

const pxyproto_t pxyprotos[] = {
/* name       aname   transport  fullname        family   hdl  chk */
 { "socks5",      "s5", "socks5",  "SOCKS5",       pfSOCKS, s5h, NULL },
#define S5P (pxyprotos+0)
 { "socks4",      "s4", "socks4",  "SOCKS4",       pfSOCKS, s4h, NULL },
#define S4P (pxyprotos+1)
 { "wingate",     "wg", "wingate", "WINGATE",      pfWG,    wgh, wgc },
#define WGP (pxyprotos+2)
 { "http-connect","hc", "http",    "HTTP CONNECT", pfHTTP,  hch, hcc },
#define HCP (pxyprotos+3)
 { "http-post",   "ho", "http",    "HTTP POST",    pfHTTP,  hxh, hxc },
#define HOP (pxyprotos+4)
 { "http-put",    "hu", "http",    "HTTP PUT",     pfHTTP,  hxh, hxc },
#define HUP (pxyprotos+5)
 { "ftp-user",    "fu", "ftp",     "FTP USER",     pfFTP,   fuh, NULL },
#define FUP (pxyprotos+6)
 {0,0,0,0,0,0,0}
};

static const ipport_t
 htc[] = {80,81,1075,3128,4480,6588,8000,8080,8081,8090,0},
 hta[] = {7033,8085,8095,8100,8105,8110,0},
 spp[] = {1080,1075,0},
 wgp[] = {23,0},
 fup[] = {21,0},
 p1813[] = {1813,0}, /* skk proxy, socks5 only */
 p5490[] = {5490,0}  /* NONAME/1.4, probably trojan - http connect only */
;

const pxyprobe_t pxyprobes[] = {
/* keep this list sorted by 3rd column (level)! */
/* pro ports adv */
 { S5P, spp,   0 }, /* socks5 basic */
 { S4P, spp,   0 }, /* socks4 basic */ 
 { HCP, htc,   0 }, /* HTTP CONNECT basic */
 { HCP, p5490, 0 }, /* NONAME/1.4, http connect only */
 { HCP, hta,   1 }, /* HTTP CONNECT extended ports */
 { HOP, htc,   1 }, /* HTTP POST basic */
 { WGP, wgp,   2 }, /* wingate/telnet */
 { HOP, hta,   2 }, /* HTTP POST extended ports */
 { S5P, p1813, 2 }, /* SKK proxy (socks5) */
 { HUP, htc,   3 }, /* HTTP PUT basic */
 { FUP, fup,   3 }, /* FTP */
 { HUP, hta,   4 }, /* HTTP PUT extended ports */
 {0,0,0}
};
