/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "Licence").
 * You may not use this file except in compliance with the Licence.
 *
 * You can obtain a copy of the licence at RISC OS path @.^.LICENCE
 * or  http://www.riscosdev.com/lanman98/LICENCE.CDDL
 * See the Licence for the specific language governing permissions
 * and limitations under the Licence.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the Licence file. If applicable, add the
 * following below this CDDL HEADER, with the fields enclosed by
 * brackets "[]" replaced with your own identifying information:
 * Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
 
/*
 *   Copyright 1996 Warm Silence Software Ltd.  All rights reserved.
 *   Use is subject to license terms.
 */

/*   PHBG 27/11/96: Initial version
 */

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "kernel.h"
#include "swis.h"
#include "LanMan98BaseLib/error.h"
#include "LanMan98BaseLib/memory.h"
#include "LanMan98BaseLib/blocked.h"
#include "LanMan98BaseLib/thread.h"
#include "LanMan98BaseLib/tcp.h"
#include <stdarg.h>
#include "md5.h"
#include "LanMan98BaseLib/time.h"
#include "netbios.h"
#include "var.h"

#include "vc.h"

#define SHORTFLUSH

#define RESOLVE_TIME_OUT (1000)
#define CONNECT_TIME_OUT (1000)
#define REQUEST_TIME_OUT (2000)
// #define REQUEST_TIME_OUT (5)

/* Used only when probing for port 445 with 139 still to fall back on, so
   that a host which silently drops the connection rather than refusing it
   does not cost the full connect timeout twice. */
#define PROBE_TIME_OUT   (300)

#define INIT_BUF_SIZE (2048)

#define MIN(x,y) ((x) < (y) ? (x) : (y))

struct vc_s
{
    tcp_addr_t addr;
    tcp_conn_t conn;
    tcp_port_t port;
    int flags2;
    int last_len;
    data_t pkt;
    int sign;                       /* messages are signed both ways */
    int dead;                       /* abandoned; every use fails at once */
    unsigned int seq;               /* number the next request carries */
    unsigned int expect;            /* number the next reply must carry */
    unsigned char mac_key[16];
};

/*
    SMB1 message signing.

    The signature is the first eight bytes of MD5 over the key followed by
    the whole message, with the eight byte signature field holding the
    sequence number - four bytes of it, little endian, then four zero -
    while the digest is taken.  The field is put back afterwards, because
    the caller still has to send or read the message.

    The key is the session key the NTLMSSP exchange produced, on its own.
    The other form, with the challenge response appended, belongs to the
    older session setup that this no longer uses.
*/
#define SMB_SIG_OFFSET (14)
#define SMB_SIG_LEN    (8)

static void smb_signature(const unsigned char *key, int key_len,
                          unsigned char *m, int len, unsigned int seq,
                          unsigned char out[SMB_SIG_LEN])
{
    struct md5_context ctx;
    unsigned char digest[MD5_RESULTLEN];
    unsigned char saved[SMB_SIG_LEN];

    memcpy(saved, m + SMB_SIG_OFFSET, SMB_SIG_LEN);
    m[SMB_SIG_OFFSET + 0] = (unsigned char) (seq);
    m[SMB_SIG_OFFSET + 1] = (unsigned char) (seq >> 8);
    m[SMB_SIG_OFFSET + 2] = (unsigned char) (seq >> 16);
    m[SMB_SIG_OFFSET + 3] = (unsigned char) (seq >> 24);
    memset(m + SMB_SIG_OFFSET + 4, 0, 4);

    md5_init(&ctx);
    md5_update(&ctx, key, (size_t) key_len);
    md5_update(&ctx, m, (size_t) len);
    md5_final(&ctx, digest);

    memcpy(out, digest, SMB_SIG_LEN);
    memcpy(m + SMB_SIG_OFFSET, saved, SMB_SIG_LEN);
    memset(digest, 0, sizeof(digest));
}

int VCCheckSignature(vc_t vc, const unsigned char *key, int key_len,
                     unsigned int seq)
{
    unsigned char want[SMB_SIG_LEN];
    unsigned char *m;

    if(vc == NULL)
        Error("No virtual circuit");
    if(vc->last_len < SMB_SIG_OFFSET + SMB_SIG_LEN)
        return 0;
    m = (unsigned char *) vc->pkt.buf + 4;
    smb_signature(key, key_len, m, vc->last_len, seq, want);
    return memcmp(want, m + SMB_SIG_OFFSET, SMB_SIG_LEN) == 0;
}

void VCMarkDead(vc_t vc)
{
    if(vc)
        vc->dead = 1;
}

void VCSignFrom(vc_t vc, const unsigned char *key, int key_len,
                unsigned int seq)
{
    if(vc == NULL)
        Error("No virtual circuit");
    if(key_len > (int) sizeof(vc->mac_key))
        key_len = sizeof(vc->mac_key);
    memset(vc->mac_key, 0, sizeof(vc->mac_key));
    memcpy(vc->mac_key, key, (size_t) key_len);
    vc->seq = seq;
    vc->expect = seq + 1;
    vc->sign = 1;
}


static void ensure_timeout(void)
{
    int timed_out;

    timed_out = ThreadTimedout();
    ThreadResetTimeout();
    if(!timed_out)
        ExceptRethrow();
}

static tcp_addr_t resolve(char *domain)
{
    tcp_addr_t addr;

    ThreadSetTimeout(RESOLVE_TIME_OUT);
    addr = NULL;
    ExceptTry
    {
        addr = TcpResolve(domain);
    }
    ExceptCatch
    {
        char msg[200];

        ensure_timeout();
        sprintf(msg, "Nothing on the network answered to the name %.80s.  "
                     "It may be switched off.", domain);
        ErrorNum(ERR_SERVER_UNREACHABLE, msg);
    }
    ThreadResetTimeout();
    if(addr == NULL)
    {
        char msg[200];

        sprintf(msg, "There is no machine called %.80s on the network.  It "
                     "may be switched off, or the name may be wrong.", domain);
        ErrorNum(ERR_SERVER_UNREACHABLE, msg);
    }
    return addr;
}

static tcp_conn_t form_connection(tcp_addr_t addr, tcp_port_t port, int timeout)
{
    tcp_conn_t conn;

    ThreadSetTimeout(timeout);
    conn = NULL;
    ExceptTry
    {
        conn = TcpCall(TCP_PORT_ANY, port, addr);
        TcpSetNoDelay(conn);
    }
    ExceptCatch
    {
        ensure_timeout();
        Error("Timed out while connecting");
    }
    ThreadResetTimeout();
    if(conn == NULL)
        Error("Connection lost (1)");
    return conn;
}

int debugging = 0;

/* Zero for the usual waits; otherwise how long to wait instead */
static int impatient = 0;

void VCImpatient(int cs)
{
    impatient = (cs > 0) ? cs : 0;
}

/*
    How long to wait for a server.

    The defaults suit a local network: twenty seconds for an answer and ten
    to establish a circuit are generous there, where a server either
    answers promptly or is not going to.  Across a wider network they are
    less generous than they look - a distant server's handshake can take
    several retransmissions before anything comes back, and a large read
    over a slow link takes as long as it takes.

    LanMan98$Timeout and LanMan98$ConnectTimeout raise them, in
    centiseconds.  Both are read once when a circuit is made rather than on
    every message: a variable lookup per request would be a SWI in the
    middle of every transfer to answer a question whose answer does not
    change.
*/
static int request_timeout = REQUEST_TIME_OUT;
static int connect_timeout = CONNECT_TIME_OUT;

static int wait_var(char *name, int fallback)
{
    char *var;
    int cs;

    var = VarRead(name);
    if(var == NULL)
        return fallback;
    cs = atoi(var);
    Free(var);
    return (cs > 0) ? cs : fallback;
}

static void read_timeouts(void)
{
    request_timeout = wait_var("LanMan98$Timeout", REQUEST_TIME_OUT);
    connect_timeout = wait_var("LanMan98$ConnectTimeout", CONNECT_TIME_OUT);
}

static int request_wait(void)
{
    return impatient ? impatient : request_timeout;
}

static int connect_wait(void)
{
    return impatient ? impatient : connect_timeout;
}
static char dump_prefix[192];
static char log_name[192];
static int logging = 0;
static int log_seq = 0;

/*
    A readable account of what happened, for sending to somebody else.

    The file is opened and closed for every line rather than held open.
    That is slower, and it does not matter at these rates, and it means the
    log is complete up to the moment of a failure that stops the machine -
    which is exactly the failure worth having a log of.
*/
static void set_log(void)
{
    char *var;

    logging = 0;
    var = getenv("LanMan98$Log");
    if(var == NULL)
        return;
    strncpy(log_name, var, sizeof(log_name) - 1);
    log_name[sizeof(log_name) - 1] = 0;
    logging = 1;
    log_seq = 0;
}

void VCLogErr(const char *what, _kernel_oserror *e)
{
    if(!logging) return;
    if(e == NULL)
        VCLog("*    %s, no error given", what);
    else
        VCLog("*    %s: %s (&%08X)", what, e->errmess, e->errnum);
}

void VCLog(const char *fmt, ...)
{
    va_list ap;
    FILE *f;

    if(!logging)
        return;
    f = fopen(log_name, "a");
    if(f == NULL)
        return;
    va_start(ap, fmt);
    vfprintf(f, fmt, ap);
    va_end(ap);
    fputc('\n', f);
    fclose(f);
}

/* Names for the two protocols, so the log reads as something rather than
   a column of numbers */
static char unknown_cmd[8];

static char *smb1_name(int cmd)
{
    switch(cmd)
    {
        case 0x00: return "make directory";
        case 0x01: return "remove directory";
        case 0x06: return "delete";
        case 0x07: return "rename";
        case 0x08: return "get information";
        case 0x09: return "set information";
        case 0x0B: return "seek";
        case 0x10: return "check directory";
        case 0x1D: return "read raw";
        case 0x1F: return "write raw";
        case 0x24: return "lock";
        case 0x2B: return "echo";
        case 0x2D: return "open";
        case 0x33: return "find close";
        case 0x34: return "find close2";
        case 0x72: return "negotiate";
        case 0x73: return "session setup";
        case 0x75: return "tree connect";
        case 0x71: return "tree disconnect";
        case 0x74: return "logoff";
        case 0x2E: return "read";
        case 0x2F: return "write";
        case 0x32: return "find";
        case 0xA2: return "open";
        case 0x04: return "close";
        case 0x25: return "transact";
        case 0xC0: return "open print file";
        default:
            sprintf(unknown_cmd, "&%02X", cmd);
            return unknown_cmd;
    }
}

static char *smb2_name(int cmd)
{
    switch(cmd)
    {
        case 0:  return "negotiate";
        case 1:  return "session setup";
        case 2:  return "logoff";
        case 3:  return "tree connect";
        case 4:  return "tree disconnect";
        case 5:  return "open";
        case 6:  return "close";
        case 8:  return "read";
        case 9:  return "write";
        case 11: return "pipe";
        case 13: return "echo";
        case 14: return "read directory";
        case 16: return "get information";
        case 17: return "set information";
        default:
            sprintf(unknown_cmd, "&%02X", cmd);
            return unknown_cmd;
    }
}

/* One line about a message, whichever protocol it is in */
static void log_message(char *p, int len, char *way)
{
    unsigned int status;
    char st[24];
    int cmd;

    if(!logging || len < 36)
        return;
    log_seq++;
    if((p[4] == (char) 0xFE) && (p[5] == 'S'))
    {
        cmd = (p[16] & 0xFF) | ((p[17] & 0xFF) << 8);
        status = ((unsigned char) p[12]) | (((unsigned char) p[13]) << 8) |
                 (((unsigned char) p[14]) << 16) | (((unsigned char) p[15]) << 24);
        if(status) sprintf(st, "  status &%08X", status); else st[0] = 0;
        VCLog("%4d %s SMB2 %-16s %5d bytes%s", log_seq, way,
              smb2_name(cmd), len, st);
    }
    else if((p[4] == (char) 0xFF) && (p[5] == 'S'))
    {
        cmd = p[8] & 0xFF;
        status = ((unsigned char) p[9]) | (((unsigned char) p[10]) << 8) |
                 (((unsigned char) p[11]) << 16) | (((unsigned char) p[12]) << 24);
        if(status) sprintf(st, "  status &%08X", status); else st[0] = 0;
        VCLog("%4d %s SMB1 %-16s %5d bytes%s", log_seq, way,
              smb1_name(cmd), len, st);
    }
}

/*
    Packet capture, for a server that cannot be reached to be tried.

    Setting LanMan98$PacketDump to a directory path, with a trailing dot,
    writes every message sent and received into it as it goes: Out1, In1,
    Out2 and so on.  It is off unless the variable is set, and it is meant
    for sending a capture to somebody who can read it, not for normal use -
    it writes a file per packet.
*/
static void set_dump(void)
{
    char *var;

    var = getenv("LanMan98$PacketDump");
    if(var == NULL)
    {
        debugging = 0;
        return;
    }
    strncpy(dump_prefix, var, sizeof(dump_prefix) - 1);
    dump_prefix[sizeof(dump_prefix) - 1] = 0;
    debugging = 1;
}

static void mb_out(char *buf, int size, char *basename, int i)
{
    char fname[256];
    FILE *f;

    if(!debugging) return;
    sprintf(fname, "%s%s%d", dump_prefix, basename, i);
    f = fopen(fname, "w");
    if(f)
    {
        fwrite(buf, 1, size, f);
    	fclose(f);
    }
}


static void request_session(vc_t vc, char *source, char *dest)
{
    char *p;
    int len;

    p = vc->pkt.buf;
    len = NetBIOSEncodeName(p+4, dest);
    len += NetBIOSEncodeName(p+len+4, source);
    p[0] = 0x81;
    p[1] = ((len >> 16) & 0xFF);
    p[2] = ((len >> 8) & 0xFF);
    p[3] = (len & 0xFF);
    // mb_out(p, len+4, "Out", 0);
    ThreadSetTimeout(request_wait());
    ExceptTry
    {
        TcpWrite(p, len+4, vc->conn);
        p[0] = 0x85;
        while(p[0] == 0x85)
            if(TcpRead(p, 4, vc->conn) != 4)
                Error("Connection lost (2)");
        len = (p[1] << 16) | (p[2] << 8) | p[3];
        if(TcpRead(p+4, len, vc->conn) != len)
            Error("Connection lost (3)");
        // mb_out(p, len+4, "In", 0);
        if(p[0] == 0x84)
            Error("Retarget request received");
        if(p[0] != 0x82)
        {
            if (p[4] == 0x82)
                Error("Session refused (NetBIOS name not present)", p[4]);
            else
                Error("Session refused 0x%02X", p[4]);
        }
    }
    ExceptCatch
    {
        ensure_timeout();
        Error("Timed out while requesting session");
    }
    ThreadResetTimeout();
}

tcp_port_t VCParsePort(char *s)
{
    char *end;
    long n;

    if(s == NULL || *s == 0)
        Error("No port number given");
    n = strtol(s, &end, 10);
    if(*end != 0 || n < 1 || n > 65535)
        Error("Bad port number '%s': expected a number between 1 and 65535", s);
    return (tcp_port_t) n;
}

static tcp_port_t configured_port(void)
{
    char *var;
    tcp_port_t port;

    port = SMB_PORT_AUTO;
    var = VarRead("LanMan98$Port");
    if(var == NULL)
        return SMB_PORT_AUTO;
    ExceptTry
    {
        port = VCParsePort(var);
    }
    ExceptCatch
    {
        Free(var);
        ExceptRethrow();
    }
    Free(var);
    return port;
}

/*
    Open the transport underneath the SMB session.

    Port 139 is the NetBIOS session service and needs a session request
    naming the server before any SMB can flow, which is also where a wrong
    server name gets rejected.  Every other port is taken to speak SMB
    directly, as 445 does, so that a forwarded or tunnelled port behaves the
    same way the port it forwards to does.

    With nothing specified we try 445 and fall back to 139, since a host
    that has stopped running NetBIOS over TCP is now the normal case rather
    than the exception.
*/
static void open_transport(vc_t vc, tcp_port_t port, char *netbios_name)
{
    volatile tcp_port_t chosen;

    chosen = (port == SMB_PORT_AUTO) ? configured_port() : port;

    if(chosen == SMB_PORT_AUTO)
    {
        ExceptTry
        {
            vc->conn = form_connection(vc->addr, SMB_PORT_DIRECT, PROBE_TIME_OUT);
            chosen = SMB_PORT_DIRECT;
        }
        ExceptCatch
        {
            vc->conn = NULL;
        }
        if(vc->conn == NULL)
            chosen = SMB_PORT_NBT;
    }

    /*
        The last attempt, and the one whose failure the user sees.

        "Timed out while connecting" is true and tells somebody who has
        just double clicked a share nothing they can act on.  The machine
        is named, and what has almost certainly happened is said plainly:
        a saved share whose server is switched off is the ordinary way to
        arrive here, not a fault to be diagnosed.

        The probe above is not covered by this - it is expected to fail on
        a server that wants the older port, and says nothing when it does.
    */
    if(vc->conn == NULL)
    {
        ExceptTry
        {
            vc->conn = form_connection(vc->addr, chosen, connect_wait());
        }
        ExceptCatch
        {
            char msg[200];

            VCLogErr("connect", ExceptCaught());
            sprintf(msg, "%.80s is not answering.  It may be switched off, "
                         "or it may have stopped sharing files.",
                    (netbios_name && *netbios_name) ? netbios_name
                                                    : "The server");
            ErrorNum(ERR_SERVER_UNREACHABLE, msg);
        }
    }

    vc->port = chosen;

    if(chosen == SMB_PORT_NBT)
        request_session(vc, TcpHostName(), netbios_name);
}

/*
    Whose hourglass this is.

    It says the machine is busy with what the user just asked for.  That is
    true of everything the filing system is entered to do, and is why the
    wait for a reply has had it up since the beginning.

    It is not true of the keepalive or the directory watch, which talk to
    the server on a timer, from a callback, with nobody waiting on the
    answer.  Their glass lands over whatever the user is actually doing,
    and it is not a flicker: the thread that asked parks inside the receive
    below with the glass still up, and is not serviced again until its
    reply arrives, so it stays up for the whole round trip - every few
    seconds, for something nobody asked for.

    So it is raised in the foreground and nowhere else.  ThreadCurrent() is
    NULL there and is the thread itself anywhere else, and it cannot have
    changed under a parked thread: servicing a thread makes it current
    again before its stack resumes, so the answer at the Off is the answer
    that was given at the On.
*/
static void glass_on(void)
{
    if(ThreadCurrent() == NULL)
        _swix(Hourglass_On, 0);
}

static void glass_off(void)
{
    if(ThreadCurrent() == NULL)
        _swix(Hourglass_Off, 0);
}

vc_t VC(char *domain, tcp_port_t port, char *netbios_name)
{
    vc_t vc;

    vc = Malloc(sizeof(*vc));
    vc->addr = NULL;
    vc->conn = NULL;
    vc->port = SMB_PORT_AUTO;
    set_dump();
    set_log();
    read_timeouts();
    vc->flags2 = 0;
    vc->sign = 0;
    vc->dead = 0;
    vc->seq = 0;
    vc->expect = 0;
    vc->last_len = 0;
    vc->pkt.buf = NULL;
    glass_on();
    ExceptTry
    {
        vc->addr = resolve(domain);
        vc->pkt.size = INIT_BUF_SIZE;
        vc->pkt.buf = Malloc(INIT_BUF_SIZE);
#ifdef DEBUG
        printf("Host name = %s\n", TcpHostName());
#endif
        open_transport(vc, port, netbios_name);
    }
    ExceptCatch
    {
        glass_off();
        VCDestruct(vc);
        ExceptRethrow();
    }
    glass_off();
    return vc;
}

tcp_port_t VCPort(vc_t vc)
{
    return vc ? vc->port : SMB_PORT_AUTO;
}

void VCSetFlags2(vc_t vc, int bits)
{
    if(vc)
        vc->flags2 = bits;
}

int VCFlags2(vc_t vc)
{
    return vc ? vc->flags2 : 0;
}

int VCLength(vc_t vc)
{
    return vc ? vc->last_len : 0;
}

void VCDestruct(vc_t vc)
{
    if(vc)
    {
        if(vc->pkt.buf) Free(vc->pkt.buf);
        if(vc->conn) TcpConnDestruct(vc->conn);
        TcpAddrDestruct(vc->addr);
        Free(vc);
    }
}

data_t VCBuffer(vc_t vc)
{
    data_t res;

    if(vc == NULL)
    	  Error("No virtual circuit");
    res.buf = vc->pkt.buf+4;
    res.size = vc->pkt.size-4;
    return res;
}

void VCResizeBuffer(vc_t vc, int newsize)
{
    int common;
    char *p;

    if(vc == NULL)
    	  Error("No virtual circuit");
    newsize += 4;
    common = MIN(vc->pkt.size, newsize);
    VCLog("     buffer: asking for %d bytes (have %d)", newsize, vc->pkt.size);
    p = Malloc(newsize);
    memcpy(p, vc->pkt.buf, common);
    Free(vc->pkt.buf);
    vc->pkt.buf = p;
    vc->pkt.size = newsize;
    VCLog("     buffer: now %d bytes", vc->pkt.size);
}

void VCSend(vc_t vc, int length)
{
    static int i = 0;
    char *p;

    if(vc == NULL)
    	  Error("No virtual circuit");
    if(vc->dead)
        Error("Connection abandoned after a signature failure");
    if(length+4 > vc->pkt.size)
    {
        VCLog("*    FAILED to send: %d bytes wanted, buffer is %d",
              length + 4, vc->pkt.size);
        Error("Overflowed packet buffer %d, %d", length+4, vc->pkt.size);
    }
    p = vc->pkt.buf;
    p[0] = 0;
    p[1] = ((length >> 16) & 0xFF);
    p[2] = ((length >> 8) & 0xFF);
    p[3] = (length & 0xFF);
    if(vc->sign)
    {
        unsigned char sig[SMB_SIG_LEN];
        unsigned char *m;

        m = (unsigned char *) p + 4;
        smb_signature(vc->mac_key, sizeof(vc->mac_key), m, length,
                      vc->seq, sig);
        memcpy(m + SMB_SIG_OFFSET, sig, SMB_SIG_LEN);
        vc->expect = vc->seq + 1;
        vc->seq += 2;
    }
    mb_out(p, length+4, "Out", ++i);
    log_message(p, length + 4, "sent    ");
    ThreadSetTimeout(request_wait());
    ExceptTry
    {
        // BlockedWait();
        TcpWrite(p, length+4, vc->conn);
    }
    ExceptCatch
    {
        ensure_timeout();
        Error("Timed out while sending");
    }
    ThreadResetTimeout();
}

void VCReceive(vc_t vc)
{
    int length;
    static int i = 0;
    char *p;

    if(vc == NULL)
    	  Error("No virtual circuit");
    if(vc->dead)
        Error("Connection abandoned after a signature failure");
    ThreadSetTimeout(request_wait());
    glass_on();
    ExceptTry
    {
        p = vc->pkt.buf;
        p[0] = 0x85;
        while(p[0] == 0x85)
            if(TcpRead(p, 4, vc->conn) != 4)
                Error("Connection lost (4)");
        length = (p[1] << 16) | (p[2] << 8) | p[3];
        if(length + 4 > vc->pkt.size)
        {
            VCLog("*    FAILED on receive: %d bytes arriving, buffer is %d",
                  length + 4, vc->pkt.size);
            Error("Oversized packet received %d, %d", length+4, vc->pkt.size);
        }
        if(TcpRead(p+4, length, vc->conn) != length)
            Error("Connection lost (5)");
        vc->last_len = length;
        mb_out(p, length+4, "In", ++i);
        log_message(p, length + 4, "received");
        if(vc->sign && !VCCheckSignature(vc, vc->mac_key,
                                         sizeof(vc->mac_key), vc->expect))
        {
            vc->dead = 1;
            Error("Reply failed its signature check");
        }
    }
    ExceptCatch
    {
        glass_off();
        ensure_timeout();
        Error("Timed out while receiving");
    }
    glass_off();
    ThreadResetTimeout();
}

int VCReceiveRaw(char *buf, int requested, vc_t vc)
{
    int length;
    char p[4];

    if(vc == NULL)
    	  Error("No virtual circuit");
    ThreadSetTimeout(request_wait());
    glass_on();
    ExceptTry
    {
        p[0] = 0x85;
        while(p[0] == 0x85)
            if(TcpRead(p, 4, vc->conn) != 4)
                Error("Connection lost (6)");
        length = (p[1] << 16) | (p[2] << 8) | p[3];
        if(length > requested)
        {
            VCFlush(vc);
            Error("Oversized response to raw read");
        }
        TcpRead(buf, length, vc->conn);
    }
    ExceptCatch
    {
        glass_off();
        ensure_timeout();
        Error("Timed out while receiving");
    }
    glass_off();
    ThreadResetTimeout();
    return length;
}

void VCCommunicate(vc_t vc, int length)
{
    if(vc == NULL)
    	  Error("No virtual circuit");
    VCSend(vc, length);
    VCReceive(vc);
}

void VCFlush(vc_t vc)
{
    static char buf[256];

    if(vc == NULL)
    	  return;
    ThreadSetTimeout(1);
    ExceptTry
    {
        while(TcpRead(buf, 256, vc->conn) != 0)
            ;
    } ExceptCatch {}
    ThreadResetTimeout();
}
