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"

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


#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
#warning "Building a non-threadsafe Net::SSH2"
#endif

/* constants */

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

/* LIBSSH2_ERROR_* values; from 0 continuing negative */
const char* 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"
};

/* SSH_FX_* values; from 0 continuing positive */
const char* 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 XLATEXT (ext ? SSH_EXTENDED_DATA_STDERR : 0)

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

/* Net::SSH2 object */
typedef struct SSH2 {
    LIBSSH2_SESSION* session;
    SV* sv_ss;  /* NB: not set until callback() called */
    SV* socket;
    SV* sv_tmp;
    int errcode;
    SV* errmsg;
    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;

static int net_ss_debug_out = 0;
static unsigned long net_ch_gensym = 0;
static unsigned long net_fi_gensym = 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);
}

/* set Net:SSH2-specific error message */
static void set_error(SSH2* ss, int errcode, const char* errmsg) {
    ss->errcode = errcode;
    if (ss->errmsg)
        SvREFCNT_dec(ss->errmsg);
    ss->errmsg = errmsg ? newSVpv(errmsg, 0) : NULL;
}    

/* clear our local error flag */
static void clear_error(SSH2* ss) {
    set_error(ss, LIBSSH2_ERROR_NONE, NULL/*errmsg*/);
}

/* split a string at commas and push each substring onto the perl stack */
static int split_comma(SV** sp, const char* str) {
    int i;
    const char* p;

    if (!str || !*str)
        return 0;

    i = 1;
    while ((p = strchr(str, ','))) {
        mXPUSHp(str, p - str);
        str = p + 1;
        ++i;
    }
    mXPUSHp(str, strlen(str));
    return i;
}

/* 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;
}

/* return NULL if undef or NULL, else return string */
static const char* default_string(SV* sv) {
    return (sv && SvPOK(sv)) ? SvPV_nolen(sv) : NULL;
}

/* return an integer constant from an SV name or value */
static int iv_constant_sv(const char *prefix, SV* c_sv, IV* piv) {
    int ret = 1;

    /* accept type as constant, constant without prefix, or numeric value */
    if (SvIOK(c_sv)) {
        *piv = SvIV(c_sv);
    } else {
        SV *sv = newSVsv(c_sv);
        char* str = SvPV_nolen(sv), * p;
        const char* pv;
        STRLEN len = strlen(prefix);

        for (p = str; *p; ++p)
            *p = toUPPER(*p);
        if (strncmp(str, prefix, len))
            sv_insert(sv, 0/*offset*/, 0/*replace*/, (char*)prefix, len);
        pv = SvPV(sv, len);
	    if (constant(aTHX_ pv, len, piv) != PERL_constant_ISIV)
            ret = 0;
        SvREFCNT_dec(sv);
    }
    return ret;
}

/* 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)

/* callback for returning a password via "keyboard-interactive" auth */
static LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC(cb_kbdint_response_password) {
    SSH2* ss = (SSH2*)*abstract;
    const char* pv_password;
    STRLEN len_password;

    if (num_prompts != 1 || prompts[0].echo) {
        int i;
        for (i = 0; i < num_prompts; ++i)
            responses[i].length = 0;
        return;
     }

    /* single prompt, no echo: assume it's a password request */
    pv_password = SvPV(ss->sv_tmp, len_password);
    New(0, responses[0].text, len_password, char);
    Copy(pv_password, responses[0].text, len_password, char);
    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) {
    SSH2* ss = (SSH2*)*abstract;
    int i;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    EXTEND(SP, 4 + num_prompts);
    PUSHs(*av_fetch((AV*)ss->sv_tmp, 1, 0/*lval*/));
    PUSHs(*av_fetch((AV*)ss->sv_tmp, 2, 0/*lval*/));
    PUSHs(sv_2mortal(newSVpvn(name, name_len)));
    PUSHs(sv_2mortal(newSVpvn(instruction, instruction_len)));
    for (i = 0; i < num_prompts; ++i) {
        HV* hv = newHV();
        responses[i].length = 0;
        hv_store(hv, "text", 4, newSVpvn(prompts[i].text, prompts[i].length),
         0/*hash*/);
        hv_store(hv, "echo", 4, newSViv(prompts[i].echo), 0/*hash*/);
        PUSHs(sv_2mortal(newRV_noinc((SV*)hv)));
    }
    PUTBACK;

    count = call_sv(*av_fetch((AV*)ss->sv_tmp, 0, 0/*lval*/), G_ARRAY);
    SPAGAIN; SP -= count; ax = (SP - PL_stack_base) + 1;

    /* translate the returned responses */
    for (i = 0; i < count; ++i) {
        STRLEN len_response;
        const char* pv_response = SvPV(ST(i), len_response);
        New(0, responses[i].text, len_response, char);
        Copy(pv_response, responses[i].text, len_response, char);
        responses[i].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) {
    SSH2* ss = (SSH2*)*abstract;

    dSP; I32 ax; int count;
    ENTER; SAVETMPS; PUSHMARK(SP);
    XPUSHs(*av_fetch((AV*)ss->sv_tmp, 1, 0/*lval*/));
    XPUSHs(*av_fetch((AV*)ss->sv_tmp, 2, 0/*lval*/));
    PUTBACK;

    *newpw = NULL;
    *newpw_len = 0;
    count = call_sv(*av_fetch((AV*)ss->sv_tmp, 0, 0/*lval*/), G_SCALAR);
    SPAGAIN; SP -= count; ax = (SP - PL_stack_base) + 1;

    if (count > 0) {
        STRLEN len_password;
        const char* pv_password = SvPV(ST(0), len_password);
        New(0, *newpw, len_password, char);
        Copy(pv_password, *newpw, len_password, char);
        *newpw_len = len_password;
    }

    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;
}

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

START_MY_CXT

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

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
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;
};

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;
}

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);
}

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

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((void (*)(int,int,const char *,int))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);
    }
}

#endif

/* 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);

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;
    }
    clear_error(RETVAL);

    debug("Net::SSH2: created new object 0x%x\n", RETVAL);
OUTPUT:
    RETVAL

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

#if LIBSSH2_VERSION_MAJOR >= 1

SV*
net_ss_block_directions(SSH2* ss)
CODE:
    RETVAL = newSViv((IV)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

void
net_ss_timeout(SSH2* ss, long timeout)
CODE:
    libssh2_session_set_timeout(ss->session, timeout);

#else

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

#endif

void
net_ss_blocking(SSH2* ss, SV* blocking)
CODE:
    clear_error(ss);
    libssh2_session_set_blocking(ss->session, SvTRUE(blocking));
    XSRETURN_IV(1);

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

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

void
net_ss_version(SV* name = NULL)
CODE:
    switch (GIMME_V) {
    case G_SCALAR:
        XSRETURN_PV(LIBSSH2_VERSION);
    case G_ARRAY:
        EXTEND(SP, 3);
        ST(0) = sv_2mortal(newSVpv(LIBSSH2_VERSION, 0));
#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);
    }

void
net_ss_banner(SSH2* ss, SV* banner)
PREINIT:
    int success;
    SV* sv_banner;
CODE:
    clear_error(ss);
    sv_banner = newSVsv(banner);
    sv_insert(sv_banner, 0/*offset*/, 0/*len*/, "SSH-2.0-", 8);
    success = !libssh2_banner_set(ss->session, SvPV_nolen(sv_banner));
    SvREFCNT_dec(sv_banner);
    XSRETURN_IV(success);

void
net_ss_error(SSH2* ss, ...)
PREINIT:
    SV* errmsg;
    int errcode;
CODE:
    if (items == 3) {
        set_error(ss, SvIV(ST(1)), SvPV_nolen(ST(2)));
        XSRETURN_EMPTY;
    } else if(items != 1)
        croak("%s::error: too many arguments", class);

    /* if we have a local error, take it, else use libSSH2's value */
    if (ss->errcode != LIBSSH2_ERROR_NONE && ss->errmsg != NULL) {
        errcode = ss->errcode;
        errmsg = SvREFCNT_inc(ss->errmsg);
    } else {
        char* errstr;
        int errlen;
        errcode = libssh2_session_last_error(
         ss->session, &errstr, &errlen, 0/*want_buf*/);
        errmsg = errstr ? newSVpvn(errstr, errlen) : NULL;
    }

    if (errcode == LIBSSH2_ERROR_NONE && errmsg == NULL)
        XSRETURN_EMPTY;
    switch (GIMME_V) {
    case G_SCALAR:
        XSRETURN_IV(errcode);
    case G_ARRAY: {
        SV* code;
        EXTEND(SP, 3);
        ST(0) = sv_2mortal(newSViv(errcode));
        if (errcode < 0) {
            code = (-errcode < countof(xs_libssh2_error)) ? 
             newSVpvf("LIBSSH2_ERROR_%s", xs_libssh2_error[-errcode]) :
             newSVpvf("LIBSSH2_ERROR_UNKNOWN(%d)", errcode);
        } else if(errcode > 0)
            code = newSVpv(Strerror(errcode), 0);
        else
            code = newSVpvn("", 0);  /* possibly set via set_error */
        ST(1) = sv_2mortal(code);
        ST(2) = sv_2mortal(errmsg);
        XSRETURN(3);
    }
    }

void
net_ss_method(SSH2* ss, SV* method_type, ...)
PREINIT:
    IV type;
    int i;
    SV* prefs;
    STRLEN len;
PPCODE:
    clear_error(ss);
    if (!iv_constant_sv("LIBSSH2_METHOD_", method_type, &type))
        croak("%s::method: unknown method type: %s",
         class, SvPV_nolen(method_type));
    
    /* 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);
    }
        
    /* accept prefs as a string or multiple strings, joining with "," */
    prefs = newSVpvn("", 0);
    for (i = 2; i < items; ++i) {
        const char* pv_pref;
        if (i > 2)
            sv_catpvn(prefs, ",", 1);
        pv_pref = SvPV(ST(i), len);
        sv_catpvn(prefs, pv_pref, len);
    }

    /* call and clean up */
    i = libssh2_session_method_pref(ss->session,
     (int)type, SvPV_nolen(prefs));
    SvREFCNT_dec(prefs);
    XSRETURN_IV(!i);

void
net_ss_callback(SSH2* ss, SV* type, SV* callback = NULL)
PREINIT:
    IV i_type;
CODE:
    clear_error(ss);
    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 (!iv_constant_sv("LIBSSH2_CALLBACK_", type, &i_type))
        croak("%s::callback: invalid callback type: %s",
         class, SvPV_nolen(callback));
    if (i_type < 0 || i_type >= countof(msg_cb))
        croak("%s::callback: don't know how to handle: %s",
         class, SvPV_nolen(callback));

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

void
net_ss__startup(SSH2* ss, int socket, SV *store)
PREINIT:
    int success;
CODE:
    clear_error(ss);
    success = !libssh2_session_startup(ss->session, socket);
    if (success && store) {
        ss->socket = SvREFCNT_inc(SvRV(store));
    }
    XSRETURN_IV(success);

SV *
net_ss_sock(SSH2* ss)
CODE:
    if (ss->socket) {
        RETVAL = newRV_inc((SV *)ss->socket);
    } else {
        RETVAL = &PL_sv_undef;
    }
OUTPUT:
    RETVAL

void
net_ss_disconnect(SSH2* ss, const char* description = "", \
 int reason = SSH_DISCONNECT_BY_APPLICATION, const char *lang = "")
CODE:
    clear_error(ss);
    XSRETURN_IV(!libssh2_session_disconnect_ex(
     ss->session, reason, description, lang));

void
net_ss_hostkey(SSH2* ss, SV* hash_type)
PREINIT:
    IV type;
    const char* hash;
    static STRLEN rglen[] = { 16/*MD5*/, 20/*SHA1*/ };
PPCODE:
    clear_error(ss);
    if (!iv_constant_sv("LIBSSH2_HOSTKEY_HASH_", hash_type, &type) ||
     type < 1 || type > countof(rglen)) {
        croak("%s::hostkey: unknown hostkey hash: %s",
         class, SvPV_nolen(hash_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_auth_list(SSH2* ss, SV* username = NULL)
PREINIT:
    const char* pv_username = NULL;
    char* auth;
    STRLEN len_username = 0;
    int count = 1;
PPCODE:
    clear_error(ss);
    if (username && SvPOK(username))
        pv_username = SvPV(username, len_username);
    auth = libssh2_userauth_list(ss->session, pv_username, len_username);
    if (!auth)
        XSRETURN_EMPTY;
    if (GIMME_V == G_ARRAY)
        count = split_comma(sp, auth);
    else
        PUSHs(sv_2mortal(newSVpv(auth, 0)));
    /* Safefree(auth); this causes a double-free segfault */
    XSRETURN(count);

void
net_ss_auth_ok(SSH2* ss)
CODE:
    clear_error(ss);
    XSRETURN_IV(libssh2_userauth_authenticated(ss->session));

void
net_ss_auth_password(SSH2* ss, SV* username, SV* password = NULL, \
 SV* callback = NULL)
PREINIT:
    STRLEN len_username, len_password;
    const char* pv_username, * pv_password;
    int i;
CODE:
    clear_error(ss);
    if (callback && SvOK(callback) &&
     !(SvROK(callback) && SvTYPE(SvRV(callback)) == SVt_PVCV))
        croak("%s::auth_password: callback must be CODE ref", class);
    pv_username = SvPV(username, len_username);

    /* if we don't have a password, try for an unauthenticated login */
    if (!password || !SvPOK(password)) {
        char* auth = libssh2_userauth_list(ss->session,
         pv_username, len_username);
        /* This causes a double free segfault
         * Safefree(auth);
         */
        XSRETURN_IV(!auth && libssh2_userauth_authenticated(ss->session));
    }

    /* if we have a callback, setup its parameters */
    if (callback) {
        AV* args = (AV*)sv_2mortal((SV*)newAV());
        av_store(args, 0, newSVsv(callback));
        av_store(args, 1, newSVsv(ST(0)));
        av_store(args, 2, newSVsv(username));
        ss->sv_tmp = (SV*)args;
    }

    pv_password = SvPV(password, len_password);
    XSRETURN_IV(!libssh2_userauth_password_ex(ss->session, pv_username,
     len_username, pv_password, len_password,
     callback ? cb_password_change_callback : NULL));

    if (callback)
        ss->sv_tmp = NULL;

#if LIBSSH2_VERSION_NUM >= 0x010203

void
net_ss_auth_agent(SSH2* ss, SV* username)
PREINIT:
    STRLEN len_username;
    const char* pv_username;
    LIBSSH2_AGENT *agent = NULL;
    int agent_end, rc;
    struct libssh2_agent_publickey *identity, *prev_identity = NULL;
CODE:
    clear_error(ss);
    pv_username = SvPV(username, len_username);
    agent = libssh2_agent_init(ss->session);
    if(!agent) {
        XSRETURN_IV(0);
    }
    if(libssh2_agent_connect(agent)) {
        XSRETURN_IV(0);
    }
    if(libssh2_agent_list_identities(agent)) {
        XSRETURN_IV(0);
    }
    while(1) {
        agent_end = libssh2_agent_get_identity(agent, &identity, prev_identity);

        if(agent_end == 1) {
            // Reached end, not successfully authenticated.
            XSRETURN_IV(0);
        }

        if(agent_end < 0) {
            // error
            XSRETURN_IV(agent_end);
        }

        rc = libssh2_agent_userauth(agent, pv_username, identity);

        if (rc == LIBSSH2_ERROR_EAGAIN &&
            libssh2_session_get_blocking(ss->session) == 0) {
            XSRETURN_IV(rc);
        }

        while (rc == LIBSSH2_ERROR_EAGAIN) {
          rc = libssh2_agent_userauth(agent, pv_username, identity);
        }

        if(rc >= 0) {
            // authenticated
            XSRETURN_IV(!rc);
        }

        prev_identity = identity;
    }

    if(agent_end) {
        XSRETURN_IV(agent_end);
    }

#else

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

#endif

void
net_ss_auth_publickey(SSH2* ss, SV* username, const char* publickey, \
 const char* privatekey, SV* passphrase = NULL)
PREINIT:
    const char* pv_username;
    STRLEN len_username;
CODE:
    clear_error(ss);
    pv_username = SvPV(username, len_username);
    XSRETURN_IV(!libssh2_userauth_publickey_fromfile_ex(ss->session,
     pv_username, len_username, publickey, privatekey,
     default_string(passphrase)));

void
net_ss_auth_hostbased(SSH2* ss, SV* username, const char* publickey, \
 const char* privatekey, SV* hostname, SV* local_username = NULL, \
 SV* passphrase = NULL)
PREINIT:
    const char* pv_username, * pv_hostname, * pv_local_username;
    STRLEN len_username, len_hostname, len_local_username;
CODE:
    clear_error(ss);
    pv_username = SvPV(username, len_username);
    pv_hostname = SvPV(hostname, len_hostname);

    if (!local_username || !SvPOK(local_username)) {
        pv_local_username = pv_username;
        len_local_username = len_username;
    } else
        pv_local_username = SvPV(local_username, len_local_username);

    XSRETURN_IV(!libssh2_userauth_hostbased_fromfile_ex(ss->session,
     pv_username, len_username, publickey, privatekey,
     default_string(passphrase),
     pv_hostname, len_hostname, pv_local_username, len_local_username));

void
net_ss_auth_keyboard(SSH2* ss, SV* username, SV* password = NULL)
PREINIT:
    const char* pv_username;
    STRLEN len_username;
    int success;
CODE:
    clear_error(ss);
    pv_username = SvPV(username, len_username);

    /* we either have a password, or a reference to a callback */
    if (password && SvPOK(password)) {
        ss->sv_tmp = password;
        success = !libssh2_userauth_keyboard_interactive_ex(
         ss->session, pv_username, len_username, cb_kbdint_response_password);
        ss->sv_tmp = NULL;
        XSRETURN_IV(success);
    }

    /* alright, reference to callback it is */
    if (!password || !SvOK(password))
        password = sv_2mortal(newRV_noinc((SV*)get_cv(
         "Net::SSH2::_cb_kbdint_response_default", 0/*create*/)));
    if (!SvROK(password) || SvTYPE(SvRV(password)) != SVt_PVCV)
        croak("%s::auth_keyboard requires password or CODE ref", class);

    /* set up parameters for callback */
    {
        SV* rgsv[3];  /* callback, params... */
        int i;

			 	rgsv[0] = password;
				rgsv[1] = ST(0);
				rgsv[2] = username;

        for (i = 0; i < countof(rgsv); ++i)
            SvREFCNT_inc(rgsv[i]);
        ss->sv_tmp = (SV*)av_make(countof(rgsv), rgsv);
    }
    SvREFCNT_inc(SvRV(password));

    success = !libssh2_userauth_keyboard_interactive_ex(
     ss->session, pv_username, len_username, cb_kbdint_response_callback);

    SvREFCNT_dec(SvRV(password));
    SvREFCNT_dec(ss->sv_tmp);
    ss->sv_tmp = NULL;
    XSRETURN_IV(success);

SSH2_CHANNEL*
net_ss_channel(SSH2* ss, SV* channel_type = NULL, \
 int window_size = LIBSSH2_CHANNEL_WINDOW_DEFAULT, \
 int packet_size = LIBSSH2_CHANNEL_PACKET_DEFAULT)
PREINIT:
    const char* pv_channel_type;
    STRLEN len_channel_type;
CODE:
    clear_error(ss);
    if (channel_type)
        pv_channel_type = SvPV(channel_type, len_channel_type);
    else {
        pv_channel_type = "session";
        len_channel_type = 7;
    }

    NEW_CHANNEL(libssh2_channel_open_ex(ss->session,
     pv_channel_type, len_channel_type, window_size, packet_size,
     NULL/*message*/, 0/*message_len*/));
OUTPUT:
    RETVAL

SSH2_CHANNEL*
net_ss__scp_get(SSH2* ss, const char* path, HV* stat = NULL)
PREINIT:
    struct stat st;
CODE:
    clear_error(ss);
    NEW_CHANNEL(libssh2_scp_recv(ss->session, path, &st));
    if (stat) {
        hv_clear(stat);
        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, newSViv((time_t)st.st_mtime), 0/*hash*/);
    }
OUTPUT:
    RETVAL

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

SSH2_CHANNEL*
net_ss_tcpip(SSH2* ss, const char* host, int port, \
 const char* shost = NULL, int sport = 0)
CODE:
    if (!shost)
        shost = "127.0.0.1";
    if (!sport)
        sport = 22;
    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");
    } 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

void
net_ss__poll(SSH2* ss, int timeout, AV* event)
PREINIT:
    LIBSSH2_POLLFD* pollfd;
    int i, count, changed;
CODE:
    clear_error(ss);
    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) {
        set_error(ss, 0, "out of memory allocating pollfd structures");
        XSRETURN_EMPTY;
    }
    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, package, i);
            }
        } 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, SvPV_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:
    clear_error(ss);
    NEW_SFTP(libssh2_sftp_init(ss->session));
OUTPUT:
    RETVAL

SSH2_PUBLICKEY*
net_ss_public_key(SSH2* ss)
CODE:
    clear_error(ss);
    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);
    clear_error(ch->ss);
    libssh2_channel_free(ch->channel);
    SvREFCNT_dec(ch->sv_ss);
    Safefree(ch);

void
net_ch_session(SSH2_CHANNEL* ch)
CODE:
    ST(0) = sv_2mortal(newRV_inc(ch->sv_ss));
    XSRETURN(1);

void
net_ch_setenv(SSH2_CHANNEL* ch, ...)
PREINIT:
    int i, success = 0;
    const char* pv_key, * pv_value;
    STRLEN len_key, len_value;
CODE:
    clear_error(ch->ss);
    for (i = 1; i < items; i += 2) {
        if (i + 1 == items)
            croak("%s::setenv: key without value", class);
        pv_key = SvPV(ST(i), len_key);
        pv_value = SvPV(ST(i + 1), len_value);
        success += !libssh2_channel_setenv_ex(ch->channel,
         (char*)pv_key, len_key, (char*)pv_value, len_value);
    }
    XSRETURN_IV(success);

#if LIBSSH2_VERSION_NUM >= 0x010208

SV*
net_ch_exit_signal(SSH2_CHANNEL* ch)
CODE:
    clear_error(ch->ss);
    RETVAL = NULL;
    char *exitsignal = NULL;  
    libssh2_channel_get_exit_signal(ch->channel, &exitsignal,
        NULL, NULL, NULL, NULL, NULL);
    if (exitsignal) {
        RETVAL = newSVpv(exitsignal, 0);
        Safefree(exitsignal);
    }
OUTPUT:
    RETVAL

#else

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

#endif

void
net_ch_blocking(SSH2_CHANNEL* ch, SV* blocking)
CODE:
    clear_error(ch->ss);
    libssh2_channel_set_blocking(ch->channel, SvTRUE(blocking));
    XSRETURN_IV(1);

void
net_ch_eof(SSH2_CHANNEL* ch)
CODE:
   clear_error(ch->ss);
   XSRETURN_IV(libssh2_channel_eof(ch->channel));

void
net_ch_send_eof(SSH2_CHANNEL* ch)
CODE:
    clear_error(ch->ss);
    XSRETURN_IV(!libssh2_channel_send_eof(ch->channel));

void
net_ch_close(SSH2_CHANNEL* ch)
CODE:
    clear_error(ch->ss);
    XSRETURN_IV(!libssh2_channel_close(ch->channel));

void
net_ch_wait_closed(SSH2_CHANNEL* ch)
CODE:
    clear_error(ch->ss);
    XSRETURN_IV(!libssh2_channel_wait_closed(ch->channel));

void
net_ch_exit_status(SSH2_CHANNEL* ch)
CODE:
    clear_error(ch->ss);
    XSRETURN_IV(libssh2_channel_get_exit_status(ch->channel));

#if LIBSSH2_VERSION_MAJOR >= 1

void
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 = SvPV(terminal, len_terminal);
    if (modes && SvPOK(modes))
        pv_modes = SvPV(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;
    }

    XSRETURN_IV(!libssh2_channel_request_pty_ex(ch->channel,
     (char*)pv_terminal, len_terminal, (char*)pv_modes, len_modes,
     width, height, width_px, height_px));

void
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;
    }

    XSRETURN_IV(!libssh2_channel_request_pty_size_ex(ch->channel,
     width, height, width_px, height_px));

#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

void
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 = SvPV(request, len_request);
    if (message && SvPOK(message))
        pv_message = SvPV(message, len_message);

    XSRETURN_IV(!libssh2_channel_process_startup(ch->channel,
     pv_request, len_request, pv_message, len_message));

void
net_ch_ext_data(SSH2_CHANNEL* ch, SV* mode)
PREINIT:
    IV i_mode;
CODE:
    if (!iv_constant_sv("LIBSSH2_CHANNEL_EXTENDED_DATA_", mode, &i_mode))
        croak("%s::ext_data: unknown extended data mode: %s",
         class, SvPV_nolen(mode));
    libssh2_channel_handle_extended_data(ch->channel, i_mode);
    XSRETURN_IV(1);

void
net_ch_read(SSH2_CHANNEL* ch, SV* buffer, size_t size, int ext = 0)
PREINIT:
    char* pv_buffer;
    int count, total = 0;
CODE:
    debug("%s::read(size = %d, ext = %d)\n", class, size, ext);
    clear_error(ch->ss);
    SvPOK_on(buffer);
    pv_buffer = sv_grow(buffer, size + 1/*NUL*/);  /* force PV */

    again:
    count = libssh2_channel_read_ex(ch->channel, XLATEXT, pv_buffer, size);
    debug("- read %d bytes\n", count);

    if (count < 0) {
        if (!total) {
            SvCUR_set(buffer, 0);
            XSRETURN_EMPTY;
        }
        count = 0;
    }

    total += count;
    if (count > 0 && (unsigned)count < size) {
        pv_buffer += count;
        size -= count;
        goto again;
    }

    pv_buffer[count] = '\0';
    SvCUR_set(buffer, total);
    debug("- read %d total\n", total);
    XSRETURN_IV(total);

void
net_ch_write(SSH2_CHANNEL* ch, SV* buffer, int ext = 0)
PREINIT:
    const char* pv_buffer;
    STRLEN len_buffer;
    int count;
CODE:
    clear_error(ch->ss);
    pv_buffer = SvPV(buffer, len_buffer);
    do {
        count = libssh2_channel_write_ex(ch->channel, XLATEXT,
         pv_buffer, len_buffer);
        if (count < 0 && LIBSSH2_ERROR_EAGAIN != count)
            XSRETURN_EMPTY;
        if (LIBSSH2_ERROR_EAGAIN == count
                && libssh2_session_get_blocking(ch->ss->session) == 0)
            XSRETURN_IV(LIBSSH2_ERROR_EAGAIN);
    } while (LIBSSH2_ERROR_EAGAIN == count);
    XSRETURN_IV(count);

void
net_ch_flush(SSH2_CHANNEL* ch, int ext = 0)
PREINIT:
    int count;
CODE:
    clear_error(ch->ss);
    count = libssh2_channel_flush_ex(ch->channel, XLATEXT);
    if (count < 0)
        XSRETURN_EMPTY;
    XSRETURN_IV(count);

#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);
    clear_error(ls->ss);
    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:
    clear_error(ss = ls->ss);
    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);
    clear_error(sf->ss);
    libssh2_sftp_shutdown(sf->sftp);
    debug("%s::DESTROY freeing session\n", class);
    SvREFCNT_dec(sf->sv_ss);
    Safefree(sf);

void
net_sf_session(SSH2_SFTP* sf)
CODE:
    ST(0) = sv_2mortal(newRV_inc(sf->sv_ss));
    XSRETURN(1);

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

#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:
    clear_error(sf->ss);
    pv_file = SvPV(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:
    clear_error(sf->ss);
    pv_dir = SvPV(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

void
net_sf_unlink(SSH2_SFTP* sf, SV* file)
PREINIT:
    const char* pv_file;
    STRLEN len_file;
CODE:
    clear_error(sf->ss);
    pv_file = SvPV(file, len_file);
    XSRETURN_IV(!libssh2_sftp_unlink_ex(sf->sftp, (char*)pv_file, len_file));

void
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:
    clear_error(sf->ss);
    pv_old = SvPV(old, len_old);
    pv_new = SvPV(new, len_new);
    XSRETURN_IV(!libssh2_sftp_rename_ex(sf->sftp,
     (char*)pv_old, len_old, (char*)pv_new, len_new, flags));

void
net_sf_mkdir(SSH2_SFTP* sf, SV* dir, int mode = 0777)
PREINIT:
    const char* pv_dir;
    STRLEN len_dir;
CODE:
    clear_error(sf->ss);
    pv_dir = SvPV(dir, len_dir);
    XSRETURN_IV(!libssh2_sftp_mkdir_ex(sf->sftp, (char*)pv_dir, len_dir, mode));

void
net_sf_rmdir(SSH2_SFTP* sf, SV* dir)
PREINIT:
    const char* pv_dir;
    STRLEN len_dir;
CODE:
    clear_error(sf->ss);
    pv_dir = SvPV(dir, len_dir);
    XSRETURN_IV(!libssh2_sftp_rmdir_ex(sf->sftp, (char*)pv_dir, len_dir));

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

void
net_sf_setstat(SSH2_SFTP* sf, SV* path, ...)
PREINIT:
    const char* pv_path;
    STRLEN len_path;
    LIBSSH2_SFTP_ATTRIBUTES attrs;
    int i;
CODE:
    clear_error(sf->ss);
    pv_path = SvPV(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 = SvPV_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);
    }
    
    XSRETURN_IV(!libssh2_sftp_stat_ex(sf->sftp, (char*)pv_path, len_path,
     LIBSSH2_SFTP_SETSTAT, &attrs));

void
net_sf_symlink(SSH2_SFTP* sf, SV* path, SV* target)
PREINIT:
    const char* pv_path, * pv_target;
    STRLEN len_path, len_target;
CODE:
    clear_error(sf->ss);
    pv_path = SvPV(path, len_path);
    pv_target = SvPV(target, len_target);
    XSRETURN_IV(!libssh2_sftp_symlink_ex(sf->sftp,
     pv_path, len_path, (char*)pv_target, len_target, LIBSSH2_SFTP_SYMLINK));

void
net_sf_readlink(SSH2_SFTP* sf, SV* path)
PREINIT:
    SV* link;
    const char* pv_path;
    char* pv_link;
    STRLEN len_path;
    int count;
CODE:
    clear_error(sf->ss);
    pv_path = SvPV(path, len_path);
    link = newSV(MAXPATHLEN + 1);
    SvPOK_on(link);
    pv_link = SvPVX(link);

    count = libssh2_sftp_symlink_ex(sf->sftp,
     pv_path, len_path, pv_link, MAXPATHLEN, LIBSSH2_SFTP_READLINK);

    if (count < 0) {
        SvREFCNT_dec(link);
        XSRETURN_EMPTY;
    }
    pv_link[count] = '\0';
    SvCUR_set(link, count);
    ST(0) = sv_2mortal(link);
    XSRETURN(1);

void
net_sf_realpath(SSH2_SFTP* sf, SV* path)
PREINIT:
    SV* real;
    const char* pv_path;
    char* pv_real;
    STRLEN len_path;
    int count;
CODE:
    clear_error(sf->ss);
    pv_path = SvPV(path, len_path);
    real = newSV(MAXPATHLEN + 1);
    SvPOK_on(real);
    pv_real = SvPVX(real);

    count = libssh2_sftp_symlink_ex(sf->sftp,
     pv_path, len_path, pv_real, MAXPATHLEN, LIBSSH2_SFTP_REALPATH);

    if (count < 0) {
        SvREFCNT_dec(real);
        XSRETURN_EMPTY;
    }
    pv_real[count] = '\0';
    SvCUR_set(real, count);
    ST(0) = sv_2mortal(real);
    XSRETURN(1);

#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);
    clear_error(fi->sf->ss);
    libssh2_sftp_close_handle(fi->handle);
    SvREFCNT_dec(fi->sv_sf);
    Safefree(fi);

void
net_fi_read(SSH2_FILE* fi, SV* buffer, size_t size)
PREINIT:
    char* pv_buffer;
    int count;
CODE:
    clear_error(fi->sf->ss);
    SvPOK_on(buffer);
    pv_buffer = sv_grow(buffer, size + 1/*NUL*/);  /* force PV */
    pv_buffer[size] = '\0';

    count = libssh2_sftp_read(fi->handle, pv_buffer, size);
    if (count < 0) {
        SvCUR_set(buffer, 0);
        XSRETURN_EMPTY;
    }
    SvCUR_set(buffer, count);
    XSRETURN_IV(count);

void
net_fi_write(SSH2_FILE* fi, SV* buffer)
PREINIT:
    const char* pv_buffer;
    STRLEN len_buffer;
    size_t count;
CODE:
    clear_error(fi->sf->ss);
    pv_buffer = SvPV(buffer, len_buffer);
    count = libssh2_sftp_write(fi->handle, pv_buffer, len_buffer);
    if (count < 0)
        XSRETURN_EMPTY;
    XSRETURN_UV(count);    

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

void
net_fi_setstat(SSH2_FILE* fi, ...)
PREINIT:
    LIBSSH2_SFTP_ATTRIBUTES attrs;
    int i;
CODE:
    clear_error(fi->sf->ss);
    Zero(&attrs, 1, LIBSSH2_SFTP_ATTRIBUTES);

    /* read key/value pairs; cf. hv_from_attrs */
    for (i = 1; i < items; i += 2) {
        const char* key = SvPV_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);
    }
    
    XSRETURN_IV(!libssh2_sftp_fsetstat(fi->handle, &attrs));

void
net_fi_seek(SSH2_FILE* fi, size_t offset)
CODE:
    clear_error(fi->sf->ss);
    libssh2_sftp_seek(fi->handle, offset);
    XSRETURN(1);

void
net_fi_tell(SSH2_FILE* fi)
CODE:
    clear_error(fi->sf->ss);
    XSRETURN_UV(libssh2_sftp_tell(fi->handle));
        
#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);
    clear_error(di->sf->ss);
    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:
    clear_error(di->sf->ss);
    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);
    clear_error(pk->ss);
    libssh2_publickey_shutdown(pk->pkey);
    SvREFCNT_dec(pk->sv_ss);
    Safefree(pk);

void
net_pk_add(SSH2_PUBLICKEY* pk, SV* name, SV* blob, int overwrite, ...)
PREINIT:
    int success;
    const char* pv_name, * pv_blob;
    STRLEN len_name, len_blob;
    unsigned long num_attrs, i;
    libssh2_publickey_attribute *attrs;
CODE:
    clear_error(pk->ss);
    pv_name = SvPV(name, len_name);
    pv_blob = SvPV(blob, len_blob);

    num_attrs = items - 4;
    New(0, attrs, num_attrs, libssh2_publickey_attribute);
    if (!attrs) {
        set_error(pk->ss, 0, "out of memory allocating attribute structures");
        XSRETURN_EMPTY;
    }
    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 %d 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 %d missing name", class, i);
        attrs[i].name = SvPV(*tmp, len_tmp);
        attrs[i].name_len = len_tmp;

        if ((tmp = hv_fetch(hv, "value", 5, 0/*lval*/)) && *tmp) {
            attrs[i].value = SvPV(*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;
    }

    success = !libssh2_publickey_add_ex(pk->pkey,
     pv_name, len_name, pv_blob, len_blob, overwrite, num_attrs, attrs);

    Safefree(attrs);
    XSRETURN_IV(!success);
    
void
net_pk_remove(SSH2_PUBLICKEY* pk, SV* name, SV* blob)
PREINIT:
    const char* pv_name, * pv_blob;
    STRLEN len_name, len_blob;
CODE:
    clear_error(pk->ss);
    pv_name = SvPV(name, len_name);
    pv_blob = SvPV(blob, len_blob);
    XSRETURN_IV(!libssh2_publickey_remove_ex(pk->pkey,
     pv_name, len_name, pv_blob, len_blob));

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

# vim: set et ts=4: