├── .gitignore ├── binding.gyp ├── .jshintrc ├── examples ├── client.py ├── server.js ├── server.py └── client.js ├── package.json ├── LICENSE ├── lib └── abstract_socket.js ├── README.md ├── src └── abstract_socket.cc └── test └── abstract-socket.test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | 4 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | 'targets': [ 3 | { 4 | 'target_name': 'bindings', 5 | 'defines': [ '_GNU_SOURCE=1' ], 6 | 'sources': [ 'src/abstract_socket.cc' ], 7 | 'include_dirs': [ 8 | ' { 5 | console.log('client connected'); 6 | client.on('end', () => { 7 | console.log('client disconnected'); 8 | }) 9 | .pipe(client) 10 | .write('hello from server\r\n'); 11 | }) 12 | .on('error', err => { 13 | console.log('server error', err); 14 | }) 15 | .listen('\0foo2'); 16 | 17 | -------------------------------------------------------------------------------- /examples/server.py: -------------------------------------------------------------------------------- 1 | 2 | import socket 3 | import thread 4 | 5 | 6 | def handle_connection(sock, addr): 7 | print 'Incoming connection %r' % sock 8 | while True: 9 | data = sock.recv(1024) 10 | if not data: 11 | print 'Connection closed' 12 | break 13 | sock.send(data) 14 | 15 | 16 | server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 17 | print 'FD: %d' % server.fileno() 18 | server.bind('\x00foo') 19 | server.listen(128) 20 | 21 | while True: 22 | sock, addr = server.accept() 23 | thread.start_new_thread(handle_connection, (sock, addr)) 24 | 25 | -------------------------------------------------------------------------------- /examples/client.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const client = require('../lib/abstract_socket.js') 4 | .connect('\0foo2', () => { //'connect' listener 5 | console.log('client connected'); 6 | }) 7 | .on('data', data => { 8 | console.log(data.toString()); 9 | }) 10 | .on('error', err => { 11 | console.log('caught', err); 12 | }) 13 | .on('end', () => { 14 | console.log('client ended'); 15 | }); 16 | 17 | process.stdin.setEncoding('utf8') 18 | .on('readable', () => { 19 | const chunk = process.stdin.read(); 20 | if (chunk !== null) 21 | client.write(chunk); 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Saúl Ibarra Corretgé", 4 | "email": "s@saghul.net", 5 | "url": "https://bettercallsaghul.com" 6 | }, 7 | "scripts": { 8 | "test": "./node_modules/.bin/mocha --growl --timeout 10000 test/*.test.js" 9 | }, 10 | "name": "abstract-socket", 11 | "version": "2.1.1", 12 | "description": "Abstract domain socket support for Node", 13 | "main": "lib/abstract_socket", 14 | "homepage": "https://github.com/saghul/node-abstractsocket", 15 | "license": "MIT", 16 | "repository": { 17 | "type": "git", 18 | "url": "git://github.com/saghul/node-abstractsocket.git" 19 | }, 20 | "engines": { 21 | "node": ">=4.0.0" 22 | }, 23 | "dependencies": { 24 | "bindings": "^1.2.1", 25 | "nan": "^2.12.1" 26 | }, 27 | "devDependencies": { 28 | "mocha": "3.x", 29 | "should": "11.x" 30 | }, 31 | "os": [ 32 | "linux" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014-present by Saúl Ibarra Corretgé 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /lib/abstract_socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const net = require('net'); 4 | const binding = require('bindings')(); 5 | 6 | const socket = binding.socket; 7 | const bind = binding.bind; 8 | const connect = binding.connect; 9 | const close = binding.close; 10 | 11 | const errnoException = require('util')._errnoException; 12 | 13 | 14 | class AbstractSocketServer extends net.Server { 15 | constructor(listener) { 16 | super(listener); 17 | } 18 | 19 | listen(name, listener) { 20 | let err = socket(); 21 | if (err < 0) { 22 | this.emit(errnoException(err, 'socket')); 23 | } 24 | 25 | const handle = {fd: err}; 26 | 27 | err = bind(err, name); 28 | if (err < 0) { 29 | close(handle.fd); 30 | this.emit(errnoException(err, 'bind')); 31 | } 32 | super.listen(handle, listener); 33 | } 34 | } 35 | 36 | 37 | exports.createServer = function(listener) { 38 | return new AbstractSocketServer(listener); 39 | }; 40 | 41 | 42 | exports.connect = exports.createConnection = function(name, connectListener) { 43 | const defaultOptions = { 44 | readable: true, 45 | writable: true 46 | }; 47 | 48 | let err = socket(); 49 | if (err < 0) { 50 | const sock = new net.Socket(defaultOptions); 51 | setImmediate(() => sock.emit('error', errnoException(err, 'socket'))); 52 | return sock; 53 | } 54 | 55 | const options = Object.assign({fd: err}, defaultOptions); 56 | 57 | // yes, connect is synchronous, so sue me 58 | err = connect(err, name); 59 | if (err < 0) { 60 | close(options.fd); 61 | const sock = new net.Socket(defaultOptions); 62 | setImmediate(() => sock.emit('error', errnoException(err, 'connect'))); 63 | return sock; 64 | } 65 | 66 | const sock = new net.Socket(options); 67 | if (typeof connectListener === 'function') { 68 | setImmediate(() => connectListener(sock)); 69 | } 70 | return sock; 71 | }; 72 | 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-abstractsocket 2 | 3 | Because I like my sockets like my Picasso paintings: abstract. 4 | 5 | [![NPM](https://nodei.co/npm/abstract-socket.png)](https://nodei.co/npm/abstract-socket/) 6 | 7 | 8 | ## Abstract what? 9 | 10 | Go read this: http://man7.org/linux/man-pages/man7/unix.7.html, I'll wait. 11 | 12 | 13 | ## Examples 14 | 15 | Server: 16 | 17 | ```js 18 | // abstract echo server 19 | const abs = require('./lib/abstract_socket'); 20 | 21 | const server = abs.createServer(function(c) { //'connection' listener 22 | console.log('client connected'); 23 | c.on('end', function() { 24 | console.log('client disconnected'); 25 | }); 26 | c.write('hello\r\n'); 27 | c.pipe(c); 28 | }); 29 | server.listen('\0foo'); 30 | 31 | ``` 32 | 33 | Client: 34 | 35 | ```js 36 | const abs = require('./lib/abstract_socket'); 37 | 38 | var client = abs.connect('\0foo', function() { //'connect' listener 39 | console.log('client connected'); 40 | }); 41 | 42 | client.on('data', function(data) { 43 | console.log(data.toString()); 44 | }); 45 | 46 | process.stdin.setEncoding('utf8'); 47 | process.stdin.on('readable', function() { 48 | const chunk = process.stdin.read(); 49 | if (chunk !== null) 50 | client.write(chunk); 51 | }); 52 | 53 | ``` 54 | 55 | 56 | ## API 57 | 58 | ### abs.createServer(connectionListener) 59 | 60 | Returns a new `AbstractSocketServer` object. `listen` can be called on 61 | it passing the name of the abstract socket to bind to and listen, it follows 62 | the API used for normal Unix domain sockets. NOTE: you must prepend the path with 63 | the NULL byte ('\0') to indicate it's an abstract socket. 64 | 65 | Emits an error if the `socket(2)` system call fails. 66 | 67 | ### AbstractSocketServer.listen(name, [callback] 68 | 69 | Binds the server to the specified abstract socket name. 70 | 71 | Emits an error if the `bind(2)` system call fails, or the given `name` 72 | is invalid. 73 | 74 | This function is asynchronous. When the server has been bound, 'listening' event 75 | will be emitted. the last parameter callback will be added as an listener for the 76 | 'listening' event. 77 | 78 | ### abs.connect(name, connectListener) 79 | 80 | Creates a connection to the given `path` in the abstract domain. NOTE: you must 81 | prepend the path with the NULL byte ('\0') to indicate it's an abstract 82 | socket. 83 | 84 | Returns a new net.Socket object. 85 | 86 | Emits an error if the `socket(2)` or `connect(2)` system calls fail, 87 | or the given `name` is invalid. 88 | 89 | ## Tests 90 | 91 | Run tests with `npm test`. 92 | 93 | ## Thanks 94 | 95 | I borrowed massive amounts of inspiration/code from node-unix-dgram by @bnoordhuis :-) 96 | 97 | @mmalecki taught me how to inherit like a pro. 98 | @randunel refactored it heavily in v2. 99 | 100 | -------------------------------------------------------------------------------- /src/abstract_socket.cc: -------------------------------------------------------------------------------- 1 | #if !defined(__linux__) 2 | # error "Only Linux is supported" 3 | #endif 4 | 5 | #include 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | namespace { 15 | 16 | using v8::FunctionTemplate; 17 | using v8::Local; 18 | using v8::Object; 19 | using v8::String; 20 | using v8::Value; 21 | 22 | 23 | NAN_METHOD(Socket) { 24 | int fd; 25 | int type; 26 | 27 | assert(info.Length() == 0); 28 | 29 | type = SOCK_STREAM; 30 | type |= SOCK_NONBLOCK | SOCK_CLOEXEC; 31 | 32 | fd = socket(AF_UNIX, type, 0); 33 | if (fd == -1) { 34 | fd = -errno; 35 | goto out; 36 | } 37 | 38 | out: 39 | info.GetReturnValue().Set(fd); 40 | } 41 | 42 | 43 | NAN_METHOD(Bind) { 44 | sockaddr_un s; 45 | socklen_t namelen; 46 | int err; 47 | int fd; 48 | unsigned int len; 49 | 50 | assert(info.Length() == 2); 51 | 52 | fd = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); 53 | Nan::Utf8String path(info[1]); 54 | 55 | if ((*path)[0] != '\0') { 56 | err = -EINVAL; 57 | goto out; 58 | } 59 | 60 | len = path.length(); 61 | if (len > sizeof(s.sun_path)) { 62 | err = -EINVAL; 63 | goto out; 64 | } 65 | 66 | memset(&s, 0, sizeof s); 67 | memcpy(s.sun_path, *path, len); 68 | s.sun_family = AF_UNIX; 69 | namelen = offsetof(struct sockaddr_un, sun_path) + len; 70 | 71 | err = 0; 72 | if (bind(fd, reinterpret_cast(&s), namelen)) 73 | err = -errno; 74 | 75 | out: 76 | info.GetReturnValue().Set(err); 77 | } 78 | 79 | 80 | NAN_METHOD(Connect) { 81 | sockaddr_un s; 82 | socklen_t namelen; 83 | int err; 84 | int fd; 85 | unsigned int len; 86 | 87 | assert(info.Length() == 2); 88 | 89 | fd = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); 90 | Nan::Utf8String path(info[1]); 91 | 92 | if ((*path)[0] != '\0') { 93 | err = -EINVAL; 94 | goto out; 95 | } 96 | 97 | len = path.length(); 98 | if (len > sizeof(s.sun_path)) { 99 | err = -EINVAL; 100 | goto out; 101 | } 102 | 103 | memset(&s, 0, sizeof s); 104 | memcpy(s.sun_path, *path, len); 105 | s.sun_family = AF_UNIX; 106 | namelen = offsetof(struct sockaddr_un, sun_path) + len; 107 | 108 | err = 0; 109 | if (connect(fd, reinterpret_cast(&s), namelen)) 110 | err = -errno; 111 | 112 | out: 113 | info.GetReturnValue().Set(err); 114 | } 115 | 116 | 117 | NAN_METHOD(Close) { 118 | int err; 119 | int fd; 120 | 121 | assert(info.Length() == 1); 122 | fd = info[0]->Int32Value(Nan::GetCurrentContext()).FromJust(); 123 | 124 | // POSIX 2008 states that it unspecified what the state of a file descriptor 125 | // is if close() is interrupted by a signal and fails with EINTR. This is a 126 | // problem for multi-threaded programs since if the fd was actually closed 127 | // it may already be reused by another thread hence it is unsafe to attempt 128 | // to close it again. In 2012, POSIX approved a clarification that aimed 129 | // to deal with this mess: http://austingroupbugs.net/view.php?id=529#c1200 130 | // 131 | // The short summary is that if the fd is never valid after close(), as is 132 | // the case in Linux, then should add: 133 | // 134 | // #define POSIX_CLOSE_RESTART 0 135 | // 136 | // and the new posix_close() should be implemented as something like: 137 | // 138 | // int posix_close(int fd, int flags) { 139 | // int r = close(fd); 140 | // if (r < 0 && errno == EINTR) 141 | // return 0 or set errno to EINPROGRESS; 142 | // return r; 143 | // } 144 | // 145 | // In contrast, on systems where EINTR means the close() didn't happen (like 146 | // HP-UX), POSIX_CLOSE_RESTART should be non-zero and if passed as flag to 147 | // posix_close() it should automatically retry close() on EINTR. 148 | // 149 | // Of course this is nice and all, but apparently adding one constant and 150 | // a trivial wrapper was way too much effort for the glibc project: 151 | // https://sourceware.org/bugzilla/show_bug.cgi?id=16302 152 | // 153 | // But since we actually only care about Linux, we don't have to worry about 154 | // all this anyway. close() always means the fd is gone, even if an error 155 | // occurred. This elevates EINTR to the status of real error, since it 156 | // implies behaviour associated with close (e.g. flush) was aborted and can 157 | // not be retried since the fd is gone. 158 | 159 | err = 0; 160 | if (close(fd)) 161 | err = -errno; 162 | 163 | info.GetReturnValue().Set(err); 164 | } 165 | 166 | 167 | void Initialize(Local target) { 168 | Nan::Set(target, Nan::New("socket").ToLocalChecked(), 169 | Nan::GetFunction(Nan::New(Socket)).ToLocalChecked()); 170 | Nan::Set(target, Nan::New("bind").ToLocalChecked(), 171 | Nan::GetFunction(Nan::New(Bind)).ToLocalChecked()); 172 | Nan::Set(target, Nan::New("connect").ToLocalChecked(), 173 | Nan::GetFunction(Nan::New(Connect)).ToLocalChecked()); 174 | Nan::Set(target, Nan::New("close").ToLocalChecked(), 175 | Nan::GetFunction(Nan::New(Close)).ToLocalChecked()); 176 | } 177 | 178 | 179 | } // anonymous namespace 180 | 181 | NODE_MODULE(abstract_socket, Initialize) 182 | -------------------------------------------------------------------------------- /test/abstract-socket.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const spawn = require('child_process').spawn; 4 | const should = require('should'); // jshint ignore: line 5 | const abs = require('../lib/abstract_socket.js'); 6 | 7 | const SOCKET_NAME = '\0test312'; 8 | const SOME_DATA = 'asdqq\n'; 9 | 10 | describe('server', function() { 11 | describe('listening', function() { 12 | let server; 13 | beforeEach(() => (server = abs.createServer()) && server.listen(SOCKET_NAME)); 14 | afterEach(() => server.close()); 15 | 16 | it('should listen on abstract socket', () => exec('lsof -U') 17 | .then(output => output.should.containEql(`@${SOCKET_NAME.slice(1)}`))); 18 | 19 | it('should emit error when socket is busy', done => { 20 | const server = abs.createServer(); 21 | server.listen(SOCKET_NAME); 22 | server.on('error', err => { 23 | err.syscall.should.equal('listen'); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should stop listening when close is called', () => new Promise(resolve => { 29 | server.close(resolve); 30 | }) 31 | .then(() => exec('lsof -U')) 32 | .then(output => output.should.not.containEql(`@${SOCKET_NAME.slice(1)}`))); 33 | }); 34 | 35 | describe('client connections', function() { 36 | let server; 37 | beforeEach(() => (server = abs.createServer()) && server.listen(SOCKET_NAME)); 38 | afterEach(() => server.close()); 39 | 40 | it('should emit event when client connects', done => { 41 | server.on('connection', () => done()); 42 | abs.connect(SOCKET_NAME); 43 | }); 44 | 45 | it('should receive client data', done => { 46 | server.on('connection', client => { 47 | client.on('data', data => { 48 | data.toString().should.equal(SOME_DATA); 49 | done(); 50 | }); 51 | }); 52 | abs.connect(SOCKET_NAME).write(SOME_DATA); 53 | }); 54 | }); 55 | 56 | describe('messages', function() { 57 | let server; 58 | beforeEach(() => (server = abs.createServer()) && server.listen(SOCKET_NAME)); 59 | afterEach(() => server.close()); 60 | 61 | it('should be received from the client', done => { 62 | server.on('connection', client => { 63 | client.on('data', data => { 64 | data.toString().should.equal(SOME_DATA); 65 | done(); 66 | }); 67 | }); 68 | const client = abs.connect(SOCKET_NAME, () => { 69 | client.write(SOME_DATA); 70 | }); 71 | }); 72 | 73 | it('should be sent from the server', done => { 74 | server.on('connection', client => { 75 | client.end(SOME_DATA); 76 | }); 77 | const client = abs.connect(SOCKET_NAME); 78 | client.on('data', data => { 79 | data.toString().should.equal(SOME_DATA); 80 | done(); 81 | }); 82 | }); 83 | 84 | it('should be able to send large data', done => { 85 | const LENGTH = 65537; 86 | const FILL_CHAR = 't'; 87 | const buf = new Buffer(LENGTH); 88 | buf.fill(FILL_CHAR); 89 | server.on('connection', client => { 90 | client.end(buf); 91 | }); 92 | const client = abs.connect(SOCKET_NAME); 93 | let res = new Buffer(0); 94 | client.on('data', data => { 95 | res = Buffer.concat([res, data], res.length + data.length); 96 | }); 97 | client.on('end', () => { 98 | res.length.should.equal(LENGTH); 99 | res.toString().split('').forEach(t => t.should.equal(FILL_CHAR)); 100 | done(); 101 | }); 102 | }); 103 | }); 104 | }); 105 | 106 | describe('client', function() { 107 | describe('should emit error', function() { 108 | it('when connecting to a non existent socket', done => { 109 | abs.connect('\0non-existent-socket').on('error', () => done()); 110 | }); 111 | 112 | it('when connecting to a non abstract socket', done => { 113 | abs.connect('non-abstract-socket').on('error', () => done()); 114 | }); 115 | }); 116 | 117 | describe('connect callback', function() { 118 | let server; 119 | beforeEach(() => (server = abs.createServer()) && server.listen(SOCKET_NAME)); 120 | afterEach(() => server.close()); 121 | 122 | it('should be called when connected', done => { 123 | abs.connect(SOCKET_NAME, () => done()); 124 | }); 125 | 126 | it('should be called asynchronously', done => { 127 | let counter = 0; 128 | abs.connect(SOCKET_NAME, () => { 129 | counter.should.equal(1); 130 | done(); 131 | }); 132 | ++counter; 133 | }); 134 | }); 135 | 136 | describe('messages', function() { 137 | let server; 138 | beforeEach(() => (server = abs.createServer()) && server.listen(SOCKET_NAME)); 139 | afterEach(() => server.close()); 140 | 141 | it('should be sent to the server', done => { 142 | server.on('connection', client => { 143 | client.on('data', data => { 144 | data.toString().should.equal(SOME_DATA); 145 | done(); 146 | }); 147 | }); 148 | const client = abs.connect(SOCKET_NAME, () => { 149 | client.write(SOME_DATA); 150 | }); 151 | }); 152 | 153 | it('should be able to send large data', done => { 154 | const LENGTH = 65537; 155 | const FILL_CHAR = 't'; 156 | const buf = new Buffer(LENGTH); 157 | buf.fill(FILL_CHAR); 158 | server.on('connection', client => { 159 | let res = new Buffer(0); 160 | client.on('data', data => { 161 | res = Buffer.concat([res, data], res.length + data.length); 162 | if (res.length < LENGTH) { 163 | return; 164 | } 165 | res.length.should.equal(LENGTH); 166 | res.toString().split('').forEach(t => t.should.equal(FILL_CHAR)); 167 | done(); 168 | }); 169 | }); 170 | const client = abs.connect(SOCKET_NAME, () => { 171 | client.end(buf); 172 | }); 173 | }); 174 | 175 | it('should be received from the server', done => { 176 | server.on('connection', client => { 177 | client.end(SOME_DATA); 178 | }); 179 | const client = abs.connect(SOCKET_NAME); 180 | client.on('data', data => { 181 | data.toString().should.equal(SOME_DATA); 182 | done(); 183 | }); 184 | }); 185 | }); 186 | }); 187 | 188 | function exec(cmd, options) { 189 | return new Promise((resolve, reject) => { 190 | let bin = cmd.split(' ').shift(); 191 | let params = cmd.split(' ').slice(1); 192 | let child = spawn(bin, params, options); 193 | let res = new Buffer(0); 194 | let err = new Buffer(0); 195 | 196 | child.stdout.on('data', buf => res = Buffer.concat([res, buf], res.length + buf.length)); 197 | child.stderr.on('data', buf => err = Buffer.concat([err, buf], err.length + buf.length)); 198 | child.on('close', code => { 199 | return setImmediate(() => { 200 | // setImmediate is required because there are often still 201 | // pending write requests in both stdout and stderr at this point 202 | if (code) { 203 | reject(err.toString()); 204 | } else { 205 | resolve(res.toString()); 206 | } 207 | }); 208 | }); 209 | child.on('error', reject); 210 | }); 211 | } 212 | 213 | --------------------------------------------------------------------------------