NodeJS : Недокументированные IPC в nodejs для windows

Nodejs предоставляет довольно скромные возможности IPC в принципе, а для windows уж и подавно. Конечно, можно для коммуникации в пределах одной машины делать клиент-сервера на сокетах, если это windows. Так же можно юзать UNIX Sockets для линукса и прочих производных, которые это дело поддерживают. Так же известны костыли с mkfifo,  но это *nix и потому нам не интересны. В официальной документации (на данный момент для версии v0.10.26) ничего про IPC  винде не сказано в принципе. Можно юзать модуль CLUSTER, но мне это решение показалось топорным, так как идеальным для меня было бы некая IPC, от которой мог бы абстрагироваться класс Socket.

В первую очередь, необходимо было выяснить, какие средства нам  предоставляет libuv (https://github.com/joyent/libuv), так как на ней  стоится весь слой абстракции над ОС. Названия функций libuv стандартизированы в виде uv_PROTO_ACT, где  PROTO — протокол, для которого написана группа функций, а ACT — функция например :
uv_udp_bind
uv_udp_open
uv_udp_getsockname
Благодаря этому, поиск поддерживаемых функций несложен. Идеаль IPC для нас это, конечноже, пайпы, так как в винде они написаны  отлично и довольно быстро работают. Довольно просто и быстро были найдены вкусные названия :
/*
 * Initialize a pipe. The last argument is a boolean to indicate if
 * this pipe will be used for handle passing between processes.
 */
UV_EXTERN int uv_pipe_init(uv_loop_t*, uv_pipe_t* handle, int ipc);

/*
 * Opens an existing file descriptor or HANDLE as a pipe.
 */
UV_EXTERN int uv_pipe_open(uv_pipe_t*, uv_file file);

/*
 * Bind the pipe to a file path (UNIX) or a name (Windows.)
 *
 * Paths on UNIX get truncated to `sizeof(sockaddr_un.sun_path)` bytes,
 * typically between 92 and 108 bytes.
 */
UV_EXTERN int uv_pipe_bind(uv_pipe_t* handle, const char* name);

открываем uv_pipe_open (src/win/pipe.c), видим классическую картину :

/* Creates a pipe server. */
int uv_pipe_bind(uv_pipe_t* handle, const char* name) {
  uv_loop_t* loop = handle->loop;
  int i, err, nameSize;
  uv_pipe_accept_t* req;
  
  // ---------8<-------------------- 
  
  handle->accept_reqs[0].pipeHandle = CreateNamedPipeW(handle->name,
      PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
      FILE_FLAG_FIRST_PIPE_INSTANCE,
      PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
      PIPE_UNLIMITED_INSTANCES, 65536, 65536, 0, NULL);
  
  // ---------8<-------------------- 
  
  return uv_translate_sys_error(err);
}

То что надо. Все уже написано до нас. Осталось найти как это использовать.
В ноде этот код вызывается из pipe_wrap.cc :

Handle<Value> PipeWrap::Open(const Arguments& args) {
  HandleScope scope;

  UNWRAP(PipeWrap)

  int fd = args[0]->IntegerValue();

  uv_pipe_open(&wrap->handle_, fd);

  return scope.Close(v8::Null());
}

Сам же PipeWrap проврапплен в жс под видом нативного модуля pipe_wrap и может быть найден простым вызовом require(‘pipe_wrap’) :

NODE_MODULE(node_pipe_wrap, node::PipeWrap::Initialize)

Следующий шаг — поиск pipe_wrap по модулям в папке node/lib/ Модуль используется по своему прямому назначению, в net.js для создания пайпа

// constructor for lazy loading
function createPipe() {
  var Pipe = process.binding('pipe_wrap').Pipe;
  return new Pipe();
}

// constructor for lazy loading
function createTCP() {
  var TCP = process.binding('tcp_wrap').TCP;
  return new TCP();
}

и (ура!) он же используется в классе Socket, который так хотелось использовать в IPC :

Socket.prototype.connect = function(options, cb) {
  if (this.write !== Socket.prototype.write)
    this.write = Socket.prototype.write;

  if (typeof options !== 'object') {
    // Old API:
    // connect(port, [host], [cb])
    // connect(path, [cb]);
    var args = normalizeConnectArgs(arguments);
    return Socket.prototype.connect.apply(this, args);
  }

  if (this.destroyed) {
    this._readableState.reading = false;
    this._readableState.ended = false;
    this._writableState.ended = false;
    this._writableState.ending = false;
    this._writableState.finished = false;
    this.destroyed = false;
    this._handle = null;
  }

  var self = this;
  var pipe = !!options.path;

  if (!this._handle) {
    this._handle = pipe ? createPipe() : createTCP();
    initSocketHandle(this);
  }

Но это для коннекта. Что касаемо серверной части кода, то тут не обойтись без костылей. Дело в том, что просто так не заставить ноду слушать именованный пайп, так как,

1) для начала, имя пайпа не очевидно. Оно по всем канонам windows обязано выглядеть как .pipepipe_name
2) Сервер (net.XXX) создает экземпляр пайпа только если в качестве порта и типа адреса (addressType) передано -1, о чем, опять таки можн узнать из кода :

var createServerHandle = exports._createServerHandle =
    function(address, port, addressType, fd) {
  var r = 0;
  // assign handle in listen, and clean up if bind or listen fails
  var handle;

  if (typeof fd === 'number' && fd >= 0) {
    try {
      handle = createHandle(fd);
    }
    catch (e) {
      // Not a fd we can listen on.  This will trigger an error.
      debug('listen invalid fd=' + fd + ': ' + e.message);
      process._errno = 'EINVAL'; // hack, callers expect that errno is set
      return null;
    }
    handle.open(fd);
    handle.readable = true;
    handle.writable = true;
    return handle;

  } else if (port == -1 && addressType == -1) { //!!! <------------------------------
    handle = createPipe();
    if (process.platform === 'win32') {
      var instances = parseInt(process.env.NODE_PENDING_PIPE_INSTANCES);
      if (!isNaN(instances)) {
        handle.setPendingInstances(instances);
      }
    }
  } else {
    handle = createTCP();
  }

  if (address || port) {
    debug('bind to ' + address);
    if (addressType == 6) {
      r = handle.bind6(address, port);
    } else {
      r = handle.bind(address, port);
    }
  }

  if (r) {
    handle.close();
    handle = null;
  }

  return handle;
};

Пришло время экспериментов и итогов. Так как мы можем создать пайп и юзать его в Socket, то нам доступна вся сетевая подсистема ноды. То есть на том конце пайпа может быть, например, веб-сервер или просто сервер, ожидающий команд. Вот пример использования в качестве сервера:

var net = require('net');
var util = require('util');
var http = require('http');

var http = require('http');
http.createServer(function (req, res) {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('Hello World');
}).listen('\.pipeteeest2', -1 /*!!!!!!*/, -1 /*!!!!!!*/);

console.log('Server running at ', '\.pipeteeest2');

И по все тойже причины (Socket), нам доступны и клиенты для такого локального хттп сервера. Для хттп нам заботливо предоставили Agent API, позволяя хукать открытие соединения и направлять сокет куда угодно:

var a = new http.Agent();

options = {
    host: 'www.localhost.com',
    port: 80,
    path: '/index.html',
    method: 'GET'
};

a.createConnection = function () {
    return ((new net.Socket()).connect('\.pipeteeest2'));
};

var req = http.request(options, function (res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log('BODY: ' + chunk);
    });
});
req.on('error', function (e) {
    console.log("Got error " + e.message);
});
req.end();

На этом все. тему виндозного IPC считаю раскрытой.

 

 

Click to rate this post!
[Total: 4 Average: 3.3]

Специалист в области кибер-безопасности. Работал в ведущих компаниях занимающихся защитой и аналитикой компьютерных угроз. Цель данного блога - простым языком рассказать о сложных моментах защиты IT инфраструктур и сетей.

1 comments On NodeJS : Недокументированные IPC в nodejs для windows

  • a — чет почти не используется, наверно вместо «http.request» хотели написать «a.request»…

Leave a reply:

Your email address will not be published.