The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/*
 * SSH2.xs - C functions for Net::SSH2
 *
 * D. Robins, 20051022
 */

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#define NEED_sv_2pv_flags
#define NEED_newRV_noinc
#define NEED_sv_2pv_nolen
#include "ppport.h"

#if PERL_REVISION == 5 && (PERL_VERSION < 8 || (PERL_VERSION == 8 && PERL_SUBVERSION < 4 ))

#  ifdef SvPVbyte_force
#    undef SvPVbyte_force
#  endif
#  define SvPVbyte_force(sv,lp) SvPV_force(sv,lp)

#  ifdef SvPVbyte_nolen
#    undef SvPVbyte_nolen
#  endif
#  define SvPVbyte_nolen SvPV_nolen

#endif


#include <libssh2.h>
#include <libssh2_sftp.h>
#include <libssh2_publickey.h>

#define LIBSSH2_HOSTKEY_POLICY_STRICT   1
#define LIBSSH2_HOSTKEY_POLICY_ASK      2
#define LIBSSH2_HOSTKEY_POLICY_TOFU     3
#define LIBSSH2_HOSTKEY_POLICY_ADVISORY 4

#define LIBSSH2_EXTENDED_DATA_STDERR    SSH_EXTENDED_DATA_STDERR
#define LIBSSH2_CHANNEL_FLUSH_STDERR    SSH_EXTENDED_DATA_STDERR

#include "const-c.inc"

#if defined(USE_ITHREADS) && (defined(I_PTHREAD) || defined(WIN32))

#ifdef USE_GCRYPT
#define HAVE_GCRYPT
#include <gcrypt.h>
#else /* OpenSSL */
#define HAVE_OPENSSL
#include <openssl/crypto.h>
#endif

#else
/* is #warning portable across C compilers? */
/* NO, an error with -pedantic */
/* #warning "Building a non-threadsafe Net::SSH2" */
#endif

#ifndef MULTIPLICITY
/* for debugging output */
#define my_perl ((void *)0)
#endif

/* constants */

#ifndef LIBSSH2_ERROR_NONE
#define LIBSSH2_ERROR_NONE 0
#endif  /* LIBSSH2_ERROR_NONE */

/* LIBSSH2_ERROR_* values; from 0 continuing negative */
static const char *const xs_libssh2_error[] = {
    "NONE",
    "SOCKET_NONE",
    "BANNER_NONE",
    "BANNER_SEND",
    "INVALID_MAC",
    "KEX_FAILURE",
    "ALLOC",
    "SOCKET_SEND",
    "KEY_EXCHANGE_FAILURE",
    "TIMEOUT",
    "HOSTKEY_INIT",
    "HOSTKEY_SIGN",
    "DECRYPT",
    "SOCKET_DISCONNECT",
    "PROTO",
    "PASSWORD_EXPIRED",
    "FILE",
    "METHOD_NONE",
    "PUBLICKEY_UNRECOGNIZED",
    "PUBLICKEY_UNVERIFIED",
    "CHANNEL_OUTOFORDER",
    "CHANNEL_FAILURE",
    "CHANNEL_REQUEST_DENIED",
    "CHANNEL_UNKNOWN",
    "CHANNEL_WINDOW_EXCEEDED",
    "CHANNEL_PACKET_EXCEEDED",
    "CHANNEL_CLOSED",
    "CHANNEL_EOF_SENT",
    "SCP_PROTOCOL",
    "ZLIB",
    "SOCKET_TIMEOUT",
    "SFTP_PROTOCOL",
    "REQUEST_DENIED",
    "METHOD_NOT_SUPPORTED",
    "INVAL",
    "INVALID_POLL_TYPE",
    "PUBLICKEY_PROTOCOL",
    "EAGAIN",
    "ERROR_BUFFER_TOO_SMALL",
    "BAD_USE",
    "ERROR_COMPRESS",
    "OUT_OF_BOUNDARY",
    "AGENT_PROTOCOL",
    "SOCKET_RECV",
    "ENCRYPT",
    "BAD_SOCKET",
    "KNOWN_HOSTS",
};

/* SSH_FX_* values; from 0 continuing positive */
static const char *const sftp_error[] = {
    "OK",
    "EOF",
    "NO_SUCH_FILE",
    "PERMISSION_DENIED",
    "FAILURE",
    "BAD_MESSAGE",
    "NO_CONNECTION",
    "CONNECTION_LOST",
    "OP_UNSUPPORTED",
    "INVALID_HANDLE",
    "NO_SUCH_PATH",
    "FILE_ALREADY_EXISTS",
    "WRITE_PROTECT",
    "NO_MEDIA",
    "NO_SPACE_ON_FILESYSTEM",
    "QUOTA_EXCEEDED",
    "UNKNOWN_PRINCIPLE",
    "LOCK_CONFLICT",
    "DIR_NOT_EMPTY",
    "NOT_A_DIRECTORY",
    "INVALID_FILENAME",
    "LINK_LOOP"
};

/* private internal functions */

#define countof(x) (sizeof(x)/sizeof(*x))

#define XLATATTR(name, field, flag) \
    else if (strEQ(key, name)) { \
        attrs.field = SvUV(ST(i + 1)); \
        attrs.flags |= LIBSSH2_SFTP_ATTR_##flag; \
    }

typedef int SSH2_RC; /* for converting true/false to 1/undef */
typedef int SSH2_BYTES; /* for functions returning a byte count or a negative number to signal an error */
typedef libssh2_int64_t SSH2_BYTES64; /* the same for unsigned 64bit numbers */
typedef libssh2_uint64_t SSH2_BYTESU64; /* the same for unsigned 64bit numbers */
typedef int SSH2_ERROR; /* for returning SSH2 error numbers */
typedef int SSH2_NERROR; /* for converting SSH2 error code to boolean just indicating success or failure */
typedef int SSH2_BOOL; /* for yes/no responses */

typedef IV SSH2_METHOD;       /* LIBSSH2_METHOD_ constants */
typedef IV SSH2_FLAG;         /* LIBSSH2_FLAG_ constants */
typedef IV SSH2_CALLBACK;     /* LIBSSH2_CALLBACK_ constants */
typedef IV SSH2_HOSTKEY_HASH; /* LIBSSH2_HOSTKEY_HASH_ constants */
typedef IV SSH2_CHANNEL_EXTENDED_DATA; /* SSH2_CHANNEL_EXTENDED_DATA_ constants */
typedef IV SSH2_STREAM_ID;    /* stream_id or LIBSSH2_CHANNEL_FLUSH macros */
typedef char * SSH2_CHARP;         /* string that can not be NULL */
typedef char * SSH2_CHARP_OR_NULL; /* string that can be NULL */

/* Net::SSH2 object */
typedef struct SSH2 {
    LIBSSH2_SESSION* session;
    SV* sv_ss;  /* NB: not set until callback() called */
    SV* socket;
    SV* hostname;
    int port;
    SV* sv_tmp;
    SV* rgsv_cb[LIBSSH2_CALLBACK_X11 + 1];
} SSH2;

/* Net::SSH2::Channel object */
typedef struct SSH2_CHANNEL {
    SSH2* ss;
    SV* sv_ss;
    LIBSSH2_CHANNEL* channel;
} SSH2_CHANNEL;

/* Net::SSH2::SFTP object */
typedef struct SSH2_SFTP {
    SSH2* ss;
    SV* sv_ss;
    LIBSSH2_SFTP* sftp;
} SSH2_SFTP;

/* Net::SSH2::Listener object */
typedef struct SSH2_LISTENER {
    SSH2* ss;
    SV* sv_ss;
    LIBSSH2_LISTENER* listener;
} SSH2_LISTENER;

/* Net::SSH2::File object */
typedef struct SSH2_FILE {
    SSH2_SFTP* sf;
    SV* sv_sf;
    LIBSSH2_SFTP_HANDLE* handle;
} SSH2_FILE;

/* Net::SSH2::Dir object */
typedef struct SSH2_DIR {
    SSH2_SFTP* sf;
    SV* sv_sf;
    LIBSSH2_SFTP_HANDLE* handle;
} SSH2_DIR;

/* Net::SSH2::PublicKey object */
typedef struct SSH2_PUBLICKEY {
    SSH2* ss;
    SV* sv_ss;
    LIBSSH2_PUBLICKEY* pkey;
} SSH2_PUBLICKEY;

#if LIBSSH2_VERSION_NUM >= 0x010200

/* Net::SSH2::KnownHosts object */
typedef struct SSH2_KNOWNHOSTS {
    SSH2 *ss;
    SV *sv_ss;
    LIBSSH2_KNOWNHOSTS* knownhosts;
} SSH2_KNOWNHOSTS;

#endif

static int net_ss_debug_out = 0;
static unsigned long gensym_count = 0;

/* debug output */
static void debug(const char* format, ...) {
    if (net_ss_debug_out) {
        va_list va;
        va_start(va, format);
        vwarn(format, &va);
        va_end(va);
    }
}

/* libssh2 allocator thunks */
LIBSSH2_ALLOC_FUNC(local_alloc) {
    void *buf;
    New(0, buf, count, char);
    return buf;
}
LIBSSH2_REALLOC_FUNC(local_realloc) {
    return Renew(ptr, count, char);
}
LIBSSH2_FREE_FUNC(local_free) {
    Safefree(ptr);
}

#define SV2TYPE(sv, type) ((type)((sizeof(IV) < sizeof(type)) ? SvNV(sv) : SvIV(sv)))
#define SV2UTYPE(sv, type) ((type)((sizeof(IV) < sizeof(type)) ? SvNV(sv) : SvUV(sv)))

static void
wrap_tied_into(SV *to, const char *pkg, void *object) {
    GV* gv = (GV*)newSVrv(to, pkg);
    IO* io = (IO*)newSV(0);
    SV* name_sv = sv_2mortal(newSVpvf("_GEN_%ld", (long)gensym_count++));
    STRLEN name_len;
    const char *name = SvPVbyte(name_sv, name_len);
        
    SvUPGRADE((SV*)gv, SVt_PVGV);
    gv_init(gv, gv_stashpv(pkg, GV_ADD), name, name_len, 0);
    SvUPGRADE((SV*)io, SVt_PVIO);

    GvSV(gv) = newSViv(PTR2IV(object));
    GvIOp(gv) = io;
#if PERL_VERSION > 6
    sv_magic((SV*)io, newRV((SV*)gv), PERL_MAGIC_tiedscalar, Nullch, 0);
#else
    sv_magic((SV*)gv, newRV((SV*)gv), PERL_MAGIC_tiedscalar, Nullch, 0);
#endif
}

static IV
unwrap(SV *sv, const char *pkg, const char *method) {
    if (SvROK(sv) && sv_isa(sv, pkg)) {
        SV *inner = SvRV(sv);
        if (SvIOK(inner))
            return SvIVX(inner);
    }
    croak("%s::%s: invalid object %s", pkg, method, SvPV_nolen(sv));
}

static IV
unwrap_tied(SV *sv, const char *pkg, const char *method) {
    if (SvROK(sv) && sv_isa(sv, pkg)) {
        SV *gv = SvRV(sv);
        if (SvTYPE(gv) == SVt_PVGV) {
            SV *inner = GvSV((GV*)gv);
            if (inner && SvIOK(inner))
                return SvIVX(inner);
        }
    }
    croak("%s::%s: invalid object %s", pkg, method, SvPV_nolen(sv));
}

/* push a hash of values onto the return stack, for '%hash = func()' */
static int push_hv(SV** sp, HV* hv) {
    I32 keys = hv_iterinit(hv);
    const char* pv_key;
    I32 len_key;
    SV* value;

    EXTEND(SP, keys * 2);
    while ((value = hv_iternextsv(hv, (char**)&pv_key, &len_key))) {
        PUSHs(sv_2mortal(newSVpvn(pv_key, len_key)));
        PUSHs(sv_2mortal(SvREFCNT_inc(value)));
    }
    SvREFCNT_dec(hv);
    return keys * 2;
}

static SV *
sv_upper(SV *sv) {
    STRLEN len, i;
    char *pv = SvPVbyte(sv, len);
    for (i = 0; i < len; i++) {
        if (isLOWER(pv[i])) {
            sv = sv_2mortal(newSVpvn(pv, len));
            pv = SvPVX(sv);
            for (; i < len; i++)
                pv[i] = toUPPER(pv[i]);
            break;
        }
    }
    return sv;
}

static IV
sv2iv_constant_or_croak(const char *name, SV *sv) {
    if (!SvOK(sv) || SvIOK(sv) || looks_like_number(sv))
        return SvIV(sv);
    else {
        STRLEN len;
        char *pv;
        int type, i;
        IV value;
        sv = sv_upper(sv);
        pv = SvPVbyte(sv, len);
        type = constant(aTHX_ pv, len, &value);
        if (type == PERL_constant_NOTFOUND) {
            sv = sv_2mortal(newSVpvf("LIBSSH2_%s_%s", name, pv));
            pv = SvPVbyte(sv, len);
            type = constant(aTHX_ SvPV_nolen(sv), len, &value);
        }
        if (type == PERL_constant_ISIV)
            return value;

        croak("Invalid constant of type LIBSSH2_%s (%s)", name, pv);
    }
}

/* create a hash from an SFTP attributes structure */
static HV*
hv_from_attrs(LIBSSH2_SFTP_ATTRIBUTES* attrs) {
    HV* hv = newHV();
    debug("hv_from_attrs: attrs->flags = %d\n", attrs->flags);
    if (attrs->flags & LIBSSH2_SFTP_ATTR_SIZE)
        hv_store(hv, "size", 4, newSVuv(attrs->filesize), 0/*hash*/);
    if (attrs->flags & LIBSSH2_SFTP_ATTR_UIDGID) {
        hv_store(hv, "uid", 3, newSVuv(attrs->uid), 0/*hash*/);
        hv_store(hv, "gid", 3, newSVuv(attrs->gid), 0/*hash*/);
    }
    if (attrs->flags & LIBSSH2_SFTP_ATTR_PERMISSIONS)
        hv_store(hv, "mode", 4, newSVuv(attrs->permissions), 0/*hash*/);
    if (attrs->flags & LIBSSH2_SFTP_ATTR_ACMODTIME) {
        hv_store(hv, "atime", 5, newSVuv(attrs->atime), 0/*hash*/);
        hv_store(hv, "mtime", 5, newSVuv(attrs->mtime), 0/*hash*/);
    }
    return hv;
}

/* return attributes from function, as flat hash or hashref */
#define XSRETURN_STAT_ATTRS(name) XSRETURN(return_stat_attrs(sp, &attrs, name))
    
static int return_stat_attrs(SV** sp, LIBSSH2_SFTP_ATTRIBUTES* attrs,
 SV* name) {
    HV* hv_attrs = hv_from_attrs(attrs);
    if (name)
        hv_store(hv_attrs, "name", 4, name, 0/*hash*/);

    switch (GIMME_V) {
    case G_SCALAR:
        PUSHs(sv_2mortal(newRV_noinc((SV*)hv_attrs)));
        return 1;
    case G_ARRAY:
        return push_hv(sp, hv_attrs);
    default:
        SvREFCNT_dec(hv_attrs);
    }
    return 0;
}

/* general wrapper */
#define NEW_ITEM(type, field, create, parent) do { \
    Newz(0/*id*/, RETVAL, 1, type); \
    if (RETVAL) { \
        RETVAL->parent = parent; \
        RETVAL->sv_##parent = SvREFCNT_inc(SvRV(ST(0))); \
        RETVAL->field = create; \
        debug(#create " -> 0x%p\n", RETVAL->field); \
    } \
    if (!RETVAL || !RETVAL->field) { \
        if (RETVAL) \
            SvREFCNT_dec(RETVAL->sv_##parent); \
        Safefree(RETVAL); \
        XSRETURN_EMPTY; \
    } \
} while(0)

/* wrap a libSSH2 channel */
#define NEW_CHANNEL(create) NEW_ITEM(SSH2_CHANNEL, channel, create, ss)

/* wrap a libSSH2 listener */
#define NEW_LISTENER(create) NEW_ITEM(SSH2_LISTENER, listener, create, ss)

/* wrap a libSSH2 SFTP connection */
#define NEW_SFTP(create) NEW_ITEM(SSH2_SFTP, sftp, create, ss)

/* wrap a libSSH2 SFTP file */
#define NEW_FILE(create) NEW_ITEM(SSH2_FILE, handle, create, sf)

/* wrap a libSSH2 SFTP directory */
#define NEW_DIR(create) NEW_ITEM(SSH2_DIR, handle, create, sf)

/* wrap a libSSH2 public key object */
#define NEW_PUBLICKEY(create) NEW_ITEM(SSH2_PUBLICKEY, pkey, create, ss)

/* wrap a libSSH2 knownhosts object */
#define NEW_KNOWNHOSTS(create) NEW_ITEM(SSH2_KNOWNHOSTS, knownhosts, create, ss)

static void
set_cb_args(pTHX_ AV* data) {
    GV *gv = gv_fetchpv("Net::SSH2::_cb_args", 1, SVt_PV);
    SV *sv = save_scalar(gv);
    sv_setsv(sv, sv_2mortal(newRV_inc((SV*)data)));
}

static SV*
get_cb_arg(pTHX_ I32 ix) {
    SV *sv = get_sv("Net::SSH2::_cb_args", 1);
    if (SvROK(sv)) {
        AV *data = (AV*)SvRV(sv);
        if (SvTYPE(data) == SVt_PVAV) {
            SV **svp = av_fetch(data, ix, 0);
            if (svp && *svp)
                return *svp;
            Perl_croak(aTHX_ "internal error: unable to fetch callback data slot %d", ix);
        }
    }
    Perl_croak(aTHX_ "internal error: unexpected structure found for callback data");
}

/* callback for returning a password via "keyboard-interactive" auth */
static LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC(cb_kbdint_response_password) {
    if (num_prompts != 1 || prompts[0].echo) {
        int i;
        for (i = 0; i < num_prompts; ++i) {
            responses[i].text = NULL;
            responses[i].length = 0;
        }
    }
    else {
        /* single prompt, no echo: assume it's a password request */
        dTHX;
        SV *password = get_cb_arg(aTHX_ 0);
        STRLEN len_password;
        const char* pv_password = SvPVbyte(password, len_password);

        responses[0].text = savepvn(pv_password, len_password);
        responses[0].length = len_password;
    }
}

/* thunk to call perl input-reading function for "keyboard-interactive" auth */
static LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC(cb_kbdint_response_callback) {
    dTHX; dSP;
    int count, i;
    SV *cb = get_cb_arg(aTHX_ 0);
    SV *self = get_cb_arg(aTHX_ 1);
    SV *username = get_cb_arg(aTHX_ 2);

    ENTER;
    SAVETMPS;
    PUSHMARK(SP);
    EXTEND(SP, 4 + num_prompts);
    PUSHs(self);
    PUSHs(username);
    PUSHs(sv_2mortal(newSVpvn(name, name_len)));
    PUSHs(sv_2mortal(newSVpvn(instruction, instruction_len)));
    for (i = 0; i < num_prompts; ++i) {
        HV* hv = newHV();
        /* Perl_warn(aTHX_ "prompt %d: text: %p, length: %d, echo: %d\n", */
        /* i, prompts[i].text, prompts[i].length, prompts[i].echo); */
        PUSHs(sv_2mortal(newRV_noinc((SV*)hv)));
        hv_store(hv, "text", 4, newSVpvn(prompts[i].text, prompts[i].length), 0);
        hv_store(hv, "echo", 4, newSVuv(prompts[i].echo), 0);
        responses[i].text = NULL;
        responses[i].length = 0;
    }
    PUTBACK;
    count = call_sv(cb, G_ARRAY);
    SPAGAIN;
    if (count > num_prompts) {
        Perl_warn(aTHX_ "Too many responses from callback, %d expected but %d found!",
                  num_prompts, count);
        while (count-- > num_prompts)
            POPs;
    }
    while (count-- > 0) {
        STRLEN len_response;
        SV *sv = POPs;
        char *pv_response = SvPVbyte(sv, len_response);
        responses[count].text = savepvn(pv_response, len_response);
        responses[count].length = len_response;
    }
    PUTBACK;
    FREETMPS;
    LEAVE;
}

/* thunk to call perl password change function for "password" auth */
static LIBSSH2_PASSWD_CHANGEREQ_FUNC(cb_password_change_callback) {
    dTHX; dSP;
    int count;
    SV *cb = get_cb_arg(aTHX_ 0);
    SV *self = get_cb_arg(aTHX_ 1);
    SV *username = get_cb_arg(aTHX_ 2);

    ENTER;
    SAVETMPS;
    PUSHMARK(SP);
    XPUSHs(self);
    XPUSHs(username);
    PUTBACK;
    count = call_sv(cb, G_SCALAR);
    SPAGAIN;
    if (count > 0) {
        STRLEN len_password;
        const char* pv_password = SvPVbyte(POPs, len_password);
        *newpw = savepvn(pv_password, len_password);
        *newpw_len = len_password;
    }
    else {
        *newpw = NULL;
        *newpw_len = 0;
    }
    PUTBACK;
    FREETMPS;
    LEAVE;
}

/* thunk to call perl SSH_MSG_IGNORE packet function */
static LIBSSH2_IGNORE_FUNC(cb_ignore_callback) {
    SSH2* ss = (SSH2*)*abstract;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(sv_2mortal(newRV_inc(ss->sv_ss)));
    mXPUSHp(message, message_len);
    PUTBACK;

    count = call_sv(ss->rgsv_cb[LIBSSH2_CALLBACK_IGNORE], G_VOID);
    SPAGAIN; SP -= count; ax = (SP - PL_stack_base) + 1;

    PUTBACK; FREETMPS; LEAVE;
}

/* thunk to call perl SSH_MSG_DEBUG packet function */
static LIBSSH2_DEBUG_FUNC(cb_debug_callback) {
    SSH2* ss = (SSH2*)*abstract;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(sv_2mortal(newRV_inc(ss->sv_ss)));
    mXPUSHi(always_display);
    mXPUSHp(message, message_len);
    mXPUSHp(language, language_len);
    PUTBACK;

    count = call_sv(ss->rgsv_cb[LIBSSH2_CALLBACK_DEBUG], G_VOID);
    SPAGAIN; SP -= count; ax = (SP - PL_stack_base) + 1;

    PUTBACK; FREETMPS; LEAVE;
}

/* thunk to call perl SSH_MSG_DISCONNECT packet function */
static LIBSSH2_DISCONNECT_FUNC(cb_disconnect_callback) {
    SSH2* ss = (SSH2*)*abstract;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(sv_2mortal(newRV_inc(ss->sv_ss)));
    mXPUSHi(reason);
    mXPUSHp(message, message_len);
    mXPUSHp(language, language_len);
    PUTBACK;

    count = call_sv(ss->rgsv_cb[LIBSSH2_CALLBACK_DISCONNECT], G_VOID);
    SPAGAIN; SP -= count; ax = (SP - PL_stack_base) + 1;

    PUTBACK; FREETMPS; LEAVE;
}

/* thunk to call perl SSH_MSG_MACERROR packet function */
static LIBSSH2_MACERROR_FUNC(cb_macerror_callback) {
    SSH2* ss = (SSH2*)*abstract;
    int ret = 0;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(sv_2mortal(newRV_inc(ss->sv_ss)));
    mXPUSHp(packet, packet_len);
    PUTBACK;

    count = call_sv(ss->rgsv_cb[LIBSSH2_CALLBACK_MACERROR], G_SCALAR);
    SPAGAIN; SP -= count ; ax = (SP - PL_stack_base) + 1;

    if (count > 0)
        ret = SvIV(ST(0));
 
    PUTBACK; FREETMPS; LEAVE;
    return ret;
}

/* thunk to call perl X11 forwarder packet function */
static LIBSSH2_X11_OPEN_FUNC(cb_x11_open_callback) {
    SSH2* ss = (SSH2*)*abstract;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(sv_2mortal(newRV_inc(ss->sv_ss)));
    /*TODO: we actually need to push a channel here, but we don't know the
     *      SV of the channel (use a local hash?) */
    XPUSHs(&PL_sv_undef);
    mXPUSHp(shost, strlen(shost));
    mXPUSHi(sport);
    PUTBACK;

    count = call_sv(ss->rgsv_cb[LIBSSH2_CALLBACK_X11], G_VOID);
    SPAGAIN; SP -= count ; ax = (SP - PL_stack_base) + 1 ;

    PUTBACK; FREETMPS; LEAVE;
}

void * cb_as_void_ptr(void (*cb)()) {
    void * addr;
    memcpy(&addr, &cb, sizeof addr);
    return addr;
}

static void (*msg_cb[])() = {
    (void (*)())cb_ignore_callback,
    (void (*)())cb_debug_callback,
    (void (*)())cb_disconnect_callback,
    (void (*)())cb_macerror_callback,
    (void (*)())cb_x11_open_callback
};

#define MY_CXT_KEY "Net::SSH2::_guts" XS_VERSION

#ifdef HAVE_GCRYPT
#ifndef WIN32
GCRY_THREAD_OPTION_PTHREAD_IMPL;
#else
GCRY_THREAD_OPTION_PTH_IMPL;
#endif
#endif

typedef struct {
    HV* global_cb_data;
    UV tid;
} my_cxt_t;

START_MY_CXT

static UV get_my_thread_id(void) /* returns threads->tid() value */
{
    dSP;
    UV tid = 0;
    int count = 0;

#ifdef USE_ITHREADS
    ENTER;
    SAVETMPS;
    PUSHMARK(SP);
    XPUSHs(sv_2mortal(newSVpv("threads", 0)));
    PUTBACK;
    count = call_method("tid", G_SCALAR|G_EVAL);
    SPAGAIN;
    if (SvTRUE(ERRSV) || count != 1)
       /* if threads not loaded or an error occurs return 0 */
       tid = 0;
    else
       tid = (UV)POPi;
    PUTBACK;
    FREETMPS;
    LEAVE;
#endif

    return tid;
}

#if defined(USE_ITHREADS) && !defined(HAVE_GCRYPT)
/* IMPORTANT NOTE:
 * openssl locking was implemented according to http://www.openssl.org/docs/crypto/threads.html
 * we implement both static and dynamic locking as described on URL above
 * locking is supported when OPENSSL_THREADS macro is defined which means openssl-0.9.7 or newer
 * we intentionally do not implement cleanup of openssl's threading as it causes troubles
 * with apache-mpm-worker+mod_perl+mod_ssl+net-ssleay
 */

static perl_mutex *GLOBAL_openssl_mutex = NULL;

static void openssl_locking_function(int mode, int type, const char *file, int line)
{
    if (!GLOBAL_openssl_mutex) return;
    if (mode & CRYPTO_LOCK)
      MUTEX_LOCK(&GLOBAL_openssl_mutex[type]);
    else
      MUTEX_UNLOCK(&GLOBAL_openssl_mutex[type]);
}

#if OPENSSL_VERSION_NUMBER < 0x10000000L
static unsigned long openssl_threadid_func(void)
{
    dMY_CXT;
    return (unsigned long)(MY_CXT.tid);
}
#else
static void openssl_threadid_func(CRYPTO_THREADID *id)
{
    dMY_CXT;
    CRYPTO_THREADID_set_numeric(id, (unsigned long)(MY_CXT.tid));
}
#endif

struct CRYPTO_dynlock_value
{
    perl_mutex mutex;
};

static struct CRYPTO_dynlock_value *openssl_dynlocking_create_function(const char *file, int line)
{
    struct CRYPTO_dynlock_value *retval;
    New(0, retval, 1, struct CRYPTO_dynlock_value);
    if (!retval) return NULL;
    MUTEX_INIT(&retval->mutex);
    return retval;
}

static void openssl_dynlocking_lock_function(int mode, struct CRYPTO_dynlock_value *l, const char *file, int line)
{
    if (!l) return;
    if (mode & CRYPTO_LOCK)
      MUTEX_LOCK(&l->mutex);
    else
      MUTEX_UNLOCK(&l->mutex);
}

static void openssl_dynlocking_destroy_function(struct CRYPTO_dynlock_value *l, const char *file, int line)
{
    if (!l) return;
    MUTEX_DESTROY(&l->mutex);
    Safefree(l);
}

static void openssl_threads_init(void)
{
    int i;

    /* initialize static locking */
    if ( !CRYPTO_get_locking_callback() ) {
#if OPENSSL_VERSION_NUMBER < 0x10000000L
        if ( !CRYPTO_get_id_callback() ) {
#else
        if ( !CRYPTO_THREADID_get_callback() ) {
#endif
            New(0, GLOBAL_openssl_mutex, CRYPTO_num_locks(), perl_mutex);
            if (!GLOBAL_openssl_mutex) return;
            for (i=0; i<CRYPTO_num_locks(); i++) MUTEX_INIT(&GLOBAL_openssl_mutex[i]);
            CRYPTO_set_locking_callback(openssl_locking_function);

#ifndef WIN32
            /* no need for threadid_func() on Win32 */
#if OPENSSL_VERSION_NUMBER < 0x10000000L
            CRYPTO_set_id_callback(openssl_threadid_func);
#else
            CRYPTO_THREADID_set_callback(openssl_threadid_func);
#endif
#endif
        }
    }

    /* initialize dynamic locking */
    if ( !CRYPTO_get_dynlock_create_callback() &&
         !CRYPTO_get_dynlock_lock_callback() &&
         !CRYPTO_get_dynlock_destroy_callback() ) {
        CRYPTO_set_dynlock_create_callback(openssl_dynlocking_create_function);
        CRYPTO_set_dynlock_lock_callback(openssl_dynlocking_lock_function);
        CRYPTO_set_dynlock_destroy_callback(openssl_dynlocking_destroy_function);
    }
}

#else

/* no threads */
static void openssl_threads_init(void)
{
}

#endif

static void
croak_last_error(SSH2 *ss, const char *class, const char *method) {
    char *errmsg = NULL;
    int err = libssh2_session_last_error(ss->session, &errmsg, NULL, 0);
    croak("%s::%s: %s (%d)", class, method, errmsg, err);
}

#define CROAK_LAST_ERROR(session, method) (croak_last_error((session), class, (method)))

#if LIBSSH2_VERSION_NUM < 0x010601
#define libssh2_session_set_last_error(ss, errcode, errmsg)   0
#endif

static void
save_eagain(LIBSSH2_SESSION *session, int error) {
    if (error == LIBSSH2_ERROR_EAGAIN)
        libssh2_session_set_last_error(session, LIBSSH2_ERROR_EAGAIN, "Operation would block");
}

/* perl module exports */

MODULE = Net::SSH2		PACKAGE = Net::SSH2		PREFIX = net_ss_
PROTOTYPES: DISABLE

INCLUDE: const-xs.inc

BOOT:
{
    MY_CXT_INIT;
#ifdef HAVE_GCRYPT
    gcry_error_t ret;
#ifndef WIN32
    ret = gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread);
#else
    ret = gcry_control(GCRYCTL_SET_THREAD_CBS, &gcry_threads_pth);
#endif
    if (gcry_err_code(ret) != GPG_ERR_NO_ERROR)
        croak("could not initialize libgcrypt for threads (%d: %s/%s)",
         gcry_err_code(ret),
         gcry_strsource(ret),
         gcry_strerror(ret));

    if (!gcry_check_version(GCRYPT_VERSION))
        croak("libgcrypt version mismatch (needed: %s)", GCRYPT_VERSION);
#else /* OpenSSL */
    openssl_threads_init();
    MY_CXT.global_cb_data = newHV();
    MY_CXT.tid = get_my_thread_id();
    debug("Net::SSH2::BOOT: tid=%d my_perl=0x%p\n", MY_CXT.tid, my_perl);
#endif
}

#define class "Net::SSH2"

void
CLONE(...)
CODE:
    MY_CXT_CLONE;
    MY_CXT.global_cb_data = newHV();
    MY_CXT.tid = get_my_thread_id();
    debug("%s::CLONE: tid=%d my_perl=0x%p\n", class, MY_CXT.tid, my_perl);

IV
_parse_constant(char *prefix, SV *value)
CODE:
    RETVAL = sv2iv_constant_or_croak(prefix, value);
OUTPUT:
    RETVAL

SSH2*
net_ss__new(SV* proto)
CODE:
    Newz(0/*id*/, RETVAL, 1, SSH2);
    if (RETVAL) {
        RETVAL->session = libssh2_session_init_ex(
         local_alloc, local_free, local_realloc, RETVAL);
    }
    if (!RETVAL || !RETVAL->session) {
        Safefree(RETVAL);
        XSRETURN_EMPTY;
    }
    debug("Net::SSH2: created new object 0x%x\n", RETVAL);
OUTPUT:
    RETVAL

void
net_ss_trace(SSH2* ss, IV bitmask)
CODE:
    libssh2_trace(ss->session, bitmask);

#if LIBSSH2_VERSION_MAJOR >= 1

IV
net_ss_block_directions(SSH2* ss)
CODE:
    RETVAL = libssh2_session_block_directions(ss->session);
OUTPUT:
    RETVAL

#else

void
net_ss_block_directions(SSH2* ss)
CODE:
    croak("libssh2 version 1.0 or higher required for block_directions support");

#endif

#if LIBSSH2_VERSION_NUM >= 0x010209

SV *
net_ss_timeout(SSH2* ss, SV *timeout = &PL_sv_undef)
PREINIT:
    long r;
CODE:
    if (items > 1)
        libssh2_session_set_timeout(ss->session,
                                    (SvOK(timeout) ? SvUV(timeout) : 0));
    r = libssh2_session_get_timeout(ss->session);
    RETVAL = (r > 0 ? newSVuv(r) : &PL_sv_undef);
OUTPUT:
    RETVAL

#else

void
net_ss_timeout(SSH2* ss, long timeout)
CODE:
    croak("libssh2 version 1.2.9 or higher required for set_timeout support");

#endif

SSH2_BOOL
net_ss_blocking(SSH2* ss, SSH2_BOOL blocking = 0)
CODE:
    if (items > 1)
        libssh2_session_set_blocking(ss->session, blocking);
    RETVAL = libssh2_session_get_blocking(ss->session);
OUTPUT:
    RETVAL

void
net_ss_DESTROY(SSH2* ss)
CODE:
    debug("%s::DESTROY object 0x%x\n", class, ss);
    libssh2_session_free(ss->session);
    if (ss->socket)
        SvREFCNT_dec(ss->socket);
    if (ss->hostname)
        SvREFCNT_dec(ss->hostname);
    Safefree(ss);

void
net_ss_debug(SV*, IV debug)
CODE:
    net_ss_debug_out = debug & 1;  /* allow for future flags */

void
net_ss_version(...)
PPCODE:
    EXTEND(SP, 3);
    ST(0) = sv_2mortal(newSVpv(LIBSSH2_VERSION, 0));
    if (GIMME_V != G_ARRAY)
        XSRETURN(1);
#ifdef LIBSSH2_VERSION_NUM
    ST(1) = sv_2mortal(newSVuv(LIBSSH2_VERSION_NUM));
#else
    ST(1) = &PL_sv_undef;
#endif
    ST(2) = sv_2mortal(newSVpv(LIBSSH2_SSH_DEFAULT_BANNER, 0));
    XSRETURN(3);

SSH2_NERROR
net_ss_banner(SSH2* ss, SSH2_CHARP banner)
PREINIT:
    SV* full_banner;
CODE:
    full_banner = sv_2mortal(newSVpvf("SSH-2.0-%s", banner));
    RETVAL = libssh2_banner_set(ss->session, SvPVbyte_nolen(full_banner));
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_ERROR
net_ss_error(SSH2* ss)
PREINIT:
    char* errstr;
    int errlen;
CODE:
    RETVAL = libssh2_session_last_error(ss->session, &errstr, &errlen, 0);
    if(GIMME_V == G_ARRAY) {
        SV *errcode_sv;
        if (RETVAL == LIBSSH2_ERROR_NONE)
            XSRETURN_EMPTY;
        EXTEND(SP, 3);
        ST(0) = sv_2mortal(newSViv(RETVAL));
        if ((-RETVAL > 0) && (-RETVAL < countof(xs_libssh2_error)))
            errcode_sv = newSVpvf("LIBSSH2_ERROR_%s", xs_libssh2_error[-RETVAL]);
        else
            errcode_sv = newSVpvf("LIBSSH2_ERROR_UNKNOWN(%d)", RETVAL);
        ST(1) = sv_2mortal(errcode_sv);
        ST(2) = (errstr ? sv_2mortal(newSVpvn(errstr, errlen)) : &PL_sv_undef);
        XSRETURN(3);
    }
OUTPUT:
    RETVAL

void
net_ss__set_error(SSH2 *ss, int errcode = 0, SSH2_CHARP_OR_NULL errmsg = NULL)
CODE:
    libssh2_session_set_last_error(ss->session, errcode, errmsg);

SSH2_NERROR
net_ss__method(SSH2* ss, SSH2_METHOD type, SSH2_CHARP_OR_NULL prefs = NULL)
CODE:
    /* if there are no other parameters, return the current value */
    if (items == 2) {
        const char *method = libssh2_session_methods(ss->session, (int)type);
        if (!method)
            XSRETURN_EMPTY;
        XSRETURN_PV(method);
    }
    RETVAL = libssh2_session_method_pref(ss->session,
                                         (int)type, prefs);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010200

SSH2_NERROR
net_ss_flag(SSH2* ss, SSH2_FLAG flag, int value)
CODE:
    RETVAL = libssh2_session_flag(ss->session, (int)flag, value);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

#else

void
net_ss_flag(SSH2* ss, SV* flag, int value)
CODE:
    croak("libssh2 version 1.2 or higher required for flag support");

#endif

SSH2_RC
net_ss_callback(SSH2* ss, SSH2_CALLBACK type, SV* callback = NULL)
CODE:
    if (callback && !SvOK(callback))
        callback = NULL;
    if (callback && !(SvROK(callback) && SvTYPE(SvRV(callback)) == SVt_PVCV))
        croak("%s::callback: callback must be CODE ref", class);
    if (type < 0 || type >= countof(msg_cb))
        croak("%s::callback: don't know how to handle: %s",
              class, SvPVbyte_nolen(callback));

    ss->sv_ss = SvRV(ST(0));  /* don't keep a reference, just store it */
    SvREFCNT_dec(ss->rgsv_cb[type]);
    libssh2_session_callback_set(ss->session,
     type, callback ? cb_as_void_ptr(msg_cb[type]) : NULL);
    SvREFCNT_inc(callback);
    ss->rgsv_cb[type] = callback;
    RETVAL = 1;
OUTPUT:
    RETVAL

SSH2_NERROR
net_ss__startup(SSH2* ss, int fd, SV *socket, SV* hostname, int port)
CODE:
    RETVAL = libssh2_session_startup(ss->session, fd);
    if ((RETVAL >= 0) && SvOK(socket)) {
        if (ss->socket)
            sv_2mortal(ss->socket);
        ss->socket = newSVsv(socket);
        ss->hostname = newSVsv(hostname);
        ss->port = port;
    }
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

SV *
net_ss_hostname(SSH2* ss)
CODE:
    RETVAL = (ss->hostname ? newSVsv(ss->hostname) : &PL_sv_undef);
OUTPUT:
    RETVAL

int
net_ss_port(SSH2* ss)
CODE:
    RETVAL = ss->port;
 OUTPUT:
    RETVAL

SV *
net_ss_sock(SSH2* ss)
CODE:
    RETVAL = (ss->socket ? newSVsv(ss->socket) : &PL_sv_undef);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ss_disconnect(SSH2* ss, SSH2_CHARP description = "",       \
                  int reason = SSH_DISCONNECT_BY_APPLICATION, SSH2_CHARP lang = "")
CODE:
    RETVAL = libssh2_session_disconnect_ex(ss->session, reason, description, lang);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

void
net_ss_hostkey_hash(SSH2* ss, SSH2_HOSTKEY_HASH type)
PREINIT:
    const char* hash;
    static STRLEN rglen[] = { 16/*MD5*/, 20/*SHA1*/ };
PPCODE:
    if (type < 1 || type > countof(rglen)) {
        croak("%s::hostkey: unknown hostkey hash: %d",
              class, (int)type);
    }
    if ((hash = (const char*)libssh2_hostkey_hash(ss->session, type))) {
        PUSHs(sv_2mortal(newSVpvn(hash, rglen[type-1])));
        XSRETURN(1);
    }
    XSRETURN_EMPTY;

void
net_ss_remote_hostkey(SSH2* ss)
PREINIT:
    const char *key_pv;
    size_t key_len;
    int type_int;
PPCODE:
    if ((key_pv = libssh2_session_hostkey(ss->session, &key_len, &type_int))) {
        XPUSHs(sv_2mortal(newSVpvn(key_pv, key_len)));
        if (GIMME_V != G_ARRAY)
            XSRETURN(1);
        else {
            XPUSHs(sv_2mortal(newSViv(type_int)));
            XSRETURN(2);
        }
    }
    else
        XSRETURN_EMPTY;

SSH2_CHARP_OR_NULL
net_ss__auth_list(SSH2* ss, SV *username = &PL_sv_undef)
PREINIT:
    const char* pv_username = NULL;
    STRLEN len_username = 0;
CODE:
    if (SvOK(username))
        pv_username = SvPVbyte(username, len_username);
    RETVAL = libssh2_userauth_list(ss->session, pv_username, len_username);
OUTPUT:
    RETVAL

SSH2_RC
net_ss_auth_ok(SSH2* ss)
CODE:
    RETVAL = libssh2_userauth_authenticated(ss->session);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ss_auth_password(SSH2* ss,                                  \
                     SV* username, SV* password = &PL_sv_undef, \
                     SV* callback = &PL_sv_undef)
PREINIT:
    STRLEN len_username, len_password;
    const char *pv_username, *pv_password;
    int i, ok;
CODE:
    pv_username = SvPVbyte(username, len_username);

    /* if we don't have a password, try for an unauthenticated login */
    if (!SvPOK(password)) {
        /* That's how libssh2 tells you authentication 'none' is valid */
        RETVAL = (((libssh2_userauth_list(ss->session, pv_username, len_username) == NULL) &&
                   libssh2_userauth_authenticated(ss->session)) ? 0 : -1);
    }
    else {
        if (SvOK(callback)) {
            if (!(SvROK(callback) && SvTYPE(SvRV(callback)) == SVt_PVCV))
                Perl_croak(aTHX_ "%s::auth_password: callback must be CODE ref", class);
            else {
                AV *cb_args = (AV*)sv_2mortal((SV*)newAV());
                av_push(cb_args, newSVsv(callback));
                av_push(cb_args, newSVsv(ST(0))); /*session */
                av_push(cb_args, newSVsv(username));
                set_cb_args(aTHX_ cb_args);
            }
        }

        pv_password = SvPVbyte(password, len_password);
        RETVAL = libssh2_userauth_password_ex(ss->session,
                                              pv_username, len_username,
                                              pv_password, len_password,
                                              (SvOK(callback) ? cb_password_change_callback : NULL));
        save_eagain(ss->session, RETVAL);
    }
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010203

SV *
net_ss_auth_agent(SSH2* ss, SSH2_CHARP username)
PREINIT:
    LIBSSH2_AGENT *agent;
    int old_blocking;
CODE:
    RETVAL = &PL_sv_undef;
    /* unfortunatelly this can't be make to work on nb mode */
    old_blocking = libssh2_session_get_blocking(ss->session);
    libssh2_session_set_blocking(ss->session, 1);
    if ((agent = libssh2_agent_init(ss->session)) != NULL) {
        if (libssh2_agent_connect(agent) == LIBSSH2_ERROR_NONE) {
            if (libssh2_agent_list_identities(agent) == LIBSSH2_ERROR_NONE) {
                struct libssh2_agent_publickey *identity = NULL;
                while (libssh2_agent_get_identity(agent, &identity, identity) == 0) {
                    if (libssh2_agent_userauth(agent, username, identity) == LIBSSH2_ERROR_NONE) {
                        RETVAL = &PL_sv_yes;
                        break;
                    }
                }
            }
        }
        libssh2_agent_free(agent);
    }
    libssh2_session_set_blocking(ss->session, old_blocking);
OUTPUT:
    RETVAL

#else

void
net_ss_auth_agent(SSH2* ss, SV* username)
CODE:
    croak("libssh2 version 1.2.3 or higher required for agent support");

#endif

SSH2_NERROR
net_ss_auth_publickey(SSH2* ss, SV* username, SSH2_CHARP_OR_NULL publickey, \
                      SSH2_CHARP privatekey, SSH2_CHARP_OR_NULL passphrase = NULL);
PREINIT:
    const char* pv_username;
    STRLEN len_username;
CODE:
    pv_username = SvPVbyte(username, len_username);
    RETVAL = libssh2_userauth_publickey_fromfile_ex(ss->session,
                                                    pv_username, len_username,
                                                    publickey, privatekey,
                                                    passphrase);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010600

SSH2_NERROR
net_ss_auth_publickey_frommemory(SSH2* ss, SV* username, SV* publickey, \
                                 SV* privatekey, SSH2_CHARP_OR_NULL passphrase = NULL)
PREINIT:
    const char *pv_username, *pv_publickey, *pv_privatekey;
    STRLEN len_username, len_publickey, len_privatekey;
CODE:
    pv_username = SvPVbyte(username, len_username);
    pv_publickey = SvPVbyte(publickey, len_publickey);
    pv_privatekey = SvPVbyte(privatekey, len_privatekey);

    RETVAL = libssh2_userauth_publickey_frommemory(ss->session,
                                                   pv_username, len_username, pv_publickey, len_publickey,
                                                   pv_privatekey, len_privatekey,
                                                   passphrase);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

#endif

SSH2_NERROR
net_ss_auth_hostbased(SSH2* ss, SV* username, const char* publickey, \
                      const char* privatekey, SV* hostname,          \
                      SV* local_username = &PL_sv_undef,             \
                      SSH2_CHARP_OR_NULL passphrase = NULL)
PREINIT:
    const char* pv_username, * pv_hostname, * pv_local_username;
    STRLEN len_username, len_hostname, len_local_username;
CODE:
    pv_username = SvPVbyte(username, len_username);
    pv_hostname = SvPVbyte(hostname, len_hostname);

    if (SvPOK(local_username)) {
        pv_local_username = SvPVbyte(local_username, len_local_username);
    }
    else {
        pv_local_username = pv_username;
        len_local_username = len_username;
    }
    RETVAL = libssh2_userauth_hostbased_fromfile_ex(ss->session,
                                                    pv_username, len_username, publickey, privatekey,
                                                    passphrase,
                                                    pv_hostname, len_hostname,
                                                    pv_local_username, len_local_username);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ss_auth_keyboard(SSH2* ss, SV* username, SV* password = NULL)
PREINIT:
    const char* pv_username;
    STRLEN len_username;
    AV *cb_args;
CODE:
    pv_username = SvPVbyte(username, len_username);

    /* we either have a password, or a reference to a callback */

    if (!password || !SvOK(password)) {
        password = sv_2mortal(newRV_inc((SV*)get_cv("Net::SSH2::_cb_kbdint_response_default", 1)));
        if (!SvOK(password))
            Perl_croak(aTHX_ "Internal error: unable to retrieve callback");
    }

    cb_args = (AV*)sv_2mortal((SV*)newAV());
    av_push(cb_args, newSVsv(password));
    av_push(cb_args, newSVsv(ST(0))); /*session */
    av_push(cb_args, newSVsv(username));
    set_cb_args(aTHX_ cb_args);

    if (SvROK(password) && (SvTYPE(SvRV(password)) == SVt_PVCV))
        RETVAL = libssh2_userauth_keyboard_interactive_ex(ss->session,
                                                          pv_username, len_username,
                                                          cb_kbdint_response_callback);
    else
        RETVAL = libssh2_userauth_keyboard_interactive_ex(ss->session,
                                                          pv_username, len_username,
                                                          cb_kbdint_response_password);
    save_eagain(ss->session, RETVAL);
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010205

void
net_ss_keepalive_config(SSH2 *ss, int want_reply, unsigned int interval)
CODE:
    libssh2_keepalive_config(ss->session, want_reply, interval);

SSH2_BYTES
net_ss_keepalive_send(SSH2 *ss)
PREINIT:
    int seconds_to_next;
CODE:
    RETVAL = libssh2_keepalive_send(ss->session, &seconds_to_next);
    save_eagain(ss->session, RETVAL);
    if (RETVAL >= 0) RETVAL = seconds_to_next;
OUTPUT:
    RETVAL

#else

void
net_ss_keepalive_config(SSH2 *ss, int want_reply, unsigned int interval)
CODE:
    croak("libssh2 version 1.2.5 or higher required for keepalive_config support");

void
net_ss_keepalive_send(SSH2 *ss)
CODE:
    croak("libssh2 version 1.2.5 or higher required for keepalive_send support");

#endif

SSH2_CHANNEL*
net_ss_channel(SSH2* ss,  SSH2_CHARP_OR_NULL channel_type = NULL,   \
               int window_size = LIBSSH2_CHANNEL_WINDOW_DEFAULT,    \
               int packet_size = LIBSSH2_CHANNEL_PACKET_DEFAULT)
PREINIT:
    static const char mandatory_type[] = "session";
CODE:
    if (channel_type && strcmp(channel_type, mandatory_type))
        Perl_croak(aTHX_ "channel_type must be 'session' ('%s' given)", channel_type);
    NEW_CHANNEL(libssh2_channel_open_ex(ss->session,
        mandatory_type, strlen(mandatory_type), window_size, packet_size,
        NULL/*message*/, 0/*message_len*/));
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x10601

SSH2_CHANNEL*
net_ss__scp_get(SSH2* ss, SSH2_CHARP path, HV* stat)
PREINIT:
    libssh2_struct_stat st;
CODE:
    NEW_CHANNEL(libssh2_scp_recv2(ss->session, path, &st));
    hv_store(stat, "mode",  4, newSVuv(st.st_mode),  0/*hash*/);
    hv_store(stat, "uid",   3, newSVuv(st.st_uid),   0/*hash*/);
    hv_store(stat, "gid",   3, newSVuv(st.st_gid),   0/*hash*/);
#if IVSIZE >= 8
    hv_store(stat, "size",  4, newSVuv(st.st_size),  0/*hash*/);
#else
    hv_store(stat, "size",  4, newSVnv(st.st_size),  0/*hash*/);
#endif
    hv_store(stat, "atime", 5, newSVuv((time_t)st.st_atime), 0/*hash*/);
    hv_store(stat, "mtime", 5, newSVuv((time_t)st.st_mtime), 0/*hash*/);
OUTPUT:
    RETVAL

#else

SSH2_CHANNEL*
net_ss__scp_get(SSH2* ss, SSH2_CHARP path, HV* stat)
PREINIT:
    struct stat st;
CODE:
    NEW_CHANNEL(libssh2_scp_recv(ss->session, path, &st));
    hv_store(stat, "mode",  4, newSVuv(st.st_mode),  0/*hash*/);
    hv_store(stat, "uid",   3, newSVuv(st.st_uid),   0/*hash*/);
    hv_store(stat, "gid",   3, newSVuv(st.st_gid),   0/*hash*/);
    hv_store(stat, "size",  4, newSVuv(st.st_size),  0/*hash*/);
    hv_store(stat, "atime", 5, newSVuv((time_t)st.st_atime), 0/*hash*/);
    hv_store(stat, "mtime", 5, newSVuv((time_t)st.st_mtime), 0/*hash*/);
OUTPUT:
    RETVAL

#endif

#if LIBSSH2_VERSION_NUM >= 0x10206

SSH2_CHANNEL*
net_ss__scp_put(SSH2* ss, SSH2_CHARP path, int mode, SSH2_BYTESU64 size, \
                time_t mtime = 0, time_t atime = 0)
CODE:
    NEW_CHANNEL(libssh2_scp_send64(ss->session,
                                   path, mode, size, mtime, atime));
OUTPUT:
    RETVAL

#else
SSH2_CHANNEL*
net_ss__scp_put(SSH2* ss, SSH2_CHARP path, int mode, size_t size, \
                long mtime = 0, long atime = 0)
CODE:
    NEW_CHANNEL(libssh2_scp_send_ex(ss->session,
                                    path, mode, size, mtime, atime));
OUTPUT:
    RETVAL

#endif

SSH2_CHANNEL*
net_ss_tcpip(SSH2* ss, SSH2_CHARP host, int port, \
             SSH2_CHARP shost = "127.0.0.1", int sport = 22)
CODE:
    NEW_CHANNEL(libssh2_channel_direct_tcpip_ex(ss->session,
                                                (char*)host, port,
                                                (char*)shost, sport));
OUTPUT:
    RETVAL

SSH2_LISTENER*
net_ss_listen(SSH2* ss, int port, const char* host = NULL, \
              SV* bound_port = NULL, int queue_maxsize = 16)
PREINIT:
    int i_bound_port;
CODE:
    if (bound_port && SvOK(bound_port)) {
        if (!SvROK(bound_port) && SvTYPE(SvRV(bound_port)) <= SVt_PVNV)
            croak("%s::listen: bound port must be scalar reference", class);
    } else
        bound_port = NULL;
    NEW_LISTENER(libssh2_channel_forward_listen_ex(ss->session,
     (char*)host, port, bound_port ? &i_bound_port : NULL, queue_maxsize));
    if (RETVAL && bound_port)
        sv_setiv(SvRV(bound_port), i_bound_port);
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010200

SSH2_KNOWNHOSTS*
net_ss_known_hosts(SSH2 *ss)
CODE:
    NEW_KNOWNHOSTS(libssh2_knownhost_init(ss->session));
OUTPUT:
    RETVAL

#else

void
net_ss_known_hosts(SSH2 *ss)
CODE:
    croak("libssh2 version 1.2 or higher required for known_hosts support");

#endif

void
net_ss__poll(SSH2* ss, int timeout, AV* event)
PREINIT:
    LIBSSH2_POLLFD* pollfd;
    int i, count, changed;
CODE:
    count = av_len(event) + 1;
    debug("%s::poll: timeout = %d, array[%d]\n", class, timeout, count);
    if (!count)  /* some architectures return null for malloc(0) */
        XSRETURN_IV(0);

    New(0, pollfd, count, LIBSSH2_POLLFD);
    if (!pollfd)
        Perl_croak(aTHX_ "Out of memory!");

    for (i = 0; i < count; ++i) {
        SV* sv = *av_fetch(event, i, 0/*lval*/), ** handle, ** events;
        HV* hv;

        if (!SvROK(sv) || SvTYPE(SvRV(sv)) != SVt_PVHV)
            croak("%s::poll: array element %d is not hash", class, i);
        hv = (HV*)SvRV(sv);

        if (!(handle = hv_fetch(hv, "handle", 6, 0/*lval*/)) || !*handle)
            croak("%s::poll: array element %d missing handle", class, i);
        if (sv_isobject(*handle)) {
            const char* package = HvNAME(SvSTASH(SvRV(*handle)));
            if (strEQ(package, "Net::SSH2::Channel")) {
                debug("- [%d] = channel\n", i);
                pollfd[i].type = LIBSSH2_POLLFD_CHANNEL;
                pollfd[i].fd.channel =
                 ((SSH2_CHANNEL*)SvIVX(GvSV((GV*)SvRV(*handle))))->channel;
            } else if(strEQ(package, "Net::SSH2::Listener")) {
                debug("- [%d] = listener\n", i);
                pollfd[i].type = LIBSSH2_POLLFD_LISTENER;
                pollfd[i].fd.listener =
                 ((SSH2_LISTENER*)SvIVX(SvRV(*handle)))->listener;
            } else {
                croak("%s::poll: invalid handle object in array (%d): %s",
                 class, i, package);
            }
        } else if(SvIOK(*handle)) {
            pollfd[i].type = LIBSSH2_POLLFD_SOCKET;
            pollfd[i].fd.socket = SvIV(*handle);
            debug("- [%d] = file(%d)\n", i, pollfd[i].fd.socket);
        } else {
            croak("%s::poll: invalid handle in array (%d): %s",
             class, i, SvPVbyte_nolen(*handle));
        }

        events = hv_fetch(hv, "events", 6, 0/*lval*/);
        if (!events || !*events || !SvIOK(*events)) {
            croak("%s::poll: bad or missing event mask in array (%d)",
             class, i);
        }
        pollfd[i].events = SvIV(*events);
        pollfd[i].revents = 0;
        debug("- [%d] events %d\n", i, pollfd[i].events);
    }
        
    changed = libssh2_poll(pollfd, count, timeout);
    debug("- libssh2_poll returned %d\n", changed);

    if (changed < 0)
        count = 0;
    for (i = 0; i < count; ++i) {
        HV* hv = (HV*)SvRV(*av_fetch(event, i, 0/*lval*/));
        hv_store(hv, "revents", 7, newSViv(pollfd[i].revents), 0/*hash*/);
        debug("- [%d] revents %d\n", i, pollfd[i].revents);
    }

    Safefree(pollfd);
    if (changed < 0)
        XSRETURN_EMPTY;
    XSRETURN_IV(changed);

SSH2_SFTP*
net_ss_sftp(SSH2* ss)
CODE:
    NEW_SFTP(libssh2_sftp_init(ss->session));
OUTPUT:
    RETVAL

SSH2_PUBLICKEY*
net_ss_public_key(SSH2* ss)
CODE:
    NEW_PUBLICKEY(libssh2_publickey_init(ss->session));
OUTPUT:
    RETVAL

#undef class


MODULE = Net::SSH2		PACKAGE = Net::SSH2::Channel    PREFIX = net_ch_
PROTOTYPES: DISABLE

#define class "Net::SSH2::Channel"

void
net_ch_DESTROY(SSH2_CHANNEL* ch)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_channel_free(ch->channel);
    SvREFCNT_dec(ch->sv_ss);
    Safefree(ch);

SV *
net_ch_session(SSH2_CHANNEL* ch)
CODE:
    RETVAL = newRV_inc(ch->sv_ss);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ch__setenv(SSH2_CHANNEL* ch, SV *key, SV *value)
PREINIT:
    int i, success = 0;
    const char* pv_key, * pv_value;
    STRLEN len_key, len_value;
CODE:
    pv_key = SvPVbyte(key, len_key);
    pv_value = SvPVbyte(value, len_value);
    RETVAL = libssh2_channel_setenv_ex(ch->channel,
                                       (char*)pv_key, len_key,
                                       (char*)pv_value, len_value);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010208

void
net_ch__exit_signal(SSH2_CHANNEL* ch)
PREINIT:
    char *exitsignal;
    char *errmsg;
    char *langtag;
    size_t exitsignal_len;
    size_t errmsg_len;
    size_t langtag_len;
    int retcount = 1;
PPCODE:
    if (!libssh2_channel_get_exit_signal(ch->channel,
                                         &exitsignal, &exitsignal_len,
                                         &errmsg, &errmsg_len,
                                         &langtag, &langtag_len)) {
        LIBSSH2_SESSION *session = ch->ss->session;
        libssh2_session_set_last_error(session, 0, NULL);
        if (exitsignal) {
            XPUSHs(sv_2mortal(newSVpvn(exitsignal, exitsignal_len)));
            if (GIMME_V == G_ARRAY) {
                XPUSHs(errmsg ? sv_2mortal(newSVpvn(errmsg, errmsg_len)) : &PL_sv_undef);
                XPUSHs(langtag ? sv_2mortal(newSVpvn(langtag, langtag_len)) : &PL_sv_undef);
                retcount = 3;
            }
            libssh2_free(session, exitsignal);
            if (errmsg) libssh2_free(session, errmsg);
            if (langtag) libssh2_free(session, langtag);
        }
        else
            XPUSHs(&PL_sv_no);
        XSRETURN(retcount);
    }
    else
        XSRETURN(0);

#else

void
net_ch__exit_signal(SSH2_CHANNEL* ch)
CODE:
    croak("libssh2 version 1.2.8 or higher required for exit_signal support");

#endif

SSH2_BYTES
net_ch_eof(SSH2_CHANNEL* ch)
CODE:
    RETVAL = libssh2_channel_eof(ch->channel);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ch_send_eof(SSH2_CHANNEL* ch)
CODE:
    RETVAL = libssh2_channel_send_eof(ch->channel);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ch_close(SSH2_CHANNEL* ch)
CODE:
    RETVAL = libssh2_channel_close(ch->channel);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ch__wait_closed(SSH2_CHANNEL* ch)
CODE:
    RETVAL = libssh2_channel_wait_closed(ch->channel);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ch_wait_eof(SSH2_CHANNEL* ch)
CODE:
    RETVAL = libssh2_channel_wait_eof(ch->channel);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

int
net_ch__exit_status(SSH2_CHANNEL* ch)
CODE:
    RETVAL = libssh2_channel_get_exit_status(ch->channel);
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_MAJOR >= 1

SSH2_NERROR
net_ch_pty(SSH2_CHANNEL* ch, SV* terminal, SV* modes = NULL, \
           int width = 0, int height = 0)
PREINIT:
    const char* pv_terminal, * pv_modes = NULL;
    STRLEN len_terminal, len_modes = 0;
    int width_px = LIBSSH2_TERM_WIDTH_PX, height_px = LIBSSH2_TERM_HEIGHT_PX;
CODE:
    pv_terminal = SvPVbyte(terminal, len_terminal);
    if (modes && SvPOK(modes))
        pv_modes = SvPVbyte(modes, len_modes);
    if (!width)
        width = LIBSSH2_TERM_WIDTH;
    else if(width < 0) {
        width_px = -width;
        width = 0;
    }
    if (!height)
        height = LIBSSH2_TERM_HEIGHT;
    else if(height < 0) {
        height_px = -height;
        height = 0;
    }
    RETVAL = libssh2_channel_request_pty_ex(ch->channel,
                                            (char*)pv_terminal, len_terminal,
                                            (char*)pv_modes, len_modes,
                                            width, height, width_px, height_px);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_ch_pty_size(SSH2_CHANNEL* ch, int width = 0, int height = 0)
PREINIT:
    int width_px = LIBSSH2_TERM_WIDTH_PX, height_px = LIBSSH2_TERM_HEIGHT_PX;
CODE:
    if (!width)
        croak("%s::pty_size: required parameter width missing", class);
    else if(width < 0) {
        width_px = -width;
        width = 0;
    }

    if (!height)
        croak("%s::pty_size: required parameter height missing", class);
    else if(height < 0) {
        height_px = -height;
        height = 0;
    }

    RETVAL = libssh2_channel_request_pty_size_ex(ch->channel,
                                                 width, height, width_px, height_px);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

#else

void
net_ch_pty(SSH2_CHANNEL* ch, SV* terminal, SV* modes = NULL, \
 int width = 0, int height = 0)
CODE:
    croak("libssh2 version 1.0 or higher required for PTY support");

void
net_ch_pty_size(SSH2_CHANNEL* ch, int width = 0, int height = 0)
CODE:
    croak("libssh2 version 1.0 or higher required for PTY support");

#endif

SSH2_NERROR
net_ch_process(SSH2_CHANNEL* ch, SV* request, SV* message = NULL)
PREINIT:
    const char* pv_request, * pv_message = NULL;
    STRLEN len_request, len_message = 0;
CODE:
    pv_request = SvPVbyte(request, len_request);
    if (message && SvPOK(message))
        pv_message = SvPVbyte(message, len_message);
    RETVAL = libssh2_channel_process_startup(ch->channel,
                                             pv_request, len_request,
                                             pv_message, len_message);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

int
net_ch_ext_data(SSH2_CHANNEL* ch, SSH2_CHANNEL_EXTENDED_DATA mode)
CODE:
    libssh2_channel_handle_extended_data(ch->channel, (int)mode);
    RETVAL = 1;
OUTPUT:
    RETVAL

SSH2_BYTES64
net_ch_read(SSH2_CHANNEL* ch, SV* buffer, size_t size = 32768, SSH2_STREAM_ID ext = 0)
PREINIT:
    char* pv_buffer;
    STRLEN len_buffer;
    int blocking, count = 0;
    size_t total = 0;
CODE:
    debug("%s::read(size = %d, ext = %d)\n", class, size, ext);
    sv_force_normal(buffer);
    sv_setpvn_mg(buffer, "", 0);
    SvPVbyte_force(buffer, len_buffer);
    pv_buffer = sv_grow(buffer, size + 1);
    blocking = libssh2_session_get_blocking(ch->ss->session);
    while (size) {
        count = libssh2_channel_read_ex(ch->channel, ext, pv_buffer, size);
        debug("- read %d bytes\n", count);
        if (count > 0) {
            total += count;
            pv_buffer += count;
            size -= count;
            if (blocking) break;
        }
        else {
            if ((count != LIBSSH2_ERROR_EAGAIN) || !blocking) break;
        }
    }
    debug("- read %d total\n", total);
    if (total || (count == 0)) {
        pv_buffer[0] = '\0';
        SvPOK_only(buffer);
        SvCUR_set(buffer, total);
        SvSETMAGIC(buffer);
        RETVAL = total;
    }
    else {
        SvOK_off(buffer);
        SvSETMAGIC(buffer);
        save_eagain(ch->ss->session, count);
        RETVAL = count;
    }
OUTPUT:
    RETVAL

SV *
net_ch_getc(SSH2_CHANNEL* ch, SSH2_STREAM_ID ext = 0)
PREINIT:
    char buffer[2];
    int count;
CODE:
    debug("%s::getc(ext = %d)\n", class, ext);
    count = libssh2_channel_read_ex(ch->channel, ext, buffer, 1);
    if (count >= 0) {
        buffer[count] = '\0';
        RETVAL = newSVpvn(buffer, count);
    }
    else {
        save_eagain(ch->ss->session, count);
        RETVAL = &PL_sv_undef;
    }
OUTPUT:
    RETVAL

SSH2_BYTES
net_ch_write(SSH2_CHANNEL* ch, SV* buffer, SSH2_STREAM_ID ext = 0)
PREINIT:
    const char* pv_buffer;
    STRLEN len_buffer, offset = 0;
    int count = 0;
CODE:
    /*
       1. in blocking mode, write all the data.
       2. in non-blocking mode, write as much data as possible without
          blocking.
       3. if some error happens...
          a. if some data was already written, discard the error and
             report the number of bytes written.
          b. if no data was written, report the error.
    */
    pv_buffer = SvPVbyte(buffer, len_buffer);
    while (offset < len_buffer) {
        count = libssh2_channel_write_ex(ch->channel, ext,
                                         pv_buffer + offset,
                                         len_buffer - offset);
        if (count >= 0)
            offset += count;
        else if ((count != LIBSSH2_ERROR_EAGAIN) ||
                 !libssh2_session_get_blocking(ch->ss->session))
            break;
    }
    if (offset || (count == 0)) /* yes, zero is a valid value */
        RETVAL = offset;
    else {
        save_eagain(ch->ss->session, count);
        RETVAL = -1;
    }
OUTPUT:
    RETVAL

#if LIBSSH2_VERSION_NUM >= 0x010100

SSH2_BYTES
net_ch_receive_window_adjust(SSH2_CHANNEL *ch, unsigned long adjustment, SV *force = &PL_sv_undef)
PREINIT:
    unsigned int bytes;
CODE:
    RETVAL = libssh2_channel_receive_window_adjust2(ch->channel, adjustment,
                                                    SvTRUE(force), &bytes);
    if (RETVAL)
        save_eagain(ch->ss->session, RETVAL);
    else
        RETVAL = bytes;
OUTPUT:
    RETVAL

#else

void
net_ch_receive_window_adjust(SSH2_CHANNEL* ch, ...)
CODE:
    croak("libssh2 version 1.1 or higher required for receive_window_adjust support");

#endif

#if LIBSSH2_VERSION_NUM >= 0x010200

void
net_ch_window_write(SSH2_CHANNEL* ch)
PREINIT:
    unsigned long window_size_initial = 0;
PPCODE:
    XPUSHs(sv_2mortal(newSVuv(libssh2_channel_window_write_ex(ch->channel,
                                                              &window_size_initial))));
    if (GIMME_V == G_ARRAY) {
        XPUSHs(sv_2mortal(newSVuv(window_size_initial)));
        XSRETURN(2);
    }
    else
        XSRETURN(1);

void
net_ch_window_read(SSH2_CHANNEL *ch)
PREINIT:
    unsigned long read_avail = 0;
    unsigned long window_size_initial = 0;
PPCODE:
    XPUSHs(sv_2mortal(newSVuv(libssh2_channel_window_read_ex(ch->channel,
                                                             &read_avail,
                                                             &window_size_initial))));
    if (GIMME_V == G_ARRAY) {
        XPUSHs(sv_2mortal(newSVuv(read_avail)));
        XPUSHs(sv_2mortal(newSVuv(window_size_initial)));
        XSRETURN(3);
    }
    else
        XSRETURN(1);

#else

void
net_ch_window_write(SSH2_CHANNEL* ch)
CODE:
    croak("libssh2 version 1.2 or higher required for window_write support");

void
net_ch_window_read(SSH2_CHANNEL* ch)
CODE:
    croak("libssh2 version 1.2 or higher required for window_read support");

#endif

SSH2_BYTES
net_ch_flush(SSH2_CHANNEL* ch, SSH2_STREAM_ID ext = 0)
CODE:
    RETVAL = libssh2_channel_flush_ex(ch->channel, ext);
    save_eagain(ch->ss->session, RETVAL);
OUTPUT:
    RETVAL

#undef class


MODULE = Net::SSH2		PACKAGE = Net::SSH2::Listener    PREFIX = net_ls_
PROTOTYPES: DISABLE

#define class "Net::SSH2::Listener"

void
net_ls_DESTROY(SSH2_LISTENER* ls)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_channel_forward_cancel(ls->listener);
    SvREFCNT_dec(ls->sv_ss);
    Safefree(ls);

SSH2_CHANNEL*
net_ls_accept(SSH2_LISTENER* ls)
PREINIT:
    SSH2* ss;
CODE:
    NEW_CHANNEL(libssh2_channel_forward_accept(ls->listener));
OUTPUT:
    RETVAL

#undef class


MODULE = Net::SSH2		PACKAGE = Net::SSH2::SFTP   PREFIX = net_sf_
PROTOTYPES: DISABLE

#define class "Net::SSH2::SFTP"

void
net_sf_DESTROY(SSH2_SFTP* sf)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_sftp_shutdown(sf->sftp);
    debug("%s::DESTROY freeing session\n", class);
    SvREFCNT_dec(sf->sv_ss);
    Safefree(sf);

SV *
net_sf_session(SSH2_SFTP* sf)
CODE:
    RETVAL = newRV_inc(sf->sv_ss);
OUTPUT:
    RETVAL

void
net_sf_error(SSH2_SFTP* sf)
PREINIT:
    unsigned long error;
    SV *errstr;
PPCODE:
    error = libssh2_sftp_last_error(sf->sftp);
    ST(0) = sv_2mortal(newSVuv(error));
    if (GIMME_V == G_ARRAY) {
        EXTEND(SP, 2);
        if ((error >= 0) && (error < countof(sftp_error)))
            errstr = newSVpvf("SSH_FX_%s", sftp_error[error]);
        else
            errstr = newSVpvf("SSH_FX_UNKNOWN(%lu)", error);
        ST(1) = sv_2mortal(errstr);
        XSRETURN(2);
    }
    else
        XSRETURN(1);

#define XLATFLAG(posix, fxf) do { \
    if (flags & posix || \
     l_flags == 0 && posix == 0 && flags == posix /* 0-valued flag */) { \
        l_flags |= fxf; \
        flags &= ~posix; \
    } \
} while(0)
    
SSH2_FILE*
net_sf_open(SSH2_SFTP* sf, SV* file, int flags = O_RDONLY, int mode = 0666)
PREINIT:
    long l_flags = 0;
    const char* pv_file;
    STRLEN len_file;
CODE:
    pv_file = SvPVbyte(file, len_file);
    
    /* map POSIX O_* to LIBSSH2_FXF_* (can't assume they're the same) */
    XLATFLAG(O_RDWR,   LIBSSH2_FXF_READ | LIBSSH2_FXF_WRITE);
    XLATFLAG(O_RDONLY, LIBSSH2_FXF_READ);
    XLATFLAG(O_WRONLY, LIBSSH2_FXF_WRITE);
    XLATFLAG(O_APPEND, LIBSSH2_FXF_APPEND);
    XLATFLAG(O_CREAT,  LIBSSH2_FXF_CREAT);
    XLATFLAG(O_TRUNC,  LIBSSH2_FXF_TRUNC);
    XLATFLAG(O_EXCL,   LIBSSH2_FXF_EXCL);
    if (flags)
        croak("%s::open: unknown flag value: %d", class, flags);

    NEW_FILE(libssh2_sftp_open_ex(sf->sftp, (char*)pv_file, len_file,
     l_flags, mode, LIBSSH2_SFTP_OPENFILE));
OUTPUT:
    RETVAL

#undef XLATFLAG

SSH2_DIR*
net_sf_opendir(SSH2_SFTP* sf, SV* dir)
PREINIT:
    const char* pv_dir;
    STRLEN len_dir;
CODE:
    pv_dir = SvPVbyte(dir, len_dir);
    NEW_DIR(libssh2_sftp_open_ex(sf->sftp, (char*)pv_dir, len_dir,
     0/*flags*/, 0/*mode*/, LIBSSH2_SFTP_OPENDIR));
OUTPUT:
    RETVAL

SSH2_NERROR
net_sf_unlink(SSH2_SFTP* sf, SV* file)
PREINIT:
    char* pv_file;
    STRLEN len_file;
CODE:
    pv_file = SvPVbyte(file, len_file);
    RETVAL = libssh2_sftp_unlink_ex(sf->sftp, (char*)pv_file, len_file);
OUTPUT:
    RETVAL

SSH2_NERROR
net_sf_rename(SSH2_SFTP* sf, SV* old, SV* new,                  \
              long flags = ( LIBSSH2_SFTP_RENAME_OVERWRITE |    \
                             LIBSSH2_SFTP_RENAME_ATOMIC    |    \
                             LIBSSH2_SFTP_RENAME_NATIVE ) )
PREINIT:
    const char* pv_old, * pv_new;
    STRLEN len_old, len_new;
CODE:
    pv_old = SvPVbyte(old, len_old);
    pv_new = SvPVbyte(new, len_new);
    RETVAL = libssh2_sftp_rename_ex(sf->sftp,
                                    (char*)pv_old, len_old, (char*)pv_new, len_new, flags);
OUTPUT:
    RETVAL

SSH2_NERROR
net_sf_mkdir(SSH2_SFTP* sf, SV* dir, int mode = 0777)
PREINIT:
    const char* pv_dir;
    STRLEN len_dir;
CODE:
    pv_dir = SvPVbyte(dir, len_dir);
    RETVAL = libssh2_sftp_mkdir_ex(sf->sftp, (char*)pv_dir, len_dir, mode);
OUTPUT:
    RETVAL

SSH2_NERROR
net_sf_rmdir(SSH2_SFTP* sf, SV* dir)
PREINIT:
    const char* pv_dir;
    STRLEN len_dir;
CODE:
    pv_dir = SvPVbyte(dir, len_dir);
    RETVAL = libssh2_sftp_rmdir_ex(sf->sftp, (char*)pv_dir, len_dir);
OUTPUT:
    RETVAL

void
net_sf_stat(SSH2_SFTP* sf, SV* path, int follow = 1)
PREINIT:
    const char* pv_path;
    STRLEN len_path;
    int error;
    LIBSSH2_SFTP_ATTRIBUTES attrs;
PPCODE:
    pv_path = SvPVbyte(path, len_path);
    error = !libssh2_sftp_stat_ex(sf->sftp, (char*)pv_path, len_path,
                                  (follow ? LIBSSH2_SFTP_STAT : LIBSSH2_SFTP_LSTAT),
                                  &attrs);
    if (error < 0)
        XSRETURN_EMPTY;
    XSRETURN_STAT_ATTRS(SvREFCNT_inc(path));

SSH2_NERROR
net_sf_setstat(SSH2_SFTP* sf, SV* path, ...)
PREINIT:
    const char* pv_path;
    STRLEN len_path;
    LIBSSH2_SFTP_ATTRIBUTES attrs;
    int i;
CODE:
    pv_path = SvPVbyte(path, len_path);
    Zero(&attrs, 1, LIBSSH2_SFTP_ATTRIBUTES);

    /* read key/value pairs; cf. hv_from_attrs */
    for (i = 2; i < items; i += 2) {
        const char* key = SvPVbyte_nolen(ST(i));
        if (i + 1 == items)
            croak("%s::setstat: key without value", class);
        if (0);  /* prime the chain */
        XLATATTR("size",  filesize,    SIZE)
        XLATATTR("uid",   uid,         UIDGID)
        XLATATTR("gid",   gid,         UIDGID)
        XLATATTR("mode",  permissions, PERMISSIONS)
        XLATATTR("atime", atime,       ACMODTIME)
        XLATATTR("mtime", mtime,       ACMODTIME)
        else
            croak("%s::setstat: unknown attribute: %s", class, key);
    }
    RETVAL = libssh2_sftp_stat_ex(sf->sftp, (char*)pv_path, len_path,
                                  LIBSSH2_SFTP_SETSTAT, &attrs);
OUTPUT:
    RETVAL

SSH2_NERROR
net_sf_symlink(SSH2_SFTP* sf, SV* path, SV* target)
PREINIT:
    char *pv_path, *pv_target;
    STRLEN len_path, len_target;
CODE:
    pv_path = SvPVbyte(path, len_path);
    pv_target = SvPVbyte(target, len_target);
    RETVAL = libssh2_sftp_symlink_ex(sf->sftp,
                                     pv_path, len_path,
                                     pv_target, len_target,
                                     LIBSSH2_SFTP_SYMLINK);
OUTPUT:
    RETVAL

SV *
net_sf_readlink(SSH2_SFTP* sf, SV* path)
PREINIT:
    SV* link;
    const char* pv_path;
    char* pv_link;
    STRLEN len_path;
    int count;
CODE:
    pv_path = SvPVbyte(path, len_path);
    link = newSV(MAXPATHLEN + 1);
    pv_link = SvPVX(link);
    count = libssh2_sftp_symlink_ex(sf->sftp,
                                    pv_path, len_path,
                                    pv_link, MAXPATHLEN,
                                    LIBSSH2_SFTP_READLINK);
    if (count >= 0) {
        SvPOK_on(link);
        pv_link[count] = '\0';
        SvCUR_set(link, count);
    }
OUTPUT:
    RETVAL

SV *
net_sf_realpath(SSH2_SFTP* sf, SV* path)
PREINIT:
    SV* real;
    const char* pv_path;
    char* pv_real;
    STRLEN len_path;
    int count;
CODE:
    pv_path = SvPVbyte(path, len_path);
    real = newSV(MAXPATHLEN + 1);
    pv_real = SvPVX(real);
    count = libssh2_sftp_symlink_ex(sf->sftp,
                                    pv_path, len_path,
                                    pv_real, MAXPATHLEN,
                                    LIBSSH2_SFTP_REALPATH);
    if (count >= 0) {
        SvPOK_on(real);
        pv_real[count] = '\0';
        SvCUR_set(real, count);
    }
OUTPUT:
    RETVAL

#undef class


MODULE = Net::SSH2		PACKAGE = Net::SSH2::File   PREFIX = net_fi_
PROTOTYPES: DISABLE

#define class "Net::SSH2::File"

void
net_fi_DESTROY(SSH2_FILE* fi)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_sftp_close_handle(fi->handle);
    SvREFCNT_dec(fi->sv_sf);
    Safefree(fi);

SSH2_BYTES
net_fi_read(SSH2_FILE* fi, SV* buffer, size_t size)
PREINIT:
    char* pv_buffer;
    STRLEN len_buffer;
CODE:
    sv_force_normal(buffer);
    sv_setpvn_mg(buffer, "", 0);
    SvPVbyte_force(buffer, len_buffer);
    pv_buffer = sv_grow(buffer, size + 1);
    RETVAL = libssh2_sftp_read(fi->handle, pv_buffer, size);
    if (RETVAL < 0)
        SvOK_off(buffer);
    else {
        SvPOK_only(buffer);
        pv_buffer[RETVAL] = '\0';
        SvCUR_set(buffer, RETVAL);
    }
    SvSETMAGIC(buffer);
OUTPUT:
    RETVAL

SV *
net_fi_getc(SSH2_FILE* fi)
PREINIT:
    char buffer[2];
    int count;
CODE:
    count = libssh2_sftp_read(fi->handle, buffer, 1);
    if (count == 1) {
        buffer[count] = '\0';
        RETVAL = newSVpvn(buffer, count);
    }
    else
        RETVAL = &PL_sv_undef;
OUTPUT:
    RETVAL

SSH2_BYTES
net_fi_write(SSH2_FILE* fi, SV* buffer)
PREINIT:
    const char* pv_buffer;
    STRLEN len_buffer;
CODE:
    sv_utf8_downgrade(buffer, 0);
    pv_buffer = SvPVbyte(buffer, len_buffer);
    RETVAL = libssh2_sftp_write(fi->handle, pv_buffer, len_buffer);
OUTPUT:
    RETVAL

void
net_fi_stat(SSH2_FILE* fi)
PREINIT:
    LIBSSH2_SFTP_ATTRIBUTES attrs;
PPCODE:
    if (libssh2_sftp_fstat(fi->handle, &attrs))
        XSRETURN_EMPTY;
    XSRETURN_STAT_ATTRS(NULL/*name*/);

SSH2_NERROR
net_fi_setstat(SSH2_FILE* fi, ...)
PREINIT:
    LIBSSH2_SFTP_ATTRIBUTES attrs;
    int i;
CODE:
    Zero(&attrs, 1, LIBSSH2_SFTP_ATTRIBUTES);

    /* read key/value pairs; cf. hv_from_attrs */
    for (i = 1; i < items; i += 2) {
        const char* key = SvPVbyte_nolen(ST(i));
        if (i + 1 == items)
            croak("%s::setstat: key without value", class);
        if (0);  /* prime the chain */
        XLATATTR("size",  filesize,    SIZE)
        XLATATTR("uid",   uid,         UIDGID)
        XLATATTR("gid",   gid,         UIDGID)
        XLATATTR("mode",  permissions, PERMISSIONS)
        XLATATTR("atime", atime,       ACMODTIME)
        XLATATTR("mtime", mtime,       ACMODTIME)
        else
            croak("%s::setstat: unknown attribute: %s", class, key);
    }
    RETVAL = libssh2_sftp_fsetstat(fi->handle, &attrs);
OUTPUT:
    RETVAL

int
net_fi_seek(SSH2_FILE* fi, size_t offset)
CODE:
    libssh2_sftp_seek64(fi->handle, offset);
    RETVAL = 1;
OUTPUT:
    RETVAL

SSH2_BYTES64
net_fi_tell(SSH2_FILE* fi)
CODE:
    RETVAL = libssh2_sftp_tell64(fi->handle);
OUTPUT:
    RETVAL
        
#undef class

MODULE = Net::SSH2		PACKAGE = Net::SSH2::Dir   PREFIX = net_di_
PROTOTYPES: DISABLE

#define class "Net::SSH2::Dir"

void
net_di_DESTROY(SSH2_DIR* di)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_sftp_close_handle(di->handle);
    SvREFCNT_dec(di->sv_sf);
    Safefree(di);

void
net_di_read(SSH2_DIR* di)
PREINIT:
    SV* buffer;
    char* pv_buffer;
    int count;
    LIBSSH2_SFTP_ATTRIBUTES attrs;
PPCODE:
    buffer = newSV(MAXPATHLEN + 1);
    SvPOK_on(buffer);
    pv_buffer = SvPVX(buffer);

    count = libssh2_sftp_readdir(di->handle, pv_buffer, MAXPATHLEN, &attrs);

    if (count <= 0) {
        SvREFCNT_dec(buffer);
        XSRETURN_EMPTY;
    }
    pv_buffer[count] = '\0';
    SvCUR_set(buffer, count);
    XSRETURN_STAT_ATTRS(buffer);

#undef class


MODULE = Net::SSH2		PACKAGE = Net::SSH2::PublicKey   PREFIX = net_pk_
PROTOTYPES: DISABLE

#define class "Net::SSH2::PublicKey"

void
net_pk_DESTROY(SSH2_PUBLICKEY* pk)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_publickey_shutdown(pk->pkey);
    SvREFCNT_dec(pk->sv_ss);
    Safefree(pk);

SSH2_NERROR
net_pk_add(SSH2_PUBLICKEY* pk, SV* name, SV* blob, int overwrite, ...)
PREINIT:
    const char* pv_name, * pv_blob;
    STRLEN len_name, len_blob;
    unsigned long num_attrs, i;
    libssh2_publickey_attribute *attrs;
CODE:
    pv_name = SvPVbyte(name, len_name);
    pv_blob = SvPVbyte(blob, len_blob);

    num_attrs = items - 4;
    New(0, attrs, num_attrs, libssh2_publickey_attribute);
    if (!attrs)
        Perl_croak(aTHX_ "Out of memory!");

    for (i = 0; i < num_attrs; ++i) {
        HV* hv;
        SV** tmp;
        STRLEN len_tmp;

        if (!SvROK(ST(i + 4)) || SvTYPE(SvRV(ST(i + 4))) != SVt_PVHV)
            croak("%s::add: attribute %lu is not hash", class, i);
        hv = (HV*)SvRV(ST(i + 4));

        if (!(tmp = hv_fetch(hv, "name", 4, 0/*lval*/)) || !*tmp)
            croak("%s::add: attribute %lu missing name", class, i);
        attrs[i].name = SvPVbyte(*tmp, len_tmp);
        attrs[i].name_len = len_tmp;

        if ((tmp = hv_fetch(hv, "value", 5, 0/*lval*/)) && *tmp) {
            attrs[i].value = SvPVbyte(*tmp, len_tmp);
            attrs[i].value_len = len_tmp;
        } else
            attrs[i].value_len = 0;

        if ((tmp = hv_fetch(hv, "mandatory", 9, 0/*lval*/)) && *tmp)
            attrs[i].mandatory = (char)SvIV(*tmp);
        else
            attrs[i].mandatory = 0;
    }

    RETVAL = libssh2_publickey_add_ex(pk->pkey,
                                      (const unsigned char *)pv_name, len_name,
                                      (const unsigned char *)pv_blob, len_blob, overwrite, num_attrs, attrs);
    Safefree(attrs);
OUTPUT:
    RETVAL
    
SSH2_NERROR
net_pk_remove(SSH2_PUBLICKEY* pk, SV* name, SV* blob)
PREINIT:
    const char* pv_name, * pv_blob;
    STRLEN len_name, len_blob;
CODE:
    pv_name = SvPVbyte(name, len_name);
    pv_blob = SvPVbyte(blob, len_blob);
    RETVAL = libssh2_publickey_remove_ex(pk->pkey,
                                         (const unsigned char *)pv_name, len_name,
                                         (const unsigned char *)pv_blob, len_blob);
OUTPUT:
    RETVAL

void
net_pk_fetch(SSH2_PUBLICKEY* pk)
PREINIT:
    unsigned long keys, i, j;
    libssh2_publickey_list* list = NULL;
PPCODE:
    if (!libssh2_publickey_list_fetch(pk->pkey, &keys, &list) || !list)
        XSRETURN_EMPTY;

    if (GIMME_V == G_ARRAY) {
        EXTEND(SP, keys);

        for (i = 0; i < keys; ++i) {
            HV* hv = newHV();
            AV* av = newAV();

            hv_store(hv, "name", 4,
             newSVpvn((char*)list[i].name, list[i].name_len), 0/*hash*/);
            hv_store(hv, "blob", 4,
             newSVpvn((char*)list[i].blob, list[i].blob_len), 0/*hash*/);

            hv_store(hv, "attr", 4, newRV_noinc((SV*)av), 0/*hash*/);
            av_extend(av, list[i].num_attrs - 1);
            for (j = 0; j < list[i].num_attrs; ++j) {
                HV* attr = newHV();
                hv_store(attr, "name", 4, newSVpvn(list[i].attrs[j].name,
                 list[i].attrs[j].name_len), 0/*hash*/);
                hv_store(attr, "value", 5, newSVpvn(list[i].attrs[j].value,
                 list[i].attrs[j].value_len), 0/*hash*/);
                hv_store(attr, "mandatory", 9,
                 newSViv(list[i].attrs[j].mandatory), 0/*hash*/);
                av_store(av, j, newRV_noinc((SV*)attr));
            }
            
            ST(i) = sv_2mortal(newRV_noinc((SV*)hv));
        }
    }

    libssh2_publickey_list_free(pk->pkey, list);

    if (GIMME_V == G_ARRAY)
        XSRETURN(keys);
    XSRETURN_UV(keys);

#undef class

MODULE = Net::SSH2		PACKAGE = Net::SSH2::KnownHosts   PREFIX = net_kh_
PROTOTYPES: DISABLE

#define class "Net::SSH2::KnownHosts"

#if LIBSSH2_VERSION_NUM >= 0x010200

void
net_kh_DESTROY(SSH2_KNOWNHOSTS *kh)
CODE:
    debug("%s::DESTROY\n", class);
    libssh2_knownhost_free(kh->knownhosts);
    SvREFCNT_dec(kh->sv_ss);
    Safefree(kh);

SSH2_BYTES
net_kh_readfile(SSH2_KNOWNHOSTS *kh, SSH2_CHARP filename)
CODE:
    RETVAL = libssh2_knownhost_readfile(kh->knownhosts, filename, LIBSSH2_KNOWNHOST_FILE_OPENSSH);
OUTPUT:
    RETVAL

SSH2_NERROR
net_kh_writefile(SSH2_KNOWNHOSTS *kh, SSH2_CHARP filename)
CODE:
    RETVAL = libssh2_knownhost_writefile(kh->knownhosts, filename, LIBSSH2_KNOWNHOST_FILE_OPENSSH);
    save_eagain(kh->ss->session, RETVAL);
OUTPUT:
    RETVAL

SSH2_NERROR
net_kh_add(SSH2_KNOWNHOSTS *kh, SSH2_CHARP host, SSH2_CHARP salt, SV *key, SV *comment, int typemask)
PREINIT:
    STRLEN key_len, comment_len;
    const char *key_pv, *comment_pv;
CODE:
    key_pv = SvPVbyte(key, key_len);
    if (SvOK(comment))
        comment_pv = SvPVbyte(comment, comment_len);
    else {
        comment_pv = NULL;
        comment_len = 0;
    }
#if LIBSSH2_VERSION_NUM >= 0x010205
    RETVAL = libssh2_knownhost_addc(kh->knownhosts, host, salt, key_pv, key_len,
                                    comment_pv, comment_len, typemask, NULL);
#else
    RETVAL = libssh2_knownhost_add(kh->knownhosts, host, salt, key_pv, key_len,
                                   typemask, NULL);
#endif
    save_eagain(kh->ss->session, RETVAL);
OUTPUT:
    RETVAL

int
net_kh_check(SSH2_KNOWNHOSTS *kh, SSH2_CHARP host, SV *port, SV *key, int typemask)
PREINIT:
    STRLEN key_len;
    const char *key_pv;
    UV port_uv;
CODE:
    key_pv = SvPVbyte(key, key_len);
    port_uv = (SvOK(port) ? SvUV(port) : 0);
#if LIBSSH2_VERSION_NUM >= 0x010206
    RETVAL = libssh2_knownhost_checkp(kh->knownhosts, host, port_uv,
                                      key_pv, key_len, typemask, NULL);
#else
    if ((port_uv != 0) && (port_uv != 22))
        croak("libssh2 version 1.2.6 is required when using a custom TCP port");
    RETVAL = libssh2_knownhost_check(kh->knownhosts, host,
                                     key_pv, key_len, typemask, NULL);
#endif
OUTPUT:
    RETVAL

SSH2_NERROR
net_kh_readline(SSH2_KNOWNHOSTS *kh, SV *line)
PREINIT:
    STRLEN line_len;
    const char *line_pv;
CODE:
    line_pv = SvPVbyte(line, line_len);
    RETVAL = libssh2_knownhost_readline(kh->knownhosts, line_pv, line_len, LIBSSH2_KNOWNHOST_FILE_OPENSSH);
    save_eagain(kh->ss->session, RETVAL);
OUTPUT:
    RETVAL

SV *
net_kh_writeline(SSH2_KNOWNHOSTS *kh, SSH2_CHARP host, SV *port, SV *key, int typemask)
PREINIT:
    int rc;
    STRLEN key_len;
    const char *key_pv;
    UV port_uv;
    size_t line_len;
    STRLEN buffer_len;
    SV *buffer;
    struct libssh2_knownhost *entry = NULL;
CODE:
    RETVAL = &PL_sv_undef;
    key_pv = SvPVbyte(key, key_len);
    port_uv = (SvOK(port) ? SvUV(port) : 0);
#if LIBSSH2_VERSION_NUM >= 0x010206
    rc = libssh2_knownhost_checkp(kh->knownhosts, host, port_uv,
                                  key_pv, key_len, typemask, &entry);
#else
    if ((port_uv != 0) && (port_uv != 22))
        croak("libssh2 version 1.2.6 is required when using a custom TCP port");
    rc = libssh2_knownhost_check(kh->knownhosts, host,
                                 key_pv, key_len, typemask, &entry);
#endif
    if ((rc != LIBSSH2_KNOWNHOST_CHECK_MATCH) || !entry) {
#if LIBSSH2_VERSION_NUM >= 0x010601
        libssh2_session_set_last_error(kh->ss->session, LIBSSH2_ERROR_KNOWN_HOSTS, "matching host key not found");
#endif
    }
    else {
        buffer = sv_2mortal(newSV(512));
        SvPOK_on(buffer);
        while (1) {
            rc = libssh2_knownhost_writeline(kh->knownhosts, entry,
                                             SvPVX(buffer), SvLEN(buffer),
                                             &line_len, LIBSSH2_KNOWNHOST_FILE_OPENSSH);
            if (rc == LIBSSH2_ERROR_NONE) {
                SvPVX(buffer)[line_len] = '\0';
                SvCUR_set(buffer, line_len);
                RETVAL = SvREFCNT_inc(buffer);
                break;
            }

            if ((rc != LIBSSH2_ERROR_BUFFER_TOO_SMALL) ||
                (SvLEN(buffer) > 256 * 1024)) break;

            SvGROW(buffer, SvLEN(buffer) * 2);
        }
    }
OUTPUT:
    RETVAL

# /* TODO */
# libssh2_knownhost_del()
# libssh2_knownhost_get()
# libssh2_knownhost_writeline()

#endif

#undef class

# vim: set et ts=4: