#include <Python.h>
#define crypto_MODULE
#include "crypto.h"

#ifdef _WIN32
#define strcasecmp(string1, string2) _stricmp(string1, string2)
#endif

/* http://www.openssl.org/docs/apps/x509v3_config.html#CRL_distribution_points_ */
/* which differs from crl_reasons of crypto/x509v3/v3_enum.c that matches */
/* OCSP_crl_reason_str.  We use the latter, just like the command line program.  */
static const char *crl_reasons[] = {
    "unspecified",
    "keyCompromise",
    "CACompromise",
    "affiliationChanged",
    "superseded",
    "cessationOfOperation",
    "certificateHold",
    NULL,
    "removeFromCRL",
};

#define NUM_REASONS (sizeof(crl_reasons) / sizeof(char *))

static char crypto_Revoked_all_reasons_doc[] = "\n\
Return a list of all the supported reason strings.\n\
\n\
@return: A list of reason strings.\n\
";
static PyObject *
crypto_Revoked_all_reasons(crypto_RevokedObj *self, PyObject *args) {
    PyObject *list, *str;
    int j;

    list = PyList_New(0);
    for (j = 0; j < NUM_REASONS; j++) {
        if(crl_reasons[j]) {
            str = PyBytes_FromString(crl_reasons[j]);
            PyList_Append(list, str);
            Py_DECREF(str);
        }
    }
    return list;
}

static PyObject *
X509_EXTENSION_value_to_PyString(X509_EXTENSION *ex) {
    BIO *bio = NULL;
    PyObject *str = NULL;
    int str_len;
    char *tmp_str;

    /* Create a openssl BIO buffer */
    bio = BIO_new(BIO_s_mem());
    if (bio == NULL) {
        goto err;
    }

    /* These are not the droids you are looking for. */
    if (!X509V3_EXT_print(bio, ex, 0, 0)) {
        if (M_ASN1_OCTET_STRING_print(bio, ex->value) == 0) {
            goto err;
        }
    }

    /* Convert to a Python string. */
    str_len = BIO_get_mem_data(bio, &tmp_str);
    str = PyBytes_FromStringAndSize(tmp_str, str_len);

    /* Cleanup */
    BIO_free(bio);
    return str;

 err:
    if (bio) {
        BIO_free(bio);
    }
    if (str) {
        Py_DECREF(str);
    }
    return NULL;
}

static void
delete_reason(STACK_OF(X509_EXTENSION) *sk) {
    X509_EXTENSION * ext;
    int j;

    for (j = 0; j < sk_X509_EXTENSION_num(sk); j++) {
         ext = sk_X509_EXTENSION_value(sk, j);
         if (OBJ_obj2nid(ext->object) == NID_crl_reason) {
             X509_EXTENSION_free(ext);
             (void) sk_X509_EXTENSION_delete(sk, j);
             break;
         }
    }
}

static int
reason_str_to_code(const char * reason_str) {
    int reason_code = -1, j;
    char *spaceless_reason, * sp;

    /*  Remove spaces so that the responses of
     *  get_reason() work in set_reason()  */
    if ((spaceless_reason = strdup(reason_str)) == NULL) {
        return -1;
    }

    while ((sp = strchr(spaceless_reason, ' '))) {
       memmove(sp, sp+1, strlen(sp));
    }

    for (j = 0; j < NUM_REASONS; j++) {
        if(crl_reasons[j] && !strcasecmp(spaceless_reason, crl_reasons[j])) {
            reason_code = j;
            break;
        }
    }
    free(spaceless_reason);
    return reason_code;
}


static char crypto_Revoked_set_reason_doc[] = "\n\
Set the reason of a Revoked object.\n\
\n\
@param reason: The reason string.\n\
@type reason: L{str}\n\
@return: None\n\
";
static PyObject *
crypto_Revoked_set_reason(crypto_RevokedObj *self, PyObject *args, PyObject *keywds) {
    static char *kwlist[] = {"reason", NULL};
    const char *reason_str = NULL;
    int reason_code;
    ASN1_ENUMERATED *rtmp = NULL;

    if (!PyArg_ParseTupleAndKeywords(
            args, keywds, "O&:set_reason", kwlist,
            crypto_byte_converter, &reason_str)) {
        return NULL;
    }

    if(reason_str == NULL) {
        delete_reason(self->revoked->extensions);
        goto done;
    }

    reason_code = reason_str_to_code(reason_str);
    if (reason_code == -1) {
        PyErr_SetString(PyExc_ValueError, "bad reason string");
        return NULL;
    }

    rtmp = ASN1_ENUMERATED_new();
    if (!rtmp || !ASN1_ENUMERATED_set(rtmp, reason_code)) {
        goto err;
    }
    delete_reason(self->revoked->extensions);
    if (!X509_REVOKED_add1_ext_i2d(self->revoked, NID_crl_reason, rtmp, 0, 0)) {
        goto err;
    }

 done:
    Py_INCREF(Py_None);
    return Py_None;

 err:
    exception_from_error_queue(crypto_Error);
    return NULL;
}


static char crypto_Revoked_get_reason_doc[] = "\n\
Return the reason of a Revoked object.\n\
\n\
@return: The reason as a string\n\
";
static PyObject *
crypto_Revoked_get_reason(crypto_RevokedObj *self, PyObject *args) {
    X509_EXTENSION * ext;
    int j;
    STACK_OF(X509_EXTENSION) *sk = NULL;

    if (!PyArg_ParseTuple(args, ":get_reason")) {
        return NULL;
    }

    sk = self->revoked->extensions;
    for (j = 0; j < sk_X509_EXTENSION_num(sk); j++) {
         ext = sk_X509_EXTENSION_value(sk, j);
         if (OBJ_obj2nid(ext->object) == NID_crl_reason) {
             return X509_EXTENSION_value_to_PyString(ext);
         }
    }

    Py_INCREF(Py_None);
    return Py_None;
}


static char crypto_Revoked_get_rev_date_doc[] = "\n\
Retrieve the revocation date\n\
\n\
@return: A string giving the timestamp, in the format:\n\
\n\
                 YYYYMMDDhhmmssZ\n\
                 YYYYMMDDhhmmss+hhmm\n\
                 YYYYMMDDhhmmss-hhmm\n\
";

static PyObject*
crypto_Revoked_get_rev_date(crypto_RevokedObj *self, PyObject *args) {
    /* returns a borrowed reference.  */
    return _get_asn1_time(
        ":get_rev_date", self->revoked->revocationDate, args);
}

static char crypto_Revoked_set_rev_date_doc[] = "\n\
Set the revocation timestamp\n\
\n\
@param when: A string giving the timestamp, in the format:\n\
\n\
                 YYYYMMDDhhmmssZ\n\
                 YYYYMMDDhhmmss+hhmm\n\
                 YYYYMMDDhhmmss-hhmm\n\
\n\
@return: None\n\
";

static PyObject*
crypto_Revoked_set_rev_date(crypto_RevokedObj *self, PyObject *args) {
    return _set_asn1_time(
        BYTESTRING_FMT ":set_rev_date", self->revoked->revocationDate, args);
}

/* The integer is converted to an upper-case hex string
 * without a '0x' prefix. */
static PyObject *
ASN1_INTEGER_to_PyString(ASN1_INTEGER *asn1_int) {
    BIO *bio = NULL;
    PyObject *str = NULL;
    int str_len;
    char *tmp_str;

    /* Create a openssl BIO buffer */
    bio = BIO_new(BIO_s_mem());
    if (bio == NULL) {
        goto err;
    }

    /* Write the integer to the BIO as a hex string. */
    if (i2a_ASN1_INTEGER(bio, asn1_int) < 0) {
        goto err;
    }

    /* Convert to a Python string. */
    str_len = BIO_get_mem_data(bio, &tmp_str);
    str = PyBytes_FromStringAndSize(tmp_str, str_len);

    /* Cleanup */
    BIO_free(bio);
    return str;

 err:
    if (bio) {
        BIO_free(bio);
    }
    if (str) {
        Py_DECREF(str);
    }
    return NULL;
}


static char crypto_Revoked_get_serial_doc[] = "\n\
Return the serial number of a Revoked structure\n\
\n\
@return: The serial number as a string\n\
";
static PyObject *
crypto_Revoked_get_serial(crypto_RevokedObj *self, PyObject *args) {
    if (!PyArg_ParseTuple(args, ":get_serial")) {
        return NULL;
    }

    if (self->revoked->serialNumber == NULL) {
        /* never happens */
        Py_INCREF(Py_None);
        return Py_None;
    } else {
        return ASN1_INTEGER_to_PyString(self->revoked->serialNumber);
    }
}

static char crypto_Revoked_set_serial_doc[] = "\n\
Set the serial number of a revoked Revoked structure\n\
\n\
@param hex_str: The new serial number.\n\
@type hex_str: L{str}\n\
@return: None\n\
";
static PyObject *
crypto_Revoked_set_serial(crypto_RevokedObj *self, PyObject *args, PyObject *keywds) {
    static char *kwlist[] = {"hex_str", NULL};
    const char *hex_str = NULL;
    BIGNUM *serial = NULL;
    ASN1_INTEGER *tmpser = NULL;

    if (!PyArg_ParseTupleAndKeywords(args, keywds, BYTESTRING_FMT ":set_serial",
                                     kwlist, &hex_str)) {
        return NULL;
    }

    if (!BN_hex2bn(&serial, hex_str) ) {
        PyErr_SetString(PyExc_ValueError, "bad hex string");
        return NULL;
    }

    tmpser = BN_to_ASN1_INTEGER(serial, NULL);
    BN_free(serial);
    serial = NULL;
    X509_REVOKED_set_serialNumber(self->revoked, tmpser);
    ASN1_INTEGER_free(tmpser);

    Py_INCREF(Py_None);
    return Py_None;
}


crypto_RevokedObj *
crypto_Revoked_New(X509_REVOKED *revoked) {
    crypto_RevokedObj *self;

    self = PyObject_New(crypto_RevokedObj, &crypto_Revoked_Type);
    if (self == NULL) {
        return NULL;
    }
    self->revoked = revoked;
    return self;
}

/*
 * ADD_METHOD(name) expands to a correct PyMethodDef declaration
 *   {  'name', (PyCFunction)crypto_Revoked_name, METH_VARARGS, crypto_Revoked_name_doc }
 * for convenience
 */
#define ADD_METHOD(name)        \
    { #name, (PyCFunction)crypto_Revoked_##name, METH_VARARGS, crypto_Revoked_##name##_doc }
#define ADD_KW_METHOD(name)        \
    { #name, (PyCFunction)crypto_Revoked_##name, METH_VARARGS | METH_KEYWORDS, crypto_Revoked_##name##_doc }
static PyMethodDef crypto_Revoked_methods[] = {
    ADD_METHOD(all_reasons),
    ADD_METHOD(get_reason),
    ADD_KW_METHOD(set_reason),
    ADD_METHOD(get_rev_date),
    ADD_METHOD(set_rev_date),
    ADD_METHOD(get_serial),
    ADD_KW_METHOD(set_serial),
    { NULL, NULL }
};
#undef ADD_METHOD


static void
crypto_Revoked_dealloc(crypto_RevokedObj *self) {
    X509_REVOKED_free(self->revoked);
    self->revoked = NULL;

    PyObject_Del(self);
}

static char crypto_Revoked_doc[] = "\n\
Revoked() -> Revoked instance\n\
\n\
Create a new empty Revoked object.\n\
\n\
@returns: The Revoked object\n\
";

static PyObject* crypto_Revoked_new(PyTypeObject *subtype, PyObject *args, PyObject *kwargs) {
    if (!PyArg_ParseTuple(args, ":Revoked")) {
        return NULL;
    }

    return (PyObject *)crypto_Revoked_New(X509_REVOKED_new());
}

PyTypeObject crypto_Revoked_Type = {
    PyOpenSSL_HEAD_INIT(&PyType_Type, 0)
    "Revoked",
    sizeof(crypto_RevokedObj),
    0,
    (destructor)crypto_Revoked_dealloc,
    NULL, /* print */
    NULL, /* getattr */
    NULL, /* setattr */
    NULL, /* compare */
    NULL, /* repr */
    NULL, /* as_number */
    NULL, /* as_sequence */
    NULL, /* as_mapping */
    NULL, /* hash */
    NULL, /* call */
    NULL, /* str */
    NULL, /* getattro */
    NULL, /* setattro */
    NULL, /* as_buffer */
    Py_TPFLAGS_DEFAULT,
    crypto_Revoked_doc, /* doc */
    NULL, /* traverse */
    NULL, /* clear */
    NULL, /* tp_richcompare */
    0, /* tp_weaklistoffset */
    NULL, /* tp_iter */
    NULL, /* tp_iternext */
    crypto_Revoked_methods, /* tp_methods */
    NULL, /* tp_members */
    NULL, /* tp_getset */
    NULL, /* tp_base */
    NULL, /* tp_dict */
    NULL, /* tp_descr_get */
    NULL, /* tp_descr_set */
    0, /* tp_dictoffset */
    NULL, /* tp_init */
    NULL, /* tp_alloc */
    crypto_Revoked_new, /* tp_new */
};

int init_crypto_revoked(PyObject *module) {
    if(PyType_Ready(&crypto_Revoked_Type) < 0) {
        return 0;
    }

    /* PyModule_AddObject steals a reference.
     */
    Py_INCREF((PyObject *)&crypto_Revoked_Type);
    if (PyModule_AddObject(module, "Revoked", (PyObject *)&crypto_Revoked_Type) != 0) {
        return 0;
    }
    return 1;
}
