/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 *
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (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 mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Justin Bradford <jab@atdot.org>
 *   Bradley Baetz <bbaetz@acm.org>
 *   Darin Fisher <darin@meer.net>
 *   Malcolm Smith <malsmith@cs.rmit.edu.au>
 *   Christopher Davis <chrisd@torproject.org>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include "nspr.h"
#include "nsString.h"
#include "nsCRT.h"

#include "nsIServiceManager.h"
#include "nsIDNSService.h"
#include "nsIDNSRecord.h"
#include "nsISOCKSSocketInfo.h"
#include "nsISocketProvider.h"
#include "nsSOCKSIOLayer.h"
#include "nsNetCID.h"

static PRDescIdentity	nsSOCKSIOLayerIdentity;
static PRIOMethods	nsSOCKSIOLayerMethods;
static bool firstTime = true;

#if defined(PR_LOGGING)
static PRLogModuleInfo *gSOCKSLog;
#define LOGDEBUG(args) PR_LOG(gSOCKSLog, PR_LOG_DEBUG, args)
#define LOGERROR(args) PR_LOG(gSOCKSLog, PR_LOG_ERROR , args)

#else
#define LOGDEBUG(args)
#define LOGERROR(args)
#endif

class nsSOCKSSocketInfo : public nsISOCKSSocketInfo
{
    enum State {
        SOCKS_INITIAL,
        SOCKS_CONNECTING_TO_PROXY,
        SOCKS4_WRITE_CONNECT_REQUEST,
        SOCKS4_READ_CONNECT_RESPONSE,
        SOCKS5_WRITE_AUTH_REQUEST,
        SOCKS5_READ_AUTH_RESPONSE,
        SOCKS5_WRITE_CONNECT_REQUEST,
        SOCKS5_READ_CONNECT_RESPONSE_TOP,
        SOCKS5_READ_CONNECT_RESPONSE_BOTTOM,
        SOCKS_CONNECTED,
        SOCKS_FAILED
    };

    // A buffer of 262 bytes should be enough for any request and response
    // in case of SOCKS4 as well as SOCKS5
    static const PRUint32 BUFFER_SIZE = 262;
    static const PRUint32 MAX_HOSTNAME_LEN = 255;

public:
    nsSOCKSSocketInfo();
    virtual ~nsSOCKSSocketInfo() { HandshakeFinished(); }

    NS_DECL_ISUPPORTS
    NS_DECL_NSISOCKSSOCKETINFO

    void Init(PRInt32 version,
              const char *proxyHost,
              PRInt32 proxyPort,
              const char *destinationHost,
              PRUint32 flags);

    void SetConnectTimeout(PRIntervalTime to);
    PRStatus DoHandshake(PRFileDesc *fd, PRInt16 oflags = -1);
    PRInt16 GetPollFlags() const;
    bool IsConnected() const { return mState == SOCKS_CONNECTED; }

private:
    void HandshakeFinished(PRErrorCode err = 0);
    PRStatus ConnectToProxy(PRFileDesc *fd);
    PRStatus ContinueConnectingToProxy(PRFileDesc *fd, PRInt16 oflags);
    PRStatus WriteV4ConnectRequest();
    PRStatus ReadV4ConnectResponse();
    PRStatus WriteV5AuthRequest();
    PRStatus ReadV5AuthResponse();
    PRStatus WriteV5ConnectRequest();
    PRStatus ReadV5AddrTypeAndLength(PRUint8 *type, PRUint32 *len);
    PRStatus ReadV5ConnectResponseTop();
    PRStatus ReadV5ConnectResponseBottom();

    void WriteUint8(PRUint8 d);
    void WriteUint16(PRUint16 d);
    void WriteUint32(PRUint32 d);
    void WriteNetAddr(const PRNetAddr *addr);
    void WriteNetPort(const PRNetAddr *addr);
    void WriteString(const nsACString &str);

    PRUint8 ReadUint8();
    PRUint16 ReadUint16();
    PRUint32 ReadUint32();
    void ReadNetAddr(PRNetAddr *addr, PRUint16 fam);
    void ReadNetPort(PRNetAddr *addr);

    void WantRead(PRUint32 sz);
    PRStatus ReadFromSocket(PRFileDesc *fd);
    PRStatus WriteToSocket(PRFileDesc *fd);

private:
    State     mState;
    PRUint8 * mData;
    PRUint8 * mDataIoPtr;
    PRUint32  mDataLength;
    PRUint32  mReadOffset;
    PRUint32  mAmountToRead;
    nsCOMPtr<nsIDNSRecord> mDnsRec;

    nsCString mDestinationHost;
    nsCString mProxyHost;
    PRInt32   mProxyPort;
    PRInt32   mVersion;   // SOCKS version 4 or 5
    PRUint32  mFlags;
    PRNetAddr mInternalProxyAddr;
    PRNetAddr mExternalProxyAddr;
    PRNetAddr mDestinationAddr;
    PRIntervalTime mTimeout;
};

nsSOCKSSocketInfo::nsSOCKSSocketInfo()
    : mState(SOCKS_INITIAL)
    , mDataIoPtr(nsnull)
    , mDataLength(0)
    , mReadOffset(0)
    , mAmountToRead(0)
    , mProxyPort(-1)
    , mVersion(-1)
    , mFlags(0)
    , mTimeout(PR_INTERVAL_NO_TIMEOUT)
{
    mData = new PRUint8[BUFFER_SIZE];
    PR_InitializeNetAddr(PR_IpAddrAny, 0, &mInternalProxyAddr);
    PR_InitializeNetAddr(PR_IpAddrAny, 0, &mExternalProxyAddr);
    PR_InitializeNetAddr(PR_IpAddrAny, 0, &mDestinationAddr);
}

void
nsSOCKSSocketInfo::Init(PRInt32 version, const char *proxyHost, PRInt32 proxyPort, const char *host, PRUint32 flags)
{
    mVersion         = version;
    mProxyHost       = proxyHost;
    mProxyPort       = proxyPort;
    mDestinationHost = host;
    mFlags           = flags;
}

NS_IMPL_THREADSAFE_ISUPPORTS1(nsSOCKSSocketInfo, nsISOCKSSocketInfo)

NS_IMETHODIMP 
nsSOCKSSocketInfo::GetExternalProxyAddr(PRNetAddr * *aExternalProxyAddr)
{
    memcpy(*aExternalProxyAddr, &mExternalProxyAddr, sizeof(PRNetAddr));
    return NS_OK;
}

NS_IMETHODIMP 
nsSOCKSSocketInfo::SetExternalProxyAddr(PRNetAddr *aExternalProxyAddr)
{
    memcpy(&mExternalProxyAddr, aExternalProxyAddr, sizeof(PRNetAddr));
    return NS_OK;
}

NS_IMETHODIMP 
nsSOCKSSocketInfo::GetDestinationAddr(PRNetAddr * *aDestinationAddr)
{
    memcpy(*aDestinationAddr, &mDestinationAddr, sizeof(PRNetAddr));
    return NS_OK;
}

NS_IMETHODIMP 
nsSOCKSSocketInfo::SetDestinationAddr(PRNetAddr *aDestinationAddr)
{
    memcpy(&mDestinationAddr, aDestinationAddr, sizeof(PRNetAddr));
    return NS_OK;
}

NS_IMETHODIMP 
nsSOCKSSocketInfo::GetInternalProxyAddr(PRNetAddr * *aInternalProxyAddr)
{
    memcpy(*aInternalProxyAddr, &mInternalProxyAddr, sizeof(PRNetAddr));
    return NS_OK;
}

NS_IMETHODIMP 
nsSOCKSSocketInfo::SetInternalProxyAddr(PRNetAddr *aInternalProxyAddr)
{
    memcpy(&mInternalProxyAddr, aInternalProxyAddr, sizeof(PRNetAddr));
    return NS_OK;
}

// There needs to be a means of distinguishing between connection errors
// that the SOCKS server reports when it rejects a connection request, and
// connection errors that happen while attempting to connect to the SOCKS
// server. Otherwise, Firefox will report incorrectly that the proxy server
// is refusing connections when a SOCKS request is rejected by the proxy.
// When a SOCKS handshake failure occurs, the PR error is set to
// PR_UNKNOWN_ERROR, and the real error code is returned via the OS error.
void
nsSOCKSSocketInfo::HandshakeFinished(PRErrorCode err)
{
    if (err == 0) {
        mState = SOCKS_CONNECTED;
    } else {
        mState = SOCKS_FAILED;
        PR_SetError(PR_UNKNOWN_ERROR, err);
    }

    // We don't need the buffer any longer, so free it.
    delete [] mData;
    mData = nsnull;
    mDataIoPtr = nsnull;
    mDataLength = 0;
    mReadOffset = 0;
    mAmountToRead = 0;
}

PRStatus
nsSOCKSSocketInfo::ConnectToProxy(PRFileDesc *fd)
{
    PRStatus status;
    nsresult rv;

    NS_ABORT_IF_FALSE(mState == SOCKS_INITIAL,
                      "Must be in initial state to make connection!");

    // If we haven't performed the DNS lookup, do that now.
    if (!mDnsRec) {
        nsCOMPtr<nsIDNSService> dns = do_GetService(NS_DNSSERVICE_CONTRACTID);
        if (!dns)
            return PR_FAILURE;

        rv = dns->Resolve(mProxyHost, 0, getter_AddRefs(mDnsRec));
        if (NS_FAILED(rv)) {
            LOGERROR(("socks: DNS lookup for SOCKS proxy %s failed",
                     mProxyHost.get()));
            return PR_FAILURE;
        }
    }

    PRInt32 addresses = 0;
    do {
        if (addresses++)
            mDnsRec->ReportUnusable(mProxyPort);
        
        rv = mDnsRec->GetNextAddr(mProxyPort, &mInternalProxyAddr);
        // No more addresses to try? If so, we'll need to bail
        if (NS_FAILED(rv)) {
            LOGERROR(("socks: unable to connect to SOCKS proxy, %s",
                     mProxyHost.get()));
            return PR_FAILURE;
        }

#if defined(PR_LOGGING)
        char buf[64];
        PR_NetAddrToString(&mInternalProxyAddr, buf, sizeof(buf));
        LOGDEBUG(("socks: trying proxy server, %s:%hu",
                 buf, PR_ntohs(PR_NetAddrInetPort(&mInternalProxyAddr))));
#endif
        status = fd->lower->methods->connect(fd->lower,
                        &mInternalProxyAddr, mTimeout);
        if (status != PR_SUCCESS) {
            PRErrorCode c = PR_GetError();
            // If EINPROGRESS, return now and check back later after polling
            if (c == PR_WOULD_BLOCK_ERROR || c == PR_IN_PROGRESS_ERROR) {
                mState = SOCKS_CONNECTING_TO_PROXY;
                return status;
            }
        }
    } while (status != PR_SUCCESS);

    // Connected now, start SOCKS
    if (mVersion == 4)
        return WriteV4ConnectRequest();
    return WriteV5AuthRequest();
}

PRStatus
nsSOCKSSocketInfo::ContinueConnectingToProxy(PRFileDesc *fd, PRInt16 oflags)
{
    PRStatus status;

    NS_ABORT_IF_FALSE(mState == SOCKS_CONNECTING_TO_PROXY,
                      "Continuing connection in wrong state!");

    LOGDEBUG(("socks: continuing connection to proxy"));

    status = fd->lower->methods->connectcontinue(fd->lower, oflags);
    if (status != PR_SUCCESS) {
        PRErrorCode c = PR_GetError();
        if (c != PR_WOULD_BLOCK_ERROR && c != PR_IN_PROGRESS_ERROR) {
            // A connection failure occured, try another address
            mState = SOCKS_INITIAL;
            return ConnectToProxy(fd);
        }

        // We're still connecting
        return PR_FAILURE;
    }

    // Connected now, start SOCKS
    if (mVersion == 4)
        return WriteV4ConnectRequest();
    return WriteV5AuthRequest();
}

PRStatus
nsSOCKSSocketInfo::WriteV4ConnectRequest()
{
    PRNetAddr *addr = &mDestinationAddr;
    PRInt32 proxy_resolve;

    NS_ABORT_IF_FALSE(mState == SOCKS_CONNECTING_TO_PROXY,
                      "Invalid state!");
    
    proxy_resolve = mFlags & nsISocketProvider::PROXY_RESOLVES_HOST;

    mDataLength = 0;
    mState = SOCKS4_WRITE_CONNECT_REQUEST;

    LOGDEBUG(("socks4: sending connection request (socks4a resolve? %s)",
             proxy_resolve? "yes" : "no"));

    // Send a SOCKS 4 connect request.
    WriteUint8(0x04); // version -- 4
    WriteUint8(0x01); // command -- connect
    WriteNetPort(addr);
    if (proxy_resolve) {
        // Add the full name, null-terminated, to the request
        // according to SOCKS 4a. A fake IP address, with the first
        // four bytes set to 0 and the last byte set to something other
        // than 0, is used to notify the proxy that this is a SOCKS 4a
        // request. This request type works for Tor and perhaps others.
        WriteUint32(PR_htonl(0x00000001)); // Fake IP
        WriteUint8(0x00); // Send an emtpy username
        if (mDestinationHost.Length() > MAX_HOSTNAME_LEN) {
            LOGERROR(("socks4: destination host name is too long!"));
            HandshakeFinished(PR_BAD_ADDRESS_ERROR);
            return PR_FAILURE;
        }
        WriteString(mDestinationHost); // Hostname
        WriteUint8(0x00);
    } else if (PR_NetAddrFamily(addr) == PR_AF_INET) {
        WriteNetAddr(addr); // Add the IPv4 address
        WriteUint8(0x00); // Send an emtpy username
    } else if (PR_NetAddrFamily(addr) == PR_AF_INET6) {
        LOGERROR(("socks: SOCKS 4 can't handle IPv6 addresses!"));
        HandshakeFinished(PR_BAD_ADDRESS_ERROR);
        return PR_FAILURE;
    }

    return PR_SUCCESS;
}

PRStatus
nsSOCKSSocketInfo::ReadV4ConnectResponse()
{
    NS_ABORT_IF_FALSE(mState == SOCKS4_READ_CONNECT_RESPONSE,
                      "Handling SOCKS 4 connection reply in wrong state!");
    NS_ABORT_IF_FALSE(mDataLength == 8,
                      "SOCKS 4 connection reply must be 8 bytes!");

    LOGDEBUG(("socks4: checking connection reply"));

    if (ReadUint8() != 0x00) {
        LOGERROR(("socks4: wrong connection reply"));
        HandshakeFinished(PR_CONNECT_REFUSED_ERROR);
        return PR_FAILURE;
    }

    // See if our connection request was granted
    if (ReadUint8() == 90) {
        LOGDEBUG(("socks4: connection successful!"));
        HandshakeFinished();
        return PR_SUCCESS;
    }

    LOGERROR(("socks4: unable to connect"));
    HandshakeFinished(PR_CONNECT_REFUSED_ERROR);
    return PR_FAILURE;
}

PRStatus
nsSOCKSSocketInfo::WriteV5AuthRequest()
{
    NS_ABORT_IF_FALSE(mVersion == 5, "SOCKS version must be 5!");

    mState = SOCKS5_WRITE_AUTH_REQUEST;

    // Send an initial SOCKS 5 greeting
    LOGDEBUG(("socks5: sending auth methods"));
    WriteUint8(0x05); // version -- 5
    WriteUint8(0x01); // # auth methods -- 1
    WriteUint8(0x00); // we don't support authentication

    return PR_SUCCESS;
}

PRStatus
nsSOCKSSocketInfo::ReadV5AuthResponse()
{
    NS_ABORT_IF_FALSE(mState == SOCKS5_READ_AUTH_RESPONSE,
                      "Handling SOCKS 5 auth method reply in wrong state!");
    NS_ABORT_IF_FALSE(mDataLength == 2,
                      "SOCKS 5 auth method reply must be 2 bytes!");

    LOGDEBUG(("socks5: checking auth method reply"));

    // Check version number
    if (ReadUint8() != 0x05) {
        LOGERROR(("socks5: unexpected version in the reply"));
        HandshakeFinished(PR_CONNECT_REFUSED_ERROR);
        return PR_FAILURE;
    }

    // Make sure our authentication choice was accepted
    if (ReadUint8() != 0x00) {
        LOGERROR(("socks5: server did not accept our authentication method"));
        HandshakeFinished(PR_CONNECT_REFUSED_ERROR);
        return PR_FAILURE;
    }

    return WriteV5ConnectRequest();
}

PRStatus
nsSOCKSSocketInfo::WriteV5ConnectRequest()
{
    // Send SOCKS 5 connect request
    PRNetAddr *addr = &mDestinationAddr;
    PRInt32 proxy_resolve;
    proxy_resolve = mFlags & nsISocketProvider::PROXY_RESOLVES_HOST;

    LOGDEBUG(("socks5: sending connection request (socks5 resolve? %s)",
             proxy_resolve? "yes" : "no"));

    mDataLength = 0;
    mState = SOCKS5_WRITE_CONNECT_REQUEST;

    WriteUint8(0x05); // version -- 5
    WriteUint8(0x01); // command -- connect
    WriteUint8(0x00); // reserved
   
    // Add the address to the SOCKS 5 request. SOCKS 5 supports several
    // address types, so we pick the one that works best for us.
    if (proxy_resolve) {
        // Add the host name. Only a single byte is used to store the length,
        // so we must prevent long names from being used.
        if (mDestinationHost.Length() > MAX_HOSTNAME_LEN) {
            LOGERROR(("socks5: destination host name is too long!"));
            HandshakeFinished(PR_BAD_ADDRESS_ERROR);
            return PR_FAILURE;
        }
        WriteUint8(0x03); // addr type -- domainname
        WriteUint8(mDestinationHost.Length()); // name length
        WriteString(mDestinationHost);
    } else if (PR_NetAddrFamily(addr) == PR_AF_INET) {
        WriteUint8(0x01); // addr type -- IPv4
        WriteNetAddr(addr);
    } else if (PR_NetAddrFamily(addr) == PR_AF_INET6) {
        WriteUint8(0x04); // addr type -- IPv6
        WriteNetAddr(addr);
    } else {
        LOGERROR(("socks5: destination address of unknown type!"));
        HandshakeFinished(PR_BAD_ADDRESS_ERROR);
        return PR_FAILURE;
    }

    WriteNetPort(addr); // port

    return PR_SUCCESS;
}

PRStatus
nsSOCKSSocketInfo::ReadV5AddrTypeAndLength(PRUint8 *type, PRUint32 *len)
{
    NS_ABORT_IF_FALSE(mState == SOCKS5_READ_CONNECT_RESPONSE_TOP ||
                      mState == SOCKS5_READ_CONNECT_RESPONSE_BOTTOM,
                      "Invalid state!");
    NS_ABORT_IF_FALSE(mDataLength >= 5,
                      "SOCKS 5 connection reply must be at least 5 bytes!");
 
    // Seek to the address location 
    mReadOffset = 3;
   
    *type = ReadUint8();

    switch (*type) {
        case 0x01: // ipv4
            *len = 4 - 1;
            break;
        case 0x04: // ipv6
            *len = 16 - 1;
            break;
        case 0x03: // fqdn
            *len = ReadUint8();
            break;
        default:   // wrong address type
            LOGERROR(("socks5: wrong address type in connection reply!"));
            return PR_FAILURE;
    }

    return PR_SUCCESS;
}

PRStatus
nsSOCKSSocketInfo::ReadV5ConnectResponseTop()
{
    PRUint8 res;
    PRUint32 len;

    NS_ABORT_IF_FALSE(mState == SOCKS5_READ_CONNECT_RESPONSE_TOP,
                      "Invalid state!");
    NS_ABORT_IF_FALSE(mDataLength == 5,
                      "SOCKS 5 connection reply must be exactly 5 bytes!");

    LOGDEBUG(("socks5: checking connection reply"));

    // Check version number
    if (ReadUint8() != 0x05) {
        LOGERROR(("socks5: unexpected version in the reply"));
        HandshakeFinished(PR_CONNECT_REFUSED_ERROR);
        return PR_FAILURE;
    }

    // Check response
    res = ReadUint8();
    if (res != 0x00) {
        PRErrorCode c = PR_CONNECT_REFUSED_ERROR;

        switch (res) {
            case 0x01:
                LOGERROR(("socks5: connect failed: "
                          "01, General SOCKS server failure."));
                break;
            case 0x02:
                LOGERROR(("socks5: connect failed: "
                          "02, Connection not allowed by ruleset."));
                break;
            case 0x03:
                LOGERROR(("socks5: connect failed: 03, Network unreachable."));
                c = PR_NETWORK_UNREACHABLE_ERROR;
                break;
            case 0x04:
                LOGERROR(("socks5: connect failed: 04, Host unreachable."));
                break;
            case 0x05:
                LOGERROR(("socks5: connect failed: 05, Connection refused."));
                break;
            case 0x06:  
                LOGERROR(("socks5: connect failed: 06, TTL expired."));
                c = PR_CONNECT_TIMEOUT_ERROR;
                break;
            case 0x07:
                LOGERROR(("socks5: connect failed: "
                          "07, Command not supported."));
                break;
            case 0x08:
                LOGERROR(("socks5: connect failed: "
                          "08, Address type not supported."));
                c = PR_BAD_ADDRESS_ERROR;
                break;
            default:
                LOGERROR(("socks5: connect failed."));
                break;
        }

        HandshakeFinished(c);
        return PR_FAILURE;
    }

    if (ReadV5AddrTypeAndLength(&res, &len) != PR_SUCCESS) {
        HandshakeFinished(PR_BAD_ADDRESS_ERROR);
        return PR_FAILURE;
    }

    mState = SOCKS5_READ_CONNECT_RESPONSE_BOTTOM;
    WantRead(len + 2);

    return PR_SUCCESS;
}

PRStatus
nsSOCKSSocketInfo::ReadV5ConnectResponseBottom()
{
    PRUint8 type;
    PRUint32 len;

    NS_ABORT_IF_FALSE(mState == SOCKS5_READ_CONNECT_RESPONSE_BOTTOM,
                      "Invalid state!");

    if (ReadV5AddrTypeAndLength(&type, &len) != PR_SUCCESS) {
        HandshakeFinished(PR_BAD_ADDRESS_ERROR);
        return PR_FAILURE;
    }

    NS_ABORT_IF_FALSE(mDataLength == 7+len,
                      "SOCKS 5 unexpected length of connection reply!");

    LOGDEBUG(("socks5: loading source addr and port"));
    // Read what the proxy says is our source address
    switch (type) {
        case 0x01: // ipv4
            ReadNetAddr(&mExternalProxyAddr, PR_AF_INET);
            break;
        case 0x04: // ipv6
            ReadNetAddr(&mExternalProxyAddr, PR_AF_INET6);
            break;
        case 0x03: // fqdn (skip)
            mReadOffset += len;
            mExternalProxyAddr.raw.family = PR_AF_INET;
            break;
    }

    ReadNetPort(&mExternalProxyAddr);

    LOGDEBUG(("socks5: connected!"));
    HandshakeFinished();

    return PR_SUCCESS;
}

void
nsSOCKSSocketInfo::SetConnectTimeout(PRIntervalTime to)
{
    mTimeout = to;
}

PRStatus
nsSOCKSSocketInfo::DoHandshake(PRFileDesc *fd, PRInt16 oflags)
{
    LOGDEBUG(("socks: DoHandshake(), state = %d", mState));

    switch (mState) {
        case SOCKS_INITIAL:
            return ConnectToProxy(fd);
        case SOCKS_CONNECTING_TO_PROXY:
            return ContinueConnectingToProxy(fd, oflags);
        case SOCKS4_WRITE_CONNECT_REQUEST:
            if (WriteToSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;
            WantRead(8);
            mState = SOCKS4_READ_CONNECT_RESPONSE;
            return PR_SUCCESS;
        case SOCKS4_READ_CONNECT_RESPONSE:
            if (ReadFromSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;
            return ReadV4ConnectResponse();

        case SOCKS5_WRITE_AUTH_REQUEST:
            if (WriteToSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;
            WantRead(2);
            mState = SOCKS5_READ_AUTH_RESPONSE;
            return PR_SUCCESS;
        case SOCKS5_READ_AUTH_RESPONSE:
            if (ReadFromSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;
            return ReadV5AuthResponse();
        case SOCKS5_WRITE_CONNECT_REQUEST:
            if (WriteToSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;

            // The SOCKS 5 response to the connection request is variable
            // length. First, we'll read enough to tell how long the response
            // is, and will read the rest later.
            WantRead(5);
            mState = SOCKS5_READ_CONNECT_RESPONSE_TOP;
            return PR_SUCCESS;
        case SOCKS5_READ_CONNECT_RESPONSE_TOP:
            if (ReadFromSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;
            return ReadV5ConnectResponseTop();
        case SOCKS5_READ_CONNECT_RESPONSE_BOTTOM:
            if (ReadFromSocket(fd) != PR_SUCCESS)
                return PR_FAILURE;
            return ReadV5ConnectResponseBottom();

        case SOCKS_CONNECTED:
            LOGERROR(("socks: already connected"));
            HandshakeFinished(PR_IS_CONNECTED_ERROR);
            return PR_FAILURE;
        case SOCKS_FAILED:
            LOGERROR(("socks: already failed"));
            return PR_FAILURE;
    }

    LOGERROR(("socks: executing handshake in invalid state, %d", mState));
    HandshakeFinished(PR_INVALID_STATE_ERROR);

    return PR_FAILURE;
}

PRInt16
nsSOCKSSocketInfo::GetPollFlags() const
{
    switch (mState) {
        case SOCKS_CONNECTING_TO_PROXY:
            return PR_POLL_EXCEPT | PR_POLL_WRITE;
        case SOCKS4_WRITE_CONNECT_REQUEST:
        case SOCKS5_WRITE_AUTH_REQUEST:
        case SOCKS5_WRITE_CONNECT_REQUEST:
            return PR_POLL_WRITE;
        case SOCKS4_READ_CONNECT_RESPONSE:
        case SOCKS5_READ_AUTH_RESPONSE:
        case SOCKS5_READ_CONNECT_RESPONSE_TOP:
        case SOCKS5_READ_CONNECT_RESPONSE_BOTTOM:
            return PR_POLL_READ;
        default:
            break;
    }

    return 0;
}

inline void
nsSOCKSSocketInfo::WriteUint8(PRUint8 v)
{
    NS_ABORT_IF_FALSE(mDataLength + sizeof(v) <= BUFFER_SIZE,
                      "Can't write that much data!");
    mData[mDataLength] = v;
    mDataLength += sizeof(v);
}

inline void
nsSOCKSSocketInfo::WriteUint16(PRUint16 v)
{
    NS_ABORT_IF_FALSE(mDataLength + sizeof(v) <= BUFFER_SIZE,
                      "Can't write that much data!");
    memcpy(mData + mDataLength, &v, sizeof(v));
    mDataLength += sizeof(v);
}

inline void
nsSOCKSSocketInfo::WriteUint32(PRUint32 v)
{
    NS_ABORT_IF_FALSE(mDataLength + sizeof(v) <= BUFFER_SIZE,
                      "Can't write that much data!");
    memcpy(mData + mDataLength, &v, sizeof(v));
    mDataLength += sizeof(v);
}

void
nsSOCKSSocketInfo::WriteNetAddr(const PRNetAddr *addr)
{
    const char *ip = NULL;
    PRUint32 len = 0;

    if (PR_NetAddrFamily(addr) == PR_AF_INET) {
        ip = (const char*)&addr->inet.ip;
        len = sizeof(addr->inet.ip);
    } else if (PR_NetAddrFamily(addr) == PR_AF_INET6) {
        ip = (const char*)addr->ipv6.ip.pr_s6_addr;
        len = sizeof(addr->ipv6.ip.pr_s6_addr);
    }

    NS_ABORT_IF_FALSE(ip != NULL, "Unknown address");
    NS_ABORT_IF_FALSE(mDataLength + len <= BUFFER_SIZE,
                      "Can't write that much data!");
 
    memcpy(mData + mDataLength, ip, len);
    mDataLength += len;
}

void
nsSOCKSSocketInfo::WriteNetPort(const PRNetAddr *addr)
{
    WriteUint16(PR_NetAddrInetPort(addr));
}

void
nsSOCKSSocketInfo::WriteString(const nsACString &str)
{
    NS_ABORT_IF_FALSE(mDataLength + str.Length() <= BUFFER_SIZE,
                      "Can't write that much data!");
    memcpy(mData + mDataLength, str.Data(), str.Length());
    mDataLength += str.Length();
}

inline PRUint8
nsSOCKSSocketInfo::ReadUint8()
{
    PRUint8 rv;
    NS_ABORT_IF_FALSE(mReadOffset + sizeof(rv) <= mDataLength,
                      "Not enough space to pop a uint8!");
    rv = mData[mReadOffset];
    mReadOffset += sizeof(rv);
    return rv;
}

inline PRUint16
nsSOCKSSocketInfo::ReadUint16()
{
    PRUint16 rv;
    NS_ABORT_IF_FALSE(mReadOffset + sizeof(rv) <= mDataLength,
                      "Not enough space to pop a uint16!");
    memcpy(&rv, mData + mReadOffset, sizeof(rv));
    mReadOffset += sizeof(rv);
    return rv;
}

inline PRUint32
nsSOCKSSocketInfo::ReadUint32()
{
    PRUint32 rv;
    NS_ABORT_IF_FALSE(mReadOffset + sizeof(rv) <= mDataLength,
                      "Not enough space to pop a uint32!");
    memcpy(&rv, mData + mReadOffset, sizeof(rv));
    mReadOffset += sizeof(rv);
    return rv;
}

void
nsSOCKSSocketInfo::ReadNetAddr(PRNetAddr *addr, PRUint16 fam)
{
    PRUint32 amt;
    const PRUint8 *ip = mData + mReadOffset;

    addr->raw.family = fam;
    if (fam == PR_AF_INET) {
        amt = sizeof(addr->inet.ip);
        NS_ABORT_IF_FALSE(mReadOffset + amt <= mDataLength,
                          "Not enough space to pop an ipv4 addr!");
        memcpy(&addr->inet.ip, ip, amt);
    } else if (fam == PR_AF_INET6) {
        amt = sizeof(addr->ipv6.ip.pr_s6_addr);
        NS_ABORT_IF_FALSE(mReadOffset + amt <= mDataLength,
                          "Not enough space to pop an ipv6 addr!");
        memcpy(addr->ipv6.ip.pr_s6_addr, ip, amt);
    }

    mReadOffset += amt;
}

void
nsSOCKSSocketInfo::ReadNetPort(PRNetAddr *addr)
{
    addr->inet.port = ReadUint16();
}

void
nsSOCKSSocketInfo::WantRead(PRUint32 sz)
{
    NS_ABORT_IF_FALSE(mDataIoPtr == NULL,
                      "WantRead() called while I/O already in progress!");
    NS_ABORT_IF_FALSE(mDataLength + sz <= BUFFER_SIZE,
                      "Can't read that much data!");
    mAmountToRead = sz;
}

PRStatus
nsSOCKSSocketInfo::ReadFromSocket(PRFileDesc *fd)
{
    PRInt32 rc;
    const PRUint8 *end;

    if (!mAmountToRead) {
        LOGDEBUG(("socks: ReadFromSocket(), nothing to do"));
        return PR_SUCCESS;
    }

    if (!mDataIoPtr) {
        mDataIoPtr = mData + mDataLength;
        mDataLength += mAmountToRead;
    }

    end = mData + mDataLength;

    while (mDataIoPtr < end) {
        rc = PR_Read(fd, mDataIoPtr, end - mDataIoPtr);
        if (rc <= 0) {
            if (rc == 0) {
                LOGERROR(("socks: proxy server closed connection"));
                HandshakeFinished(PR_CONNECT_REFUSED_ERROR);
                return PR_FAILURE;
            } else if (PR_GetError() == PR_WOULD_BLOCK_ERROR) {
                LOGDEBUG(("socks: ReadFromSocket(), want read"));
            }
            break;
        }

        mDataIoPtr += rc;
    }

    LOGDEBUG(("socks: ReadFromSocket(), have %u bytes total",
             unsigned(mDataIoPtr - mData)));
    if (mDataIoPtr == end) {
        mDataIoPtr = nsnull;
        mAmountToRead = 0;
        mReadOffset = 0;
        return PR_SUCCESS;
    }

    return PR_FAILURE;
}

PRStatus
nsSOCKSSocketInfo::WriteToSocket(PRFileDesc *fd)
{
    PRInt32 rc;
    const PRUint8 *end;

    if (!mDataLength) {
        LOGDEBUG(("socks: WriteToSocket(), nothing to do"));
        return PR_SUCCESS;
    }

    if (!mDataIoPtr)
        mDataIoPtr = mData;

    end = mData + mDataLength;

    while (mDataIoPtr < end) {
        rc = PR_Write(fd, mDataIoPtr, end - mDataIoPtr);
        if (rc < 0) {
            if (PR_GetError() == PR_WOULD_BLOCK_ERROR) {
                LOGDEBUG(("socks: WriteToSocket(), want write"));
            }
            break;
        }
        
        mDataIoPtr += rc;
    }

    if (mDataIoPtr == end) {
        mDataIoPtr = nsnull;
        mDataLength = 0;
        mReadOffset = 0;
        return PR_SUCCESS;
    }
    
    return PR_FAILURE;
}

static PRStatus
nsSOCKSIOLayerConnect(PRFileDesc *fd, const PRNetAddr *addr, PRIntervalTime to)
{
    PRStatus status;
    PRNetAddr dst;

    nsSOCKSSocketInfo * info = (nsSOCKSSocketInfo*) fd->secret;
    if (info == NULL) return PR_FAILURE;

    if (PR_NetAddrFamily(addr) == PR_AF_INET6 &&
        PR_IsNetAddrType(addr, PR_IpAddrV4Mapped)) {
        const PRUint8 *srcp;

        LOGDEBUG(("socks: converting ipv4-mapped ipv6 address to ipv4"));

        // copied from _PR_ConvertToIpv4NetAddr()
        PR_InitializeNetAddr(PR_IpAddrAny, 0, &dst);
        srcp = addr->ipv6.ip.pr_s6_addr;
        memcpy(&dst.inet.ip, srcp + 12, 4);
        dst.inet.family = PR_AF_INET;
        dst.inet.port = addr->ipv6.port;
    } else {
        memcpy(&dst, addr, sizeof(dst));
    }

    info->SetDestinationAddr(&dst);
    info->SetConnectTimeout(to);

    do {
        status = info->DoHandshake(fd, -1);
    } while (status == PR_SUCCESS && !info->IsConnected());

    return status;
}

static PRStatus
nsSOCKSIOLayerConnectContinue(PRFileDesc *fd, PRInt16 oflags)
{
    PRStatus status;

    nsSOCKSSocketInfo * info = (nsSOCKSSocketInfo*) fd->secret;
    if (info == NULL) return PR_FAILURE;

    do { 
        status = info->DoHandshake(fd, oflags);
    } while (status == PR_SUCCESS && !info->IsConnected());

    return status;
}

static PRInt16
nsSOCKSIOLayerPoll(PRFileDesc *fd, PRInt16 in_flags, PRInt16 *out_flags)
{
    nsSOCKSSocketInfo * info = (nsSOCKSSocketInfo*) fd->secret;
    if (info == NULL) return PR_FAILURE;

    if (!info->IsConnected()) {
        *out_flags = 0;
        return info->GetPollFlags();
    }

    return fd->lower->methods->poll(fd->lower, in_flags, out_flags);
}

static PRStatus
nsSOCKSIOLayerClose(PRFileDesc *fd)
{
    nsSOCKSSocketInfo * info = (nsSOCKSSocketInfo*) fd->secret;
    PRDescIdentity id = PR_GetLayersIdentity(fd);

    if (info && id == nsSOCKSIOLayerIdentity)
    {
        NS_RELEASE(info);
        fd->identity = PR_INVALID_IO_LAYER;
    }

    return fd->lower->methods->close(fd->lower);
}

static PRFileDesc*
nsSOCKSIOLayerAccept(PRFileDesc *fd, PRNetAddr *addr, PRIntervalTime timeout)
{
    // TODO: implement SOCKS support for accept
    return fd->lower->methods->accept(fd->lower, addr, timeout);
}

static PRInt32
nsSOCKSIOLayerAcceptRead(PRFileDesc *sd, PRFileDesc **nd, PRNetAddr **raddr, void *buf, PRInt32 amount, PRIntervalTime timeout)
{
    // TODO: implement SOCKS support for accept, then read from it
    return sd->lower->methods->acceptread(sd->lower, nd, raddr, buf, amount, timeout);
}

static PRStatus
nsSOCKSIOLayerBind(PRFileDesc *fd, const PRNetAddr *addr)
{
    // TODO: implement SOCKS support for bind (very similar to connect)
    return fd->lower->methods->bind(fd->lower, addr);
}

static PRStatus
nsSOCKSIOLayerGetName(PRFileDesc *fd, PRNetAddr *addr)
{
    nsSOCKSSocketInfo * info = (nsSOCKSSocketInfo*) fd->secret;
    
    if (info != NULL && addr != NULL) {
        if (info->GetExternalProxyAddr(&addr) == NS_OK)
            return PR_SUCCESS;
    }

    return PR_FAILURE;
}

static PRStatus
nsSOCKSIOLayerGetPeerName(PRFileDesc *fd, PRNetAddr *addr)
{
    nsSOCKSSocketInfo * info = (nsSOCKSSocketInfo*) fd->secret;

    if (info != NULL && addr != NULL) {
        if (info->GetDestinationAddr(&addr) == NS_OK)
            return PR_SUCCESS;
    }

    return PR_FAILURE;
}

static PRStatus
nsSOCKSIOLayerListen(PRFileDesc *fd, PRIntn backlog)
{
    // TODO: implement SOCKS support for listen
    return fd->lower->methods->listen(fd->lower, backlog);
}

// add SOCKS IO layer to an existing socket
nsresult
nsSOCKSIOLayerAddToSocket(PRInt32 family,
                          const char *host, 
                          PRInt32 port,
                          const char *proxyHost,
                          PRInt32 proxyPort,
                          PRInt32 socksVersion,
                          PRUint32 flags,
                          PRFileDesc *fd, 
                          nsISupports** info)
{
    NS_ENSURE_TRUE((socksVersion == 4) || (socksVersion == 5), NS_ERROR_NOT_INITIALIZED);


    if (firstTime)
    {
        nsSOCKSIOLayerIdentity		= PR_GetUniqueIdentity("SOCKS layer");
        nsSOCKSIOLayerMethods		= *PR_GetDefaultIOMethods();

        nsSOCKSIOLayerMethods.connect	= nsSOCKSIOLayerConnect;
        nsSOCKSIOLayerMethods.connectcontinue	= nsSOCKSIOLayerConnectContinue;
        nsSOCKSIOLayerMethods.poll	= nsSOCKSIOLayerPoll;
        nsSOCKSIOLayerMethods.bind	= nsSOCKSIOLayerBind;
        nsSOCKSIOLayerMethods.acceptread = nsSOCKSIOLayerAcceptRead;
        nsSOCKSIOLayerMethods.getsockname = nsSOCKSIOLayerGetName;
        nsSOCKSIOLayerMethods.getpeername = nsSOCKSIOLayerGetPeerName;
        nsSOCKSIOLayerMethods.accept	= nsSOCKSIOLayerAccept;
        nsSOCKSIOLayerMethods.listen	= nsSOCKSIOLayerListen;
        nsSOCKSIOLayerMethods.close	= nsSOCKSIOLayerClose;

        firstTime			= false;

#if defined(PR_LOGGING)
        gSOCKSLog = PR_NewLogModule("SOCKS");
#endif

    }

    LOGDEBUG(("Entering nsSOCKSIOLayerAddToSocket()."));

    PRFileDesc *	layer;
    PRStatus	rv;

    layer = PR_CreateIOLayerStub(nsSOCKSIOLayerIdentity, &nsSOCKSIOLayerMethods);
    if (! layer)
    {
        LOGERROR(("PR_CreateIOLayerStub() failed."));
        return NS_ERROR_FAILURE;
    }

    nsSOCKSSocketInfo * infoObject = new nsSOCKSSocketInfo();
    if (!infoObject)
    {
        // clean up IOLayerStub
        LOGERROR(("Failed to create nsSOCKSSocketInfo()."));
        PR_DELETE(layer);
        return NS_ERROR_FAILURE;
    }

    NS_ADDREF(infoObject);
    infoObject->Init(socksVersion, proxyHost, proxyPort, host, flags);
    layer->secret = (PRFilePrivate*) infoObject;
    rv = PR_PushIOLayer(fd, PR_GetLayersIdentity(fd), layer);

    if (NS_FAILED(rv))
    {
        LOGERROR(("PR_PushIOLayer() failed. rv = %x.", rv));
        NS_RELEASE(infoObject);
        PR_DELETE(layer);
        return NS_ERROR_FAILURE;
    }

    *info = infoObject;
    NS_ADDREF(*info);
    return NS_OK;
}
