diff --git a/.gitignore b/.gitignore index f356293..6203a47 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ logs results npm-debug.log +node_modules +build/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a53b0c3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2013, Santiago Gimeno + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md index d5f282a..45f3cac 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,54 @@ node-pcsclite ============= Bindings over pcsclite to access Smart Cards + +Example + + var pcsc = require('pcsclite'); + + var pcsc = pcsc(); + /* Check for new card reader detection */ + pcsc.on('reader', function(reader) { + console.log('New reader detected', reader.name); + + /* Check for reader status changes such a new card insertion */ + reader.on('status', function(status) { + console.log('Status(', this.name, '):', status); + /* check what has changed */ + var changes = this.status ^ status; + if (changes) { + if ((changes & this.SCARD_STATE_EMPTY) && (status & this.SCARD_STATE_EMPTY)) { + console.log("card removed");/* card removed */ + } else if ((changes & this.SCARD_STATE_PRESENT) && (status & this.SCARD_STATE_PRESENT)) { + console.log("card inserted");/* card inserted */ + } + } + }); + + /* You can connect to a smart card */ + reader.connect(function(err, protocol) { + if (err) { + console.log(err); + } else { + console.log('Protocol(', this.name, '):', protocol); + /* And transmit data */ + reader.transmit(new Buffer([0x00, 0xB0, 0x00, 0x00, 0x20]), 40, 1, function(err, data) { + if (err) console.log(err); + else console.log('Data received', data); + }) + } + }); + + reader.on('end', function() { + console.log('Reader', this.name, 'removed'); + }); + + reader.on('error', function(err) { + console.log('Error(', this.name, '):', err.message); + }); + }); + + pcsc.on('error', function(err) { + console.log('PCSC error', err.message); + }); + diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000..79bb73f --- /dev/null +++ b/binding.gyp @@ -0,0 +1,19 @@ +{ + 'targets': [ + { + 'target_name': 'pcsclite', + 'sources': [ 'src/addon.cpp', 'src/pcsclite.cpp', 'src/cardreader.cpp' ], + 'include_dirs': [ + '/usr/include/PCSC' + ], + 'link_settings': { + 'libraries': [ + '-lpcsclite' + ], + 'library_dirs': [ + '/usr/lib' + ] + } + } + ] +} diff --git a/examples/example.js b/examples/example.js new file mode 100644 index 0000000..af0c30d --- /dev/null +++ b/examples/example.js @@ -0,0 +1,44 @@ +var pcsc = require('../index'); + +var pcsc = pcsc(); +pcsc.on('reader', function(reader) { + + console.log('New reader detected', reader.name); + + reader.on('error', function(err) { + console.log('Error(', this.name, '):', err.message); + }); + + reader.on('status', function(status) { + console.log('Status(', this.name, '):', status); + /* check what has changed */ + var changes = this.status ^ status; + if (changes) { + if ((changes & this.SCARD_STATE_EMPTY) && (status & this.SCARD_STATE_EMPTY)) { + console.log("card removed");/* card removed */ + } else if ((changes & this.SCARD_STATE_PRESENT) && (status & this.SCARD_STATE_PRESENT)) { + console.log("card inserted");/* card inserted */ + } + } + }); + + reader.on('end', function() { + console.log('Reader', this.name, 'removed'); + }); + + reader.connect(function(err, protocol) { + if (err) { + console.log(err); + } else { + console.log('Protocol(', this.name, '):', protocol); + reader.transmit(new Buffer([0x00, 0xB0, 0x00, 0x00, 0x20]), 40, 1, function(err, data) { + if (err) console.log(err); + else console.log('Data received', data); + }) + } + }); +}); + +pcsc.on('error', function(err) { + console.log('PCSC error', err.message); +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..05862f8 --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./lib/pcsclite'); diff --git a/lib/pcsclite.js b/lib/pcsclite.js new file mode 100644 index 0000000..8251309 --- /dev/null +++ b/lib/pcsclite.js @@ -0,0 +1,104 @@ +var events = require('events'); +require('buffertools'); + +/* Make sure we choose the correct build directory */ +var bindings = require('bindings')('pcsclite'); +var PCSCLite = bindings.PCSCLite; +var CardReader = bindings.CardReader; +inherits(PCSCLite, events.EventEmitter); +inherits(CardReader, events.EventEmitter); + +module.exports = function() { + + var readers = []; + var p = new PCSCLite(); + process.nextTick(function() { + p.start(function(e, data) { + if (e) { + p.emit('error', e); + } else { + /* parse data buffer to get the card reader name, and get the reader */ + var readers_aux = []; + var ini = 0; + var pos = 0; + while((pos = data.slice(ini).indexOf('\0')) > 0) { + var name = data.slice(ini, ini + pos).toString(); + var is_old = false; + for (var i = 0; i < readers.length; ++i) { + if (readers[i].name === name) { + readers_aux.push(readers[i]); + is_old = true; + break; + } + } + + if (!is_old) { + var r = new CardReader(name); + readers_aux.push(r); + p.emit('reader', r); + r.get_status(function(e, status) { + if (e) { + r.emit('error', e); + } else { + r.emit('status', status); + r.status = status; + } + }); + } + + ini += pos + 1; + } + + readers = readers_aux; + } + }); + }); + + return p; +}; + +CardReader.prototype.connect = function(cb) { + + if (!this.connected) { + this._connect(cb); + } else { + cb(); + } +}; + +CardReader.prototype.disconnect = function(cb) { + + if (this.connected) { + this._disconnect(cb); + } else { + cb(); + } +}; + +CardReader.prototype.transmit = function(data, res_len, protocol, cb) { + + if (!this.connected) { + return cb(new Error("Card Reader not connected")); + } + + this._transmit(data, res_len, protocol, cb); +}; + +CardReader.prototype.SCARD_STATE_UNAWARE = 0x0000; +CardReader.prototype.SCARD_STATE_IGNORE = 0x0001; +CardReader.prototype.SCARD_STATE_CHANGED = 0x0002; +CardReader.prototype.SCARD_STATE_UNKNOWN = 0x0004; +CardReader.prototype.SCARD_STATE_UNAVAILABLE = 0x0008; +CardReader.prototype.SCARD_STATE_EMPTY = 0x0010; +CardReader.prototype.SCARD_STATE_PRESENT = 0x0020; +CardReader.prototype.SCARD_STATE_ATRMATCH = 0x0040; +CardReader.prototype.SCARD_STATE_EXCLUSIVE = 0x0080; +CardReader.prototype.CARD_STATE_INUSE = 0x0100; +CardReader.prototype.SCARD_STATE_MUTE = 0x0200; + +// extend prototype +function inherits(target, source) { + for (var k in source.prototype) { + target.prototype[k] = source.prototype[k]; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a79a398 --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "pcsclite", + "version": "0.0.1", + "engines": { + "node": "~0.8.0" + }, + "description": "Bindings over pcsclite to access Smart Cards", + "main": "index.js", + "directories": { + "test": "test" + }, + "dependencies": { + "buffertools": "~1.1.1", + "bindings": "~1.1.0" + }, + "devDependencies": { + "mocha": "~1.11.0", + "sinon": "~1.3.4", + "should": "~1.2.2" + }, + "scripts": { + "test": "mocha", + "install": "node-gyp rebuild" + }, + "repository": "https://github.com/santigimeno/node-pcsclite.git", + "keywords": [ + "pcsc", + "pcsclite", + "smartcards" + ], + "author": "Santiago Gimeno ", + "license": { + "type": "ISC", + "url": "https://github.com/santigimeno/node-pcsclite/blob/master/LICENSE" + }, + "gypfile": true +} diff --git a/src/addon.cpp b/src/addon.cpp new file mode 100644 index 0000000..ca38b30 --- /dev/null +++ b/src/addon.cpp @@ -0,0 +1,11 @@ +#include + +#include "pcsclite.h" +#include "cardreader.h" + +void init_all(v8::Handle target) { + PCSCLite::init(target); + CardReader::init(target); +} + +NODE_MODULE(pcsclite, init_all); diff --git a/src/cardreader.cpp b/src/cardreader.cpp new file mode 100644 index 0000000..c2a3aac --- /dev/null +++ b/src/cardreader.cpp @@ -0,0 +1,470 @@ +#include "cardreader.h" +#include "common.h" + +#include +#include +#include +#include + +using namespace v8; +using namespace node; + +Persistent CardReader::constructor; + +void CardReader::init(Handle target) { + + // Prepare constructor template + Local tpl = FunctionTemplate::New(New); + tpl->SetClassName(String::NewSymbol("CardReader")); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + + // Symbol + name_symbol = NODE_PSYMBOL("name"); + connected_symbol = NODE_PSYMBOL("connected"); + + // Prototype + NODE_SET_PROTOTYPE_METHOD(tpl, "get_status", GetStatus); + NODE_SET_PROTOTYPE_METHOD(tpl, "_connect", Connect); + NODE_SET_PROTOTYPE_METHOD(tpl, "_disconnect", Disconnect); + NODE_SET_PROTOTYPE_METHOD(tpl, "_transmit", Transmit); + NODE_SET_PROTOTYPE_METHOD(tpl, "close", Close); + + constructor = Persistent::New(tpl->GetFunction()); + target->Set(String::NewSymbol("CardReader"), constructor); +} + +CardReader::CardReader(const std::string &reader_name): m_card_context(0), + m_card_handle(0), + m_name(reader_name) { + pthread_mutex_init(&m_mutex, NULL); +} + +CardReader::~CardReader() { + + fprintf(stderr, "CARDREADER destructor\n"); + + if (m_card_context) { + SCardReleaseContext(m_card_context); + } + + if (m_status_card_context) { + LONG result = SCardCancel(m_status_card_context); + fprintf(stderr, "RESULT: 0x%.8lx", result); + } + + pthread_mutex_destroy(&m_mutex); +} + +Handle CardReader::New(const Arguments& args) { + + HandleScope scope; + + v8::String::Utf8Value reader_name(args[0]->ToString()); + CardReader* obj = new CardReader(*reader_name); + obj->Wrap(args.Holder()); + obj->handle_->Set(name_symbol, args[0]->ToString()); + obj->handle_->Set(connected_symbol, Boolean::New(false)); + + return scope.Close(args.Holder()); +} + +Handle CardReader::GetStatus(const Arguments& args) { + + HandleScope scope; + + CardReader* obj = ObjectWrap::Unwrap(args.This()); + Local cb = Local::Cast(args[0]); + + AsyncBaton *async_baton = new AsyncBaton(); + async_baton->async.data = async_baton; + async_baton->callback = Persistent::New(cb); + async_baton->reader = obj; + + uv_async_init(uv_default_loop(), &async_baton->async, HandleReaderStatusChange); + pthread_create(&obj->m_status_thread, NULL, HandlerFunction, async_baton); + pthread_detach(obj->m_status_thread); + + return scope.Close(Undefined()); +} + +Handle CardReader::Connect(const Arguments& args) { + + HandleScope scope; + + if (!args[0]->IsFunction()) { + return ThrowException(Exception::TypeError( + String::New("First argument must be a callback function"))); + } + + Local cb = Local::Cast(args[0]); + + // This creates our work request, including the libuv struct. + Baton* baton = new Baton(); + baton->request.data = baton; + baton->callback = Persistent::New(cb); + baton->reader = ObjectWrap::Unwrap(args.This()); + + // Schedule our work request with libuv. Here you can specify the functions + // that should be executed in the threadpool and back in the main thread + // after the threadpool function completed. + int status = uv_queue_work(uv_default_loop(), &baton->request, DoConnect, AfterConnect); + assert(status == 0); + + return scope.Close(Undefined()); +} + +Handle CardReader::Disconnect(const Arguments& args) { + + HandleScope scope; + + if (!args[0]->IsFunction()) { + return ThrowException(Exception::TypeError( + String::New("First argument must be a callback function"))); + } + + Local cb = Local::Cast(args[0]); + + // This creates our work request, including the libuv struct. + Baton* baton = new Baton(); + baton->request.data = baton; + baton->callback = Persistent::New(cb); + baton->reader = ObjectWrap::Unwrap(args.This()); + + // Schedule our work request with libuv. Here you can specify the functions + // that should be executed in the threadpool and back in the main thread + // after the threadpool function completed. + int status = uv_queue_work(uv_default_loop(), &baton->request, DoDisconnect, AfterDisconnect); + assert(status == 0); + + return scope.Close(Undefined()); +} + +Handle CardReader::Transmit(const Arguments& args) { + + HandleScope scope; + + // The first argument is the buffer to be transmitted. + if (!Buffer::HasInstance(args[0])) { + return ThrowException(Exception::TypeError( + String::New("First argument must be a Buffer"))); + } + + // The second argument is the length of the data to be received + if (!args[1]->IsUint32()) { + return ThrowException(Exception::TypeError( + String::New("Second argument must be an integer"))); + } + + // The third argument is the protocol to be used + if (!args[2]->IsUint32()) { + return ThrowException(Exception::TypeError( + String::New("Third argument must be an integer"))); + } + + // The fourth argument is the callback function + if (!args[3]->IsFunction()) { + return ThrowException(Exception::TypeError( + String::New("Fourth argument must be a callback function"))); + } + + Local buffer_data = args[0]->ToObject(); + uint32_t out_len = args[1]->Uint32Value(); + uint32_t protocol = args[2]->Uint32Value(); + Local cb = Local::Cast(args[3]); + + // This creates our work request, including the libuv struct. + Baton* baton = new Baton(); + baton->request.data = baton; + baton->callback = Persistent::New(cb); + baton->reader = ObjectWrap::Unwrap(args.This()); + TransmitInput *ti = new TransmitInput(); + ti->card_protocol = protocol; + ti->in_data = new unsigned char[Buffer::Length(buffer_data)]; + ti->in_len = Buffer::Length(buffer_data); + memcpy(ti->in_data, Buffer::Data(buffer_data), ti->in_len); + + ti->out_len = out_len; + baton->input = ti; + + // Schedule our work request with libuv. Here you can specify the functions + // that should be executed in the threadpool and back in the main thread + // after the threadpool function completed. + int status = uv_queue_work(uv_default_loop(), &baton->request, DoTransmit, AfterTransmit); + assert(status == 0); + + return scope.Close(Undefined()); +} + +Handle CardReader::Close(const Arguments& args) { + + HandleScope scope; + + CardReader* obj = ObjectWrap::Unwrap(args.This()); + + LONG result = SCardCancel(obj->m_status_card_context); + obj->m_status_card_context = 0; + + return scope.Close(Integer::New(result)); +} + +void CardReader::HandleReaderStatusChange(uv_async_t *handle, int status) { + + AsyncBaton* async_baton = static_cast(handle->data); + AsyncResult* ar = async_baton->async_result; + + if (ar->do_exit) { + uv_close(reinterpret_cast(&async_baton->async), CloseCallback); // necessary otherwise UV will block + + /* Emit end event */ + Handle argv[1] = { + String::New("end"), // event name + }; + + MakeCallback(async_baton->reader->handle_, "emit", 1, argv); + return; + } + + if (ar->result == SCARD_S_SUCCESS) { + const unsigned argc = 2; + Handle argv[argc] = { + Undefined(), // argument + Integer::New(ar->status) + }; + + PerformCallback(async_baton->reader->handle_, async_baton->callback, argc, argv); + } else { + Local err = Exception::Error(String::New(pcsc_stringify_error(ar->result))); + // Prepare the parameters for the callback function. + const unsigned argc = 1; + Handle argv[argc] = { err }; + PerformCallback(async_baton->reader->handle_, async_baton->callback, argc, argv); + } +} + +void* CardReader::HandlerFunction(void* arg) { + + AsyncBaton* async_baton = static_cast(arg); + CardReader* reader = async_baton->reader; + async_baton->async_result = new AsyncResult(); + async_baton->async_result->do_exit = false; + + /* Lock mutex */ + pthread_mutex_lock(&reader->m_mutex); + LONG result = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &reader->m_status_card_context); + /* Unlock the mutex */ + pthread_mutex_unlock(&reader->m_mutex); + + SCARD_READERSTATE card_reader_state; + card_reader_state.szReader = reader->m_name.c_str(); + card_reader_state.dwCurrentState = SCARD_STATE_UNAWARE; + + while(result == SCARD_S_SUCCESS && + !((card_reader_state.dwCurrentState & SCARD_STATE_UNKNOWN) || + (card_reader_state.dwCurrentState & SCARD_STATE_UNAVAILABLE))) { + result = SCardGetStatusChange(reader->m_status_card_context, INFINITE, &card_reader_state, 1); + async_baton->async_result->result = result; + async_baton->async_result->status = card_reader_state.dwEventState; + uv_async_send(&async_baton->async); + card_reader_state.dwCurrentState = card_reader_state.dwEventState; + } + + async_baton->async_result->do_exit = true; + uv_async_send(&async_baton->async); + + return NULL; +} + +void CardReader::DoConnect(uv_work_t* req) { + + Baton* baton = static_cast(req->data); + + unsigned long card_protocol; + LONG result = SCARD_S_SUCCESS; + CardReader* obj = baton->reader; + + /* Lock mutex */ + pthread_mutex_lock(&obj->m_mutex); + /* Is context established */ + if (!obj->m_card_context) { + result = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &obj->m_card_context); + } + + /* Connect */ + if (result == SCARD_S_SUCCESS) { + result = SCardConnect(obj->m_card_context, obj->m_name.c_str(), + SCARD_SHARE_EXCLUSIVE, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, + &obj->m_card_handle, &card_protocol); + } + + /* Unlock the mutex */ + pthread_mutex_unlock(&obj->m_mutex); + + ConnectResult *cr = new ConnectResult(); + cr->result = result; + if (!result) { + cr->card_protocol = card_protocol; + } + + baton->result = cr; +} + +void CardReader::AfterConnect(uv_work_t* req) { + + HandleScope scope; + Baton* baton = static_cast(req->data); + ConnectResult *cr = static_cast(baton->result); + + if (cr->result) { + Local err = Exception::Error(String::New(pcsc_stringify_error(cr->result))); + // Prepare the parameters for the callback function. + const unsigned argc = 1; + Handle argv[argc] = { err }; + PerformCallback(baton->reader->handle_, baton->callback, argc, argv); + } else { + baton->reader->handle_->Set(connected_symbol, Boolean::New(true)); + const unsigned argc = 2; + Handle argv[argc] = { + Local::New(Null()), + Integer::New(cr->card_protocol) + }; + + PerformCallback(baton->reader->handle_, baton->callback, argc, argv); + } + + // The callback is a permanent handle, so we have to dispose of it manually. + baton->callback.Dispose(); + delete cr; + delete baton; +} + +void CardReader::DoDisconnect(uv_work_t* req) { + + Baton* baton = static_cast(req->data); + + LONG result = SCARD_S_SUCCESS; + CardReader* obj = baton->reader; + + /* Lock mutex */ + pthread_mutex_lock(&obj->m_mutex); + /* Connect */ + if (obj->m_card_handle) { + result = SCardDisconnect(obj->m_card_handle, SCARD_UNPOWER_CARD); + if (result == SCARD_S_SUCCESS) { + obj->m_card_handle = 0; + } + } + + /* Unlock the mutex */ + pthread_mutex_unlock(&obj->m_mutex); + + baton->result = reinterpret_cast(result); +} + +void CardReader::AfterDisconnect(uv_work_t* req) { + + HandleScope scope; + Baton* baton = static_cast(req->data); + LONG result = reinterpret_cast(baton->result); + + if (result) { + Local err = Exception::Error(String::New(pcsc_stringify_error(result))); + + // Prepare the parameters for the callback function. + const unsigned argc = 1; + Handle argv[argc] = { err }; + PerformCallback(baton->reader->handle_, baton->callback, argc, argv); + } else { + baton->reader->handle_->Set(connected_symbol, Boolean::New(false)); + const unsigned argc = 1; + Handle argv[argc] = { + Local::New(Null()) + }; + + PerformCallback(baton->reader->handle_, baton->callback, argc, argv); + } + + // The callback is a permanent handle, so we have to dispose of it manually. + baton->callback.Dispose(); + + delete baton; +} + +void CardReader::DoTransmit(uv_work_t* req) { + + Baton* baton = static_cast(req->data); + TransmitInput *ti = static_cast(baton->input); + CardReader* obj = baton->reader; + + SCARD_IO_REQUEST io_request; + TransmitResult *tr = new TransmitResult(); + tr->data = new unsigned char[ti->out_len]; + tr->len = ti->out_len; + LONG result = SCARD_E_INVALID_HANDLE; + + /* Lock mutex */ + pthread_mutex_lock(&obj->m_mutex); + /* Connected? */ + if (obj->m_card_handle) { + result = SCardTransmit(obj->m_card_handle, SCARD_PCI_T0, ti->in_data, ti->in_len, + &io_request, tr->data, &tr->len); + } + + /* Unlock the mutex */ + pthread_mutex_unlock(&obj->m_mutex); + + tr->result = result; + + baton->result = tr; +} + +void CardReader::AfterTransmit(uv_work_t* req) { + + HandleScope scope; + Baton* baton = static_cast(req->data); + TransmitInput *ti = static_cast(baton->input); + TransmitResult *tr = static_cast(baton->result); + + if (tr->result) { + Local err = Exception::Error(String::New(pcsc_stringify_error(tr->result))); + + // Prepare the parameters for the callback function. + const unsigned argc = 1; + Handle argv[argc] = { err }; + PerformCallback(baton->reader->handle_, baton->callback, argc, argv); + } else { + const unsigned argc = 2; + // get Buffer from global scope. + Local global = v8::Context::GetCurrent()->Global(); + Local bv = global->Get(String::NewSymbol("Buffer")); + assert(bv->IsFunction()); + Local b = Local::Cast(bv); + Handle argv1[3] = { Buffer::New(reinterpret_cast(tr->data), tr->len)->handle_, Integer::New(tr->len) , Integer::New(0) }; + Handle instance = b->NewInstance(3, argv1); + Handle argv[argc] = { + Local::New(Null()), + instance + }; + + PerformCallback(baton->reader->handle_, baton->callback, argc, argv); + } + + + // The callback is a permanent handle, so we have to dispose of it manually. + baton->callback.Dispose(); + delete [] ti->in_data; + delete ti; + delete [] tr->data; + delete tr; + delete baton; +} + +void CardReader::CloseCallback(uv_handle_t *handle) { + + /* cleanup process */ + AsyncBaton* async_baton = static_cast(handle->data); + AsyncResult* ar = async_baton->async_result; + delete ar; + async_baton->callback.Dispose(); + SCardReleaseContext(async_baton->reader->m_status_card_context); + delete async_baton; +} diff --git a/src/cardreader.h b/src/cardreader.h new file mode 100644 index 0000000..f15e2c1 --- /dev/null +++ b/src/cardreader.h @@ -0,0 +1,93 @@ +#ifndef CARDREADER_H +#define CARDREADER_H + +#include +#include +#include +#include + +static v8::Persistent name_symbol; +static v8::Persistent connected_symbol; + +class CardReader: public node::ObjectWrap { + + // We use a struct to store information about the asynchronous "work request". + struct Baton { + uv_work_t request; + v8::Persistent callback; + CardReader *reader; + void *input; + void *result; + }; + + struct ConnectResult { + LONG result; + unsigned long card_protocol; + }; + + struct TransmitInput { + uint32_t card_protocol; + unsigned char *in_data; + unsigned long in_len; + unsigned long out_len; + }; + + struct TransmitResult { + LONG result; + unsigned char *data; + unsigned long len; + }; + + struct AsyncResult { + LONG result; + unsigned long status; + bool do_exit; + }; + + struct AsyncBaton { + uv_async_t async; + v8::Persistent callback; + CardReader *reader; + AsyncResult *async_result; + }; + + public: + + static void init(v8::Handle target); + + + private: + + CardReader(const std::string &reader_name); + + ~CardReader(); + + static v8::Persistent constructor; + static v8::Handle New(const v8::Arguments& args); + static v8::Handle GetStatus(const v8::Arguments& args); + static v8::Handle Connect(const v8::Arguments& args); + static v8::Handle Disconnect(const v8::Arguments& args); + static v8::Handle Transmit(const v8::Arguments& args); + static v8::Handle Close(const v8::Arguments& args); + + static void HandleReaderStatusChange(uv_async_t *handle, int status); + static void* HandlerFunction(void* arg); + static void DoConnect(uv_work_t* req); + static void AfterConnect(uv_work_t* req); + static void DoDisconnect(uv_work_t* req); + static void AfterDisconnect(uv_work_t* req); + static void DoTransmit(uv_work_t* req); + static void AfterTransmit(uv_work_t* req); + static void CloseCallback(uv_handle_t *handle); + + private: + + SCARDCONTEXT m_card_context; + SCARDCONTEXT m_status_card_context; + SCARDHANDLE m_card_handle; + std::string m_name; + pthread_t m_status_thread; + pthread_mutex_t m_mutex; +}; + +#endif /* CARDREADER_H */ diff --git a/src/common.h b/src/common.h new file mode 100644 index 0000000..00370cc --- /dev/null +++ b/src/common.h @@ -0,0 +1,21 @@ +#ifndef COMMON_H +#define COMMON_H + +#include + +static void PerformCallback(v8::Handle object, + v8::Persistent &callback, + const unsigned argc, v8::Handle *argv) { + + // Wrap the callback function call in a TryCatch so that we can call + // node's FatalException afterwards. This makes it possible to catch + // the exception from JavaScript land using the + // process.on('uncaughtException') event. + v8::TryCatch try_catch; + callback->Call(object, argc, argv); + if (try_catch.HasCaught()) { + node::FatalException(try_catch); + } +} + +#endif /* COMMON_H */ diff --git a/src/pcsclite.cpp b/src/pcsclite.cpp new file mode 100644 index 0000000..ed5f949 --- /dev/null +++ b/src/pcsclite.cpp @@ -0,0 +1,197 @@ +#include "pcsclite.h" +#include "common.h" + +#include +#include +#include +#include +#include + +using namespace v8; +using namespace node; + +Persistent PCSCLite::constructor; + +void PCSCLite::init(Handle target) { + + // Prepare constructor template + Local tpl = FunctionTemplate::New(New); + tpl->SetClassName(String::NewSymbol("PCSCLite")); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + // Prototype + NODE_SET_PROTOTYPE_METHOD(tpl, "start", Start); + NODE_SET_PROTOTYPE_METHOD(tpl, "close", Close); + + constructor = Persistent::New(tpl->GetFunction()); + target->Set(String::NewSymbol("PCSCLite"), constructor); +} + +PCSCLite::PCSCLite(): m_card_context(0) { + pthread_mutex_init(&m_mutex, NULL); +} + +PCSCLite::~PCSCLite() { + + fprintf(stderr, "PCSCLite destructor\n"); + + if (m_card_context) { + SCardReleaseContext(m_card_context); + } + + pthread_mutex_destroy(&m_mutex); + pthread_cancel(m_status_thread); +} + +Handle PCSCLite::New(const Arguments& args) { + + HandleScope scope; + PCSCLite* obj = new PCSCLite(); + obj->Wrap(args.Holder()); + return scope.Close(args.Holder()); +} + +Handle PCSCLite::Start(const Arguments& args) { + + HandleScope scope; + + PCSCLite* obj = ObjectWrap::Unwrap(args.This()); + Local cb = Local::Cast(args[0]); + + AsyncBaton *async_baton = new AsyncBaton(); + async_baton->async.data = async_baton; + async_baton->callback = Persistent::New(cb); + async_baton->pcsclite = obj; + + uv_async_init(uv_default_loop(), &async_baton->async, HandleReaderStatusChange); + pthread_create(&obj->m_status_thread, NULL, HandlerFunction, async_baton); + pthread_detach(obj->m_status_thread); + + return scope.Close(Undefined()); +} + +Handle PCSCLite::Close(const Arguments& args) { + + HandleScope scope; + + PCSCLite* obj = ObjectWrap::Unwrap(args.This()); + + LONG result = SCardCancel(obj->m_card_context); + + return scope.Close(Integer::New(result)); +} + +void PCSCLite::HandleReaderStatusChange(uv_async_t *handle, int status) { + + AsyncBaton* async_baton = static_cast(handle->data); + PCSCLite* pcsclite = async_baton->pcsclite; + AsyncResult* ar = async_baton->async_result; + + if (ar->do_exit) { + uv_close(reinterpret_cast(&async_baton->async), CloseCallback); // necessary otherwise UV will block + return; + } + + if ((ar->result == SCARD_S_SUCCESS) || (ar->result == (LONG)SCARD_E_NO_READERS_AVAILABLE)) { + const unsigned argc = 2; + Handle argv[argc] = { + Undefined(), // argument + Buffer::New(ar->readers_name, ar->readers_name_length)->handle_ + }; + + PerformCallback(async_baton->pcsclite->handle_, async_baton->callback, argc, argv); + } else { + Local err = Exception::Error(String::New(pcsc_stringify_error(ar->result))); + // Prepare the parameters for the callback function. + const unsigned argc = 1; + Handle argv[argc] = { err }; + PerformCallback(async_baton->pcsclite->handle_, async_baton->callback, argc, argv); + } + + /* reset AsyncResult */ + delete [] ar->readers_name; + ar->readers_name = NULL; + ar->readers_name_length = 0; + ar->result = SCARD_S_SUCCESS; + /* Unlock the mutex */ + pthread_mutex_unlock(&pcsclite->m_mutex); +} + +void* PCSCLite::HandlerFunction(void* arg) { + + LONG result = SCARD_S_SUCCESS; + AsyncBaton* async_baton = static_cast(arg); + PCSCLite* pcsclite = async_baton->pcsclite; + async_baton->async_result = new AsyncResult(); + + SCARD_READERSTATE card_reader_state; + card_reader_state.szReader = "\\\\?PnP?\\Notification"; + card_reader_state.dwCurrentState = SCARD_STATE_UNAWARE; + + while(result == SCARD_S_SUCCESS) { + /* Lock mutex. It'll be unlocked after the callback has been sent */ + pthread_mutex_lock(&pcsclite->m_mutex); + /* Get card readers */ + result = pcsclite->get_card_readers(pcsclite, async_baton->async_result); + /* Store the result in the baton */ + async_baton->async_result->result = result; + /* Notify the nodejs thread */ + uv_async_send(&async_baton->async); + /* Start checking for status change */ + result = SCardGetStatusChange(pcsclite->m_card_context, INFINITE, &card_reader_state, 1); + } + + async_baton->async_result->do_exit = true; + uv_async_send(&async_baton->async); + + return NULL; +} + +void PCSCLite::CloseCallback(uv_handle_t *handle) { + + /* cleanup process */ + AsyncBaton* async_baton = static_cast(handle->data); + AsyncResult* ar = async_baton->async_result; + delete [] ar->readers_name; + delete ar; + async_baton->callback.Dispose(); + delete async_baton; +} + +LONG PCSCLite::get_card_readers(PCSCLite* pcsclite, AsyncResult* async_result) { + + LONG result = SCARD_S_SUCCESS; + + /* Reset the readers_name in the baton */ + async_result->readers_name = NULL; + async_result->readers_name_length = 0; + + if (!pcsclite->m_card_context) { + result = SCardEstablishContext(SCARD_SCOPE_SYSTEM, NULL, NULL, &pcsclite->m_card_context); + } + + if (result != SCARD_S_SUCCESS) { + return result; + } + + /* Find out ReaderNameLength */ + unsigned long readers_name_length; + result = SCardListReaders(pcsclite->m_card_context, NULL, NULL, &readers_name_length); + if (result != SCARD_S_SUCCESS) { + return result; + } + + /* Allocate Memory for ReaderName and retrieve all readers in the terminal */ + char* readers_name = new char[readers_name_length]; + result = SCardListReaders(pcsclite->m_card_context, NULL, readers_name, &readers_name_length); + if (result != SCARD_S_SUCCESS) { + delete [] readers_name; + readers_name = NULL; + readers_name_length = 0; + } + + /* Store the readers_name in the baton */ + async_result->readers_name = readers_name; + async_result->readers_name_length = readers_name_length; + + return result; +} diff --git a/src/pcsclite.h b/src/pcsclite.h new file mode 100644 index 0000000..8dee16c --- /dev/null +++ b/src/pcsclite.h @@ -0,0 +1,51 @@ +#ifndef PCSCLITE_H +#define PCSCLITE_H + +#include +#include + +class PCSCLite: public node::ObjectWrap { + + struct AsyncResult { + LONG result; + char *readers_name; + unsigned long readers_name_length; + bool do_exit; + }; + + struct AsyncBaton { + uv_async_t async; + v8::Persistent callback; + PCSCLite *pcsclite; + AsyncResult *async_result; + }; + + public: + + static void init(v8::Handle target); + + private: + + PCSCLite(); + + ~PCSCLite(); + + static v8::Persistent constructor; + static v8::Handle New(const v8::Arguments& args); + static v8::Handle Start(const v8::Arguments& args); + static v8::Handle Close(const v8::Arguments& args); + + static void HandleReaderStatusChange(uv_async_t *handle, int status); + static void* HandlerFunction(void* arg); + static void CloseCallback(uv_handle_t *handle); + + LONG get_card_readers(PCSCLite* pcsclite, AsyncResult* async_result); + + private: + + SCARDCONTEXT m_card_context; + pthread_t m_status_thread; + pthread_mutex_t m_mutex; +}; + +#endif /* PCSCLITE_H */ diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..2bdfabc --- /dev/null +++ b/test/test.js @@ -0,0 +1,156 @@ +var should = require('should'); +var sinon = require('sinon'); +var pcsc = require('../lib/pcsclite'); +var assert = require('assert'); + +describe('Testing PCSCLite private', function() { + + describe('#start()', function() { + it('#start() stub', function(done) { + var p = pcsc(); + var stub = sinon.stub(p, 'start', function(my_cb) { + /* "MyReader\0" */ + my_cb(undefined, new Buffer("MyReader\0")); + }); + + p.on('reader', function(reader) { + reader.name.should.equal("MyReader"); + done(); + }); + }); + + it('#start() stub', function() { + var cb = sinon.spy(); + var p = pcsc(); + var stub = sinon.stub(p, 'start', function(my_cb) { + /* "MyReader" */ + my_cb(undefined, new Buffer("MyReader")); + }); + + p.on('reader', cb); + process.nextTick(function () { + sinon.assert.notCalled(cb); + }); + }); + + it('#start() stub', function() { + var cb = sinon.spy(); + var p = pcsc(); + var stub = sinon.stub(p, 'start', function(my_cb) { + /* "MyReader1\0MyReader2\0" */ + my_cb(undefined, new Buffer("MyReader1\0MyReader2\0")); + }); + + p.on('reader', cb); + process.nextTick(function () { + sinon.assert.calledTwice(cb); + assert(cb.args[0][0]['name'], "MyReader1"); + assert(cb.args[1][0]['name'], "MyReader2"); + }); + }); + }); +}); + +describe('Testing CardReader private', function() { + + var get_reader = function() { + var p = pcsc(); + var stub = sinon.stub(p, 'start', function(my_cb) { + /* "MyReader\0" */ + my_cb(undefined, new Buffer("MyReader\0")); + }); + + return p; + }; + + describe('#_connect()', function() { + + it('#_connect() success', function(done) { + var p = get_reader(); + p.on('reader', function(reader) { + var connect_stub = sinon.stub(reader, '_connect', function(connect_cb) { + connect_cb(undefined, 1); + }); + + reader.connect(function(err, protocol) { + should.not.exist(err); + protocol.should.equal(1); + done(); + }); + }); + }); + + it('#_connect() error', function() { + var p = get_reader(); + p.on('reader', function(reader) { + var cb = sinon.spy(); + var connect_stub = sinon.stub(reader, '_connect', function(connect_cb) { + connect_cb(""); + }); + + reader.connect(cb); + sinon.assert.calledOnce(cb); + }); + }); + + it('#_connect() already connected', function() { + var p = get_reader(); + p.on('reader', function(reader) { + var cb = sinon.spy(); + reader.connected = true; + + reader.connect(cb); + process.nextTick(function () { + sinon.assert.calledOnce(cb); + }); + }); + }); + + }); + + describe('#_disconnect()', function() { + + it('#_disconnect() success', function() { + var p = get_reader(); + p.on('reader', function(reader) { + reader.connected = true; + var cb = sinon.spy(); + var connect_stub = sinon.stub(reader, '_disconnect', function(disconnect_cb) { + disconnect_cb(undefined); + }); + + reader.disconnect(cb); + sinon.assert.calledOnce(cb); + }); + }); + + it('#_disconnect() error', function() { + var p = get_reader(); + p.on('reader', function(reader) { + reader.connected = true; + var cb = sinon.spy(); + var connect_stub = sinon.stub(reader, '_disconnect', function(disconnect_cb) { + disconnect_cb(""); + }); + + reader.disconnect(cb); + sinon.assert.calledOnce(cb); + }); + }); + + it('#_disconnect() already disconnected', function() { + var p = get_reader(); + p.on('reader', function(reader) { + var cb = sinon.spy(); + var connect_stub = sinon.stub(reader, '_disconnect', function(disconnect_cb) { + disconnect_cb(undefined); + }); + + reader.disconnect(cb); + process.nextTick(function () { + sinon.assert.calledOnce(cb); + }); + }); + }); + }); +});