├── test ├── temp │ └── .gitignore ├── fixtures │ ├── banner │ ├── sshd_config │ ├── id_rsa.bad.pub │ ├── id_rsa.pub │ ├── ssh_host_rsa_key.pub │ ├── id_dsa.pub │ ├── id_dsa │ ├── id_rsa.bad │ ├── authorized_keys │ ├── id_rsa │ └── ssh_host_rsa_key ├── test.js └── test-integration.js ├── util ├── build_pagent.bat ├── pagent.exe └── pagent.c ├── TODO ├── lib ├── utils.js ├── SFTP │ ├── Stats.js │ └── SFTPv3.js ├── keyParser.js ├── agent.js ├── Parser.constants.js ├── Channel.js └── Parser.js ├── package.json ├── LICENSE └── README.md /test/temp/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/banner: -------------------------------------------------------------------------------- 1 | Node.js rules! 2 | -------------------------------------------------------------------------------- /util/build_pagent.bat: -------------------------------------------------------------------------------- 1 | @cl /Ox pagent.c User32.lib 2 | @del /Q *.obj -------------------------------------------------------------------------------- /util/pagent.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onilabs/ssh2/master/util/pagent.exe -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | TODO (in no particular order) 2 | ==== 3 | 4 | * 'hostbased' authentication 5 | -------------------------------------------------------------------------------- /test/fixtures/sshd_config: -------------------------------------------------------------------------------- 1 | # All options are passed into the command line instead 2 | -------------------------------------------------------------------------------- /test/fixtures/id_rsa.bad.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAYQDOeuMG6yNHgN4tnBJQvEUvy3MRH8oNs+9FBTn7iwcHXPEU0cUDnDd/36bIR/tFJrVecU25WjGmNROg45jyz4zioDrUg6JKNlHMOOS1CBo97zeaORSScW07ECCxz8y4CyU= unallowed test key 2 | -------------------------------------------------------------------------------- /test/fixtures/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDL0yFO4W4xbdrJk/i+CW3itPATvhRkS+x+gKmkdH739AqWYP6rkTFAmFTw9gLJ/c2tN7ow0T0QUR9iUsv/3QzTuwsjBu0feo3CVxwMkaJTo5ks9XBoOW0R3tyCcOLlAcQ1WjC7cv5Ifn4gXLLM+k8/y/m3u8ERtidNxbRqpQ/gPQ== ssh2 test key 2 | -------------------------------------------------------------------------------- /test/fixtures/ssh_host_rsa_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC57UB/5H0M+t+mopksrltCCIXghryzofJjau+8tuMT9CG6ta3SO9aKApJUUG/xtc88giVhB7HFABX/oob+jrkSthR8s/whULC8E+GhvOBjHydRUZIsaPYOMBb42HcbOsgq3li/hwOcDk0vY00hZDKCum9BgvRAb7dPEkw2dmiCQQ== ssh2 test server key 2 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var RE_STREAM = /^arcfour/i, 2 | RE_GCM = /^aes\d+-gcm/i; 3 | 4 | module.exports = { 5 | iv_inc: function(iv) { 6 | var n = 12, c = 0; 7 | do { 8 | --n; 9 | c = iv[n]; 10 | if (c === 255) 11 | iv[n] = 0; 12 | else { 13 | iv[n] = ++c; 14 | return; 15 | } 16 | } while (n > 4); 17 | }, 18 | isStreamCipher: function(name) { 19 | return RE_STREAM.test(name); 20 | }, 21 | isGCM: function(name) { 22 | return RE_GCM.test(name); 23 | } 24 | }; -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn, 2 | join = require('path').join; 3 | 4 | var files = require('fs').readdirSync(__dirname).filter(function(f) { 5 | return (f.substr(0, 5) === 'test-'); 6 | }).map(function(f) { 7 | return join(__dirname, f); 8 | }), 9 | f = -1; 10 | 11 | function next() { 12 | if (++f < files.length) { 13 | spawn(process.argv[0], [ files[f] ], { stdio: 'inherit' }) 14 | .on('exit', function(code) { 15 | if (code === 0) 16 | process.nextTick(next); 17 | }); 18 | } 19 | } 20 | next(); 21 | -------------------------------------------------------------------------------- /test/fixtures/id_dsa.pub: -------------------------------------------------------------------------------- 1 | ssh-dss AAAAB3NzaC1kc3MAAACBALOosomP35SaS5WnpkP1sDoaZQk65swR0iSIHMlP9SO9rdwURXuV2xUhSo8UaMNNhifAIzUvs+53WrZZ1nA3Dhrcx3FiH1gvi5om/GYFFhr7/d4lhu6UFeaw/DIapHNCc5ljxS9l8HJci/H6mwTDBvAbupN35IK8ygZ9tSIW+4qPAAAAFQDQi92edefLIGV2BK14mgUa0z/0QQAAAIBhS1EVkYQlFOSkBCPEywWh+46jFAveL5yP4DxPuB8FH3iACrp/30RlTDbOgpE8hnWlZMQJHHqRCFgem/Lu9oiYTEOnfBRjHmLJqt1DRmtEOWzrerW9nmLKOR1zYcqAxr52vEa6uVWQn58E+RHRzOekWleevGD2PvrRneAzBEV+LQAAAIBo1tVScPzPVpuGLi4rlZ3YU6LIMg30uWoZZ3iFx79ZsSmkNwrzIkHfl1+vFw/XxXLLXu9gUJZi5vWaeXtWMkvooIaJkGuOqh2GX+FGVfFI2FxS+tDWVEGOJG234jVdu94DBKHySNPKAUrCBOt5F2BceIyOQIgYY+a4QNf/2kZ0xQ== ssh2 test key 2 2 | -------------------------------------------------------------------------------- /test/fixtures/id_dsa: -------------------------------------------------------------------------------- 1 | -----BEGIN DSA PRIVATE KEY----- 2 | MIIBuwIBAAKBgQCzqLKJj9+UmkuVp6ZD9bA6GmUJOubMEdIkiBzJT/Ujva3cFEV7 3 | ldsVIUqPFGjDTYYnwCM1L7Pud1q2WdZwNw4a3MdxYh9YL4uaJvxmBRYa+/3eJYbu 4 | lBXmsPwyGqRzQnOZY8UvZfByXIvx+psEwwbwG7qTd+SCvMoGfbUiFvuKjwIVANCL 5 | 3Z5158sgZXYErXiaBRrTP/RBAoGAYUtRFZGEJRTkpAQjxMsFofuOoxQL3i+cj+A8 6 | T7gfBR94gAq6f99EZUw2zoKRPIZ1pWTECRx6kQhYHpvy7vaImExDp3wUYx5iyard 7 | Q0ZrRDls63q1vZ5iyjkdc2HKgMa+drxGurlVkJ+fBPkR0cznpFpXnrxg9j760Z3g 8 | MwRFfi0CgYBo1tVScPzPVpuGLi4rlZ3YU6LIMg30uWoZZ3iFx79ZsSmkNwrzIkHf 9 | l1+vFw/XxXLLXu9gUJZi5vWaeXtWMkvooIaJkGuOqh2GX+FGVfFI2FxS+tDWVEGO 10 | JG234jVdu94DBKHySNPKAUrCBOt5F2BceIyOQIgYY+a4QNf/2kZ0xQIVALD6ydax 11 | 1avYRutcD9Rx0skoScz1 12 | -----END DSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "ssh2", 2 | "version": "0.3.6", 3 | "author": "Brian White ", 4 | "description": "An SSH2 client module written in pure JavaScript for node.js", 5 | "main": "./lib/Connection", 6 | "engines": { "node": ">=0.8.7" }, 7 | "dependencies": { 8 | "readable-stream": "1.0.27-1", 9 | "streamsearch": "0.1.2", 10 | "asn1": "0.2.1" 11 | }, 12 | "scripts": { 13 | "test": "node test/test.js" 14 | }, 15 | "keywords": [ "ssh", "ssh2", "sftp", "secure", "shell", "exec", "remote", "client" ], 16 | "licenses": [ { "type": "MIT", "url": "http://github.com/mscdex/ssh2/raw/master/LICENSE" } ], 17 | "repository" : { "type": "git", "url": "http://github.com/mscdex/ssh2.git" } 18 | } 19 | -------------------------------------------------------------------------------- /test/fixtures/id_rsa.bad: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIBywIBAAJhAM564wbrI0eA3i2cElC8RS/LcxEfyg2z70UFOfuLBwdc8RTRxQOc 3 | N3/fpshH+0UmtV5xTblaMaY1E6DjmPLPjOKgOtSDoko2Ucw45LUIGj3vN5o5FJJx 4 | bTsQILHPzLgLJQIDAQABAmEAneHZNrEViNdBDB8K4jETtjgbBD0Kgu/TRTN54p2s 5 | AjVPIlxECT6qMV0SapKfz4CITste8iq3ZQVfQ1BL2qzi7xCklB2wov/HZFr2v6gs 6 | b5fa8capHBxfs3J8daLtIP0BAjEA6UR8LO+IPPKsRivz8IA0BaP1pYy+164Jj/XA 7 | hyxTz+JvQXyj+gk7ovnthKcKriqfAjEA4pocwlBLK3uZAYtt3L+Am6jMWhwi3yiY 8 | zo8ugWYs+FOtKSgx+IBoxHVsg6PW8Xe7AjBF4yndAKrtr7sjjvmX/aEYa4YmYmOv 9 | FMpyoitblFFMAEha82/hcrC2ZHDgBHfztHsCMAajfTQ0Jf+gH1tsOku9UIc+6r25 10 | FUx0ZAWpLDOeSrL7wJb0FoKxQGCBECzLIADzAQIxAIAA3oXI+W/VpSctzt5d+Oo2 11 | kreYsivovMlOiy886YqeTtbt3gFkHSLn1RPnmFMoug== 12 | -----END RSA PRIVATE KEY----- 13 | -------------------------------------------------------------------------------- /test/fixtures/authorized_keys: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDL0yFO4W4xbdrJk/i+CW3itPATvhRkS+x+gKmkdH739AqWYP6rkTFAmFTw9gLJ/c2tN7ow0T0QUR9iUsv/3QzTuwsjBu0feo3CVxwMkaJTo5ks9XBoOW0R3tyCcOLlAcQ1WjC7cv5Ifn4gXLLM+k8/y/m3u8ERtidNxbRqpQ/gPQ== ssh2 test key 2 | ssh-dss AAAAB3NzaC1kc3MAAACBALOosomP35SaS5WnpkP1sDoaZQk65swR0iSIHMlP9SO9rdwURXuV2xUhSo8UaMNNhifAIzUvs+53WrZZ1nA3Dhrcx3FiH1gvi5om/GYFFhr7/d4lhu6UFeaw/DIapHNCc5ljxS9l8HJci/H6mwTDBvAbupN35IK8ygZ9tSIW+4qPAAAAFQDQi92edefLIGV2BK14mgUa0z/0QQAAAIBhS1EVkYQlFOSkBCPEywWh+46jFAveL5yP4DxPuB8FH3iACrp/30RlTDbOgpE8hnWlZMQJHHqRCFgem/Lu9oiYTEOnfBRjHmLJqt1DRmtEOWzrerW9nmLKOR1zYcqAxr52vEa6uVWQn58E+RHRzOekWleevGD2PvrRneAzBEV+LQAAAIBo1tVScPzPVpuGLi4rlZ3YU6LIMg30uWoZZ3iFx79ZsSmkNwrzIkHfl1+vFw/XxXLLXu9gUJZi5vWaeXtWMkvooIaJkGuOqh2GX+FGVfFI2FxS+tDWVEGOJG234jVdu94DBKHySNPKAUrCBOt5F2BceIyOQIgYY+a4QNf/2kZ0xQ== ssh2 test key 2 3 | -------------------------------------------------------------------------------- /test/fixtures/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDL0yFO4W4xbdrJk/i+CW3itPATvhRkS+x+gKmkdH739AqWYP6r 3 | kTFAmFTw9gLJ/c2tN7ow0T0QUR9iUsv/3QzTuwsjBu0feo3CVxwMkaJTo5ks9XBo 4 | OW0R3tyCcOLlAcQ1WjC7cv5Ifn4gXLLM+k8/y/m3u8ERtidNxbRqpQ/gPQIDAQAB 5 | AoGABirSRC/ABNDdIOJQUXe5knWFGiPTPCGr+zvrZiV8PgZtV5WBvzE6e0jgsRXQ 6 | icobMhWQla+PGHJL786vi4NlwuhwKcF7Pd908ofej1eeBOd1u/HQ/qsfxPdxI0zF 7 | dcWPYgAOo9ydOMGcSx4v1zDIgFInELJzKbv64LJQD0/xhoUCQQD7KhJ7M8Nkwsr2 8 | iKCyWTFM2M8/VKltgaiSmsNKZETashk5tKOrM3EWX4RcB/DnvHe8VNyYpC6Sd1uQ 9 | AHwPDfxDAkEAz7+7hDybH6Cfvmr8kUOlDXiJJWXp5lP37FLzMDU6a9wTKZFnh57F 10 | e91zRmKlQTegFet93MXaFYljRkI+4lMpfwJBAPPLbNEF973Qjq4rBMDZbs9HDDRO 11 | +35+AqD7dGC7X1Jg2bd3rf66GiU7ZgDm/GIUQK0gOlg31bT6AniO39zFGH0CQFBh 12 | Yd9HR8nT7xrQ8EoQPzNYGNBUf0xz3rAcZCWZ4rHK48sojEMoBkbnputrzX7PU+xH 13 | QlqCXuAIWVXc2dHd1WcCQQDIUJHPOsgeAfTLoRRRURp/m8zZ9IpbaPTyDstPVNYe 14 | zARW3Oa/tzPqdO6NWaetCp17u7Kb6X9np7Vz17i/4KED 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /test/fixtures/ssh_host_rsa_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQC57UB/5H0M+t+mopksrltCCIXghryzofJjau+8tuMT9CG6ta3S 3 | O9aKApJUUG/xtc88giVhB7HFABX/oob+jrkSthR8s/whULC8E+GhvOBjHydRUZIs 4 | aPYOMBb42HcbOsgq3li/hwOcDk0vY00hZDKCum9BgvRAb7dPEkw2dmiCQQIDAQAB 5 | AoGAMG+HOwoaLbR5aR64yrQNYBF6Vvii1iUdURr9o2r9kygpVUuZIcim5kMvPbnK 6 | v+w+NaQt+q4XeJvCH1uG0W/69FwnphfaOVmCCUtsoJ6sU3fWr9x59MtKL2Llh8xR 7 | 50lz6R+eDXoYRDq245hG9BFn/bu0vtqQqx06mlZJcjaRocECQQDjdYFmr+DSww3x 8 | VNx0G0DUkaQZZ+iqZiT3Zund2pcBB4aLiewOrqj0GFct4+YNzgxIXPejmS0eSokN 9 | N2lC3NxZAkEA0UGjN5TG5/LEK3zcYtx2kpXryenrYORo1n2L/WPMZ0mjLQyd4LJr 10 | ibfgVUfwX/kV3vgGYLwjpgcaTiMsecv4KQJAYMmMgZSPdz+WvD1e/WznXkyG5mSn 11 | xXJngnrhQw0TulVodBIBR5IcxJli510VdIRcB6K/oXa5ky0mOmB8wv3WKQJBAKEF 12 | PxE//KbzWhyUogm4180IbD4dMDCI0ltqlFRRfTJlqZi6wqnq4XFB+u/kwYU4aKoA 13 | dPfvDgduI8HIsyqt17ECQDI/HC8PiYsDIOyVpQuQdIAsbGmoavK7X1MVEWR2nj9t 14 | 7BbUVFSnVKynL4TWIJZ6xP8WQwkDBQc5WjognHDaUTQ= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Brian White. All rights reserved. 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 5 | deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 7 | sell 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 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 19 | IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/SFTP/Stats.js: -------------------------------------------------------------------------------- 1 | var constants = process.binding('constants'); 2 | 3 | function Stats() { 4 | this.mode = undefined; 5 | this.permissions = undefined; // backwards compatiblity 6 | this.uid = undefined; 7 | this.gid = undefined; 8 | this.size = undefined; 9 | this.atime = undefined; 10 | this.mtime = undefined; 11 | } 12 | 13 | Stats.prototype._checkModeProperty = function(property) { 14 | return ((this.mode & constants.S_IFMT) === property); 15 | }; 16 | 17 | Stats.prototype.isDirectory = function() { 18 | return this._checkModeProperty(constants.S_IFDIR); 19 | }; 20 | 21 | Stats.prototype.isFile = function() { 22 | return this._checkModeProperty(constants.S_IFREG); 23 | }; 24 | 25 | Stats.prototype.isBlockDevice = function() { 26 | return this._checkModeProperty(constants.S_IFBLK); 27 | }; 28 | 29 | Stats.prototype.isCharacterDevice = function() { 30 | return this._checkModeProperty(constants.S_IFCHR); 31 | }; 32 | 33 | Stats.prototype.isSymbolicLink = function() { 34 | return this._checkModeProperty(constants.S_IFLNK); 35 | }; 36 | 37 | Stats.prototype.isFIFO = function() { 38 | return this._checkModeProperty(constants.S_IFIFO); 39 | }; 40 | 41 | Stats.prototype.isSocket = function() { 42 | return this._checkModeProperty(constants.S_IFSOCK); 43 | }; 44 | 45 | module.exports = Stats; 46 | -------------------------------------------------------------------------------- /util/pagent.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #define AGENT_COPYDATA_ID 0x804e50ba 6 | #define AGENT_MAX_MSGLEN 8192 7 | 8 | #define GET_32BIT_MSB_FIRST(cp) \ 9 | (((unsigned long)(unsigned char)(cp)[0] << 24) | \ 10 | ((unsigned long)(unsigned char)(cp)[1] << 16) | \ 11 | ((unsigned long)(unsigned char)(cp)[2] << 8) | \ 12 | ((unsigned long)(unsigned char)(cp)[3])) 13 | 14 | #define GET_32BIT(cp) GET_32BIT_MSB_FIRST(cp) 15 | 16 | #define RET_ERR_BADARGS 10 17 | #define RET_ERR_UNAVAILABLE 11 18 | #define RET_ERR_NOMAP 12 19 | #define RET_ERR_BINSTDIN 13 20 | #define RET_ERR_BINSTDOUT 14 21 | #define RET_ERR_BADLEN 15 22 | 23 | #define RET_NORESPONSE 1 24 | #define RET_RESPONSE 0 25 | 26 | int main (int argc, const char* argv[]) { 27 | HWND hwnd; 28 | char *mapname; 29 | HANDLE filemap; 30 | unsigned char *p, *ret; 31 | int id, retlen, inlen, n, rmode, r = RET_NORESPONSE; 32 | COPYDATASTRUCT cds; 33 | void *in; 34 | 35 | if (argc < 2) 36 | return RET_ERR_BADARGS; 37 | 38 | hwnd = FindWindow("Pageant", "Pageant"); 39 | if (!hwnd) 40 | return RET_ERR_UNAVAILABLE; 41 | 42 | rmode = _setmode(_fileno(stdin), _O_BINARY); 43 | if (rmode == -1) 44 | return RET_ERR_BINSTDIN; 45 | 46 | rmode = _setmode(_fileno(stdout), _O_BINARY); 47 | if (rmode == -1) 48 | return RET_ERR_BINSTDOUT; 49 | 50 | inlen = atoi(argv[1]); 51 | in = malloc(inlen); 52 | n = fread(in, 1, inlen, stdin); 53 | if (n != inlen) { 54 | free(in); 55 | return RET_ERR_BADLEN; 56 | } 57 | 58 | mapname = malloc(32); 59 | n = sprintf(mapname, "PageantRequest%08x", (unsigned)GetCurrentThreadId()); 60 | 61 | filemap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 62 | 0, AGENT_MAX_MSGLEN, mapname); 63 | if (filemap == NULL || filemap == INVALID_HANDLE_VALUE) { 64 | free(in); 65 | free(mapname); 66 | return RET_ERR_NOMAP; 67 | } 68 | 69 | p = MapViewOfFile(filemap, FILE_MAP_WRITE, 0, 0, 0); 70 | memcpy(p, in, inlen); 71 | cds.dwData = AGENT_COPYDATA_ID; 72 | cds.cbData = 1 + n; 73 | cds.lpData = mapname; 74 | 75 | id = SendMessage(hwnd, WM_COPYDATA, (WPARAM) NULL, (LPARAM) &cds); 76 | if (id > 0) { 77 | r = RET_RESPONSE; 78 | retlen = 4 + GET_32BIT(p); 79 | fwrite(p, 1, retlen, stdout); 80 | } 81 | 82 | free(in); 83 | free(mapname); 84 | UnmapViewOfFile(p); 85 | CloseHandle(filemap); 86 | 87 | return r; 88 | } 89 | -------------------------------------------------------------------------------- /lib/keyParser.js: -------------------------------------------------------------------------------- 1 | // TODO: 2 | // * handle multi-line header values (OpenSSH)? 3 | // * Putty's PPK format 4 | 5 | var RE_HEADER_PPK = /^PuTTY-User-Key-File-2: ssh-(rsa|dss)$/i, 6 | RE_HEADER_OPENSSH_PRIV = /^-----BEGIN (RSA|DSA) PRIVATE KEY-----$/i, 7 | RE_FOOTER_OPENSSH_PRIV = /^-----END (?:RSA|DSA) PRIVATE KEY-----$/i, 8 | RE_HEADER_OPENSSH_PUB = /^(ssh-(rsa|dss)(?:-cert-v0[01]@openssh.com)?) ([A-Z0-9a-z\/+=]+(?:$|\s+([\S].*)?)$)/i, 9 | RE_HEADER_RFC4716_PUB = /^---- BEGIN SSH2 PUBLIC KEY ----$/i, 10 | RE_FOOTER_RFC4716_PUB = /^---- END SSH2 PUBLIC KEY ----$/i, 11 | RE_HEADER_OPENSSH = /^([^:]+):\s*([\S].*)?$/i, 12 | RE_HEADER_RFC4716 = /^([^:]+): (.*)?$/i; 13 | 14 | module.exports = function(data) { 15 | if (Buffer.isBuffer(data)) 16 | data = data.toString('utf8'); 17 | else if (typeof data !== 'string') 18 | return new Error('Key data must be a Buffer or string'); 19 | 20 | var ret = { 21 | fulltype: undefined, 22 | type: undefined, 23 | extra: undefined, 24 | comment: undefined, 25 | encryption: undefined, 26 | private: undefined, 27 | privateOrig: undefined, 28 | public: undefined, 29 | publicOrig: undefined 30 | }, 31 | m, 32 | i, 33 | len; 34 | 35 | data = data.split(/\r\n|\n/); 36 | 37 | while (!data[0].length) 38 | data.shift(); 39 | while (!data.slice(-1)[0].length) 40 | data.pop(); 41 | 42 | var orig = data.join('\n'); 43 | 44 | if ((m = RE_HEADER_OPENSSH_PRIV.exec(data[0])) 45 | && RE_FOOTER_OPENSSH_PRIV.test(data.slice(-1))) { 46 | // OpenSSH private key 47 | ret.type = (m[1].toLowerCase() === 'dsa' ? 'dss' : 'rsa'); 48 | if (!RE_HEADER_OPENSSH.test(data[1])) { 49 | // unencrypted, no headers 50 | ret.private = new Buffer(data.slice(1, -1).join(''), 'base64'); 51 | } else { 52 | // possibly encrypted, headers 53 | for (i = 1, len = data.length; i < len; ++i) { 54 | m = RE_HEADER_OPENSSH.exec(data[i]); 55 | if (m) { 56 | m[1] = m[1].toLowerCase(); 57 | if (m[1] === 'dek-info') { 58 | m[2] = m[2].split(','); 59 | ret.encryption = m[2][0].toLowerCase(); 60 | if (m[2].length > 1) 61 | ret.extra = m[2].slice(1); 62 | } 63 | } else if (data[i].length) 64 | break; 65 | } 66 | ret.private = new Buffer(data.slice(i, -1).join(''), 'base64'); 67 | } 68 | ret.privateOrig = new Buffer(orig); 69 | } else if (m = RE_HEADER_OPENSSH_PUB.exec(data[0])) { 70 | // OpenSSH public key 71 | ret.fulltype = m[1]; 72 | ret.type = m[2].toLowerCase(); 73 | ret.public = new Buffer(m[3], 'base64'); 74 | ret.publicOrig = new Buffer(orig); 75 | ret.comment = m[4]; 76 | } else if (RE_HEADER_RFC4716_PUB.test(data[0]) 77 | && RE_FOOTER_RFC4716_PUB.test(data.slice(-1))) { 78 | if (data[1].indexOf(': ') === -1) { 79 | // no headers 80 | ret.public = new Buffer(data.slice(1, -1).join(''), 'base64'); 81 | } else { 82 | // headers 83 | for (i = 1, len = data.length; i < len; ++i) { 84 | if (data[i].indexOf(': ') === -1) { 85 | if (data[i].length) 86 | break; // start of key data 87 | else 88 | continue; // empty line 89 | } 90 | while (data[i].substr(-1) === '\\') { 91 | if (i + 1 < len) { 92 | data[i] = data[i].slice(0, -1) + data[i + 1]; 93 | data.splice(i + 1, 1); 94 | --len; 95 | } else 96 | return new Error('RFC4716 public key missing header continuation line'); 97 | } 98 | m = RE_HEADER_RFC4716.exec(data[i]); 99 | if (m) { 100 | m[1] = m[1].toLowerCase(); 101 | if (m[1] === 'comment') { 102 | ret.comment = m[2] || ''; 103 | if (ret.comment[0] === '"' && ret.comment.substr(-1) === '"') 104 | ret.comment = ret.comment.slice(1, -1); 105 | } 106 | } else 107 | return new Error('RFC4716 public key invalid header line'); 108 | } 109 | ret.public = new Buffer(data.slice(i, -1).join(''), 'base64'); 110 | } 111 | len = ret.public.readUInt32BE(0, true); 112 | var fulltype = ret.public.toString('ascii', 4, 4 + len); 113 | ret.fulltype = fulltype; 114 | if (fulltype === 'ssh-dss') 115 | ret.type = 'dss'; 116 | else if (fulltype === 'ssh-rsa') 117 | ret.type = 'rsa'; 118 | else 119 | return new Error('Unsupported RFC4716 public key type: ' + fulltype); 120 | ret.public = ret.public.slice(11); 121 | ret.publicOrig = new Buffer(orig); 122 | } else 123 | return new Error('Unsupported key format'); 124 | 125 | return ret; 126 | }; 127 | -------------------------------------------------------------------------------- /lib/agent.js: -------------------------------------------------------------------------------- 1 | var Socket = require('net').Socket, 2 | EventEmitter = require('events').EventEmitter, 3 | inherits = require('util').inherits, 4 | path = require('path'), 5 | fs = require('fs'), 6 | cp = require('child_process'); 7 | 8 | var REQUEST_IDENTITIES = 11, 9 | IDENTITIES_ANSWER = 12, 10 | SIGN_REQUEST = 13, 11 | SIGN_RESPONSE = 14, 12 | FAILURE = 5; 13 | 14 | var OLD_SIGNATURE = 1; // for ssh-dss keys 15 | 16 | var RE_CYGWIN_SOCK = /^\!(\d+) s ([A-Z0-9]{8}\-[A-Z0-9]{8}\-[A-Z0-9]{8}\-[A-Z0-9]{8})/; 17 | 18 | module.exports = function(sockPath, key, keyType, data, cb) { 19 | var sock, 20 | error, 21 | sig, 22 | datalen, 23 | flags, 24 | keylen = 0, 25 | isSigning = Buffer.isBuffer(key), 26 | type, 27 | count = 0, 28 | siglen = 0, 29 | nkeys = 0, 30 | keys, 31 | comlen = 0, 32 | comment = false, 33 | accept, 34 | reject; 35 | 36 | if (typeof key === 'function' && typeof keyType === 'function') { 37 | // agent forwarding 38 | accept = key; 39 | reject = keyType; 40 | } else if (isSigning) { 41 | keylen = key.length; 42 | datalen = data.length; 43 | flags = (keyType === 'dss' ? OLD_SIGNATURE : 0); 44 | } else { 45 | cb = key; 46 | key = undefined; 47 | } 48 | 49 | function onconnect() { 50 | var buf; 51 | if (isSigning) { 52 | /* 53 | byte SSH2_AGENTC_SIGN_REQUEST 54 | string key_blob 55 | string data 56 | uint32 flags (SSH_AGENT_OLD_SIGNATURE for ssh-dss key) 57 | */ 58 | var p = 9; 59 | buf = new Buffer(4 + 1 + 4 + keylen + 4 + datalen + 4); 60 | buf.writeUInt32BE(buf.length - 4, 0, true); 61 | buf[4] = SIGN_REQUEST; 62 | buf.writeUInt32BE(keylen, 5, true); 63 | key.copy(buf, p); 64 | buf.writeUInt32BE(datalen, p += keylen, true); 65 | data.copy(buf, p += 4); 66 | buf.writeUInt32BE(flags, p += datalen, true); 67 | sock.write(buf); 68 | } else { 69 | /* 70 | byte SSH2_AGENTC_REQUEST_IDENTITIES 71 | */ 72 | sock.write(new Buffer([0, 0, 0, 1, REQUEST_IDENTITIES])); 73 | } 74 | } 75 | function ondata(chunk) { 76 | for (var i = 0, len = chunk.length; i < len; ++i) { 77 | if (type === undefined) { 78 | // skip over packet length 79 | if (++count === 5) { 80 | type = chunk[i]; 81 | count = 0; 82 | } 83 | } else if (type === SIGN_RESPONSE) { 84 | /* 85 | byte SSH2_AGENT_SIGN_RESPONSE 86 | string signature_blob 87 | */ 88 | if (!sig) { 89 | siglen <<= 8; 90 | siglen += chunk[i]; 91 | if (++count === 4) { 92 | sig = new Buffer(siglen); 93 | count = 0; 94 | } 95 | } else { 96 | sig[count] = chunk[i]; 97 | if (++count === siglen) { 98 | sock.removeAllListeners('data'); 99 | return sock.destroy(); 100 | } 101 | } 102 | } else if (type === IDENTITIES_ANSWER) { 103 | /* 104 | byte SSH2_AGENT_IDENTITIES_ANSWER 105 | uint32 num_keys 106 | 107 | Followed by zero or more consecutive keys, encoded as: 108 | 109 | string public key blob 110 | string public key comment 111 | */ 112 | if (keys === undefined) { 113 | nkeys <<= 8; 114 | nkeys += chunk[i]; 115 | if (++count === 4) { 116 | keys = new Array(nkeys); 117 | count = 0; 118 | if (nkeys === 0) { 119 | sock.removeAllListeners('data'); 120 | return sock.destroy(); 121 | } 122 | } 123 | } else { 124 | if (!key) { 125 | keylen <<= 8; 126 | keylen += chunk[i]; 127 | if (++count === 4) { 128 | key = new Buffer(keylen); 129 | count = 0; 130 | } 131 | } else if (comment === false) { 132 | key[count] = chunk[i]; 133 | if (++count === keylen) { 134 | keys[nkeys - 1] = key; 135 | keylen = 0; 136 | count = 0; 137 | comment = true; 138 | if (--nkeys === 0) { 139 | key = undefined; 140 | sock.removeAllListeners('data'); 141 | return sock.destroy(); 142 | } 143 | } 144 | } else if (comment === true) { 145 | comlen <<= 8; 146 | comlen += chunk[i]; 147 | if (++count === 4) { 148 | count = 0; 149 | if (comlen > 0) 150 | comment = comlen; 151 | else { 152 | key = undefined; 153 | comment = false; 154 | } 155 | comlen = 0; 156 | } 157 | } else { 158 | // skip comments 159 | if (++count === comment) { 160 | comment = false; 161 | count = 0; 162 | key = undefined; 163 | } 164 | } 165 | } 166 | } else if (type === FAILURE) { 167 | if (isSigning) 168 | error = new Error('Agent unable to sign data'); 169 | else 170 | error = new Error('Unable to retrieve list of keys from agent'); 171 | sock.removeAllListeners('data'); 172 | return sock.destroy(); 173 | } 174 | } 175 | } 176 | function onerror(err) { 177 | error = err; 178 | } 179 | function onclose() { 180 | if (error) 181 | cb(error); 182 | else if ((isSigning && !sig) || (!isSigning && !keys)) 183 | cb(new Error('Unexpected disconnection from agent')); 184 | else if (isSigning && sig) 185 | cb(undefined, sig); 186 | else if (!isSigning && keys) 187 | cb(undefined, keys); 188 | } 189 | 190 | if (process.platform === 'win32') { 191 | if (sockPath === 'pageant') { 192 | // Pageant (PuTTY authentication agent) 193 | sock = new PageantSock(); 194 | } else { 195 | // cygwin ssh-agent instance 196 | fs.readFile(sockPath, function(err, data) { 197 | if (err) 198 | return cb(new Error('Invalid cygwin unix socket path')); 199 | 200 | var m; 201 | if (m = RE_CYGWIN_SOCK.exec(data.toString('ascii'))) { 202 | var port, 203 | secret, 204 | secretbuf, 205 | state, 206 | bc = 0, 207 | isRetrying = false, 208 | inbuf = [], 209 | credsbuf = new Buffer(12), 210 | i, j; 211 | 212 | // use 0 for pid, uid, and gid to ensure we get an error and also 213 | // a valid uid and gid from cygwin so that we don't have to figure it 214 | // out ourselves 215 | credsbuf.fill(0); 216 | 217 | // parse cygwin unix socket file contents 218 | port = parseInt(m[1], 10); 219 | secret = m[2].replace(/\-/g, ''); 220 | secretbuf = new Buffer(16); 221 | for (i = 0, j = 0; j < 32; ++i,j+=2) 222 | secretbuf[i] = parseInt(secret.substring(j, j + 2), 16); 223 | 224 | // convert to host order (always LE for Windows) 225 | for (i = 0; i < 16; i += 4) 226 | secretbuf.writeUInt32LE(secretbuf.readUInt32BE(i, true), i, true); 227 | 228 | function _onconnect() { 229 | bc = 0; 230 | state = 'secret'; 231 | sock.write(secretbuf); 232 | } 233 | function _ondata(data) { 234 | bc += data.length; 235 | if (state === 'secret') { 236 | // the secret we sent is echoed back to us by cygwin, not sure of 237 | // the reason for that, but we ignore it nonetheless ... 238 | if (bc === 16) { 239 | bc = 0; 240 | state = 'creds'; 241 | sock.write(credsbuf); 242 | } 243 | } else if (state === 'creds') { 244 | // if this is the first attempt, make sure to gather the valid 245 | // uid and gid for our next attempt 246 | if (!isRetrying) 247 | inbuf.push(data); 248 | 249 | if (bc === 12) { 250 | sock.removeListener('connect', _onconnect); 251 | sock.removeListener('data', _ondata); 252 | sock.removeListener('close', _onclose); 253 | if (isRetrying) { 254 | addSockListeners(); 255 | sock.emit('connect'); 256 | } else { 257 | isRetrying = true; 258 | credsbuf = Buffer.concat(inbuf); 259 | credsbuf.writeUInt32LE(process.pid, 0, true); 260 | sock.destroy(); 261 | tryConnect(); 262 | } 263 | } 264 | } 265 | } 266 | function _onclose() { 267 | cb(new Error('Problem negotiating cygwin unix socket security')); 268 | } 269 | function tryConnect() { 270 | sock = new Socket(); 271 | sock.once('connect', _onconnect); 272 | sock.on('data', _ondata); 273 | sock.once('close', _onclose); 274 | sock.connect(port); 275 | } 276 | tryConnect(); 277 | } else 278 | cb(new Error('Malformed cygwin unix socket file')); 279 | }); 280 | return; 281 | } 282 | } else 283 | sock = new Socket(); 284 | 285 | function addSockListeners() { 286 | if (!accept && !reject) { 287 | sock.once('connect', onconnect); 288 | sock.on('data', ondata); 289 | sock.once('error', onerror); 290 | sock.once('close', onclose); 291 | } else { 292 | var chan; 293 | sock.once('connect', function() { 294 | chan = accept(); 295 | chan.once('close', function() { 296 | sock.end(); 297 | }).on('data', function(data) { 298 | sock.write(data); 299 | }); 300 | sock.on('data', function(data) { 301 | chan.write(data); 302 | }); 303 | }); 304 | sock.once('close', function() { 305 | if (!chan) 306 | reject(); 307 | }); 308 | } 309 | } 310 | addSockListeners(); 311 | sock.connect(sockPath); 312 | }; 313 | 314 | 315 | // win32 only ------------------------------------------------------------------ 316 | if (process.platform === 'win32') { 317 | var RET_ERR_BADARGS = 10, 318 | RET_ERR_UNAVAILABLE = 11, 319 | RET_ERR_NOMAP = 12, 320 | RET_ERR_BINSTDIN = 13, 321 | RET_ERR_BINSTDOUT = 14, 322 | RET_ERR_BADLEN = 15; 323 | 324 | var ERROR = {}, EXEPATH = path.resolve(__dirname, '..', 'util/pagent.exe'); 325 | ERROR[RET_ERR_BADARGS] = new Error('Invalid pagent.exe arguments'); 326 | ERROR[RET_ERR_UNAVAILABLE] = new Error('Pageant is not running'); 327 | ERROR[RET_ERR_NOMAP] = new Error('pagent.exe could not create an mmap'); 328 | ERROR[RET_ERR_BINSTDIN] = new Error('pagent.exe could not set mode for stdin'); 329 | ERROR[RET_ERR_BINSTDOUT] = new Error('pagent.exe could not set mode for stdout'); 330 | ERROR[RET_ERR_BADLEN] = new Error('pagent.exe did not get expected input payload'); 331 | 332 | function PageantSock() { 333 | this.proc = undefined; 334 | } 335 | inherits(PageantSock, EventEmitter); 336 | 337 | PageantSock.prototype.write = function(buf) { 338 | var self = this, 339 | proc, 340 | hadError = false; 341 | proc = this.proc = cp.spawn(EXEPATH, [ buf.length ]); 342 | proc.stdout.on('data', function(data) { 343 | self.emit('data', data); 344 | }); 345 | proc.once('error', function(err) { 346 | if (!hadError) { 347 | hadError = true; 348 | self.emit('error', err); 349 | } 350 | }); 351 | proc.once('exit', function(code) { 352 | self.proc = undefined; 353 | if (ERROR[code] && !hadError) { 354 | hadError = true; 355 | self.emit('error', ERROR[code]); 356 | } 357 | self.emit('close', hadError); 358 | }); 359 | proc.stdin.end(buf); 360 | }; 361 | PageantSock.prototype.end = PageantSock.prototype.destroy = function() { 362 | if (this.proc) 363 | this.proc.kill(); 364 | }; 365 | PageantSock.prototype.connect = function() { 366 | this.emit('connect'); 367 | }; 368 | } -------------------------------------------------------------------------------- /lib/Parser.constants.js: -------------------------------------------------------------------------------- 1 | var i = 0, keys, len; 2 | 3 | var MESSAGE = exports.MESSAGE = { 4 | // Transport layer protocol -- generic (1-19) 5 | DISCONNECT: 1, 6 | IGNORE: 2, 7 | UNIMPLEMENTED: 3, 8 | DEBUG: 4, 9 | SERVICE_REQUEST: 5, 10 | SERVICE_ACCEPT: 6, 11 | 12 | // Transport layer protocol -- algorithm negotiation (20-29) 13 | KEXINIT: 20, 14 | NEWKEYS: 21, 15 | 16 | // Transport layer protocol -- key exchange method-specific (30-49) 17 | KEXDH_INIT: 30, 18 | KEXDH_REPLY: 31, 19 | 20 | // User auth protocol -- generic (50-59) 21 | USERAUTH_REQUEST: 50, 22 | USERAUTH_FAILURE: 51, 23 | USERAUTH_SUCCESS: 52, 24 | USERAUTH_BANNER: 53, 25 | 26 | // User auth protocol -- user auth method-specific (60-79) 27 | 28 | // Connection protocol -- generic (80-89) 29 | GLOBAL_REQUEST: 80, 30 | REQUEST_SUCCESS: 81, 31 | REQUEST_FAILURE: 82, 32 | 33 | // Connection protocol -- channel-related (90-127) 34 | CHANNEL_OPEN: 90, 35 | CHANNEL_OPEN_CONFIRMATION: 91, 36 | CHANNEL_OPEN_FAILURE: 92, 37 | CHANNEL_WINDOW_ADJUST: 93, 38 | CHANNEL_DATA: 94, 39 | CHANNEL_EXTENDED_DATA: 95, 40 | CHANNEL_EOF: 96, 41 | CHANNEL_CLOSE: 97, 42 | CHANNEL_REQUEST: 98, 43 | CHANNEL_SUCCESS: 99, 44 | CHANNEL_FAILURE: 100 45 | 46 | // Reserved for client protocols (128-191) 47 | 48 | // Local extensions (192-155) 49 | }; 50 | for (i=0,keys=Object.keys(MESSAGE),len=keys.length; i= '1.0.1') { 213 | if (process.version >= 'v0.11.12') { 214 | // node v0.11.12 introduced support for setting AAD, which is needed for 215 | // AES-GCM in SSH2 216 | CIPHER = [ 217 | // http://tools.ietf.org/html/rfc5647 218 | 'aes128-gcm', 219 | 'aes128-gcm@openssh.com', 220 | 'aes256-gcm', 221 | 'aes256-gcm@openssh.com' 222 | ].concat(CIPHER); 223 | 224 | KEX = [ 225 | 'diffie-hellman-group-exchange-sha256', 226 | 'diffie-hellman-group-exchange-sha1' 227 | ].concat(KEX); 228 | KEX_LIST = new Buffer(KEX.join(',')); 229 | } 230 | CIPHER = [ 231 | // http://tools.ietf.org/html/rfc4344#section-4 232 | 'aes256-ctr', // RECOMMENDED 233 | 'aes192-ctr', // RECOMMENDED 234 | 'aes128-ctr' // RECOMMENDED 235 | ].concat(CIPHER); 236 | CIPHER_LIST = new Buffer(CIPHER.join(',')); 237 | 238 | // http://tools.ietf.org/html/rfc5647#section-5.1: 239 | // If AES-GCM is selected as the encryption algorithm for a given 240 | // tunnel, AES-GCM MUST also be selected as the Message Authentication 241 | // Code (MAC) algorithm. ***Conversely, if AES-GCM is selected as the MAC 242 | // algorithm, it MUST also be selected as the encryption algorithm.*** 243 | // Note: @openssh.com versions deviate from the above rule 244 | /*HMAC = [ 245 | // http://tools.ietf.org/html/rfc5647 246 | 'aes128-gcm', 247 | 'aes128-gcm@openssh.com', 248 | 'aes256-gcm', 249 | 'aes256-gcm@openssh.com' 250 | ].concat(HMAC); 251 | HMAC_LIST = new Buffer(HMAC.join(','));*/ 252 | } 253 | 254 | exports.ALGORITHMS = { 255 | KEX: KEX, 256 | KEX_LIST: KEX_LIST, 257 | KEX_LIST_SIZE: KEX_LIST.length, 258 | SERVER_HOST_KEY: SERVER_HOST_KEY, 259 | SERVER_HOST_KEY_LIST: SERVER_HOST_KEY_LIST, 260 | SERVER_HOST_KEY_LIST_SIZE: SERVER_HOST_KEY_LIST.length, 261 | CIPHER: CIPHER, 262 | CIPHER_LIST: CIPHER_LIST, 263 | CIPHER_LIST_SIZE: CIPHER_LIST.length, 264 | HMAC: HMAC, 265 | HMAC_LIST: HMAC_LIST, 266 | HMAC_LIST_SIZE: HMAC_LIST.length, 267 | COMPRESS: COMPRESS, 268 | COMPRESS_LIST: COMPRESS_LIST, 269 | COMPRESS_LIST_SIZE: COMPRESS_LIST.length 270 | }; 271 | exports.SSH_TO_OPENSSL = { 272 | // ciphers 273 | 'aes128-gcm': 'aes-128-gcm', 274 | 'aes256-gcm': 'aes-256-gcm', 275 | 'aes128-gcm@openssh.com': 'aes-128-gcm', 276 | 'aes256-gcm@openssh.com': 'aes-256-gcm', 277 | '3des-cbc': 'des-ede3-cbc', 278 | 'blowfish-cbc': 'bf-cbc', 279 | 'aes256-cbc': 'aes-256-cbc', 280 | 'aes192-cbc': 'aes-192-cbc', 281 | 'aes128-cbc': 'aes-128-cbc', 282 | 'idea-cbc': 'idea-cbc', 283 | 'cast128-cbc': 'cast-cbc', 284 | 'rijndael-cbc@lysator.liu.se': 'aes-256-cbc', 285 | 'arcfour128': 'rc4', 286 | 'arcfour256': 'rc4', 287 | 'arcfour512': 'rc4', 288 | 'arcfour': 'rc4', 289 | 'camellia128-cbc': 'camellia-128-cbc', 290 | 'camellia192-cbc': 'camellia-192-cbc', 291 | 'camellia256-cbc': 'camellia-256-cbc', 292 | 'camellia128-cbc@openssh.com': 'camellia-128-cbc', 293 | 'camellia192-cbc@openssh.com': 'camellia-192-cbc', 294 | 'camellia256-cbc@openssh.com': 'camellia-256-cbc', 295 | '3des-ctr': 'des-ede3', 296 | 'blowfish-ctr': 'bf-ecb', 297 | 'aes256-ctr': 'aes-256-ctr', 298 | 'aes192-ctr': 'aes-192-ctr', 299 | 'aes128-ctr': 'aes-128-ctr', 300 | 'cast128-ctr': 'cast5-ecb', 301 | 'camellia128-ctr': 'camellia-128-ecb', 302 | 'camellia192-ctr': 'camellia-192-ecb', 303 | 'camellia256-ctr': 'camellia-256-ecb', 304 | 'camellia128-ctr@openssh.com': 'camellia-128-ecb', 305 | 'camellia192-ctr@openssh.com': 'camellia-192-ecb', 306 | 'camellia256-ctr@openssh.com': 'camellia-256-ecb', 307 | // hmac 308 | 'hmac-sha1-96': 'sha1', 309 | 'hmac-sha1': 'sha1', 310 | 'hmac-sha2-256': 'sha256', 311 | 'hmac-sha2-256-96': 'sha256', 312 | 'hmac-sha2-512': 'sha512', 313 | 'hmac-sha2-512-96': 'sha512', 314 | 'hmac-md5-96': 'md5', 315 | 'hmac-md5': 'md5', 316 | 'hmac-ripemd160': 'ripemd160' 317 | }; 318 | -------------------------------------------------------------------------------- /lib/Channel.js: -------------------------------------------------------------------------------- 1 | var inherits = require('util').inherits, 2 | DuplexStream = require('stream').Duplex 3 | || require('readable-stream').Duplex, 4 | ReadableStream = require('stream').Readable 5 | || require('readable-stream').Readable; 6 | 7 | var consts = require('./Parser.constants'); 8 | 9 | var PACKET_SIZE = 32 * 1024, 10 | MAX_WINDOW = 1 * 1024 * 1024, 11 | SIGNALS = ['ABRT', 'ALRM', 'FPE', 'HUP', 'ILL', 'INT', 'KILL', 'PIPE', 12 | 'QUIT', 'SEGV', 'TERM', 'USR1', 'USR2'], 13 | MESSAGE = consts.MESSAGE, 14 | TERMINAL_MODE = consts.TERMINAL_MODE, 15 | CUSTOM_EVENTS = [ 16 | 'CHANNEL_EOF', 17 | 'CHANNEL_CLOSE', 18 | 'CHANNEL_DATA', 19 | 'CHANNEL_EXTENDED_DATA', 20 | 'CHANNEL_WINDOW_ADJUST', 21 | 'CHANNEL_SUCCESS', 22 | 'CHANNEL_FAILURE', 23 | 'CHANNEL_REQUEST' 24 | ], 25 | CUSTOM_EVENTS_LEN = CUSTOM_EVENTS.length; 26 | 27 | function Channel(info, conn) { 28 | if (!(this instanceof Channel)) 29 | return new Channel(info, conn); 30 | 31 | var self = this; 32 | 33 | this.type = info.type; 34 | this.subtype = undefined; 35 | /* 36 | incoming and outgoing contain these properties: 37 | { 38 | id: undefined, 39 | window: undefined, 40 | packetSize: undefined, 41 | state: 'closed' 42 | } 43 | */ 44 | this.incoming = info.incoming; 45 | this.outgoing = info.outgoing; 46 | 47 | this._conn = conn; 48 | this._stream = undefined; 49 | this._callbacks = []; 50 | this._hasX11 = false; 51 | 52 | function ondrain() { 53 | var stream = self._stream; 54 | if (stream && stream._waitConDrain) { 55 | stream._waitConDrain = false; 56 | if (!stream._waitWindow) { 57 | if (stream._chunk) 58 | stream._write(stream._chunk, null, stream._chunkcb); 59 | else if (stream._chunkcb) 60 | stream._chunkcb(); 61 | } 62 | } 63 | } 64 | conn.on('drain', ondrain); 65 | 66 | conn._parser.once('CHANNEL_EOF:' + this.incoming.id, function() { 67 | self.incoming.state = 'eof'; 68 | if (self._stream) { 69 | self._stream.push(null); 70 | self._stream.stderr.push(null); 71 | } 72 | }); 73 | 74 | conn._parser.once('CHANNEL_CLOSE:' + this.incoming.id, function() { 75 | self.incoming.state = 'closed'; 76 | if (self.outgoing.state === 'open' || self.outgoing.state === 'eof') 77 | self.close(); 78 | if (self.outgoing.state === 'closing') 79 | self.outgoing.state = 'closed'; 80 | conn._channels.splice(conn._channels.indexOf(self.incoming.id), 1); 81 | if (self._stream) { 82 | var stream = self._stream, 83 | state = stream._writableState; 84 | self._stream = undefined; 85 | conn.removeListener('drain', ondrain); 86 | if (!state.ending && !state.finished) 87 | stream.end(); 88 | stream.emit('close'); 89 | stream.stderr.emit('close'); 90 | } 91 | for (var i = 0; i < CUSTOM_EVENTS_LEN; ++i) { 92 | // Since EventEmitters do not actually *delete* event names in the 93 | // emitter's event array, we must do this manually so as not to leak 94 | // our custom, channel-specific event names. 95 | delete conn._parser._events[CUSTOM_EVENTS[i] + ':' + self.incoming.id]; 96 | } 97 | }); 98 | 99 | conn._parser.on('CHANNEL_DATA:' + this.incoming.id, function(data) { 100 | self.incoming.window -= data.length; 101 | if (self._stream) { 102 | if (!self._stream.push(data)) { 103 | self._stream._waitChanDrain = true; 104 | return; 105 | } 106 | } 107 | if (self.incoming.window === 0) 108 | self._sendWndAdjust(); 109 | }); 110 | 111 | conn._parser.on('CHANNEL_EXTENDED_DATA:' + this.incoming.id, 112 | function(type, data) { 113 | self.incoming.window -= data.length; 114 | if (self._stream) { 115 | if (!self._stream.stderr.push(data)) { 116 | self._stream._waitChanDrain = true; 117 | return; 118 | } 119 | } 120 | if (self.incoming.window === 0) 121 | self._sendWndAdjust(); 122 | } 123 | ); 124 | 125 | conn._parser.on('CHANNEL_WINDOW_ADJUST:' + this.incoming.id, function(amt) { 126 | // the server is allowing us to send `amt` more bytes of data 127 | self.outgoing.window += amt; 128 | var stream = self._stream; 129 | if (stream && stream._waitWindow) { 130 | stream._waitWindow = false; 131 | if (!stream._waitConDrain) { 132 | if (stream._chunk) 133 | stream._write(stream._chunk, null, stream._chunkcb); 134 | else if (stream._chunkcb) 135 | stream._chunkcb(); 136 | } 137 | } 138 | }); 139 | 140 | conn._parser.on('CHANNEL_SUCCESS:' + this.incoming.id, function() { 141 | if (self._callbacks.length) 142 | self._callbacks.shift()(false); 143 | }); 144 | 145 | conn._parser.on('CHANNEL_FAILURE:' + this.incoming.id, function() { 146 | if (self._callbacks.length) 147 | self._callbacks.shift()(true); 148 | }); 149 | 150 | conn._parser.on('CHANNEL_REQUEST:' + this.incoming.id, function(info) { 151 | if (self._stream) { 152 | if (info.request === 'exit-status') 153 | self._stream.emit('exit', info.code); 154 | else if (info.request === 'exit-signal') { 155 | self._stream.emit('exit', 156 | null, 157 | 'SIG' + info.signal, 158 | info.coredump, 159 | info.description, 160 | info.lang); 161 | } else 162 | return; 163 | self.close(); 164 | } 165 | }); 166 | } 167 | 168 | Channel.prototype.eof = function() { 169 | if (this.outgoing.state === 'open') { 170 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent EOF'); 171 | // Note: CHANNEL_EOF does not consume window space 172 | /* 173 | byte SSH_MSG_CHANNEL_EOF 174 | uint32 recipient channel 175 | */ 176 | var buf = new Buffer(1 + 4); 177 | this.outgoing.state = 'eof'; 178 | buf[0] = MESSAGE.CHANNEL_EOF; 179 | buf.writeUInt32BE(this.outgoing.id, 1, true); 180 | return this._conn._send(buf); 181 | } else 182 | return; 183 | }; 184 | 185 | Channel.prototype.close = function() { 186 | if (this.outgoing.state === 'open' || this.outgoing.state === 'eof') { 187 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CLOSE'); 188 | // Note: CHANNEL_CLOSE does not consume window space 189 | /* 190 | byte SSH_MSG_CHANNEL_CLOSE 191 | uint32 recipient channel 192 | */ 193 | var buf = new Buffer(1 + 4); 194 | buf[0] = MESSAGE.CHANNEL_CLOSE; 195 | buf.writeUInt32BE(this.outgoing.id, 1, true); 196 | this.outgoing.state = 'closing'; 197 | return this._conn._send(buf); 198 | } else 199 | return; 200 | }; 201 | 202 | Channel.prototype._sendAgentFwd = function(cb) { 203 | // Note: CHANNEL_REQUEST does not consume window space 204 | /* 205 | byte SSH_MSG_CHANNEL_REQUEST 206 | uint32 recipient channel 207 | string "auth-agent-req@openssh.com" 208 | boolean want reply 209 | */ 210 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (auth-agent-req@openssh.com)'); 211 | var buf = new Buffer(1 + 4 + 4 + 26 + 1); 212 | buf[0] = MESSAGE.CHANNEL_REQUEST; 213 | buf.writeUInt32BE(this.outgoing.id, 1, true); 214 | buf.writeUInt32BE(26, 5, true); 215 | buf.write('auth-agent-req@openssh.com', 9, 26, 'ascii'); 216 | buf[35] = 1; 217 | 218 | var self = this; 219 | this._callbacks.push(function(had_err) { 220 | if (had_err) 221 | return cb(new Error('Unable to request agent forwarding')); 222 | self.agentForward = true; 223 | cb(); 224 | }); 225 | 226 | return this._conn._send(buf); 227 | }; 228 | 229 | Channel.prototype._sendTermSizeChg = function(rows, cols, height, width) { 230 | // Note: CHANNEL_REQUEST does not consume window space 231 | /* 232 | byte SSH_MSG_CHANNEL_REQUEST 233 | uint32 recipient channel 234 | string "window-change" 235 | boolean FALSE 236 | uint32 terminal width, columns 237 | uint32 terminal height, rows 238 | uint32 terminal width, pixels 239 | uint32 terminal height, pixels 240 | */ 241 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (window-change)'); 242 | var buf = new Buffer(1 + 4 + 4 + 13 + 1 + 4 + 4 + 4 + 4); 243 | buf[0] = MESSAGE.CHANNEL_REQUEST; 244 | buf.writeUInt32BE(this.outgoing.id, 1, true); 245 | buf.writeUInt32BE(13, 5, true); 246 | buf.write('window-change', 9, 13, 'ascii'); 247 | buf[22] = 0; 248 | buf.writeUInt32BE(cols, 23, true); 249 | buf.writeUInt32BE(rows, 27, true); 250 | buf.writeUInt32BE(width, 31, true); 251 | buf.writeUInt32BE(height, 35, true); 252 | 253 | return this._conn._send(buf); 254 | }; 255 | 256 | Channel.prototype._sendPtyReq = function(rows, cols, height, width, term, modes, 257 | cb) { 258 | // Note: CHANNEL_REQUEST does not consume window space 259 | /* 260 | byte SSH_MSG_CHANNEL_REQUEST 261 | uint32 recipient channel 262 | string "pty-req" 263 | boolean want reply 264 | string TERM environment variable value (e.g., vt100) 265 | uint32 terminal width, characters (e.g., 80) 266 | uint32 terminal height, rows (e.g., 24) 267 | uint32 terminal width, pixels (e.g., 640) 268 | uint32 terminal height, pixels (e.g., 480) 269 | string encoded terminal modes 270 | */ 271 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (pty-req)'); 272 | if (!term || !term.length) 273 | term = 'vt100'; 274 | if (!modes || !modes.length) 275 | modes = String.fromCharCode(TERMINAL_MODE.TTY_OP_END); 276 | var termLen = term.length, 277 | modesLen = modes.length, 278 | p = 21, 279 | buf = new Buffer(1 + 4 + 4 + 7 + 1 + 4 + termLen + 4 + 4 + 4 + 4 + 4 280 | + modesLen); 281 | buf[0] = MESSAGE.CHANNEL_REQUEST; 282 | buf.writeUInt32BE(this.outgoing.id, 1, true); 283 | buf.writeUInt32BE(7, 5, true); 284 | buf.write('pty-req', 9, 7, 'ascii'); 285 | buf[16] = 1; 286 | buf.writeUInt32BE(termLen, 17, true); 287 | buf.write(term, 21, termLen, 'utf8'); 288 | buf.writeUInt32BE(cols, p += termLen, true); 289 | buf.writeUInt32BE(rows, p += 4, true); 290 | buf.writeUInt32BE(width, p += 4, true); 291 | buf.writeUInt32BE(height, p += 4, true); 292 | buf.writeUInt32BE(modesLen, p += 4, true); 293 | buf.write(modes, p += 4, modesLen, 'utf8'); 294 | 295 | this._callbacks.push(function(had_err) { 296 | if (had_err) 297 | return cb(new Error('Unable to request a pseudo-terminal')); 298 | cb(); 299 | }); 300 | 301 | return this._conn._send(buf); 302 | }; 303 | 304 | Channel.prototype._sendShell = function(cb) { 305 | // Note: CHANNEL_REQUEST does not consume window space 306 | /* 307 | byte SSH_MSG_CHANNEL_REQUEST 308 | uint32 recipient channel 309 | string "shell" 310 | boolean want reply 311 | */ 312 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (shell)'); 313 | var self = this; 314 | var buf = new Buffer(1 + 4 + 4 + 5 + 1); 315 | buf[0] = MESSAGE.CHANNEL_REQUEST; 316 | buf.writeUInt32BE(this.outgoing.id, 1, true); 317 | buf.writeUInt32BE(5, 5, true); 318 | buf.write('shell', 9, 5, 'ascii'); 319 | buf[14] = 1; 320 | 321 | this._callbacks.push(function(had_err) { 322 | if (had_err) 323 | return cb(new Error('Unable to open shell')); 324 | self.subtype = 'shell'; 325 | self._stream = new ChannelStream(self); 326 | cb(undefined, self._stream); 327 | }); 328 | 329 | return this._conn._send(buf); 330 | }; 331 | 332 | Channel.prototype._sendExec = function(cmd, opts, cb) { 333 | // Note: CHANNEL_REQUEST does not consume window space 334 | /* 335 | byte SSH_MSG_CHANNEL_REQUEST 336 | uint32 recipient channel 337 | string "exec" 338 | boolean want reply 339 | string command 340 | */ 341 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (exec)'); 342 | var self = this; 343 | var cmdlen = (Buffer.isBuffer(cmd) ? cmd.length : Buffer.byteLength(cmd)), 344 | buf = new Buffer(1 + 4 + 4 + 4 + 1 + 4 + cmdlen); 345 | buf[0] = MESSAGE.CHANNEL_REQUEST; 346 | buf.writeUInt32BE(this.outgoing.id, 1, true); 347 | buf.writeUInt32BE(4, 5, true); 348 | buf.write('exec', 9, 4, 'ascii'); 349 | buf[13] = 1; 350 | buf.writeUInt32BE(cmdlen, 14, true); 351 | if (Buffer.isBuffer(cmd)) 352 | cmd.copy(buf, 18); 353 | else 354 | buf.write(cmd, 18, cmdlen, 'utf8'); 355 | 356 | this._callbacks.push(function(had_err) { 357 | if (had_err) 358 | return cb(new Error('Unable to exec')); 359 | self.subtype = 'exec'; 360 | self._stream = new ChannelStream(self, opts); 361 | cb(undefined, self._stream); 362 | }); 363 | 364 | return this._conn._send(buf); 365 | }; 366 | 367 | Channel.prototype._sendSignal = function(signal) { 368 | // Note: CHANNEL_REQUEST does not consume window space 369 | /* 370 | byte SSH_MSG_CHANNEL_REQUEST 371 | uint32 recipient channel 372 | string "signal" 373 | boolean FALSE 374 | string signal name (without the "SIG" prefix) 375 | */ 376 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (signal)'); 377 | signal = signal.toUpperCase(); 378 | if (signal.length >= 3 379 | && signal[0] === 'S' && signal[1] === 'I' && signal[2] === 'G') 380 | signal = signal.substr(3); 381 | if (SIGNALS.indexOf(signal) === -1) 382 | throw new Error('Invalid signal: ' + signal); 383 | var signalLen = signal.length, 384 | buf = new Buffer(1 + 4 + 4 + 6 + 1 + 4 + signalLen); 385 | buf[0] = MESSAGE.CHANNEL_REQUEST; 386 | buf.writeUInt32BE(this.outgoing.id, 1, true); 387 | buf.writeUInt32BE(6, 5, true); 388 | buf.write('signal', 9, 6, 'ascii'); 389 | buf[15] = 0; 390 | buf.writeUInt32BE(signalLen, 16, true); 391 | buf.write(signal, 20, signalLen, 'ascii'); 392 | 393 | return this._conn._send(buf); 394 | }; 395 | 396 | Channel.prototype._sendEnv = function(env) { 397 | var keys, buf, ret = true; 398 | if (env && (keys = Object.keys(env)).length > 0) { 399 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (env)'); 400 | // Note: CHANNEL_REQUEST does not consume window space 401 | /* 402 | byte SSH_MSG_CHANNEL_REQUEST 403 | uint32 recipient channel 404 | string "env" 405 | boolean want reply 406 | string variable name 407 | string variable value 408 | */ 409 | for (var i = 0, klen, vlen, len = keys.length; i < len; ++i) { 410 | klen = Buffer.byteLength(keys[i]); 411 | if (Buffer.isBuffer(env[keys[i]])) 412 | vlen = env[keys[i]].length; 413 | else 414 | vlen = Buffer.byteLength(env[keys[i]]); 415 | buf = new Buffer(1 + 4 + 4 + 3 + 1 + 4 + klen + 4 + vlen); 416 | buf[0] = MESSAGE.CHANNEL_REQUEST; 417 | buf.writeUInt32BE(this.outgoing.id, 1, true); 418 | buf.writeUInt32BE(3, 5, true); 419 | buf.write('env', 9, 3, 'ascii'); 420 | buf[12] = 0; 421 | buf.writeUInt32BE(klen, 13, true); 422 | buf.write(keys[i], 17, klen, 'ascii'); 423 | buf.writeUInt32BE(vlen, 17 + klen, true); 424 | if (Buffer.isBuffer(env[keys[i]])) 425 | env[keys[i]].copy(buf, 17 + klen + 4); 426 | else 427 | buf.write(env[keys[i]], 17 + klen + 4, vlen, 'utf8'); 428 | ret = this._conn._send(buf); 429 | } 430 | return ret; 431 | } else 432 | return; 433 | }; 434 | 435 | Channel.prototype._sendX11 = function(cfg, cb) { 436 | // Note: CHANNEL_REQUEST does not consume window space 437 | /* 438 | byte SSH_MSG_CHANNEL_REQUEST 439 | uint32 recipient channel 440 | string "x11-req" 441 | boolean want reply 442 | boolean single connection 443 | string x11 authentication protocol 444 | string x11 authentication cookie 445 | uint32 x11 screen number 446 | */ 447 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (x11)'); 448 | var self = this; 449 | var protolen = Buffer.byteLength(cfg.proto), 450 | cookielen = Buffer.byteLength(cfg.cookie), 451 | buf = new Buffer(1 + 4 + 4 + 7 + 1 + 1 + 4 + protolen + 4 + cookielen + 4); 452 | buf[0] = MESSAGE.CHANNEL_REQUEST; 453 | buf.writeUInt32BE(this.outgoing.id, 1, true); 454 | buf.writeUInt32BE(7, 5, true); 455 | buf.write('x11-req', 9, 7, 'ascii'); 456 | buf[16] = 1; 457 | buf[17] = (cfg.single ? 1 : 0); 458 | buf.writeUInt32BE(protolen, 18, true); 459 | var bp = 22; 460 | if (Buffer.isBuffer(cfg.proto)) 461 | cfg.proto.copy(buf, bp); 462 | else 463 | buf.write(cfg.proto, bp, protolen, 'utf8'); 464 | bp += protolen; 465 | buf.writeUInt32BE(cookielen, bp, true); 466 | bp += 4; 467 | if (Buffer.isBuffer(cfg.cookie)) 468 | cfg.cookie.copy(buf, bp); 469 | else 470 | buf.write(cfg.cookie, bp, cookielen, 'utf8'); 471 | bp += cookielen; 472 | buf.writeUInt32BE((cfg.screen || 0), bp, true); 473 | 474 | this._callbacks.push(function(had_err) { 475 | if (had_err) 476 | return cb(new Error('Unable to request X11')); 477 | self._hasX11 = true; 478 | ++self._conn._acceptX11; 479 | cb(); 480 | }); 481 | 482 | return this._conn._send(buf); 483 | }; 484 | 485 | Channel.prototype._sendSubsystem = function(name, cb) { 486 | // Note: CHANNEL_REQUEST does not consume window space 487 | /* 488 | byte SSH_MSG_CHANNEL_REQUEST 489 | uint32 recipient channel 490 | string "subsystem" 491 | boolean want reply 492 | string subsystem name 493 | */ 494 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_REQUEST (subsystem)'); 495 | var sublen = Buffer.byteLength(name), 496 | self = this, 497 | buf = new Buffer(1 + 4 + 4 + 9 + 1 + 4 + sublen); 498 | buf[0] = MESSAGE.CHANNEL_REQUEST; 499 | buf.writeUInt32BE(this.outgoing.id, 1, true); 500 | buf.writeUInt32BE(9, 5, true); 501 | buf.write('subsystem', 9, 9, 'ascii'); 502 | buf[18] = 1; 503 | buf.writeUInt32BE(sublen, 19, true); 504 | buf.write(name, 23, sublen, 'ascii'); 505 | 506 | this._callbacks.push(function(had_err) { 507 | if (had_err) 508 | return cb(new Error('Unable to start subsystem: ' + name)); 509 | self.subtype = 'subsystem'; 510 | self._stream = new ChannelStream(self); 511 | cb(undefined, self._stream); 512 | }); 513 | 514 | return this._conn._send(buf); 515 | }; 516 | 517 | Channel.prototype._sendWndAdjust = function(amt) { 518 | /* 519 | byte SSH_MSG_CHANNEL_WINDOW_ADJUST 520 | uint32 recipient channel 521 | uint32 bytes to add 522 | */ 523 | this._conn._debug&&this._conn._debug('DEBUG: Channel: Sent CHANNEL_WINDOW_ADJUST'); 524 | amt = amt || MAX_WINDOW; 525 | var buf = new Buffer(1 + 4 + 4); 526 | buf[0] = MESSAGE.CHANNEL_WINDOW_ADJUST; 527 | buf.writeUInt32BE(this.outgoing.id, 1, true); 528 | buf.writeUInt32BE(amt, 5, true); 529 | 530 | this.incoming.window += amt; 531 | 532 | return this._conn._send(buf); 533 | }; 534 | 535 | Channel.MAX_WINDOW = MAX_WINDOW; 536 | Channel.PACKET_SIZE = PACKET_SIZE; 537 | Channel.ChannelStream = ChannelStream; 538 | 539 | module.exports = Channel; 540 | 541 | function ChannelStream(channel, opts) { 542 | if (!(this instanceof ChannelStream)) 543 | return new ChannelStream(channel, opts); 544 | 545 | var streamOpts = { 546 | highWaterMark: MAX_WINDOW, 547 | allowHalfOpen: (!opts || (opts && opts.allowHalfOpen)) 548 | }; 549 | 550 | DuplexStream.call(this, streamOpts); 551 | 552 | this.stdin = this.stdout = this; 553 | this.stderr = new ReadableStream(streamOpts); 554 | var self = this; 555 | this.stderr._read = function(n) { 556 | if (self._waitChanDrain) { 557 | self._waitChanDrain = false; 558 | if (self._channel.incoming.window === 0) 559 | self._channel._sendWndAdjust(); 560 | } 561 | }; 562 | this._channel = channel; 563 | 564 | // outgoing data 565 | this._waitConDrain = false; // TCP-level backpressure 566 | this._waitWindow = false; // SSH-level backpressure 567 | 568 | // incoming data 569 | this._waitChanDrain = false; // ChannelStream Readable side backpressure 570 | 571 | this._chunk = undefined; 572 | this._chunkcb = undefined; 573 | this.on('finish', function() { 574 | self._channel.eof(); 575 | if (!self.allowHalfOpen) 576 | self._channel.close(); 577 | }); 578 | channel._conn.once('close', function() { 579 | if (self.readable) { 580 | self.push(null); 581 | self.once('end', function() { 582 | process.nextTick(function() { 583 | self.emit('close'); 584 | }); 585 | }); 586 | } 587 | if (self.writable) 588 | self.end(); 589 | }); 590 | } 591 | inherits(ChannelStream, DuplexStream); 592 | 593 | ChannelStream.prototype._read = function(n) { 594 | if (this._waitChanDrain) { 595 | this._waitChanDrain = false; 596 | if (this._channel.incoming.window === 0) 597 | this._channel._sendWndAdjust(); 598 | } 599 | }; 600 | 601 | ChannelStream.prototype._write = function(data, encoding, cb) { 602 | var chan = this._channel, 603 | len = data.length, 604 | p = 0, 605 | buf, 606 | sliceLen; 607 | 608 | while (len - p > 0 && chan.outgoing.window > 0) { 609 | sliceLen = len - p; 610 | if (sliceLen > chan.outgoing.window) 611 | sliceLen = chan.outgoing.window; 612 | if (sliceLen > chan.outgoing.packetSize) 613 | sliceLen = chan.outgoing.packetSize; 614 | 615 | chan._conn._debug&&chan._conn._debug('DEBUG: Channel: Sent CHANNEL_DATA'); 616 | /* 617 | byte SSH_MSG_CHANNEL_DATA 618 | uint32 recipient channel 619 | string data 620 | */ 621 | buf = new Buffer(1 + 4 + 4 + sliceLen); 622 | buf[0] = MESSAGE.CHANNEL_DATA; 623 | buf.writeUInt32BE(chan.outgoing.id, 1, true); 624 | buf.writeUInt32BE(sliceLen, 5, true); 625 | data.copy(buf, 9, p, p + sliceLen); 626 | 627 | p += sliceLen; 628 | chan.outgoing.window -= sliceLen; 629 | 630 | if (!chan._conn._send(buf)) { 631 | this._waitConDrain = true; 632 | this._chunk = undefined; 633 | this._chunkcb = cb; 634 | break; 635 | } 636 | } 637 | 638 | if (len - p > 0) { 639 | if (chan.outgoing.window === 0) 640 | this._waitWindow = true; 641 | if (p > 0) { 642 | // partial 643 | buf = new Buffer(len - p); 644 | data.copy(buf, 0, p); 645 | this._chunk = buf; 646 | } else 647 | this._chunk = data; 648 | this._chunkcb = cb; 649 | return; 650 | } 651 | 652 | if (!this._waitConDrain) 653 | cb(); 654 | }; 655 | 656 | ChannelStream.prototype.destroy = function() { 657 | this.end(); 658 | }; 659 | 660 | // session type-specific methods 661 | 662 | ChannelStream.prototype.setWindow = function(rows, cols, height, width) { 663 | if (this._channel.type === 'session' && this._channel.subtype === 'shell') 664 | return this._channel._sendTermSizeChg(rows, cols, height, width); 665 | }; 666 | 667 | ChannelStream.prototype.signal = function(signalName) { 668 | if (this._channel.type === 'session' 669 | && (this._channel.subtype === 'shell' 670 | || this._channel.subtype === 'exec')) 671 | return this._channel._sendSignal(signalName); 672 | }; 673 | -------------------------------------------------------------------------------- /lib/Parser.js: -------------------------------------------------------------------------------- 1 | // TODO: * Filter control codes from strings 2 | // (as per http://tools.ietf.org/html/rfc4251#section-9.2) 3 | 4 | var crypto = require('crypto'), 5 | inherits = require('util').inherits, 6 | EventEmitter = require('events').EventEmitter; 7 | var StreamSearch = require('streamsearch'); 8 | var consts = require('./Parser.constants'), 9 | isGCM = require('./utils').isGCM, 10 | iv_inc = require('./utils').iv_inc; 11 | 12 | var MESSAGE = consts.MESSAGE, 13 | DISCONNECT_REASON = consts.DISCONNECT_REASON, 14 | CHANNEL_OPEN_FAILURE = consts.CHANNEL_OPEN_FAILURE, 15 | SSH_TO_OPENSSL = consts.SSH_TO_OPENSSL, 16 | I = 0, 17 | STATE_INIT = I++, 18 | STATE_GREETING = I++, 19 | STATE_HEADER = I++, 20 | STATE_PACKETBEFORE = I++, 21 | STATE_PACKET = I++, 22 | STATE_PACKETDATA = I++, 23 | STATE_PACKETDATAVERIFY = I++, 24 | STATE_PACKETDATAAFTER = I++, 25 | MAX_SEQNO = 4294967295, 26 | EXP_TYPE_HEADER = 0, 27 | EXP_TYPE_LF = 1, 28 | EXP_TYPE_BYTES = 2; // waits until n bytes have been seen 29 | 30 | function Parser() { 31 | this.debug = undefined; 32 | this._hmacBufCompute = new Buffer(9); 33 | this.reset(); 34 | } 35 | inherits(Parser, EventEmitter); 36 | 37 | Parser.prototype.execute = function(b, start, end) { 38 | start || (start = 0); 39 | end || (end = b.length); 40 | 41 | var i = start, 42 | buffer, 43 | skipDecrypt = false, 44 | buf, 45 | self = this, 46 | p = i, 47 | r, 48 | doDecryptGCM = false; 49 | 50 | while (true) { 51 | if (this._expectType !== undefined) { 52 | if (i >= end) 53 | break; 54 | if (this._expectType === EXP_TYPE_BYTES) { 55 | if (this._expectBuf) { 56 | this._expectBuf[this._expectPtr++] = b[i++]; 57 | if (this._expectPtr === this._expect) { 58 | buffer = this._expectBuf; 59 | this._expectBuf = undefined; 60 | this._expectPtr = 0; 61 | this._expectType = undefined; 62 | } 63 | } else 64 | ++i; 65 | continue; 66 | } else if (this._expectType === EXP_TYPE_HEADER) { 67 | r = this._ss.push(b); 68 | if (this._expectType !== undefined) { 69 | i += r; 70 | continue; 71 | } 72 | } else if (this._expectType === EXP_TYPE_LF) { 73 | if (b[i] === 0x0A) { 74 | this._expectType = undefined; 75 | if (p < i) { 76 | if (this._expectBuf === undefined) 77 | this._expectBuf = b.toString('ascii', p, i); 78 | else 79 | this._expectBuf += b.toString('ascii', p, i); 80 | } 81 | buffer = this._expectBuf; 82 | this._expectBuf = undefined; 83 | ++i; 84 | } else { 85 | if (++i === end && p < i) { 86 | if (this._expectBuf === undefined) 87 | this._expectBuf = b.toString('ascii', p, i); 88 | else 89 | this._expectBuf += b.toString('ascii', p, i); 90 | } 91 | continue; 92 | } 93 | } 94 | } 95 | 96 | if (this._state === STATE_INIT) { 97 | this.debug&&this.debug('DEBUG: Parser: STATE_INIT'); 98 | // retrieve all bytes that may come before the header 99 | this.expect(EXP_TYPE_HEADER); 100 | this._ss = new StreamSearch(new Buffer('SSH-')); 101 | this._ss.on('info', function onInfo(matched, data, start, end) { 102 | if (data) { 103 | if (this._greeting === undefined) 104 | this._greeting = data.toString('binary', start, end); 105 | else 106 | this._greeting += data.toString('binary', start, end); 107 | } 108 | if (matched) { 109 | if (end !== undefined) 110 | i = end; 111 | else 112 | i += 4; 113 | self._expectType = undefined; 114 | self._ss.removeListener('info', onInfo); 115 | } 116 | }); 117 | this._state = STATE_GREETING; 118 | } else if (this._state === STATE_GREETING) { 119 | this.debug&&this.debug('DEBUG: Parser: STATE_GREETING'); 120 | this._ss = undefined; 121 | // retrieve the identification bytes after the "SSH-" header 122 | p = i; 123 | this.expect(EXP_TYPE_LF); 124 | this._state = STATE_HEADER; 125 | } else if (this._state === STATE_HEADER) { 126 | this.debug&&this.debug('DEBUG: Parser: STATE_HEADER'); 127 | buffer = buffer.trim(); 128 | var idxDash = buffer.indexOf('-'), 129 | idxSpace = buffer.indexOf(' '); 130 | var header = { 131 | // RFC says greeting SHOULD be utf8 132 | greeting: this._greeting, 133 | ident_raw: 'SSH-' + buffer, 134 | versions: { 135 | protocol: buffer.substr(0, idxDash), 136 | server: (idxSpace === -1 137 | ? buffer.substr(idxDash + 1) 138 | : buffer.substring(idxDash + 1, idxSpace)) 139 | }, 140 | comments: (idxSpace > -1 ? buffer.substring(idxSpace + 1) : undefined) 141 | }; 142 | this._greeting = undefined; 143 | this.emit('header', header); 144 | if (this._state === STATE_INIT) { 145 | // we reset from an event handler 146 | // possibly due to an unsupported SSH protocol version? 147 | return; 148 | } 149 | this._state = STATE_PACKETBEFORE; 150 | } else if (this._state === STATE_PACKETBEFORE) { 151 | this.debug&&this.debug('DEBUG: Parser: STATE_PACKETBEFORE (expecting ' 152 | + this._decryptSize + ')'); 153 | // wait for the right number of bytes so we can determine the incoming 154 | // packet length 155 | this.expect(EXP_TYPE_BYTES, this._decryptSize, '_decryptBuf'); 156 | this._state = STATE_PACKET; 157 | } else if (this._state === STATE_PACKET) { 158 | this.debug&&this.debug('DEBUG: Parser: STATE_PACKET'); 159 | doDecryptGCM = (this._decrypt && isGCM(this._decryptType)); 160 | if (this._decrypt && !isGCM(this._decryptType)) 161 | buffer = this.decrypt(buffer); 162 | this._pktLen = buffer.readUInt32BE(0, true); 163 | var remainLen = this._pktLen + 4 - this._decryptSize; 164 | if (doDecryptGCM) { 165 | this._decrypt.setAAD(buffer.slice(0, 4)); 166 | this.debug&&this.debug('DEBUG: Parser: pktLen:' + this._pktLen 167 | + ',remainLen:' + remainLen); 168 | } else { 169 | this._padLen = buffer[4]; 170 | this.debug&&this.debug('DEBUG: Parser: pktLen:' + this._pktLen 171 | + ',padLen:' + this._padLen 172 | + ',remainLen:' + remainLen); 173 | } 174 | if (remainLen > 0) { 175 | if (doDecryptGCM) 176 | this._pktExtra = buffer.slice(4); 177 | else 178 | this._pktExtra = buffer.slice(5); 179 | // grab the rest of the packet 180 | this.expect(EXP_TYPE_BYTES, remainLen); 181 | this._state = STATE_PACKETDATA; 182 | } else if (remainLen < 0) 183 | this._state = STATE_PACKETBEFORE; 184 | else { 185 | // entire message fit into one block 186 | skipDecrypt = true; 187 | this._state = STATE_PACKETDATA; 188 | continue; 189 | } 190 | } else if (this._state === STATE_PACKETDATA) { 191 | this.debug&&this.debug('DEBUG: Parser: STATE_PACKETDATA'); 192 | doDecryptGCM = (this._decrypt && isGCM(this._decryptType)); 193 | if (this._decrypt && !skipDecrypt && !doDecryptGCM) 194 | buffer = this.decrypt(buffer); 195 | else if (skipDecrypt) 196 | skipDecrypt = false; 197 | var padStart = this._pktLen - this._padLen - 1; 198 | if (this._pktExtra) { 199 | buf = new Buffer(this._pktExtra.length + buffer.length); 200 | this._pktExtra.copy(buf); 201 | buffer.copy(buf, this._pktExtra.length); 202 | this._payload = buf.slice(0, padStart); 203 | } else { 204 | // entire message fit into one block 205 | if (doDecryptGCM) 206 | buf = buffer.slice(4); 207 | else 208 | buf = buffer.slice(5); 209 | this._payload = buffer.slice(5, 5 + padStart); 210 | } 211 | if (this._hmacSize !== undefined) { 212 | // wait for hmac hash 213 | this.debug&&this.debug('DEBUG: Parser: hmacSize:' + this._hmacSize); 214 | this.expect(EXP_TYPE_BYTES, this._hmacSize, '_hmacBuf'); 215 | this._state = STATE_PACKETDATAVERIFY; 216 | this._packet = buf; 217 | } else 218 | this._state = STATE_PACKETDATAAFTER; 219 | this._pktExtra = undefined; 220 | buf = undefined; 221 | } else if (this._state === STATE_PACKETDATAVERIFY) { 222 | this.debug&&this.debug('DEBUG: Parser: STATE_PACKETDATAVERIFY'); 223 | // verify packet data integrity 224 | if (this.hmacVerify(buffer)) { 225 | this._state = STATE_PACKETDATAAFTER; 226 | this._packet = undefined; 227 | } else { 228 | this.emit('error', new Error('Invalid HMAC')); 229 | return this.reset(); 230 | } 231 | } else if (this._state === STATE_PACKETDATAAFTER) { 232 | if (this.debug) { 233 | if (this._payload[0] === 60) { 234 | if (this._authMethod === 'password') 235 | this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: USERAUTH_PASSWD_CHANGEREQ'); 236 | else if (this._authMethod === 'keyboard-interactive') 237 | this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: USERAUTH_INFO_REQUEST'); 238 | else if (this._authMethod === 'pubkey') 239 | this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: USERAUTH_PK_OK'); 240 | } else if (this._payload[0] === 31 && this._kexdh !== 'group') 241 | this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: KEXDH_GEX_GROUP'); 242 | else if (this._payload[0] === 33 && this._kexdh !== 'group') 243 | this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: KEXDH_GEX_REPLY'); 244 | else if (this._payload[0] !== MESSAGE.CHANNEL_OPEN) { 245 | this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: ' 246 | + MESSAGE[this._payload[0]]); 247 | } 248 | } 249 | this.parsePacket(); 250 | if (this._state === STATE_INIT) { 251 | // we were reset due to some error/disagreement ? 252 | return; 253 | } 254 | this._state = STATE_PACKETBEFORE; 255 | this._payload = undefined; 256 | } 257 | if (buffer !== undefined) 258 | buffer = undefined; 259 | } 260 | }; 261 | 262 | Parser.prototype.parseKEXInit = function() { 263 | var payload = this._payload; 264 | 265 | /* 266 | byte SSH_MSG_KEXINIT 267 | byte[16] cookie (random bytes) 268 | name-list kex_algorithms 269 | name-list server_host_key_algorithms 270 | name-list encryption_algorithms_client_to_server 271 | name-list encryption_algorithms_server_to_client 272 | name-list mac_algorithms_client_to_server 273 | name-list mac_algorithms_server_to_client 274 | name-list compression_algorithms_client_to_server 275 | name-list compression_algorithms_server_to_client 276 | name-list languages_client_to_server 277 | name-list languages_server_to_client 278 | boolean first_kex_packet_follows 279 | uint32 0 (reserved for future extension) 280 | */ 281 | var init = { 282 | algorithms: { 283 | kex: undefined, 284 | srvHostKey: undefined, 285 | cs: { 286 | encrypt: undefined, 287 | mac: undefined, 288 | compress: undefined 289 | }, 290 | sc: { 291 | encrypt: undefined, 292 | mac: undefined, 293 | compress: undefined 294 | } 295 | }, 296 | languages: { 297 | cs: undefined, 298 | sc: undefined 299 | } 300 | }; 301 | init.algorithms.kex = readList(payload, 17); 302 | init.algorithms.srvHostKey = readList(payload, payload._pos); 303 | init.algorithms.cs.encrypt = readList(payload, payload._pos); 304 | init.algorithms.sc.encrypt = readList(payload, payload._pos); 305 | init.algorithms.cs.mac = readList(payload, payload._pos); 306 | init.algorithms.sc.mac = readList(payload, payload._pos); 307 | init.algorithms.cs.compress = readList(payload, payload._pos); 308 | init.algorithms.sc.compress = readList(payload, payload._pos); 309 | init.languages.cs = readList(payload, payload._pos); 310 | init.languages.sc = readList(payload, payload._pos); 311 | this._kexinit = payload; 312 | this.emit('KEXINIT', init); 313 | }; 314 | 315 | Parser.prototype.parseUserAuthMisc = function() { 316 | var payload = this._payload, message, lang; 317 | 318 | if (this._authMethod === 'password') { 319 | /* 320 | byte SSH_MSG_USERAUTH_PASSWD_CHANGEREQ 321 | string prompt in ISO-10646 UTF-8 encoding 322 | string language tag 323 | */ 324 | message = readString(payload, 1, 'utf8'); 325 | lang = readString(payload, payload._pos, 'utf8'); 326 | this.emit('USERAUTH_PASSWD_CHANGEREQ', message, lang); 327 | } else if (this._authMethod === 'keyboard-interactive') { 328 | /* 329 | byte SSH_MSG_USERAUTH_INFO_REQUEST 330 | string name (ISO-10646 UTF-8) 331 | string instruction (ISO-10646 UTF-8) 332 | string language tag -- MAY be empty 333 | int num-prompts 334 | string prompt[1] (ISO-10646 UTF-8) 335 | boolean echo[1] 336 | ... 337 | string prompt[num-prompts] (ISO-10646 UTF-8) 338 | boolean echo[num-prompts] 339 | */ 340 | var name, instr, nprompts; 341 | 342 | name = readString(payload, 1, 'utf8'); 343 | instr = readString(payload, payload._pos, 'utf8'); 344 | lang = readString(payload, payload._pos, 'utf8'); 345 | nprompts = payload.readUInt32BE(payload._pos, true); 346 | 347 | payload._pos += 4; 348 | if (nprompts > 0) { 349 | var prompts = []; 350 | for (var prompt = 0; prompt < nprompts; ++prompt) { 351 | prompts.push({ 352 | prompt: readString(payload, payload._pos, 'utf8'), 353 | echo: (payload[payload._pos++] !== 0) 354 | }); 355 | } 356 | this.emit('USERAUTH_INFO_REQUEST', name, instr, lang, prompts); 357 | } else 358 | this.emit('USERAUTH_INFO_REQUEST', name, instr, lang); 359 | } else if (this._authMethod === 'pubkey') { 360 | /* 361 | byte SSH_MSG_USERAUTH_PK_OK 362 | string public key algorithm name from the request 363 | string public key blob from the request 364 | */ 365 | this.emit('USERAUTH_PK_OK'); 366 | } 367 | }; 368 | 369 | Parser.prototype.parseChRequest = function() { 370 | var payload = this._payload, 371 | info; 372 | 373 | var recipient = payload.readUInt32BE(1, true), 374 | request = readString(payload, 5, 'ascii'); 375 | if (request === 'exit-status') { 376 | /* 377 | byte SSH_MSG_CHANNEL_REQUEST 378 | uint32 recipient channel 379 | string "exit-status" 380 | boolean FALSE 381 | uint32 exit_status 382 | */ 383 | info = { 384 | recipient: recipient, 385 | request: request, 386 | code: payload.readUInt32BE(1 + payload._pos, true) 387 | }; 388 | this.emit('CHANNEL_REQUEST:' + recipient, info); 389 | } else if (request === 'exit-signal') { 390 | /* 391 | byte SSH_MSG_CHANNEL_REQUEST 392 | uint32 recipient channel 393 | string "exit-signal" 394 | boolean FALSE 395 | string signal name (without the "SIG" prefix) 396 | boolean core dumped 397 | string error message in ISO-10646 UTF-8 encoding 398 | string language tag 399 | */ 400 | info = { 401 | recipient: recipient, 402 | request: request, 403 | signal: readString(payload, 1 + payload._pos, 'ascii'), 404 | coredump: (payload[payload._pos] !== 0), 405 | description: readString(payload, ++payload._pos, 'utf8'), 406 | lang: readString(payload, payload._pos, 'utf8') 407 | }; 408 | this.emit('CHANNEL_REQUEST:' + recipient, info); 409 | } 410 | }; 411 | 412 | Parser.prototype.parsePacket = function() { 413 | var payload = this._payload, lang, message, info; 414 | 415 | if (++this._seqno > MAX_SEQNO) 416 | this._seqno = 0; 417 | 418 | // payload[0] === packet type 419 | var type = payload[0]; 420 | 421 | if (type === MESSAGE.IGNORE) { 422 | /* 423 | byte SSH_MSG_IGNORE 424 | string data 425 | */ 426 | } else if (type === MESSAGE.DISCONNECT) { 427 | /* 428 | byte SSH_MSG_DISCONNECT 429 | uint32 reason code 430 | string description in ISO-10646 UTF-8 encoding 431 | string language tag 432 | */ 433 | var reason = payload.readUInt32BE(1, true), 434 | description = readString(payload, 5, 'utf8'); 435 | lang = readString(payload, payload._pos, 'ascii'); 436 | this.emit('DISCONNECT', DISCONNECT_REASON[reason], 437 | reason, description, lang); 438 | } else if (type === MESSAGE.DEBUG) { 439 | /* 440 | byte SSH_MSG_DEBUG 441 | boolean always_display 442 | string message in ISO-10646 UTF-8 encoding 443 | string language tag 444 | */ 445 | message = readString(payload, 2, 'utf8'); 446 | lang = readString(payload, payload._pos, 'ascii'); 447 | this.emit('DEBUG', message, lang); 448 | } else if (type === MESSAGE.KEXINIT) 449 | this.parseKEXInit(); 450 | else if (type === 31) { // key exchange method-specific message 451 | if (this._kexdh !== 'group') { 452 | /* 453 | byte SSH_MSG_KEX_DH_GEX_GROUP 454 | mpint p, safe prime 455 | mpint g, generator for subgroup in GF(p) 456 | */ 457 | var prime = readString(payload, 1), 458 | gen = readString(payload, payload._pos); 459 | this.emit('KEXDH_GEX_GROUP', prime, gen); 460 | } else 461 | this.parseKEXDH_REPLY(); 462 | } else if (type === consts.KEXDH_GEX_REPLY) 463 | this.parseKEXDH_REPLY(); 464 | else if (type === MESSAGE.NEWKEYS) { 465 | /* 466 | byte SSH_MSG_NEW_KEYS 467 | */ 468 | this.emit('NEWKEYS'); 469 | } else if (type === MESSAGE.SERVICE_ACCEPT) { 470 | /* 471 | byte SSH_MSG_NEW_KEYS 472 | */ 473 | var serviceName = readString(payload, 1, 'ascii'); 474 | this.emit('SERVICE_ACCEPT', serviceName); 475 | } else if (type === MESSAGE.USERAUTH_SUCCESS) { 476 | /* 477 | byte SSH_MSG_USERAUTH_SUCCESS 478 | */ 479 | this.emit('USERAUTH_SUCCESS'); 480 | } else if (type === MESSAGE.USERAUTH_FAILURE) { 481 | /* 482 | byte SSH_MSG_USERAUTH_FAILURE 483 | name-list authentications that can continue 484 | boolean partial success 485 | */ 486 | var auths = readString(payload, 1, 'ascii').split(','), 487 | partSuccess = (payload[payload._pos] !== 0); 488 | this.emit('USERAUTH_FAILURE', auths, partSuccess); 489 | } else if (type === MESSAGE.USERAUTH_BANNER) { 490 | /* 491 | byte SSH_MSG_USERAUTH_BANNER 492 | string message in ISO-10646 UTF-8 encoding 493 | string language tag 494 | */ 495 | message = readString(payload, 1, 'utf8'); 496 | lang = readString(payload, payload._pos, 'utf8'); 497 | this.emit('USERAUTH_BANNER', message, lang); 498 | } else if (type === 60) // user auth context-specific messages 499 | this.parseUserAuthMisc(); 500 | else if (type === MESSAGE.CHANNEL_OPEN) { 501 | /* 502 | byte SSH_MSG_CHANNEL_OPEN 503 | string channel type in US-ASCII only 504 | uint32 sender channel 505 | uint32 initial window size 506 | uint32 maximum packet size 507 | .... channel type specific data follows 508 | */ 509 | var chanType = readString(payload, 1, 'ascii'), 510 | channel; 511 | this.debug&&this.debug('DEBUG: Parser: STATE_PACKETDATAAFTER, packet: CHANNEL_OPEN (' + chanType + ')'); 512 | if (chanType === 'forwarded-tcpip') { 513 | /* 514 | string address that was connected 515 | uint32 port that was connected 516 | string originator IP address 517 | uint32 originator port 518 | */ 519 | channel = { 520 | type: chanType, 521 | sender: payload.readUInt32BE(payload._pos, true), 522 | window: payload.readUInt32BE(payload._pos += 4, true), 523 | packetSize: payload.readUInt32BE(payload._pos += 4, true), 524 | data: { 525 | destIP: readString(payload, payload._pos += 4, 'ascii'), 526 | destPort: payload.readUInt32BE(payload._pos, true), 527 | srcIP: readString(payload, payload._pos += 4, 'ascii'), 528 | srcPort: payload.readUInt32BE(payload._pos, true) 529 | } 530 | }; 531 | this.emit('CHANNEL_OPEN', channel); 532 | } else if (chanType === 'x11') { 533 | /* 534 | string originator address (e.g., "192.168.7.38") 535 | uint32 originator port 536 | */ 537 | channel = { 538 | type: chanType, 539 | sender: payload.readUInt32BE(payload._pos, true), 540 | window: payload.readUInt32BE(payload._pos += 4, true), 541 | packetSize: payload.readUInt32BE(payload._pos += 4, true), 542 | data: { 543 | srcIP: readString(payload, payload._pos += 4, 'ascii'), 544 | srcPort: payload.readUInt32BE(payload._pos, true) 545 | } 546 | }; 547 | this.emit('CHANNEL_OPEN', channel); 548 | } else if (chanType === 'auth-agent@openssh.com') { 549 | channel = { 550 | type: chanType, 551 | sender: payload.readUInt32BE(payload._pos, true), 552 | window: payload.readUInt32BE(payload._pos += 4, true), 553 | packetSize: payload.readUInt32BE(payload._pos += 4, true), 554 | data: {} 555 | }; 556 | this.emit('CHANNEL_OPEN', channel); 557 | } else { 558 | // allow connection to reject unsupported channel open requests 559 | this.emit('CHANNEL_OPEN', { type: chanType }); 560 | } 561 | } else if (type === MESSAGE.CHANNEL_OPEN_CONFIRMATION) { 562 | /* 563 | byte SSH_MSG_CHANNEL_OPEN_CONFIRMATION 564 | uint32 recipient channel 565 | uint32 sender channel 566 | uint32 initial window size 567 | uint32 maximum packet size 568 | .... channel type specific data follows 569 | */ 570 | // "The 'recipient channel' is the channel number given in the 571 | // original open request, and 'sender channel' is the channel number 572 | // allocated by the other side." 573 | info = { 574 | recipient: payload.readUInt32BE(1, true), 575 | sender: payload.readUInt32BE(5, true), 576 | window: payload.readUInt32BE(9, true), 577 | packetSize: payload.readUInt32BE(13, true), 578 | data: undefined 579 | }; 580 | if (payload.length > 17) 581 | info.data = payload.slice(17); 582 | this.emit('CHANNEL_OPEN_CONFIRMATION:' + info.recipient, info); 583 | } else if (type === MESSAGE.CHANNEL_OPEN_FAILURE) { 584 | /* 585 | byte SSH_MSG_CHANNEL_OPEN_FAILURE 586 | uint32 recipient channel 587 | uint32 reason code 588 | string description in ISO-10646 UTF-8 encoding 589 | string language tag 590 | */ 591 | payload._pos = 9; 592 | info = { 593 | recipient: payload.readUInt32BE(1, true), 594 | reasonCode: payload.readUInt32BE(5, true), 595 | reason: undefined, 596 | description: readString(payload, payload._pos, 'utf8'), 597 | lang: readString(payload, payload._pos, 'utf8') 598 | }; 599 | info.reason = CHANNEL_OPEN_FAILURE[info.reasonCode]; 600 | this.emit('CHANNEL_OPEN_FAILURE:' + info.recipient, info); 601 | } else if (type === MESSAGE.CHANNEL_DATA) { 602 | /* 603 | byte SSH_MSG_CHANNEL_DATA 604 | uint32 recipient channel 605 | string data 606 | */ 607 | this.emit('CHANNEL_DATA:' + payload.readUInt32BE(1, true), 608 | readString(payload, 5)); 609 | } else if (type === MESSAGE.CHANNEL_EXTENDED_DATA) { 610 | /* 611 | byte SSH_MSG_CHANNEL_EXTENDED_DATA 612 | uint32 recipient channel 613 | uint32 data_type_code 614 | string data 615 | */ 616 | this.emit('CHANNEL_EXTENDED_DATA:' + payload.readUInt32BE(1, true), 617 | payload.readUInt32BE(5, true), 618 | readString(payload, 9)); 619 | } else if (type === MESSAGE.CHANNEL_WINDOW_ADJUST) { 620 | /* 621 | byte SSH_MSG_CHANNEL_WINDOW_ADJUST 622 | uint32 recipient channel 623 | uint32 bytes to add 624 | */ 625 | this.emit('CHANNEL_WINDOW_ADJUST:' + payload.readUInt32BE(1, true), 626 | payload.readUInt32BE(5, true)); 627 | } else if (type === MESSAGE.CHANNEL_SUCCESS) { 628 | /* 629 | byte SSH_MSG_CHANNEL_SUCCESS 630 | uint32 recipient channel 631 | */ 632 | this.emit('CHANNEL_SUCCESS:' + payload.readUInt32BE(1, true)); 633 | } else if (type === MESSAGE.CHANNEL_FAILURE) { 634 | /* 635 | byte SSH_MSG_CHANNEL_FAILURE 636 | uint32 recipient channel 637 | */ 638 | this.emit('CHANNEL_FAILURE:' + payload.readUInt32BE(1, true)); 639 | } else if (type === MESSAGE.CHANNEL_EOF) { 640 | /* 641 | byte SSH_MSG_CHANNEL_EOF 642 | uint32 recipient channel 643 | */ 644 | this.emit('CHANNEL_EOF:' + payload.readUInt32BE(1, true)); 645 | } else if (type === MESSAGE.CHANNEL_CLOSE) { 646 | /* 647 | byte SSH_MSG_CHANNEL_CLOSE 648 | uint32 recipient channel 649 | */ 650 | this.emit('CHANNEL_CLOSE:' + payload.readUInt32BE(1, true)); 651 | } else if (type === MESSAGE.CHANNEL_REQUEST) 652 | this.parseChRequest(); 653 | else if (type === MESSAGE.REQUEST_SUCCESS) { 654 | /* 655 | byte SSH_MSG_REQUEST_SUCCESS 656 | .... response specific data 657 | */ 658 | if (payload.length > 1) 659 | this.emit('REQUEST_SUCCESS', payload.slice(1)); 660 | else 661 | this.emit('REQUEST_SUCCESS'); 662 | } else if (type === MESSAGE.REQUEST_FAILURE) { 663 | /* 664 | byte SSH_MSG_REQUEST_FAILURE 665 | */ 666 | this.emit('REQUEST_FAILURE'); 667 | } else if (type === MESSAGE.UNIMPLEMENTED) { 668 | /* 669 | byte SSH_MSG_UNIMPLEMENTED 670 | uint32 packet sequence number of rejected message 671 | */ 672 | // TODO 673 | } 674 | }; 675 | 676 | Parser.prototype.parseKEXDH_REPLY = function() { 677 | var payload = this._payload; 678 | /* 679 | byte SSH_MSG_KEXDH_REPLY / SSH_MSG_KEX_DH_GEX_REPLY 680 | string server public host key and certificates (K_S) 681 | mpint f 682 | string signature of H 683 | */ 684 | var info = { 685 | hostkey: readString(payload, 1), 686 | hostkey_format: undefined, 687 | pubkey: readString(payload, payload._pos), 688 | sig: readString(payload, payload._pos), 689 | sig_format: undefined 690 | }; 691 | info.hostkey_format = readString(info.hostkey, 0, 'ascii'); 692 | info.sig_format = readString(info.sig, 0, 'ascii'); 693 | this.emit('KEXDH_REPLY', info); 694 | }; 695 | 696 | Parser.prototype.hmacVerify = function(hmac) { 697 | this.debug&&this.debug('DEBUG: Parser: Verifying MAC'); 698 | if (isGCM(this._decryptType)) { 699 | this._decrypt.setAuthTag(hmac); 700 | var payload = new Buffer(this._decrypt.update(this._packet, 'binary', 'binary'), 'binary'); 701 | this._payload = payload.slice(1, this._packet.length + 4 - payload[0]); 702 | this._decrypt.final('binary'); 703 | iv_inc(this._decryptIV); 704 | this._decrypt = crypto.createDecipheriv( 705 | SSH_TO_OPENSSL[this._decryptType], 706 | this._decryptKey, 707 | this._decryptIV 708 | ); 709 | this._decrypt.setAutoPadding(false); 710 | return true; 711 | } else { 712 | var calcHmac = crypto.createHmac(SSH_TO_OPENSSL[this._hmac], this._hmacKey); 713 | 714 | this._hmacBufCompute.writeUInt32BE(this._seqno, 0, true); 715 | this._hmacBufCompute.writeUInt32BE(this._pktLen, 4, true); 716 | this._hmacBufCompute[8] = this._padLen; 717 | 718 | calcHmac.update(this._hmacBufCompute); 719 | calcHmac.update(this._packet); 720 | 721 | return (calcHmac.digest('binary') === hmac.toString('binary')); 722 | } 723 | }; 724 | 725 | Parser.prototype.decrypt = function(data) { 726 | this.debug&&this.debug('DEBUG: Parser: Decrypting'); 727 | return new Buffer(this._decrypt.update(data, 'binary', 'binary'), 'binary'); 728 | }; 729 | 730 | Parser.prototype.expect = function(type, amount, bufferKey) { 731 | this._expect = amount; 732 | this._expectType = type; 733 | this._expectPtr = 0; 734 | if (bufferKey && this[bufferKey]) 735 | this._expectBuf = this[bufferKey]; 736 | else if (amount) 737 | this._expectBuf = new Buffer(amount); 738 | }; 739 | 740 | Parser.prototype.reset = function() { 741 | this._state = STATE_INIT; 742 | this._expect = undefined; 743 | this._expectType = undefined; 744 | this._expectPtr = 0; 745 | this._expectBuf = undefined; 746 | 747 | this._ss = undefined; 748 | this._greeting = undefined; 749 | this._decryptSize = 8; 750 | this._decrypt = false; 751 | this._decryptIV = undefined; 752 | this._decryptKey = undefined; 753 | this._decryptBuf = undefined; 754 | this._decryptType = undefined; 755 | this._authMethod = undefined; 756 | 757 | this._pktLen = undefined; 758 | this._padLen = undefined; 759 | this._pktExtra = undefined; 760 | this._payload = undefined; 761 | this._hmacBuf = undefined; 762 | this._hmacSize = undefined; 763 | this._packet = undefined; 764 | this._seqno = 0; 765 | this._kexinit = undefined; 766 | this._kexdh = undefined; 767 | }; 768 | 769 | function readString(buffer, start, encoding) { 770 | start || (start = 0); 771 | 772 | var blen = buffer.length, slen; 773 | if ((blen - start) < 4) 774 | return false; 775 | slen = buffer.readUInt32BE(start, true); 776 | if ((blen - start) < (4 + slen)) 777 | return false; 778 | buffer._pos = start + 4 + slen; 779 | if (encoding) 780 | return buffer.toString(encoding, start + 4, start + 4 + slen); 781 | else 782 | return buffer.slice(start + 4, start + 4 + slen); 783 | } 784 | 785 | function readList(buffer, start) { 786 | var list = readString(buffer, start, 'ascii'); 787 | return (list !== false ? (list.length ? list.split(',') : []) : false); 788 | } 789 | 790 | Parser.MAX_SEQNO = MAX_SEQNO; 791 | Parser.readString = readString; 792 | 793 | module.exports = Parser; 794 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | An SSH2 client module written in pure JavaScript for [node.js](http://nodejs.org/). 5 | 6 | Development/testing is done against OpenSSH (6.6 currently). 7 | 8 | Upgrading from v0.2.x? See the [list of changes](https://github.com/mscdex/ssh2/wiki/Changes-from-0.2.x-to-0.3.x) (including backwards incompatibilities). 9 | 10 | 11 | Requirements 12 | ============ 13 | 14 | * [node.js](http://nodejs.org/) -- v0.8.7 or newer 15 | 16 | 17 | Install 18 | ============ 19 | 20 | npm install ssh2 21 | 22 | 23 | Examples 24 | ======== 25 | 26 | * Authenticate using keys and execute `uptime` on a server: 27 | 28 | ```javascript 29 | var Connection = require('ssh2'); 30 | 31 | var conn = new Connection(); 32 | conn.on('ready', function() { 33 | console.log('Connection :: ready'); 34 | conn.exec('uptime', function(err, stream) { 35 | if (err) throw err; 36 | stream.on('exit', function(code, signal) { 37 | console.log('Stream :: exit :: code: ' + code + ', signal: ' + signal); 38 | }).on('close', function() { 39 | console.log('Stream :: close'); 40 | conn.end(); 41 | }).on('data', function(data) { 42 | console.log('STDOUT: ' + data); 43 | }).stderr.on('data', function(data) { 44 | console.log('STDERR: ' + data); 45 | }); 46 | }); 47 | }).connect({ 48 | host: '192.168.100.100', 49 | port: 22, 50 | username: 'frylock', 51 | privateKey: require('fs').readFileSync('/here/is/my/key') 52 | }); 53 | 54 | // example output: 55 | // Connection :: ready 56 | // STDOUT: 17:41:15 up 22 days, 18:09, 1 user, load average: 0.00, 0.01, 0.05 57 | // 58 | // Stream :: close 59 | // Stream :: exit :: code: 0, signal: undefined 60 | ``` 61 | 62 | * Authenticate using keys and start an interactive shell session: 63 | 64 | ```javascript 65 | var Connection = require('ssh2'); 66 | 67 | var conn = new Connection(); 68 | conn.on('ready', function() { 69 | console.log('Connection :: ready'); 70 | conn.shell(function(err, stream) { 71 | if (err) throw err; 72 | stream.on('close', function() { 73 | console.log('Stream :: close'); 74 | conn.end(); 75 | }).on('data', function(data) { 76 | console.log('STDOUT: ' + data); 77 | }).stderr.on('data', function(data) { 78 | console.log('STDERR: ' + data); 79 | }); 80 | stream.end('ls -l\nexit\n'); 81 | }); 82 | }).connect({ 83 | host: '192.168.100.100', 84 | port: 22, 85 | username: 'frylock', 86 | privateKey: require('fs').readFileSync('/here/is/my/key') 87 | }); 88 | 89 | // example output: 90 | // Connection :: ready 91 | // STDOUT: Last login: Sun Jun 15 09:37:21 2014 from 192.168.100.100 92 | // 93 | // STDOUT: ls -l 94 | // exit 95 | // 96 | // STDOUT: frylock@athf:~$ ls -l 97 | // 98 | // STDOUT: total 8 99 | // 100 | // STDOUT: drwxr-xr-x 2 frylock frylock 4096 Nov 18 2012 mydir 101 | // 102 | // STDOUT: -rw-r--r-- 1 frylock frylock 25 Apr 11 2013 test.txt 103 | // 104 | // STDOUT: frylock@athf:~$ exit 105 | // 106 | // STDOUT: logout 107 | // 108 | // Stream :: close 109 | ``` 110 | 111 | * Authenticate using password and send an HTTP request to port 80 on the server: 112 | 113 | ```javascript 114 | var Connection = require('ssh2'); 115 | 116 | var conn = new Connection(); 117 | conn.on('ready', function() { 118 | console.log('Connection :: ready'); 119 | conn.forwardOut('192.168.100.102', 8000, '127.0.0.1', 80, function(err, stream) { 120 | if (err) throw err; 121 | stream.on('close', function() { 122 | console.log('TCP :: CLOSED'); 123 | conn.end(); 124 | }).on('data', function(data) { 125 | console.log('TCP :: DATA: ' + data); 126 | }).end([ 127 | 'HEAD / HTTP/1.1', 128 | 'User-Agent: curl/7.27.0', 129 | 'Host: 127.0.0.1', 130 | 'Accept: */*', 131 | 'Connection: close', 132 | '', 133 | '' 134 | ].join('\r\n')); 135 | }); 136 | }).connect({ 137 | host: '192.168.100.100', 138 | port: 22, 139 | username: 'frylock', 140 | password: 'nodejsrules' 141 | }); 142 | 143 | // example output: 144 | // Connection :: ready 145 | // TCP :: DATA: HTTP/1.1 200 OK 146 | // Date: Thu, 15 Nov 2012 13:52:58 GMT 147 | // Server: Apache/2.2.22 (Ubuntu) 148 | // X-Powered-By: PHP/5.4.6-1ubuntu1 149 | // Last-Modified: Thu, 01 Jan 1970 00:00:00 GMT 150 | // Content-Encoding: gzip 151 | // Vary: Accept-Encoding 152 | // Connection: close 153 | // Content-Type: text/html; charset=UTF-8 154 | // 155 | // 156 | // TCP :: CLOSED 157 | ``` 158 | 159 | * Authenticate using password and forward remote connections on port 8000 to us: 160 | 161 | ```javascript 162 | var Connection = require('ssh2'); 163 | 164 | var conn = new Connection(); 165 | conn.on('ready', function() { 166 | console.log('Connection :: ready'); 167 | conn.forwardIn('127.0.0.1', 8000, function(err) { 168 | if (err) throw err; 169 | console.log('Listening for connections on server on port 8000!'); 170 | }); 171 | }).on('tcp connection', function(info, accept, reject) { 172 | console.log('TCP :: INCOMING CONNECTION:'); 173 | console.dir(info); 174 | accept().on('close', function() { 175 | console.log('TCP :: CLOSED'); 176 | }).on('data', function(data) { 177 | console.log('TCP :: DATA: ' + data); 178 | }).end([ 179 | 'HTTP/1.1 404 Not Found', 180 | 'Date: Thu, 15 Nov 2012 02:07:58 GMT', 181 | 'Server: ForwardedConnection', 182 | 'Content-Length: 0', 183 | 'Connection: close', 184 | '', 185 | '' 186 | ].join('\r\n')); 187 | }).connect({ 188 | host: '192.168.100.100', 189 | port: 22, 190 | username: 'frylock', 191 | password: 'nodejsrules' 192 | }); 193 | 194 | // example output: 195 | // Connection :: ready 196 | // Listening for connections on server on port 8000! 197 | // (.... then from another terminal on the server: `curl -I http://127.0.0.1:8000`) 198 | // TCP :: INCOMING CONNECTION: { destIP: '127.0.0.1', 199 | // destPort: 8000, 200 | // srcIP: '127.0.0.1', 201 | // srcPort: 41969 } 202 | // TCP DATA: HEAD / HTTP/1.1 203 | // User-Agent: curl/7.27.0 204 | // Host: 127.0.0.1:8000 205 | // Accept: */* 206 | // 207 | // 208 | // TCP :: CLOSED 209 | ``` 210 | 211 | * Authenticate using password and get a directory listing via SFTP: 212 | 213 | ```javascript 214 | var Connection = require('ssh2'); 215 | 216 | var conn = new Connection(); 217 | conn.on('ready', function() { 218 | console.log('Connection :: ready'); 219 | conn.sftp(function(err, sftp) { 220 | if (err) throw err; 221 | sftp.readdir('foo', function(err, list) { 222 | if (err) throw err; 223 | console.dir(list); 224 | conn.end(); 225 | }); 226 | }); 227 | }).connect({ 228 | host: '192.168.100.100', 229 | port: 22, 230 | username: 'frylock', 231 | password: 'nodejsrules' 232 | }); 233 | 234 | // example output: 235 | // Connection :: ready 236 | // [ { filename: 'test.txt', 237 | // longname: '-rw-r--r-- 1 frylock frylock 12 Nov 18 11:05 test.txt', 238 | // attrs: 239 | // { size: 12, 240 | // uid: 1000, 241 | // gid: 1000, 242 | // mode: 33188, 243 | // atime: 1353254750, 244 | // mtime: 1353254744 } }, 245 | // { filename: 'mydir', 246 | // longname: 'drwxr-xr-x 2 frylock frylock 4096 Nov 18 15:03 mydir', 247 | // attrs: 248 | // { size: 1048576, 249 | // uid: 1000, 250 | // gid: 1000, 251 | // mode: 16877, 252 | // atime: 1353269007, 253 | // mtime: 1353269007 } } ] 254 | ``` 255 | 256 | * Connection hopping: 257 | 258 | ```javascript 259 | var Connection = require('ssh2'); 260 | 261 | var conn1 = new Connection(), 262 | conn2 = new Connection(); 263 | 264 | conn1.on('ready', function() { 265 | console.log('FIRST :: connection ready'); 266 | conn1.exec('nc 192.168.1.2 22', function(err, stream) { 267 | if (err) { 268 | console.log('FIRST :: exec error: ' + err); 269 | return conn1.end(); 270 | } 271 | conn2.connect({ 272 | sock: stream, 273 | username: 'user2', 274 | password: 'password2', 275 | }); 276 | }); 277 | }).connect({ 278 | host: '192.168.1.1', 279 | username: 'user1', 280 | password: 'password1', 281 | }); 282 | 283 | conn2.on('ready', function() { 284 | console.log('SECOND :: connection ready'); 285 | conn2.exec('uptime', function(err, stream) { 286 | if (err) { 287 | console.log('SECOND :: exec error: ' + err); 288 | return conn1.end(); 289 | } 290 | stream.on('end', function() { 291 | conn1.end(); // close parent (and this) connection 292 | }).on('data', function(data) { 293 | console.log(data.toString()); 294 | }); 295 | }); 296 | }); 297 | ``` 298 | 299 | * Forward X11 connections (xeyes): 300 | 301 | ```javascript 302 | var net = require('net'), 303 | Connection = require('ssh2'); 304 | 305 | var conn = new Connection(); 306 | 307 | conn.on('x11', function(info, accept, reject) { 308 | var xserversock = new net.Socket(); 309 | xserversock.on('connect', function() { 310 | var xclientsock = accept(); 311 | xclientsock.pipe(xserversock).pipe(xclientsock); 312 | }); 313 | // connects to localhost:0.0 314 | xserversock.connect(6000, 'localhost'); 315 | }); 316 | 317 | conn.on('ready', function() { 318 | conn.exec('xeyes', { x11: true }, function(err, stream) { 319 | if (err) throw err; 320 | var code = 0; 321 | stream.on('end', function() { 322 | if (code !== 0) 323 | console.log('Do you have X11 forwarding enabled on your SSH server?'); 324 | conn.end(); 325 | }).on('exit', function(exitcode) { 326 | code = exitcode; 327 | }); 328 | }); 329 | }).connect({ 330 | host: '192.168.1.1', 331 | username: 'foo', 332 | password: 'bar' 333 | }); 334 | ``` 335 | 336 | * Dynamic (1:1) port forwarding using a SOCKSv5 proxy (using [socksv5](https://github.com/mscdex/socksv5)): 337 | 338 | ```javascript 339 | var socks = require('socksv5'), 340 | Connection = require('ssh2'); 341 | 342 | var ssh_config = { 343 | host: '192.168.100.1', 344 | port: 22, 345 | username: 'nodejs', 346 | password: 'rules' 347 | }; 348 | 349 | socks.createServer(function(info, accept, deny) { 350 | var conn = new Connection(); 351 | conn.on('ready', function() { 352 | conn.forwardOut(info.srcAddr, 353 | info.srcPort, 354 | info.dstAddr, 355 | info.dstPort, 356 | function(err, stream) { 357 | if (err) 358 | return deny(); 359 | 360 | var clientSocket; 361 | if (clientSocket = accept(true)) { 362 | stream.pipe(clientSocket).pipe(stream).on('close', function() { 363 | conn.end(); 364 | }); 365 | } else 366 | conn.end(); 367 | }); 368 | }).on('error', function(err) { 369 | deny(); 370 | }).connect(ssh_config); 371 | }).listen(1080, 'localhost', function() { 372 | console.log('SOCKSv5 proxy server started on port 1080'); 373 | }).useAuth(socks.auth.None()); 374 | 375 | // test with cURL: 376 | // curl -i --socks5 localhost:1080 google.com 377 | ``` 378 | 379 | * Invoke an arbitrary subsystem (netconf in this example): 380 | 381 | ```javascript 382 | var Connection = require('ssh2'), 383 | xmlhello = ''+ 384 | ''+ 385 | ' '+ 386 | ' urn:ietf:params:netconf:base:1.0'+ 387 | ' '+ 388 | ']]>]]>'; 389 | 390 | var conn = new Connection(); 391 | 392 | conn.on('ready', function() { 393 | console.log('Connection :: ready'); 394 | conn.subsys('netconf', function(err, stream) { 395 | if (err) throw err; 396 | stream.on('data', function(data) { 397 | console.log(data); 398 | }).write(xmlhello); 399 | }); 400 | }).connect({ 401 | host: '1.2.3.4', 402 | port: 22, 403 | username: 'blargh', 404 | password: 'honk' 405 | }); 406 | ``` 407 | 408 | 409 | API 410 | === 411 | 412 | `require('ssh2')` returns a **_Connection_** constructor 413 | 414 | Connection events 415 | ----------------- 416 | 417 | * **banner**(< _string_ >message, < _string_ >language) - A notice was sent by the server upon connection. 418 | 419 | * **ready**() - Authentication was successful. 420 | 421 | * **tcp connection**(< _object_ >details, < _function_ >accept, < _function_ >reject) - An incoming forwarded TCP connection is being requested. Calling `accept` accepts the connection and returns a `ChannelStream` object. Calling `reject` rejects the connection and no further action is needed. `details` contains: 422 | 423 | * **srcIP** - _string_ - The originating IP of the connection. 424 | 425 | * **srcPort** - _integer_ - The originating port of the connection. 426 | 427 | * **dstIP** - _string_ - The remote IP the connection was received on (given in earlier call to `forwardIn()`). 428 | 429 | * **dstPort** - _integer_ - The remote port the connection was received on (given in earlier call to `forwardIn()`). 430 | 431 | * **x11**(< _object_ >details, < _function_ >accept, < _function_ >reject) - An incoming X11 connection is being requested. Calling `accept` accepts the connection and returns a `ChannelStream` object. Calling `reject` rejects the connection and no further action is needed. `details` contains: 432 | 433 | * **srcIP** - _string_ - The originating IP of the connection. 434 | 435 | * **srcPort** - _integer_ - The originating port of the connection. 436 | 437 | * **keyboard-interactive**(< _string_ >name, < _string_ >instructions, < _string_ >instructionsLang, < _array_ >prompts, < _function_ >finish) - The server is asking for replies to the given `prompts` for keyboard-interactive user authentication. `name` is generally what you'd use as a window title (for GUI apps). `prompts` is an array of `{ prompt: 'Password: ', echo: false }` style objects (here `echo` indicates whether user input should be displayed on the screen). The answers for all prompts must be provided as an array of strings and passed to `finish` when you are ready to continue. Note: It's possible for the server to come back and ask more questions. 438 | 439 | * **change password**(< _string_ >message, < _string_ >language, < _function_ >done) - If using password-based user authentication, the server has requested that the user's password be changed. Call `done` with the new password. 440 | 441 | * **error**(< _Error_ >err) - An error occurred. A 'level' property indicates 'connection-socket' for socket-level errors and 'connection-ssh' for SSH disconnection messages. In the case of 'connection-ssh' messages, there may be a 'description' property that provides more detail. 442 | 443 | * **end**() - The socket was disconnected. 444 | 445 | * **close**(< _boolean_ >hadError) - The socket was closed. `hadError` is set to true if this was due to error. 446 | 447 | * **debug**(< _string_ >message) - If `debug` is set in the object passed to connect(), then this event will be emitted when the server sends debug messages. For OpenSSH, these usually are messages like "Pty allocation disabled.", "X11 forwarding disabled.", etc. when options are set for particular keys in `~/.ssh/authorized_keys`. 448 | 449 | 450 | Connection methods 451 | ------------------ 452 | 453 | * **(constructor)**() - Creates and returns a new Connection instance. 454 | 455 | * **connect**(< _object_ >config) - _(void)_ - Attempts a connection to a server using the information given in `config`: 456 | 457 | * **host** - < _string_ > - Hostname or IP address of the server. **Default:** `'localhost'` 458 | 459 | * **port** - < _integer_ > - Port number of the server. **Default:** `22` 460 | 461 | * **hostHash** - < _string_ > - 'md5' or 'sha1'. The host's key is hashed using this method and passed to the **hostVerifier** function. **Default:** (none) 462 | 463 | * **hostVerifier** - < _function_ > - Function that is passed a string hex hash of the host's key for verification purposes. Return true to continue with the connection, false to reject and disconnect. **Default:** (none) 464 | 465 | * **username** - < _string_ > - Username for authentication. **Default:** (none) 466 | 467 | * **password** - < _string_ > - Password for password-based user authentication. **Default:** (none) 468 | 469 | * **agent** - < _string_ > - Path to ssh-agent's UNIX socket for ssh-agent-based user authentication. **Windows users: set to 'pageant' for authenticating with Pageant or (actual) path to a cygwin "UNIX socket."** **Default:** (none) 470 | 471 | * **privateKey** - < _mixed_ > - Buffer or string that contains a private key for key-based user authentication (OpenSSH format). **Default:** (none) 472 | 473 | * **passphrase** - < _string_ > - For an encrypted private key, this is the passphrase used to decrypt it. **Default:** (none) 474 | 475 | * **tryKeyboard** - < _boolean_ > - Try keyboard-interactive user authentication if primary user authentication method fails. **Default:** `false` 476 | 477 | * **pingInterval** - < _integer_ > - How often (in milliseconds) to send SSH-level keepalive packets to the server. **Default:** `60000` 478 | 479 | * **readyTimeout** - < _integer_ > - How long (in milliseconds) to wait for the SSH handshake to complete. **Default:** `10000` 480 | 481 | * **sock** - < _ReadableStream_ > - A _ReadableStream_ to use for communicating with the server instead of creating and using a new TCP connection (useful for connection hopping). 482 | 483 | * **agentForward** - < _boolean_ > - Set to true to use OpenSSH agent forwarding ('auth-agent@openssh.com'). **Default:** `false` 484 | 485 | **Authentication method priorities:** Password -> Private Key -> Agent (-> keyboard-interactive if `tryKeyboard` is true) -> None 486 | 487 | * **exec**(< _string_ >command[, < _object_ >options], < _function_ >callback) - _(void)_ - Executes `command` on the server. Valid `options` properties are: 488 | 489 | * **env** - < _object_ > - An environment to use for the execution of the command. 490 | 491 | * **pty** - < _mixed_ > - Set to true to allocate a pseudo-tty with defaults, or an object containing specific pseudo-tty settings (see 'Pseudo-TTY settings'). Setting up a pseudo-tty can be useful when working with remote processes that expect input from an actual terminal (e.g. sudo's password prompt). 492 | 493 | * **x11** - < _mixed_ > - Set to true to use defaults below, a number to specify a specific screen number, or an object with the following valid properties: 494 | 495 | * **single** - < _boolean_ > - Allow just a single connection? **Default:** `false` 496 | 497 | * **screen** - < _number_ > - Screen number to use **Default:** `0` 498 | 499 | `callback` has 2 parameters: < _Error_ >err, < _ChannelStream_ >stream. 500 | 501 | * **shell**([[< _object_ >window,] < _object_ >options]< _function_ >callback) - _(void)_ - Starts an interactive shell session on the server, with optional `window` pseudo-tty settings (see 'Pseudo-TTY settings'). `options` supports the 'x11' option as described in exec(). `callback` has 2 parameters: < _Error_ >err, < _ChannelStream_ >stream. 502 | 503 | * **forwardIn**(< _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _(void)_ - Bind to `remoteAddr` on `remotePort` on the server and forward incoming connections. `callback` has 2 parameters: < _Error_ >err, < _integer_ >port (`port` is the assigned port number if `remotePort` was 0). Here are some special values for `remoteAddr` and their associated binding behaviors: 504 | 505 | * '' - Connections are to be accepted on all protocol families supported by the server. 506 | 507 | * '0.0.0.0' - Listen on all IPv4 addresses. 508 | 509 | * '::' - Listen on all IPv6 addresses. 510 | 511 | * 'localhost' - Listen on all protocol families supported by the server on loopback addresses only. 512 | 513 | * '127.0.0.1' and '::1' - Listen on the loopback interfaces for IPv4 and IPv6, respectively. 514 | 515 | * **unforwardIn**(< _string_ >remoteAddr, < _integer_ >remotePort, < _function_ >callback) - _(void)_ - Unbind `remoteAddr` on `remotePort` on the server and stop forwarding incoming connections. Until `callback` is called, more connections may still come in. `callback` has 1 parameter: < _Error_ >err. 516 | 517 | * **forwardOut**(< _string_ >srcIP, < _integer_ >srcPort, < _string_ >dstIP, < _integer_ >dstPort, < _function_ >callback) - _(void)_ - Open a connection with `srcIP` and `srcPort` as the originating address and port and `dstIP` and `dstPort` as the remote destination address and port. `callback` has 2 parameters: < _Error_ >err, < _ChannelStream_ >stream. 518 | 519 | * **sftp**(< _function_ >callback) - _(void)_ - Starts an SFTP (protocol version 3) session. `callback` has 2 parameters: < _Error_ >err, < _SFTP_ >sftpConnection. 520 | 521 | * **subsys**(< _string_ >subsystem, < _function_ >callback) - _(void)_ - Invokes `subsystem` on the server. `callback` has 2 parameters: < _Error_ >err, < _ChannelStream_ >stream. 522 | 523 | * **end**() - _(void)_ - Disconnects the socket. 524 | 525 | 526 | ChannelStream 527 | ------------- 528 | 529 | This is a normal **streams2** Duplex Stream, with the following changes: 530 | 531 | * A boolean property 'allowHalfOpen' exists and behaves similarly to the property of the same name for net.Socket. When the stream's end() is called, if 'allowHalfOpen' is `true`, only EOF will be sent (the server can still send data if they have not already sent EOF). The default value for this property is `true`. 532 | 533 | * For shell(): 534 | 535 | * **setWindow**(< _integer_ >rows, < _integer_ >cols, < _integer_ >height, < _integer_ >width) - _(void)_ - Lets the server know that the local terminal window has been resized. The meaning of these arguments are described in the 'Pseudo-TTY settings' section. 536 | 537 | * For exec(): 538 | 539 | * An 'exit' event will be emitted when the process finishes. If the process finished normally, the process's return value is passed to the 'exit' callback. If the process was interrupted by a signal, the following are passed to the 'exit' callback: null, < _string_ >signalName, < _boolean_ >didCoreDump, < _string_ >description. 540 | 541 | * For shell() and exec(): 542 | 543 | * The readable side represents stdout and the writable side represents stdin. 544 | 545 | * A `stderr` property that represents the stream of output from stderr. 546 | 547 | * **signal**(< _string_ >signalName) - _(void)_ - Sends a POSIX signal to the current process on the server. Valid signal names are: 'ABRT', 'ALRM', 'FPE', 'HUP', 'ILL', 'INT', 'KILL', 'PIPE', 'QUIT', 'SEGV', 'TERM', 'USR1', and 'USR2'. Also, from the RFC: "Some systems may not implement signals, in which case they SHOULD ignore this message." Note: If you are trying to send SIGINT and you find signal() doesn't work, try writing '\x03' to the exec/shell stream instead. 548 | 549 | 550 | SFTP events 551 | ----------- 552 | 553 | * **end**() - The SFTP session was ended. 554 | 555 | 556 | SFTP methods 557 | ------------ 558 | 559 | * **end**() - _(void)_ - Ends the SFTP session. 560 | 561 | * **fastGet**(< _string_ >remotePath, < _string_ >localPath[, < _object_ >options], < _function_ >callback) - _(void)_ - Downloads a file at `remotePath` to `localPath` using parallel reads for faster throughput. `options` can have the following properties: 562 | 563 | * concurrency - _integer_ - Number of concurrent reads **Default:** `25` 564 | 565 | * chunkSize - _integer_ - Size of each read in bytes **Default:** `32768` 566 | 567 | * step - _function_(< _integer_ >total_transferred, < _integer_ >chunk, < _integer_ >total) - Called every time a part of a file was transferred 568 | 569 | `callback` has 1 parameter: < _Error_ >err. 570 | 571 | * **fastPut**(< _string_ >localPath, < _string_ >remotePath[, < _object_ >options], < _function_ >callback) - _(void)_ - Uploads a file from `localPath` to `remotePath` using parallel reads for faster throughput. `options` can have the following properties: 572 | 573 | * concurrency - _integer_ - Number of concurrent reads **Default:** `25` 574 | 575 | * chunkSize - _integer_ - Size of each read in bytes **Default:** `32768` 576 | 577 | * step - _function_(< _integer_ >total_transferred, < _integer_ >chunk, < _integer_ >total) - Called every time a part of a file was transferred 578 | 579 | `callback` has 1 parameter: < _Error_ >err. 580 | 581 | * **createReadStream**(< _string_ >path[, < _object_ >options]) - _ReadStream_ - Returns a new readable stream for `path`. `options` has the following defaults: 582 | 583 | ```javascript 584 | { flags: 'r', 585 | encoding: null, 586 | handle: null, 587 | mode: 0666, 588 | autoClose: true 589 | } 590 | ``` 591 | 592 | `options` can include `start` and `end` values to read a range of bytes from the file instead of the entire file. Both `start` and `end` are inclusive and start at 0. The `encoding` can be `'utf8'`, `'ascii'`, or `'base64'`. 593 | 594 | If `autoClose` is false, then the file handle won't be closed, even if there's an error. It is your responsiblity to close it and make sure there's no file handle leak. If `autoClose` is set to true (default behavior), on `error` or `end` the file handle will be closed automatically. 595 | 596 | An example to read the last 10 bytes of a file which is 100 bytes long: 597 | 598 | ```javascript 599 | sftp.createReadStream('sample.txt', {start: 90, end: 99}); 600 | ``` 601 | 602 | * **createWriteStream**(< _string_ >path[, < _object_ >options]) - _WriteStream_ - Returns a new writable stream for `path`. `options` has the following defaults: 603 | 604 | ```javascript 605 | { flags: 'w', 606 | encoding: null, 607 | mode: 0666 } 608 | ``` 609 | 610 | `options` may also include a `start` option to allow writing data at some position past the beginning of the file. Modifying a file rather than replacing it may require a flags mode of 'r+' rather than the default mode 'w'. 611 | 612 | If 'autoClose' is set to false and you pipe to this stream, this stream will not automatically close after there is no more data upstream -- allowing future pipes and/or manual writes. 613 | 614 | * **open**(< _string_ >filename, < _string_ >mode, [< _ATTRS_ >attributes, ]< _function_ >callback) - _(void)_ - Opens a file `filename` for `mode` with optional `attributes`. `mode` is any of the modes supported by fs.open (except sync mode). `callback` has 2 parameters: < _Error_ >err, < _Buffer_ >handle. 615 | 616 | * **close**(< _Buffer_ >handle, < _function_ >callback) - _(void)_ - Closes the resource associated with `handle` given by open() or opendir(). `callback` has 1 parameter: < _Error_ >err. 617 | 618 | * **read**(< _Buffer_ >handle, < _Buffer_ >buffer, < _integer_ >offset, < _integer_ >length, < _integer_ >position, < _function_ >callback) - _(void)_ - Reads `length` bytes from the resource associated with `handle` starting at `position` and stores the bytes in `buffer` starting at `offset`. `callback` has 4 parameters: < _Error_ >err, < _integer_ >bytesRead, < _Buffer_ >buffer (offset adjusted), < _integer_ >position. 619 | 620 | * **write**(< _Buffer_ >handle, < _Buffer_ >buffer, < _integer_ >offset, < _integer_ >length, < _integer_ >position, < _function_ >callback) - _(void)_ - Writes `length` bytes from `buffer` starting at `offset` to the resource associated with `handle` starting at `position`. `callback` has 1 parameter: < _Error_ >err. 621 | 622 | * **fstat**(< _Buffer_ >handle, < _function_ >callback) - _(void)_ - Retrieves attributes for the resource associated with `handle`. `callback` has 2 parameters: < _Error_ >err, < _Stats_ >stats. 623 | 624 | * **fsetstat**(< _Buffer_ >handle, < _ATTRS_ >attributes, < _function_ >callback) - _(void)_ - Sets the attributes defined in `attributes` for the resource associated with `handle`. `callback` has 1 parameter: < _Error_ >err. 625 | 626 | * **futimes**(< _Buffer_ >handle, < _mixed_ >atime, < _mixed_ >mtime, < _function_ >callback) - _(void)_ - Sets the access time and modified time for the resource associated with `handle`. `atime` and `mtime` can be Date instances or UNIX timestamps. `callback` has 1 parameter: < _Error_ >err. 627 | 628 | * **fchown**(< _Buffer_ >handle, < _integer_ >uid, < _integer_ >gid, < _function_ >callback) - _(void)_ - Sets the owner for the resource associated with `handle`. `callback` has 1 parameter: < _Error_ >err. 629 | 630 | * **fchmod**(< _Buffer_ >handle, < _mixed_ >mode, < _function_ >callback) - _(void)_ - Sets the mode for the resource associated with `handle`. `mode` can be an integer or a string containing an octal number. `callback` has 1 parameter: < _Error_ >err. 631 | 632 | * **opendir**(< _string_ >path, < _function_ >callback) - _(void)_ - Opens a directory `path`. `callback` has 2 parameters: < _Error_ >err, < _Buffer_ >handle. 633 | 634 | * **readdir**(< _mixed_ >location, < _function_ >callback) - _(void)_ - Retrieves a directory listing. `location` can either be a _Buffer_ containing a valid directory handle from opendir() or a _string_ containing the path to a directory. `callback` has 2 parameters: < _Error_ >err, < _mixed_ >list. `list` is an _Array_ of `{ filename: 'foo', longname: '....', attrs: {...} }` style objects (attrs is of type _ATTR_). If `location` is a directory handle, this function may need to be called multiple times until `list` is boolean false, which indicates that no more directory entries are available for that directory handle. 635 | 636 | * **unlink**(< _string_ >path, < _function_ >callback) - _(void)_ - Removes the file/symlink at `path`. `callback` has 1 parameter: < _Error_ >err. 637 | 638 | * **rename**(< _string_ >srcPath, < _string_ >destPath, < _function_ >callback) - _(void)_ - Renames/moves `srcPath` to `destPath`. `callback` has 1 parameter: < _Error_ >err. 639 | 640 | * **mkdir**(< _string_ >path, [< _ATTRS_ >attributes, ]< _function_ >callback) - _(void)_ - Creates a new directory `path`. `callback` has 1 parameter: < _Error_ >err. 641 | 642 | * **rmdir**(< _string_ >path, < _function_ >callback) - _(void)_ - Removes the directory at `path`. `callback` has 1 parameter: < _Error_ >err. 643 | 644 | * **stat**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves attributes for `path`. `callback` has 2 parameter: < _Error_ >err, < _Stats_ >stats. 645 | 646 | * **lstat**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves attributes for `path`. If `path` is a symlink, the link itself is stat'ed instead of the resource it refers to. `callback` has 2 parameters: < _Error_ >err, < _Stats_ >stats. 647 | 648 | * **setstat**(< _string_ >path, < _ATTRS_ >attributes, < _function_ >callback) - _(void)_ - Sets the attributes defined in `attributes` for `path`. `callback` has 1 parameter: < _Error_ >err. 649 | 650 | * **utimes**(< _string_ >path, < _mixed_ >atime, < _mixed_ >mtime, < _function_ >callback) - _(void)_ - Sets the access time and modified time for `path`. `atime` and `mtime` can be Date instances or UNIX timestamps. `callback` has 1 parameter: < _Error_ >err. 651 | 652 | * **chown**(< _string_ >path, < _integer_ >uid, < _integer_ >gid, < _function_ >callback) - _(void)_ - Sets the owner for `path`. `callback` has 1 parameter: < _Error_ >err. 653 | 654 | * **chmod**(< _string_ >path, < _mixed_ >mode, < _function_ >callback) - _(void)_ - Sets the mode for `path`. `mode` can be an integer or a string containing an octal number. `callback` has 1 parameter: < _Error_ >err. 655 | 656 | * **readlink**(< _string_ >path, < _function_ >callback) - _(void)_ - Retrieves the target for a symlink at `path`. `callback` has 2 parameters: < _Error_ >err, < _string_ >target. 657 | 658 | * **symlink**(< _string_ >targetPath, < _string_ >linkPath, < _function_ >callback) - _(void)_ - Creates a symlink at `linkPath` to `targetPath`. `callback` has 1 parameter: < _Error_ >err. 659 | 660 | * **realpath**(< _string_ >path, < _function_ >callback) - _(void)_ - Resolves `path` to an absolute path. `callback` has 2 parameters: < _Error_ >err, < _string_ >absPath. 661 | 662 | 663 | ATTRS 664 | ----- 665 | 666 | An object with the following valid properties: 667 | 668 | * **mode** - < _integer_ > - Mode/permissions for the resource. 669 | 670 | * **uid** - < _integer_ > - User ID of the resource. 671 | 672 | * **gid** - < _integer_ > - Group ID of the resource. 673 | 674 | * **size** - < _integer_ > - Resource size in bytes. 675 | 676 | * **atime** - < _integer_ > - UNIX timestamp of the access time of the resource. 677 | 678 | * **mtime** - < _integer_ > - UNIX timestamp of the modified time of the resource. 679 | 680 | When supplying an ATTRS object to one of the SFTP methods: 681 | 682 | * `atime` and `mtime` can be either a Date instance or a UNIX timestamp. 683 | 684 | * `mode` can either be an integer or a string containing an octal number. 685 | 686 | 687 | Stats 688 | ----- 689 | 690 | An object with the same attributes as an ATTRS object with the addition of the following methods: 691 | 692 | * `stats.isDirectory()` 693 | 694 | * `stats.isFile()` 695 | 696 | * `stats.isBlockDevice()` 697 | 698 | * `stats.isCharacterDevice()` 699 | 700 | * `stats.isSymbolicLink()` 701 | 702 | * `stats.isFIFO()` 703 | 704 | * `stats.isSocket()` 705 | 706 | 707 | Pseudo-TTY settings 708 | ------------------- 709 | 710 | * **rows** - < _integer_ > - Number of rows **Default:** `24` 711 | 712 | * **cols** - < _integer_ > - Number of columns **Default:** `80` 713 | 714 | * **height** - < _integer_ > - Height in pixels **Default:** `480` 715 | 716 | * **width** - < _integer_ > - Width in pixels **Default:** `640` 717 | 718 | * **term** - < _string_ > - The value to use for $TERM **Default:** `'vt100'` 719 | 720 | `rows` and `cols` override `width` and `height` when `rows` and `cols` are non-zero. 721 | 722 | Pixel dimensions refer to the drawable area of the window. 723 | 724 | Zero dimension parameters are ignored. 725 | -------------------------------------------------------------------------------- /test/test-integration.js: -------------------------------------------------------------------------------- 1 | var Connection = require('../lib/Connection'); 2 | 3 | var dns = require('dns'), 4 | fs = require('fs'), 5 | net = require('net'), 6 | cpspawn = require('child_process').spawn, 7 | cpexec = require('child_process').exec, 8 | path = require('path'), 9 | join = path.join, 10 | inspect = require('util').inspect, 11 | assert = require('assert'); 12 | 13 | var t = -1, 14 | forkedTest, 15 | group = path.basename(__filename, '.js') + '/', 16 | tempdir = join(__dirname, 'temp'), 17 | fixturesdir = join(__dirname, 'fixtures'); 18 | 19 | var SSHD_PORT, 20 | LOCALHOST, 21 | HOST_FINGERPRINT = '64254520742d3d0792e918f3ce945a64', 22 | PRIVATE_KEY_RSA = fs.readFileSync(join(fixturesdir, 'id_rsa')), 23 | PRIVATE_KEY_DSA = fs.readFileSync(join(fixturesdir, 'id_dsa')), 24 | USER = process.env.LOGNAME || process.env.USER || process.env.USERNAME; 25 | DEFAULT_SSHD_OPTS = { 26 | 'AddressFamily': 'any', 27 | 'AllowUsers': USER, 28 | 'AuthorizedKeysFile': join(fixturesdir, 'authorized_keys'), 29 | 'Banner': 'none', 30 | 'Compression': 'no', 31 | 'HostbasedAuthentication': 'no', 32 | 'HostKey': join(fixturesdir, 'ssh_host_rsa_key'), 33 | 'ListenAddress': 'localhost', 34 | 'LogLevel': 'FATAL', 35 | 'PasswordAuthentication': 'no', 36 | 'PermitRootLogin': 'no', 37 | 'Protocol': '2', 38 | 'PubkeyAuthentication': 'yes', 39 | 'Subsystem': 'sftp internal-sftp', 40 | 'TCPKeepAlive': 'yes', 41 | 'UseDNS': 'no', 42 | 'UsePrivilegeSeparation': 'no' 43 | }; 44 | 45 | var tests = [ 46 | { run: function() { 47 | var self = this, 48 | what = this.what, 49 | conn = new Connection(); 50 | startServer(function() { 51 | var error, 52 | ready; 53 | conn.on('ready', function() { 54 | ready = true; 55 | this.end(); 56 | }).on('error', function(err) { 57 | error = err; 58 | }).on('close', function() { 59 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 60 | assert(ready, makeMsg(what, 'Expected ready')); 61 | next(); 62 | }).connect(self.config); 63 | }); 64 | }, 65 | config: { 66 | host: 'localhost', 67 | username: USER, 68 | privateKey: PRIVATE_KEY_RSA 69 | }, 70 | what: 'Authenticate with a RSA key' 71 | }, 72 | { run: function() { 73 | var self = this, 74 | what = this.what, 75 | conn = new Connection(); 76 | startServer(function() { 77 | var error, 78 | ready; 79 | conn.on('ready', function() { 80 | ready = true; 81 | this.end(); 82 | }).on('error', function(err) { 83 | error = err; 84 | }).on('close', function() { 85 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 86 | assert(ready, makeMsg(what, 'Expected ready')); 87 | next(); 88 | }).connect(self.config); 89 | }); 90 | }, 91 | config: { 92 | host: 'localhost', 93 | username: USER, 94 | privateKey: PRIVATE_KEY_RSA 95 | }, 96 | what: 'Authenticate with a DSA key' 97 | }, 98 | { run: function() { 99 | // use ssh-agent with a command (this test) to make agent cleanup easier 100 | if (!process.env.SSH_AUTH_SOCK) { 101 | var proc = cpspawn('ssh-agent', 102 | [process.argv[0], process.argv[1], t], 103 | { stdio: 'inherit' }); 104 | proc.on('exit', function(code, signal) { 105 | if (code === 0 && !signal) 106 | next(); 107 | }); 108 | return; 109 | } 110 | 111 | var self = this, 112 | what = this.what, 113 | conn = new Connection(); 114 | 115 | // add key first 116 | cpexec('ssh-add ' + join(fixturesdir, 'id_rsa'), function() { 117 | startServer(function() { 118 | var error, 119 | ready; 120 | conn.on('ready', function() { 121 | ready = true; 122 | this.end(); 123 | }).on('error', function(err) { 124 | error = err; 125 | }).on('close', function() { 126 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 127 | assert(ready, makeMsg(what, 'Expected ready')); 128 | }).connect(self.config); 129 | }); 130 | }); 131 | }, 132 | config: { 133 | host: 'localhost', 134 | username: USER, 135 | agent: process.env.SSH_AUTH_SOCK 136 | }, 137 | what: 'Authenticate with an agent (RSA)' 138 | }, 139 | { run: function() { 140 | // use ssh-agent with a command (this test) to make agent cleanup easier 141 | if (!process.env.SSH_AUTH_SOCK) { 142 | var proc = cpspawn('ssh-agent', 143 | [process.argv[0], process.argv[1], t], 144 | { stdio: 'inherit' }); 145 | proc.on('exit', function(code, signal) { 146 | if (code === 0 && !signal) 147 | next(); 148 | }); 149 | return; 150 | } 151 | 152 | var self = this, 153 | what = this.what, 154 | conn = new Connection(); 155 | 156 | // add key first 157 | cpexec('ssh-add ' + join(fixturesdir, 'id_dsa'), function(err, stdout, stderr) { 158 | startServer(function() { 159 | var error, 160 | ready; 161 | conn.on('ready', function() { 162 | ready = true; 163 | this.end(); 164 | }).on('error', function(err) { 165 | error = err; 166 | }).on('close', function() { 167 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 168 | assert(ready, makeMsg(what, 'Expected ready')); 169 | }).connect(self.config); 170 | }); 171 | }); 172 | }, 173 | config: { 174 | host: 'localhost', 175 | username: USER, 176 | agent: process.env.SSH_AUTH_SOCK 177 | }, 178 | what: 'Authenticate with an agent (DSA)' 179 | }, 180 | { run: function() { 181 | // use ssh-agent with a command (this test) to make agent cleanup easier 182 | if (!process.env.SSH_AUTH_SOCK) { 183 | var proc = cpspawn('ssh-agent', 184 | [process.argv[0], process.argv[1], t], 185 | { stdio: 'inherit' }); 186 | proc.on('exit', function(code, signal) { 187 | if (code === 0 && !signal) 188 | next(); 189 | }); 190 | return; 191 | } 192 | 193 | var self = this, 194 | what = this.what, 195 | conn = new Connection(); 196 | 197 | // ssh-agent has no keys 198 | startServer({ 199 | 'ChallengeResponseAuthentication': 'yes', 200 | 'UsePAM': 'yes' 201 | }, function() { 202 | var error, 203 | ready; 204 | conn.on('ready', function() { 205 | ready = true; 206 | this.end(); 207 | }).on('keyboard-interactive', function (name, instructions, lang, messages, callback) { 208 | callback(['a']); // wrong password 209 | }).on('error', function(err) { 210 | error = err; 211 | }).on('close', function() { 212 | assert(error && /Authentication failure. Available authentication methods/.test(error.message), 213 | makeMsg(what, 'Expected authentication failure error')); 214 | assert(!ready, makeMsg(what, 'Unexpected ready')); 215 | }).connect(self.config); 216 | }); 217 | }, 218 | config: { 219 | host: 'localhost', 220 | username: USER, 221 | agent: process.env.SSH_AUTH_SOCK, 222 | tryKeyboard: true 223 | }, 224 | what: 'Authenticate with empty (valid) agent and keyboard-interactive options (bad password)' 225 | }, 226 | { run: function() { 227 | // use ssh-agent with a command (this test) to make agent cleanup easier 228 | if (!process.env.SSH_AUTH_SOCK) { 229 | var proc = cpspawn('ssh-agent', 230 | [process.argv[0], process.argv[1], t], 231 | { stdio: 'inherit' }); 232 | proc.on('exit', function(code, signal) { 233 | if (code === 0 && !signal) 234 | next(); 235 | }); 236 | return; 237 | } 238 | 239 | var self = this, 240 | what = this.what, 241 | conn = new Connection(); 242 | 243 | // ssh-agent has a bad key 244 | cpexec('ssh-add ' + join(fixturesdir, 'id_rsa.bad'), function(err, stdout, stderr) { 245 | startServer({ 246 | 'ChallengeResponseAuthentication': 'yes', 247 | 'UsePAM': 'yes' 248 | }, function() { 249 | var error, 250 | ready; 251 | conn.on('ready', function() { 252 | ready = true; 253 | this.end(); 254 | }).on('keyboard-interactive', function (name, instructions, lang, messages, callback) { 255 | callback(['a']); // wrong password 256 | }).on('error', function(err) { 257 | error = err; 258 | }).on('close', function() { 259 | assert(error && /Authentication failure. Available authentication methods/.test(error.message), 260 | makeMsg(what, 'Expected authentication failure error')); 261 | assert(!ready, makeMsg(what, 'Unexpected ready')); 262 | }).connect(self.config); 263 | }); 264 | }); 265 | }, 266 | config: { 267 | host: 'localhost', 268 | username: USER, 269 | agent: process.env.SSH_AUTH_SOCK, 270 | tryKeyboard: true 271 | }, 272 | what: 'Authenticate with agent (1 bad key) and keyboard-interactive options (bad password)' 273 | }, 274 | { run: function() { 275 | var self = this, 276 | what = this.what, 277 | conn = new Connection(), 278 | fingerprint; 279 | this.config.hostVerifier = function(host) { 280 | fingerprint = host; 281 | return true; // perform actual verification at the end 282 | }; 283 | startServer(function() { 284 | var error, 285 | ready; 286 | conn.on('ready', function() { 287 | ready = true; 288 | this.end(); 289 | }).on('error', function(err) { 290 | error = err; 291 | }).on('close', function() { 292 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 293 | assert(ready, makeMsg(what, 'Expected ready')); 294 | assert(HOST_FINGERPRINT === fingerprint, 295 | makeMsg(what, 'Host fingerprint mismatch.\nSaw:\n' 296 | + inspect(fingerprint) 297 | + '\nExpected:\n' 298 | + inspect(HOST_FINGERPRINT))); 299 | next(); 300 | }).connect(self.config); 301 | }); 302 | }, 303 | config: { 304 | host: 'localhost', 305 | username: USER, 306 | privateKey: PRIVATE_KEY_RSA, 307 | hostHash: 'md5' 308 | }, 309 | what: 'Verify host fingerprint' 310 | }, 311 | { run: function() { 312 | var self = this, 313 | what = this.what, 314 | conn = new Connection(), 315 | bannerPath = join(fixturesdir, 'banner'), 316 | bannerSent = fs.readFileSync(bannerPath, 317 | { encoding: 'utf8' }); 318 | startServer({ 'Banner': bannerPath }, function() { 319 | var error, 320 | ready, 321 | bannerRecvd; 322 | conn.on('banner', function(msg) { 323 | bannerRecvd = msg; 324 | }).on('ready', function() { 325 | ready = true; 326 | this.exec('uptime', function(err) { 327 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 328 | conn.end(); 329 | }); 330 | }).on('error', function(err) { 331 | error = err; 332 | }).on('close', function() { 333 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 334 | assert(ready, makeMsg(what, 'Expected ready')); 335 | assert(bannerSent === bannerRecvd, 336 | makeMsg(what, 'Banner mismatch.\nSaw:\n' 337 | + inspect(bannerRecvd) 338 | + '\nExpected:\n' 339 | + inspect(bannerSent))); 340 | next(); 341 | }).connect(self.config); 342 | }); 343 | }, 344 | config: { 345 | host: 'localhost', 346 | username: USER, 347 | privateKey: PRIVATE_KEY_RSA 348 | }, 349 | what: 'Banner message' 350 | }, 351 | { run: function() { 352 | var self = this, 353 | what = this.what, 354 | conn = new Connection(); 355 | startServer(function() { 356 | var error, 357 | ready; 358 | conn.on('ready', function() { 359 | ready = true; 360 | this.exec('uptime', function(err) { 361 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 362 | conn.end(); 363 | }); 364 | }).on('error', function(err) { 365 | error = err; 366 | }).on('close', function() { 367 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 368 | assert(ready, makeMsg(what, 'Expected ready')); 369 | next(); 370 | }).connect(self.config); 371 | }); 372 | }, 373 | config: { 374 | host: 'localhost', 375 | username: USER, 376 | privateKey: PRIVATE_KEY_RSA 377 | }, 378 | what: 'Simple exec' 379 | }, 380 | { run: function() { 381 | var self = this, 382 | what = this.what, 383 | conn = new Connection(); 384 | startServer({ 'AcceptEnv': 'SSH2NODETEST' }, function() { 385 | var error, 386 | ready, 387 | out; 388 | conn.on('ready', function() { 389 | ready = true; 390 | this.exec('echo -n $SSH2NODETEST', 391 | { env: { SSH2NODETEST: self.expected } }, 392 | function(err, stream) { 393 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 394 | stream.stderr.resume(); 395 | bufferStream(stream, 'ascii', function(data) { 396 | out = data; 397 | conn.end(); 398 | }); 399 | }); 400 | }).on('error', function(err) { 401 | error = err; 402 | }).on('close', function() { 403 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 404 | assert(ready, makeMsg(what, 'Expected ready')); 405 | assert(out === self.expected, 406 | makeMsg(what, 'Environment variable mismatch.\nSaw:\n' 407 | + inspect(out) 408 | + '\nExpected:\n' 409 | + inspect(self.expected))); 410 | next(); 411 | }).connect(self.config); 412 | }); 413 | }, 414 | config: { 415 | host: 'localhost', 416 | username: USER, 417 | privateKey: PRIVATE_KEY_RSA 418 | }, 419 | expected: 'Hello from node.js!!!', 420 | what: 'Exec with environment set' 421 | }, 422 | { run: function() { 423 | var self = this, 424 | what = this.what, 425 | conn = new Connection(); 426 | startServer(function() { 427 | var error, 428 | ready, 429 | out; 430 | conn.on('ready', function() { 431 | ready = true; 432 | this.exec('(if [ -t 1 ] ; then echo terminal; fi); echo -e "lines\ncols"|tput -S && echo -n $TERM', 433 | { pty: { 434 | rows: 2, 435 | cols: 4, 436 | width: 0, 437 | height: 0, 438 | term: 'vt220' 439 | } 440 | }, 441 | function(err, stream) { 442 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 443 | stream.stderr.resume(); 444 | bufferStream(stream, 'ascii', function(data) { 445 | out = (data ? stripDebug(data).split(/\r?\n/g) : data); 446 | conn.end(); 447 | }); 448 | }); 449 | }).on('error', function(err) { 450 | error = err; 451 | }).on('close', function() { 452 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 453 | assert(ready, makeMsg(what, 'Expected ready')); 454 | assert.deepEqual(out, 455 | self.expected, 456 | makeMsg(what, 'Exec output mismatch.\nSaw:\n' 457 | + inspect(out) 458 | + '\nExpected:\n' 459 | + inspect(self.expected))); 460 | next(); 461 | }).connect(self.config); 462 | }); 463 | }, 464 | config: { 465 | host: 'localhost', 466 | username: USER, 467 | privateKey: PRIVATE_KEY_RSA 468 | }, 469 | expected: [ 470 | 'terminal', 471 | '2', 472 | '4', 473 | 'vt220' 474 | ], 475 | what: 'Exec with pty set' 476 | }, 477 | { run: function() { 478 | // use ssh-agent with a command (this test) to make agent cleanup easier 479 | if (!process.env.SSH_AUTH_SOCK) { 480 | var proc = cpspawn('ssh-agent', 481 | [process.argv[0], process.argv[1], t], 482 | { stdio: 'inherit' }); 483 | proc.on('exit', function(code, signal) { 484 | if (code === 0 && !signal) 485 | next(); 486 | }); 487 | return; 488 | } 489 | 490 | var self = this, 491 | what = this.what, 492 | conn = new Connection(); 493 | 494 | // add key first 495 | cpexec('ssh-add ' + join(fixturesdir, 'id_rsa'), function() { 496 | startServer(function() { 497 | var error, 498 | ready, 499 | out; 500 | conn.on('ready', function() { 501 | ready = true; 502 | this.exec('echo -n $SSH_AUTH_SOCK', function(err, stream) { 503 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 504 | stream.stderr.resume(); 505 | bufferStream(stream, 'ascii', function(data) { 506 | out = data; 507 | conn.end(); 508 | }); 509 | }); 510 | }).on('error', function(err) { 511 | error = err; 512 | }).on('close', function() { 513 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 514 | assert(ready, makeMsg(what, 'Expected ready')); 515 | assert(out && out.length, 516 | makeMsg(what, 'Expected SSH_AUTH_SOCK in exec environment')); 517 | }).connect(self.config); 518 | }); 519 | }); 520 | }, 521 | config: { 522 | host: 'localhost', 523 | username: USER, 524 | agent: process.env.SSH_AUTH_SOCK, 525 | agentForward: true 526 | }, 527 | what: 'Exec with OpenSSH agent forwarding' 528 | }, 529 | { run: function() { 530 | var self = this, 531 | what = this.what, 532 | conn = new Connection(); 533 | startServer({ 534 | 'X11DisplayOffset': '50', 535 | 'X11Forwarding': 'yes', 536 | 'X11UseLocalhost': 'yes' 537 | }, function() { 538 | var error, 539 | ready, 540 | sawX11 = false; 541 | conn.on('ready', function() { 542 | ready = true; 543 | this.exec('xeyes', { x11: true }, function(err, stream) { 544 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 545 | stream.resume(); 546 | stream.stderr.resume(); 547 | }); 548 | }).on('error', function(err) { 549 | error = err; 550 | }).on('close', function() { 551 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 552 | assert(ready, makeMsg(what, 'Expected ready')); 553 | assert(sawX11, makeMsg(what, 'Expected X11 request')); 554 | next(); 555 | }).on('x11', function(details, accept, reject) { 556 | sawX11 = true; 557 | conn.end(); 558 | }).connect(self.config); 559 | }); 560 | }, 561 | config: { 562 | host: 'localhost', 563 | username: USER, 564 | privateKey: PRIVATE_KEY_RSA 565 | }, 566 | what: 'Exec with X11 forwarding' 567 | }, 568 | { run: function() { 569 | var self = this, 570 | what = this.what, 571 | conn = new Connection(); 572 | startServer(function() { 573 | var error, 574 | ready, 575 | out = { 576 | stdout: undefined, 577 | stderr: undefined 578 | }; 579 | conn.on('ready', function() { 580 | ready = true; 581 | this.exec('echo -n "hello from stderr" 3>&1 1>&2 2>&3 3>&-', 582 | function(err, stream) { 583 | assert(!err, makeMsg(what, 'Unexpected exec error: ' + err)); 584 | bufferStream(stream, 'ascii', function(data) { 585 | out.stdout = data; 586 | }); 587 | bufferStream(stream.stderr, 'ascii', function(data) { 588 | out.stderr = stripDebug(data); 589 | conn.end(); 590 | }); 591 | }); 592 | }).on('error', function(err) { 593 | error = err; 594 | }).on('close', function() { 595 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 596 | assert(ready, makeMsg(what, 'Expected ready')); 597 | assert.deepEqual(out, 598 | self.expected, 599 | makeMsg(what, 'Exec output mismatch.\nSaw:\n' 600 | + inspect(out) 601 | + '\nExpected:\n' 602 | + inspect(self.expected))); 603 | next(); 604 | }).connect(self.config); 605 | }); 606 | }, 607 | config: { 608 | host: 'localhost', 609 | username: USER, 610 | privateKey: PRIVATE_KEY_RSA 611 | }, 612 | expected: { 613 | stdout: undefined, 614 | stderr: 'hello from stderr' 615 | }, 616 | what: 'Exec with stderr output' 617 | }, 618 | { run: function() { 619 | var self = this, 620 | what = this.what, 621 | conn = new Connection(); 622 | startServer(function() { 623 | var error, 624 | ready; 625 | conn.on('ready', function() { 626 | ready = true; 627 | this.shell(function(err) { 628 | assert(!err, makeMsg(what, 'Unexpected shell error: ' + err)); 629 | conn.end(); 630 | }); 631 | }).on('error', function(err) { 632 | error = err; 633 | }).on('close', function() { 634 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 635 | assert(ready, makeMsg(what, 'Expected ready')); 636 | next(); 637 | }).connect(self.config); 638 | }); 639 | }, 640 | config: { 641 | host: 'localhost', 642 | username: USER, 643 | privateKey: PRIVATE_KEY_RSA 644 | }, 645 | what: 'Simple shell' 646 | }, 647 | { run: function() { 648 | var self = this, 649 | what = this.what, 650 | conn = new Connection(); 651 | startServer({ 'AllowTcpForwarding': 'local' }, function() { 652 | var error, 653 | ready, 654 | out; 655 | conn.on('ready', function() { 656 | ready = true; 657 | net.createServer(function(sock) { 658 | this.close(); 659 | sock.end(self.expected); 660 | }).listen(0, 'localhost', function() { 661 | conn.forwardOut(''+this.address().address, 662 | '0', 663 | ''+this.address().address, 664 | ''+this.address().port, 665 | function(err, stream) { 666 | assert(!err, makeMsg(what, 'Unexpected forwardOut error: ' + err)); 667 | bufferStream(stream, 'ascii', function(data) { 668 | out = data; 669 | conn.end(); 670 | }); 671 | stream.end(); 672 | }); 673 | }); 674 | }).on('error', function(err) { 675 | error = err; 676 | }).on('close', function() { 677 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 678 | assert(ready, makeMsg(what, 'Expected ready')); 679 | assert.equal(out, 680 | self.expected, 681 | makeMsg(what, 'Connection output mismatch.\nSaw:\n' 682 | + inspect(out) 683 | + '\nExpected:\n' 684 | + inspect(self.expected))); 685 | next(); 686 | }).connect(self.config); 687 | }); 688 | }, 689 | config: { 690 | host: 'localhost', 691 | username: USER, 692 | privateKey: PRIVATE_KEY_RSA 693 | }, 694 | expected: 'hello from node.js and ssh2!', 695 | what: 'Local port forwarding' 696 | }, 697 | { run: function() { 698 | var self = this, 699 | what = this.what, 700 | conn = new Connection(); 701 | this.expected.srcIP = LOCALHOST; 702 | startServer({ 'AllowTcpForwarding': 'remote' }, function() { 703 | var error, 704 | ready, 705 | out; 706 | conn.on('ready', function() { 707 | ready = true; 708 | this.forwardIn('localhost', 0, function(err, port) { 709 | assert(!err, makeMsg(what, 'Unexpected forwardIn error: ' + err)); 710 | self.expected.destPort = port; 711 | (new net.Socket({ allowHalfOpen: true })) 712 | .on('connect', function() { 713 | self.expected.srcPort = this.localPort; 714 | this.end(self.expected.out); 715 | }).connect(port, 'localhost'); 716 | }); 717 | }).on('tcp connection', function(info, accept, reject) { 718 | out = info; 719 | accept().on('close', function() { 720 | conn.end(); 721 | }).on('data', function(d) { 722 | if (!out.out) 723 | out.out = d.toString('ascii'); 724 | else 725 | out.out += d.toString('ascii'); 726 | }).end(); 727 | }).on('error', function(err) { 728 | error = err; 729 | }).on('close', function() { 730 | assert(!error, makeMsg(what, 'Unexpected client error: ' + error)); 731 | assert(ready, makeMsg(what, 'Expected ready')); 732 | assert.deepEqual(out, 733 | self.expected, 734 | makeMsg(what, 'Connection output mismatch.\nSaw:\n' 735 | + inspect(out) 736 | + '\nExpected:\n' 737 | + inspect(self.expected))); 738 | next(); 739 | }).connect(self.config); 740 | }); 741 | }, 742 | config: { 743 | host: 'localhost', 744 | username: USER, 745 | privateKey: PRIVATE_KEY_RSA, 746 | //debug: console.log 747 | }, 748 | expected: { 749 | destIP: 'localhost', 750 | destPort: 0, // filled in during test 751 | srcIP: undefined, // filled in during test 752 | srcPort: 0, // filled in during test 753 | out: 'hello from node.js and ssh2!' 754 | }, 755 | what: 'Remote port forwarding (accepted)' 756 | }, 757 | ]; 758 | 759 | function bufferStream(stream, encoding, cb) { 760 | var buf; 761 | if (typeof encoding === 'function') { 762 | cb = encoding; 763 | encoding = undefined; 764 | } 765 | if (!encoding) { 766 | var nb = 0; 767 | stream.on('data', function(d) { 768 | if (nb === 0) 769 | buf = [ d ]; 770 | else 771 | buf.push(d); 772 | nb += d.length; 773 | }).on((stream.writable ? 'close' : 'end'), function() { 774 | cb(nb ? Buffer.concat(buf, nb) : buf); 775 | }); 776 | } else { 777 | stream.on('data', function(d) { 778 | if (!buf) 779 | buf = d; 780 | else 781 | buf += d; 782 | }).on((stream.writable ? 'close' : 'end'), function() { 783 | cb(buf); 784 | }).setEncoding(encoding); 785 | } 786 | } 787 | 788 | function stripDebug(str) { 789 | if (typeof str !== 'string') 790 | return str; 791 | return str.replace(/^(?:(?:[\s\S]+)?Environment:\r?\n(?: [^=]+=[^\r\n]+\r?\n)+)?/, ''); 792 | } 793 | 794 | function startServer(opts, listencb, exitcb) { 795 | var sshdOpts = {}, 796 | cmd, 797 | key, 798 | val; 799 | for (key in DEFAULT_SSHD_OPTS) 800 | sshdOpts[key] = DEFAULT_SSHD_OPTS[key]; 801 | if (typeof opts === 'function') { 802 | exitcb = listencb; 803 | listencb = opts; 804 | opts = undefined; 805 | } 806 | if (opts) { 807 | for (key in opts) 808 | sshdOpts[key] = opts[key]; 809 | } 810 | 811 | cmd = '`which sshd` -p ' 812 | + SSHD_PORT 813 | + ' -Dde -f ' 814 | + join(fixturesdir, 'sshd_config'); 815 | 816 | for (key in sshdOpts) { 817 | val = ''+sshdOpts[key]; 818 | if (val.indexOf(' ') > -1) 819 | val = '"' + val + '"'; 820 | cmd += ' -o ' + key + '=' + val; 821 | } 822 | 823 | tests[t].config.port = SSHD_PORT; 824 | 825 | stopWaiting = false; 826 | cpexec(cmd, function(err, stdout, stderr) { 827 | stopWaiting = true; 828 | //exitcb(err, stdout, stderr); 829 | }); 830 | waitForSshd(listencb); 831 | } 832 | 833 | function cleanupTemp() { 834 | // clean up any temporary files left over 835 | fs.readdirSync(tempdir).forEach(function(file) { 836 | if (file !== '.gitignore') 837 | fs.unlinkSync(join(tempdir, file)); 838 | }); 839 | } 840 | 841 | function next() { 842 | if (t === forkedTest || t === tests.length - 1) 843 | return; 844 | cleanupTemp(); 845 | var v = tests[++t]; 846 | v.run.call(v); 847 | } 848 | 849 | function makeMsg(what, msg) { 850 | return '[' + group + what + ']: ' + msg; 851 | } 852 | 853 | var stopWaiting = false; 854 | function waitForSshd(cb) { 855 | if (stopWaiting) 856 | return; 857 | cpexec('lsof -a -u ' 858 | + USER 859 | + ' -c sshd -i tcp@localhost:' 860 | + SSHD_PORT 861 | + ' &>/dev/null', function(err, stdout) { 862 | if (err) { 863 | return setTimeout(function() { 864 | waitForSshd(cb); 865 | }, 50); 866 | } 867 | cb(); 868 | }); 869 | } 870 | 871 | function cleanup(cb) { 872 | cleanupTemp(); 873 | cpexec('lsof -Fp -a -u ' 874 | + USER 875 | + ' -c sshd -i tcp@localhost:' 876 | + SSHD_PORT, function(err, stdout) { 877 | if (!err) { 878 | var pid = parseInt(stdout.trim().replace(/[^\d]/g, ''), 10); 879 | if (typeof pid === 'number' && !isNaN(pid)) { 880 | try { 881 | process.kill(pid); 882 | } catch (ex) {} 883 | } 884 | } 885 | cb(); 886 | }); 887 | } 888 | 889 | process.once('uncaughtException', function(err) { 890 | cleanup(function() { 891 | throw err; 892 | }); 893 | }); 894 | process.once('exit', function() { 895 | cleanup(function() { 896 | assert(t === tests.length - 1, 897 | makeMsg('_exit', 898 | 'Only finished ' + (t + 1) + '/' + tests.length + ' tests')); 899 | }); 900 | }); 901 | 902 | 903 | 904 | 905 | function findFreePort() { 906 | // find an unused port for sshd to listen on ... 907 | cpexec('netstat -nl --inet --inet6', function(err, stdout) { 908 | assert(!err, 'Unable to find a free port for starting sshd'); 909 | var portsInUse = stdout.trim() 910 | .split('\n') 911 | .slice(2) // skip two header lines 912 | .map(function(line) { 913 | var addr = line.split(/[ \t]+/g)[3]; 914 | return parseInt( 915 | addr.substr(addr.lastIndexOf(':') + 1), 916 | 10 917 | ); 918 | }); 919 | for (var port = 9000; port < 65535; ++port) { 920 | if (portsInUse.indexOf(port) === -1) { 921 | SSHD_PORT = port; 922 | // get localhost address for reference 923 | return dns.resolve('localhost', function(err, ips) { 924 | if (err) 925 | throw err; 926 | else if (ips.length === 0) 927 | throw new Error('Could not find localhost IP'); 928 | LOCALHOST = ips[0]; 929 | 930 | // start tests 931 | next(); 932 | }); 933 | } 934 | } 935 | assert(false, 'Unable to find a free port for starting sshd'); 936 | }); 937 | } 938 | 939 | // check for test prerequisites 940 | cpexec('which sshd', function(err) { 941 | if (err) { 942 | return console.error('[' 943 | + path.basename(__filename, '.js') 944 | + ']: OpenSSH server is required for integration tests.'); 945 | } 946 | findFreePort(); 947 | }); 948 | 949 | // check for forked process 950 | if (process.argv.length > 2) { 951 | forkedTest = parseInt(process.argv[2], 10); 952 | if (!isNaN(forkedTest)) 953 | t = forkedTest - 1; 954 | else 955 | process.exit(100); 956 | } 957 | 958 | // ensure permissions are less permissive to appease sshd 959 | [ 'id_rsa', 'id_rsa.pub', 960 | 'id_dsa', 'id_dsa.pub', 961 | 'ssh_host_rsa_key', 'ssh_host_rsa_key.pub', 962 | 'authorized_keys' 963 | ].forEach(function(f) { 964 | fs.chmodSync(join(fixturesdir, f), '0600'); 965 | }); 966 | -------------------------------------------------------------------------------- /lib/SFTP/SFTPv3.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter, 2 | util = require('util'), 3 | inherits = util.inherits, 4 | isDate = util.isDate, 5 | fs = require('fs'), 6 | ReadableStream = require('stream').Readable 7 | || require('readable-stream').Readable, 8 | WritableStream = require('stream').Writable 9 | || require('readable-stream').Writable, 10 | Stats = require('./Stats'); 11 | 12 | var MAX_REQID = Math.pow(2, 32) - 1, 13 | VERSION_BUFFER = new Buffer([0, 0, 0, 5 /* length */, 14 | 1 /* REQUEST.INIT */, 15 | 0, 0, 0, 3 /* version */]), 16 | EMPTY_CALLBACK = function() {}, 17 | /* 18 | http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02: 19 | 20 | The maximum size of a packet is in practice determined by the client 21 | (the maximum size of read or write requests that it sends, plus a few 22 | bytes of packet overhead). All servers SHOULD support packets of at 23 | least 34000 bytes (where the packet size refers to the full length, 24 | including the header above). This should allow for reads and writes 25 | of at most 32768 bytes. 26 | 27 | OpenSSH caps this to 256kb instead of the ~34kb as mentioned in the sftpv3 28 | spec. 29 | */ 30 | RE_OPENSSH = /^SSH-2.0-(?:OpenSSH|dropbear)/, 31 | OPENSSH_MAX_DATA_LEN = (256 * 1024) - (2 * 1024)/*account for header data*/; 32 | 33 | module.exports = SFTP; 34 | 35 | function SFTP(stream, server_ident_raw) { 36 | var self = this; 37 | 38 | this._stream = stream; 39 | this._requests = {}; 40 | this._reqid = 0; 41 | this._reqidmaxed = false; 42 | 43 | this._count = 0; 44 | this._value = 0; 45 | this._string = undefined; 46 | this._field = 'packet_length'; 47 | this._data = { 48 | len: 0, 49 | type: undefined, 50 | subtype: undefined, 51 | reqid: undefined, 52 | version: undefined, 53 | statusCode: undefined, 54 | errMsg: undefined, 55 | lang: undefined, 56 | handle: undefined, 57 | data: undefined, 58 | count: undefined, 59 | names: undefined, 60 | c: undefined, 61 | attrs: undefined, 62 | _attrs: undefined, 63 | _flags: undefined 64 | }; 65 | 66 | if (RE_OPENSSH.test(server_ident_raw)) 67 | this._max_data_len = OPENSSH_MAX_DATA_LEN; 68 | else 69 | this._max_data_len = 32768; 70 | 71 | stream.on('data', function(data) { 72 | self._parse(data); 73 | }); 74 | stream.once('timeout', function() { 75 | self.emit('timeout'); 76 | }); 77 | stream.on('error', function(err) { 78 | self.emit('error', err); 79 | }); 80 | stream.once('end', function() { 81 | self.emit('end'); 82 | }); 83 | stream.once('close', function(had_err) { 84 | self.emit('close', had_err); 85 | }); 86 | } 87 | inherits(SFTP, EventEmitter); 88 | 89 | SFTP.prototype.end = function() { 90 | this._stream.end(); 91 | this.ended = true; 92 | }; 93 | 94 | SFTP.prototype.createReadStream = function(path, options) { 95 | return new ReadStream(this, path, options); 96 | }; 97 | 98 | SFTP.prototype.createWriteStream = function(path, options) { 99 | return new WriteStream(this, path, options); 100 | }; 101 | 102 | SFTP.prototype.open = function(path, flags, attrs, cb) { 103 | if (typeof attrs === 'function') { 104 | cb = attrs; 105 | attrs = undefined; 106 | } 107 | 108 | if (flags === 'r') 109 | flags = OPEN_MODE.READ; 110 | else if (flags === 'r+') 111 | flags = OPEN_MODE.READ | OPEN_MODE.WRITE; 112 | else if (flags === 'w') 113 | flags = OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE; 114 | else if (flags === 'wx' || flags === 'xw') 115 | flags = OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL; 116 | else if (flags === 'w+') 117 | flags = OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE; 118 | else if (flags === 'wx+' || flags === 'xw+') { 119 | flags = OPEN_MODE.TRUNC | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE 120 | | OPEN_MODE.EXCL; 121 | } else if (flags === 'a') 122 | flags = OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE; 123 | else if (flags === 'ax' || flags === 'xa') 124 | flags = OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.WRITE | OPEN_MODE.EXCL; 125 | else if (flags === 'a+') 126 | flags = OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE; 127 | else if (flags === 'ax+' || flags === 'xa+') { 128 | flags = OPEN_MODE.APPEND | OPEN_MODE.CREAT | OPEN_MODE.READ | OPEN_MODE.WRITE 129 | | OPEN_MODE.EXCL; 130 | } else 131 | throw new Error('Unknown file open flags: ' + flags); 132 | 133 | var attrFlags = 0, 134 | attrBytes = 0; 135 | if (typeof attrs === 'object') { 136 | attrs = attrsToBytes(attrs); 137 | attrFlags = attrs[0]; 138 | attrBytes = attrs[1]; 139 | attrs = attrs[2]; 140 | } 141 | 142 | /* 143 | uint32 id 144 | string filename 145 | uint32 pflags 146 | ATTRS attrs 147 | */ 148 | var pathlen = Buffer.byteLength(path), 149 | p = 9, 150 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen + 4 + 4 + attrBytes); 151 | buf[4] = REQUEST.OPEN; 152 | buf.writeUInt32BE(pathlen, p, true); 153 | buf.write(path, p += 4, pathlen, 'utf8'); 154 | buf.writeUInt32BE(flags, p += pathlen, true); 155 | buf.writeUInt32BE(attrFlags, p += 4, true); 156 | if (attrs && attrFlags) { 157 | p += 4; 158 | for (var i = 0, len = attrs.length; i < len; ++i) 159 | for (var j = 0, len2 = attrs[i].length; j < len2; ++j) 160 | buf[p++] = attrs[i][j]; 161 | } 162 | 163 | return this._send(buf, cb); 164 | }; 165 | 166 | SFTP.prototype.close = function(handle, cb) { 167 | if (!Buffer.isBuffer(handle)) 168 | throw new Error('handle is not a Buffer'); 169 | /* 170 | uint32 id 171 | string handle 172 | */ 173 | var handlelen = handle.length, 174 | p = 9, 175 | buf = new Buffer(4 + 1 + 4 + 4 + handlelen); 176 | buf[4] = REQUEST.CLOSE; 177 | buf.writeUInt32BE(handlelen, p, true); 178 | handle.copy(buf, p += 4); 179 | 180 | return this._send(buf, cb); 181 | }; 182 | 183 | SFTP.prototype.read = function(handle, buffer, offset, length, position, cb) { 184 | // TODO: emulate support for position === null to match fs.read() 185 | if (!Buffer.isBuffer(handle)) 186 | throw new Error('handle is not a Buffer'); 187 | 188 | if (!Buffer.isBuffer(buffer)) 189 | throw new Error('buffer is not a Buffer'); 190 | else if (offset >= buffer.length) 191 | throw new Error('offset is out of bounds'); 192 | else if (offset + length > buffer.length) 193 | throw new Error('length extends beyond buffer'); 194 | 195 | if (position === null) 196 | throw new Error('null position currently unsupported'); 197 | /* 198 | uint32 id 199 | string handle 200 | uint64 offset 201 | uint32 len 202 | */ 203 | var handlelen = handle.length, 204 | p = 9, 205 | pos = position, 206 | buf = new Buffer(4 + 1 + 4 + 4 + handlelen + 8 + 4); 207 | buf[4] = REQUEST.READ; 208 | buf.writeUInt32BE(handlelen, p, true); 209 | handle.copy(buf, p += 4); 210 | p += handlelen; 211 | for (var i = 7; i >= 0; --i) { 212 | buf[p + i] = pos & 0xFF; 213 | pos /= 256; 214 | } 215 | buf.writeUInt32BE(length, p += 8, true); 216 | 217 | return this._send(buf, function(err, bytesRead, data) { 218 | if (err) 219 | return cb(err); 220 | cb(undefined, bytesRead || 0, data, position); 221 | }, buffer.slice(offset, offset + length)); 222 | }; 223 | 224 | SFTP.prototype.write = function(handle, buffer, offset, length, position, cb) { 225 | var self = this; 226 | // TODO: emulate support for position === null to match fs.write() 227 | if (!Buffer.isBuffer(handle)) 228 | throw new Error('handle is not a Buffer'); 229 | else if (!Buffer.isBuffer(buffer)) 230 | throw new Error('buffer is not a Buffer'); 231 | else if (offset > buffer.length) 232 | throw new Error('offset is out of bounds'); 233 | else if (offset + length > buffer.length) 234 | throw new Error('length extends beyond buffer'); 235 | else if (position === null) 236 | throw new Error('null position currently unsupported'); 237 | 238 | if (!length) { 239 | cb && process.nextTick(function() { cb(undefined, 0); }); 240 | return; 241 | } 242 | 243 | var overflow = (length > this._max_data_len 244 | ? length - this._max_data_len 245 | : 0), 246 | origPosition = position; 247 | 248 | if (overflow) 249 | length = this._max_data_len; 250 | 251 | /* 252 | uint32 id 253 | string handle 254 | uint64 offset 255 | string data 256 | */ 257 | var handlelen = handle.length, 258 | p = 9, 259 | buf = new Buffer(4 + 1 + 4 + 4 + handlelen + 8 + 4 + length); 260 | buf[4] = REQUEST.WRITE; 261 | buf.writeUInt32BE(handlelen, p, true); 262 | handle.copy(buf, p += 4); 263 | p += handlelen; 264 | for (var i = 7; i >= 0; --i) { 265 | buf[p + i] = position & 0xFF; 266 | position /= 256; 267 | } 268 | buf.writeUInt32BE(length, p += 8, true); 269 | buffer.copy(buf, p += 4, offset, offset + length); 270 | 271 | return this._send(buf, function(err) { 272 | if (err) 273 | cb && cb(err); 274 | else if (overflow) { 275 | self.write(handle, 276 | buffer, 277 | offset + length, 278 | overflow, 279 | origPosition + length, 280 | cb); 281 | } else 282 | cb && cb(undefined, length); 283 | }); 284 | }; 285 | 286 | function fastXfer(src, dst, srcPath, dstPath, opts, cb) { 287 | var concurrency = 25, 288 | chunkSize = 32768, 289 | onstep; 290 | 291 | if (typeof opts === 'function') 292 | cb = opts; 293 | else if (typeof opts === 'object') { 294 | if (typeof opts.concurrency === 'number' 295 | && opts.concurrency > 0 296 | && !isNaN(opts.concurrency)) 297 | concurrency = opts.concurrency; 298 | if (typeof opts.chunkSize === 'number' 299 | && opts.chunkSize > 0 300 | && !isNaN(opts.chunkSize)) 301 | chunkSize = opts.chunkSize; 302 | if (typeof opts.step === 'function') 303 | onstep = opts.step; 304 | } 305 | 306 | // internal state variables 307 | var fsize, 308 | chunk, 309 | psrc = 0, 310 | pdst = 0, 311 | reads = 0, 312 | total = 0, 313 | srcHandle, 314 | dstHandle, 315 | readbuf = new Buffer(chunkSize * concurrency); 316 | 317 | function onerror(err) { 318 | var left = 0, 319 | cbfinal; 320 | 321 | if (srcHandle || dstHandle) { 322 | cbfinal = function() { 323 | if (--left === 0) 324 | cb(err); 325 | }; 326 | if (srcHandle) 327 | ++left; 328 | if (dstHandle) 329 | ++left; 330 | if (srcHandle) 331 | src.close(srcHandle, cbfinal); 332 | if (dstHandle) 333 | dst.close(dstHandle, cbfinal); 334 | } else 335 | cb(err); 336 | } 337 | 338 | src.open(srcPath, 'r', function(err, sourceHandle) { 339 | if (err) 340 | return onerror(err); 341 | srcHandle = sourceHandle; 342 | 343 | src.fstat(srcHandle, function(err, attrs) { 344 | if (err) 345 | return onerror(err); 346 | fsize = attrs.size; 347 | 348 | dst.open(dstPath, 'w', function(err, destHandle) { 349 | if (err) 350 | return onerror(err); 351 | dstHandle = destHandle; 352 | 353 | if (fsize <= 0) 354 | return onerror(); 355 | 356 | function onread(err, nb, data, dstpos, datapos) { 357 | if (err) 358 | return onerror(err); 359 | 360 | dst.write(dstHandle, data, datapos || 0, nb, dstpos, function(err) { 361 | if (err) 362 | return onerror(err); 363 | 364 | onstep && onstep(total, nb, fsize); 365 | 366 | if (--reads === 0) { 367 | if (total === fsize) { 368 | dst.close(dstHandle, function(err) { 369 | dstHandle = undefined; 370 | if (err) 371 | return onerror(err); 372 | src.close(srcHandle, function(err) { 373 | srcHandle = undefined; 374 | if (err) 375 | return onerror(err); 376 | cb(); 377 | }); 378 | }); 379 | } else 380 | read(); 381 | } 382 | }); 383 | total += nb; 384 | } 385 | 386 | function makeCb(psrc, pdst) { 387 | return function(err, nb, data) { 388 | onread(err, nb, data, pdst, psrc); 389 | }; 390 | } 391 | 392 | function read() { 393 | while (pdst < fsize && reads < concurrency) { 394 | chunk = (pdst + chunkSize > fsize ? fsize - pdst : chunkSize); 395 | if (src === fs) 396 | src.read(srcHandle, readbuf, psrc, chunk, pdst, makeCb(psrc, pdst)); 397 | else 398 | src.read(srcHandle, readbuf, psrc, chunk, pdst, onread); 399 | psrc += chunk; 400 | pdst += chunk; 401 | ++reads; 402 | } 403 | psrc = 0; 404 | } 405 | read(); 406 | }); 407 | }); 408 | }); 409 | } 410 | 411 | SFTP.prototype.fastGet = function(remotePath, localPath, opts, cb) { 412 | fastXfer(this, fs, remotePath, localPath, opts, cb); 413 | }; 414 | 415 | SFTP.prototype.fastPut = function(localPath, remotePath, opts, cb) { 416 | fastXfer(fs, this, localPath, remotePath, opts, cb); 417 | }; 418 | 419 | SFTP.prototype.readFile = function(path, options, callback_) { 420 | var callback = (typeof callback_ === 'function' ? callback_ : undefined); 421 | var self = this; 422 | 423 | if (typeof options === 'function' || !options) 424 | options = { encoding: null, flag: 'r' }; 425 | else if (typeof options === 'string') 426 | options = { encoding: options, flag: 'r' }; 427 | else if (!options) 428 | options = { encoding: null, flag: 'r' }; 429 | else if (typeof options !== 'object') 430 | throw new TypeError('Bad arguments'); 431 | 432 | var encoding = options.encoding; 433 | if (encoding && !Buffer.isEncoding(encoding)) 434 | throw new Error('Unknown encoding: ' + encoding); 435 | 436 | // first, stat the file, so we know the size. 437 | var size; 438 | var buffer; // single buffer with file data 439 | var buffers; // list for when size is unknown 440 | var pos = 0; 441 | var handle; 442 | 443 | // SFTPv3 does not support using -1 for read position, so we have to track 444 | // read position manually 445 | var bytesRead = 0; 446 | 447 | var flag = options.flag || 'r'; 448 | this.open(path, flag, 438 /*=0666*/, function(er, handle_) { 449 | if (er) 450 | return callback && callback(er); 451 | handle = handle_; 452 | 453 | self.fstat(handle, function(er, st) { 454 | if (er) { 455 | return self.close(handle, function() { 456 | callback && callback(er); 457 | }); 458 | } 459 | 460 | size = st.size; 461 | if (size === 0) { 462 | // the kernel lies about many files. 463 | // Go ahead and try to read some bytes. 464 | buffers = []; 465 | return read(); 466 | } 467 | 468 | buffer = new Buffer(size); 469 | read(); 470 | }); 471 | }); 472 | 473 | function read() { 474 | if (size === 0) { 475 | buffer = new Buffer(8192); 476 | self.read(handle, buffer, 0, 8192, bytesRead, afterRead); 477 | } else 478 | self.read(handle, buffer, pos, size - pos, bytesRead, afterRead); 479 | } 480 | 481 | function afterRead(er, nbytes) { 482 | if (er) { 483 | return self.close(handle, function() { 484 | return callback && callback(er); 485 | }); 486 | } 487 | 488 | if (nbytes === 0) 489 | return close(); 490 | 491 | bytesRead += nbytes; 492 | pos += nbytes; 493 | if (size !== 0) { 494 | if (pos === size) 495 | close(); 496 | else 497 | read(); 498 | } else { 499 | // unknown size, just read until we don't get bytes. 500 | buffers.push(buffer.slice(0, nbytes)); 501 | read(); 502 | } 503 | } 504 | 505 | function close() { 506 | self.close(handle, function(er) { 507 | if (size === 0) { 508 | // collected the data into the buffers list. 509 | buffer = Buffer.concat(buffers, pos); 510 | } else if (pos < size) 511 | buffer = buffer.slice(0, pos); 512 | 513 | if (encoding) 514 | buffer = buffer.toString(encoding); 515 | return callback && callback(er, buffer); 516 | }); 517 | } 518 | }; 519 | 520 | SFTP.prototype.writeFile = function(path, data, options, callback_) { 521 | var callback = (typeof callback_ === 'function' ? callback_ : undefined); 522 | var self = this; 523 | 524 | if (typeof options === 'function' || !options) { 525 | callback = options; 526 | options = { encoding: 'utf8', mode: 438 /*=0666*/, flag: 'w' }; 527 | } else if (typeof options === 'string') 528 | options = { encoding: options, mode: 438, flag: 'w' }; 529 | else if (!options) 530 | options = { encoding: 'utf8', mode: 438 /*=0666*/, flag: 'w' }; 531 | else if (typeof options !== 'object') 532 | throw new TypeError('Bad arguments'); 533 | 534 | if (options.encoding && !Buffer.isEncoding(options.encoding)) 535 | throw new Error('Unknown encoding: ' + options.encoding); 536 | 537 | var flag = options.flag || 'w'; 538 | this.open(path, flag, options.mode, function(openErr, handle) { 539 | if (openErr) 540 | callback && callback(openErr); 541 | else { 542 | var buffer = (Buffer.isBuffer(data) 543 | ? data 544 | : new Buffer('' + data, options.encoding || 'utf8')); 545 | var position = (/a/.test(flag) ? null : 0); 546 | 547 | // SFTPv3 does not support the notion of 'current position' 548 | // (null position), so we just append to the end of the file instead 549 | if (position === null) { 550 | self.fstat(handle, function(er, st) { 551 | if (er) { 552 | return self.close(handle, function() { 553 | callback && callback(er); 554 | }); 555 | } 556 | self._writeAll(handle, buffer, 0, buffer.length, st.size, callback); 557 | }); 558 | return; 559 | } 560 | self._writeAll(handle, buffer, 0, buffer.length, position, callback); 561 | } 562 | }); 563 | }; 564 | 565 | SFTP.prototype.appendFile = function(path, data, options, callback_) { 566 | var callback = (typeof callback_ === 'function' ? callback_ : undefined); 567 | if (typeof options === 'function' || !options) { 568 | callback = options; 569 | options = { encoding: 'utf8', mode: 438 /*=0666*/, flag: 'a' }; 570 | } else if (typeof options === 'string') 571 | options = { encoding: options, mode: 438, flag: 'a' }; 572 | else if (!options) 573 | options = { encoding: 'utf8', mode: 438 /*=0666*/, flag: 'a' }; 574 | else if (typeof options !== 'object') 575 | throw new TypeError('Bad arguments'); 576 | 577 | if (!options.flag) 578 | options = util._extend({ flag: 'a' }, options); 579 | this.writeFile(path, data, options, callback_); 580 | }; 581 | 582 | SFTP.prototype.exists = function(path, cb) { 583 | this.stat(path, function(err) { 584 | cb && cb(err ? false : true); 585 | }); 586 | }; 587 | 588 | SFTP.prototype.unlink = function(filename, cb) { 589 | /* 590 | uint32 id 591 | string filename 592 | */ 593 | var fnamelen = Buffer.byteLength(filename), 594 | p = 9, 595 | buf = new Buffer(4 + 1 + 4 + 4 + fnamelen); 596 | buf[4] = REQUEST.REMOVE; 597 | buf.writeUInt32BE(fnamelen, p, true); 598 | buf.write(filename, p += 4, fnamelen, 'utf8'); 599 | 600 | return this._send(buf, cb); 601 | }; 602 | 603 | SFTP.prototype.rename = function(oldPath, newPath, cb) { 604 | /* 605 | uint32 id 606 | string oldpath 607 | string newpath 608 | */ 609 | var oldlen = Buffer.byteLength(oldPath), 610 | newlen = Buffer.byteLength(newPath), 611 | p = 9, 612 | buf = new Buffer(4 + 1 + 4 + 4 + oldlen + 4 + newlen); 613 | buf[4] = REQUEST.RENAME; 614 | buf.writeUInt32BE(oldlen, p, true); 615 | buf.write(oldPath, p += 4, oldlen, 'utf8'); 616 | buf.writeUInt32BE(newlen, p += oldlen, true); 617 | buf.write(newPath, p += 4, newlen, 'utf8'); 618 | 619 | return this._send(buf, cb); 620 | }; 621 | 622 | SFTP.prototype.mkdir = function(path, attrs, cb) { 623 | var flags = 0, attrBytes = 0; 624 | if (typeof attrs === 'function') { 625 | cb = attrs; 626 | attrs = undefined; 627 | } 628 | if (typeof attrs === 'object') { 629 | attrs = attrsToBytes(attrs); 630 | flags = attrs[0]; 631 | attrBytes = attrs[1]; 632 | attrs = attrs[2]; 633 | } 634 | /* 635 | uint32 id 636 | string path 637 | ATTRS attrs 638 | */ 639 | var pathlen = Buffer.byteLength(path), 640 | p = 9, 641 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen + 4 + attrBytes); 642 | buf[4] = REQUEST.MKDIR; 643 | buf.writeUInt32BE(pathlen, p, true); 644 | buf.write(path, p += 4, pathlen, 'utf8'); 645 | buf.writeUInt32BE(flags, p += pathlen); 646 | if (flags) { 647 | p += 4; 648 | for (var i = 0, len = attrs.length; i < len; ++i) 649 | for (var j = 0, len2 = attrs[i].length; j < len2; ++j) 650 | buf[p++] = attrs[i][j]; 651 | } 652 | 653 | return this._send(buf, cb); 654 | }; 655 | 656 | SFTP.prototype.rmdir = function(path, cb) { 657 | /* 658 | uint32 id 659 | string path 660 | */ 661 | var pathlen = Buffer.byteLength(path), 662 | p = 9, 663 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen); 664 | buf[4] = REQUEST.RMDIR; 665 | buf.writeUInt32BE(pathlen, p, true); 666 | buf.write(path, p += 4, pathlen, 'utf8'); 667 | 668 | return this._send(buf, cb); 669 | }; 670 | 671 | SFTP.prototype.readdir = function(where, cb) { 672 | if (!Buffer.isBuffer(where) && typeof where !== 'string') 673 | throw new Error('missing directory handle or path'); 674 | 675 | if (typeof where === 'string') { 676 | var self = this, 677 | entries = []; 678 | return this.opendir(where, function reread(err, handle) { 679 | if (err) 680 | return cb(err); 681 | self.readdir(handle, function(err, list) { 682 | if (err) { 683 | return self.close(handle, function() { 684 | cb(err); 685 | }); 686 | } 687 | if (list === false) { 688 | return self.close(handle, function(err) { 689 | if (err) 690 | return cb(err); 691 | cb(undefined, entries); 692 | }); 693 | } 694 | entries = entries.concat(list); 695 | reread(undefined, handle); 696 | }); 697 | }); 698 | } 699 | 700 | /* 701 | uint32 id 702 | string handle 703 | */ 704 | var handlelen = where.length, 705 | p = 9, 706 | buf = new Buffer(4 + 1 + 4 + 4 + handlelen); 707 | buf[4] = REQUEST.READDIR; 708 | buf.writeUInt32BE(handlelen, p, true); 709 | where.copy(buf, p += 4); 710 | 711 | return this._send(buf, function(err, list) { 712 | if (err || list === false) 713 | return cb(err, list); 714 | for (var i = list.length - 1; i >= 0; --i) { 715 | if (list[i].filename === '.' || list[i].filename === '..') 716 | list.splice(i, 1); 717 | } 718 | cb(err, list); 719 | }); 720 | }; 721 | 722 | SFTP.prototype.fstat = function(handle, cb) { 723 | if (!Buffer.isBuffer(handle)) 724 | throw new Error('handle is not a Buffer'); 725 | /* 726 | uint32 id 727 | string handle 728 | */ 729 | var handlelen = handle.length, 730 | p = 9, 731 | buf = new Buffer(4 + 1 + 4 + 4 + handlelen); 732 | buf[4] = REQUEST.FSTAT; 733 | buf.writeUInt32BE(handlelen, p, true); 734 | handle.copy(buf, p += 4); 735 | 736 | return this._send(buf, cb); 737 | }; 738 | 739 | SFTP.prototype.stat = function(path, cb) { 740 | /* 741 | uint32 id 742 | string path 743 | */ 744 | var pathlen = Buffer.byteLength(path), 745 | p = 9, 746 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen); 747 | buf[4] = REQUEST.STAT; 748 | buf.writeUInt32BE(pathlen, p, true); 749 | buf.write(path, p += 4, pathlen, 'utf8'); 750 | 751 | return this._send(buf, cb); 752 | }; 753 | 754 | SFTP.prototype.lstat = function(path, cb) { 755 | /* 756 | uint32 id 757 | string path 758 | */ 759 | var pathlen = Buffer.byteLength(path), 760 | p = 9, 761 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen); 762 | buf[4] = REQUEST.LSTAT; 763 | buf.writeUInt32BE(pathlen, p, true); 764 | buf.write(path, p += 4, pathlen, 'utf8'); 765 | 766 | return this._send(buf, cb); 767 | }; 768 | 769 | SFTP.prototype.opendir = function(path, cb) { 770 | /* 771 | uint32 id 772 | string path 773 | */ 774 | var pathlen = Buffer.byteLength(path), 775 | p = 9, 776 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen); 777 | buf[4] = REQUEST.OPENDIR; 778 | buf.writeUInt32BE(pathlen, p, true); 779 | buf.write(path, p += 4, pathlen, 'utf8'); 780 | 781 | return this._send(buf, cb); 782 | }; 783 | 784 | SFTP.prototype.setstat = function(path, attrs, cb) { 785 | var flags = 0, attrBytes = 0; 786 | if (typeof attrs === 'object') { 787 | attrs = attrsToBytes(attrs); 788 | flags = attrs[0]; 789 | attrBytes = attrs[1]; 790 | attrs = attrs[2]; 791 | } else if (typeof attrs === 'function') 792 | cb = attrs; 793 | 794 | /* 795 | uint32 id 796 | string path 797 | ATTRS attrs 798 | */ 799 | var pathlen = Buffer.byteLength(path), 800 | p = 9, 801 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen + 4 + attrBytes); 802 | buf[4] = REQUEST.SETSTAT; 803 | buf.writeUInt32BE(pathlen, p, true); 804 | buf.write(path, p += 4, pathlen, 'utf8'); 805 | buf.writeUInt32BE(flags, p += pathlen); 806 | if (flags) { 807 | p += 4; 808 | for (var i = 0, len = attrs.length; i < len; ++i) 809 | for (var j = 0, len2 = attrs[i].length; j < len2; ++j) 810 | buf[p++] = attrs[i][j]; 811 | } 812 | 813 | return this._send(buf, cb); 814 | }; 815 | 816 | SFTP.prototype.fsetstat = function(handle, attrs, cb) { 817 | var flags = 0, attrBytes = 0; 818 | 819 | if (!Buffer.isBuffer(handle)) 820 | throw new Error('handle is not a Buffer'); 821 | 822 | if (typeof attrs === 'object') { 823 | attrs = attrsToBytes(attrs); 824 | flags = attrs[0]; 825 | attrBytes = attrs[1]; 826 | attrs = attrs[2]; 827 | } else if (typeof attrs === 'function') 828 | cb = attrs; 829 | 830 | /* 831 | uint32 id 832 | string handle 833 | ATTRS attrs 834 | */ 835 | var handlelen = handle.length, 836 | p = 9, 837 | buf = new Buffer(4 + 1 + 4 + 4 + handlelen + 4 + attrBytes); 838 | buf[4] = REQUEST.FSETSTAT; 839 | buf.writeUInt32BE(handlelen, p, true); 840 | handle.copy(buf, p += 4); 841 | buf.writeUInt32BE(flags, p += handlelen); 842 | if (flags) { 843 | p += 4; 844 | for (var i = 0, len = attrs.length; i < len; ++i) 845 | for (var j = 0, len2 = attrs[i].length; j < len2; ++j) 846 | buf[p++] = attrs[i][j]; 847 | } 848 | 849 | return this._send(buf, cb); 850 | }; 851 | 852 | SFTP.prototype.futimes = function(handle, atime, mtime, cb) { 853 | return this.fsetstat(handle, { 854 | atime: toUnixTimestamp(atime), 855 | mtime: toUnixTimestamp(mtime) 856 | }, cb); 857 | }; 858 | 859 | SFTP.prototype.utimes = function(path, atime, mtime, cb) { 860 | return this.setstat(path, { 861 | atime: toUnixTimestamp(atime), 862 | mtime: toUnixTimestamp(mtime) 863 | }, cb); 864 | }; 865 | 866 | SFTP.prototype.fchown = function(handle, uid, gid, cb) { 867 | return this.fsetstat(handle, { 868 | uid: uid, 869 | gid: gid 870 | }, cb); 871 | }; 872 | 873 | SFTP.prototype.chown = function(path, uid, gid, cb) { 874 | return this.setstat(path, { 875 | uid: uid, 876 | gid: gid 877 | }, cb); 878 | }; 879 | 880 | SFTP.prototype.fchmod = function(handle, mode, cb) { 881 | return this.fsetstat(handle, { 882 | mode: mode 883 | }, cb); 884 | }; 885 | 886 | SFTP.prototype.chmod = function(path, mode, cb) { 887 | return this.setstat(path, { 888 | mode: mode 889 | }, cb); 890 | }; 891 | 892 | SFTP.prototype.readlink = function(path, cb) { 893 | /* 894 | uint32 id 895 | string path 896 | */ 897 | var pathlen = Buffer.byteLength(path), 898 | p = 9, 899 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen); 900 | buf[4] = REQUEST.READLINK; 901 | buf.writeUInt32BE(pathlen, p, true); 902 | buf.write(path, p += 4, pathlen, 'utf8'); 903 | 904 | return this._send(buf, function(err, names) { 905 | if (err) 906 | return cb(err); 907 | cb(undefined, names[0].filename); 908 | }); 909 | }; 910 | 911 | SFTP.prototype.symlink = function(targetPath, linkPath, cb) { 912 | /* 913 | uint32 id 914 | string linkpath 915 | string targetpath 916 | */ 917 | var linklen = Buffer.byteLength(linkPath), 918 | targetlen = Buffer.byteLength(targetPath), 919 | p = 9, 920 | buf = new Buffer(4 + 1 + 4 + 4 + linklen + 4 + targetlen); 921 | buf[4] = REQUEST.SYMLINK; 922 | buf.writeUInt32BE(targetlen, p, true); 923 | buf.write(targetPath, p += 4, targetlen, 'utf8'); 924 | buf.writeUInt32BE(linklen, p += targetlen, true); 925 | buf.write(linkPath, p += 4, linklen, 'utf8'); 926 | 927 | return this._send(buf, cb); 928 | }; 929 | 930 | SFTP.prototype.realpath = function(path, cb) { 931 | /* 932 | uint32 id 933 | string path 934 | */ 935 | var pathlen = Buffer.byteLength(path), 936 | p = 9, 937 | buf = new Buffer(4 + 1 + 4 + 4 + pathlen); 938 | buf[4] = REQUEST.REALPATH; 939 | buf.writeUInt32BE(pathlen, p, true); 940 | buf.write(path, p += 4, pathlen, 'utf8'); 941 | 942 | return this._send(buf, function(err, names) { 943 | if (err) 944 | return cb(err); 945 | cb(undefined, names[0].filename); 946 | }); 947 | }; 948 | 949 | // used by writeFile and appendFile 950 | SFTP.prototype._writeAll = function(handle, buffer, offset, length, position, callback_) { 951 | var callback = (typeof callback_ === 'function' ? callback_ : undefined); 952 | var self = this; 953 | 954 | this.write(handle, buffer, offset, length, position, function(writeErr, written) { 955 | if (writeErr) { 956 | return self.close(handle, function() { 957 | callback && callback(writeErr); 958 | }); 959 | } 960 | if (written === length) 961 | self.close(handle, callback); 962 | else { 963 | offset += written; 964 | length -= written; 965 | position += written; 966 | self._writeAll(handle, buffer, offset, length, position, callback); 967 | } 968 | }); 969 | }; 970 | 971 | SFTP.prototype._send = function(data, cb, buffer) { 972 | if (this.ended) { 973 | if (typeof cb == 'function') 974 | cb(new Error('Stream has ended')); 975 | return; 976 | } 977 | var err; 978 | if (this._reqid === MAX_REQID && !this._reqidmaxed) { 979 | this._reqid = 0; 980 | this._reqidmaxed = true; 981 | } 982 | if (this._reqidmaxed) { 983 | var found = false; 984 | for (var i = 0; i < MAX_REQID; ++i) { 985 | if (!this._requests[i]) { 986 | this._reqid = i; 987 | found = true; 988 | break; 989 | } 990 | } 991 | if (!found) { 992 | err = new Error('Exhausted available SFTP request IDs'); 993 | if (typeof cb === 'function') 994 | cb(err); 995 | else 996 | this.emit('error', err); 997 | return; 998 | } 999 | } 1000 | 1001 | if (!this._stream.writable) { 1002 | err = new Error('Underlying stream not writable'); 1003 | if (typeof cb === 'function') 1004 | cb(err); 1005 | else 1006 | this.emit('error', err); 1007 | return; 1008 | } 1009 | 1010 | if (typeof cb !== 'function') 1011 | cb = EMPTY_CALLBACK; 1012 | 1013 | this._requests[this._reqid] = { cb: cb, buffer: buffer }; 1014 | 1015 | /* 1016 | uint32 length 1017 | byte type 1018 | byte[length - 1] data payload 1019 | */ 1020 | data.writeUInt32BE(data.length - 4, 0, true); 1021 | data.writeUInt32BE(this._reqid++, 5, true); 1022 | 1023 | // if this._stream is closed / dead, write() throws synchronously 1024 | try { 1025 | return this._stream.write(data); 1026 | } catch(err) { 1027 | cb(err); 1028 | } 1029 | }; 1030 | 1031 | SFTP.prototype._init = function() { 1032 | /* 1033 | uint32 version 1034 | 1035 | */ 1036 | if (!this._stream.writable) { 1037 | var err = new Error('Underlying stream not writable'); 1038 | return this.emit('error', err); 1039 | } 1040 | 1041 | return this._stream.write(VERSION_BUFFER); 1042 | }; 1043 | 1044 | SFTP.prototype._parse = function(chunk) { 1045 | if (this.ended) return; 1046 | 1047 | var data = this._data, chunklen = chunk.length, cb; 1048 | chunk.i = 0; 1049 | while (chunk.i < chunklen) { 1050 | if (data.type === 'discard') 1051 | --data.len; 1052 | else if (this._field === 'packet_length') { 1053 | if ((data.len = this._readUInt32BE(chunk)) !== false) 1054 | this._field = 'type'; 1055 | } else if (this._field === 'type') { 1056 | --data.len; 1057 | data.type = chunk[chunk.i]; 1058 | if (!data.type) 1059 | throw new Error('Unsupported packet type: ' + chunk[chunk.i]); 1060 | this._field = 'payload'; 1061 | } else if (data.type === RESPONSE.VERSION) { 1062 | /* 1063 | uint32 version 1064 | 1065 | */ 1066 | if (!data.subtype) { 1067 | if ((data.version = this._readUInt32BE(chunk)) !== false) { 1068 | if (data.version !== 3) 1069 | return this.emit('error', new Error('Incompatible SFTP version')); 1070 | //data.subtype = 'extension'; 1071 | data.type = 'discard'; 1072 | this.emit('ready'); 1073 | } 1074 | } else if (data.subtype === 'extension') { 1075 | // TODO 1076 | } 1077 | } else if (data.type === RESPONSE.STATUS) { 1078 | /* 1079 | uint32 id 1080 | uint32 error/status code 1081 | string error message (ISO-10646 UTF-8) 1082 | string language tag 1083 | */ 1084 | if (!data.subtype) { 1085 | if ((data.reqid = this._readUInt32BE(chunk)) !== false) 1086 | data.subtype = 'status code'; 1087 | } else if (data.subtype === 'status code') { 1088 | if ((data.statusCode = this._readUInt32BE(chunk)) !== false) 1089 | data.subtype = 'error message'; 1090 | } else if (data.subtype === 'error message') { 1091 | if ((data.errMsg = this._readString(chunk, 'utf8')) !== false) 1092 | data.subtype = 'language'; 1093 | } else if (data.subtype === 'language') { 1094 | if ((data.lang = this._readString(chunk, 'utf8')) !== false) { 1095 | data.type = 'discard'; 1096 | cb = this._requests[data.reqid].cb; 1097 | delete this._requests[data.reqid]; 1098 | if (data.statusCode === STATUS_CODE.OK) 1099 | cb(); 1100 | else if (data.statusCode === STATUS_CODE.EOF) 1101 | cb(undefined, false); 1102 | else { 1103 | var err = new Error(data.errMsg); 1104 | err.type = STATUS_CODE[data.statusCode]; 1105 | err.lang = data.lang; 1106 | cb(err); 1107 | } 1108 | } 1109 | } 1110 | } else if (data.type === RESPONSE.HANDLE) { 1111 | /* 1112 | uint32 id 1113 | string handle 1114 | */ 1115 | if (!data.subtype) { 1116 | if ((data.reqid = this._readUInt32BE(chunk)) !== false) 1117 | data.subtype = 'handle blob'; 1118 | } else if (data.subtype === 'handle blob') { 1119 | if ((data.handle = this._readString(chunk)) !== false) { 1120 | data.type = 'discard'; 1121 | cb = this._requests[data.reqid].cb; 1122 | delete this._requests[data.reqid]; 1123 | cb(undefined, data.handle); 1124 | } 1125 | } 1126 | } else if (data.type === RESPONSE.DATA) { 1127 | /* 1128 | uint32 id 1129 | string data 1130 | */ 1131 | if (!data.subtype) { 1132 | if ((data.reqid = this._readUInt32BE(chunk)) !== false) 1133 | data.subtype = 'data'; 1134 | } else if (data.subtype === 'data') { 1135 | if ((data.data = this._readString(chunk)) !== false) { 1136 | data.type = 'discard'; 1137 | cb = this._requests[data.reqid].cb; 1138 | var nbytes = this._requests[data.reqid].nbytes; 1139 | delete this._requests[data.reqid]; 1140 | cb(undefined, nbytes, data.data); 1141 | } 1142 | } 1143 | } else if (data.type === RESPONSE.NAME) { 1144 | /* 1145 | uint32 id 1146 | uint32 count 1147 | repeats count times: 1148 | string filename 1149 | string longname 1150 | ATTRS attrs 1151 | */ 1152 | if (!data.subtype) { 1153 | if ((data.reqid = this._readUInt32BE(chunk)) !== false) 1154 | data.subtype = 'count'; 1155 | } else if (data.subtype === 'count') { 1156 | if ((data.count = this._readUInt32BE(chunk)) !== false) { 1157 | data.names = new Array(data.count); 1158 | if (data.count > 0) { 1159 | data.c = 0; 1160 | data.subtype = 'filename'; 1161 | } else { 1162 | data.type = 'discard'; 1163 | cb = this._requests[data.reqid].cb; 1164 | delete this._requests[data.reqid]; 1165 | cb(undefined, data.names); 1166 | } 1167 | } 1168 | } else if (data.subtype === 'filename') { 1169 | if (!data.names[data.c]) { 1170 | data.names[data.c] = { 1171 | filename: undefined, 1172 | longname: undefined, 1173 | attrs: undefined 1174 | }; 1175 | } 1176 | if ((data.names[data.c].filename = this._readString(chunk, 'utf8')) !== false) 1177 | data.subtype = 'longname'; 1178 | } else if (data.subtype === 'longname') { 1179 | if ((data.names[data.c].longname = this._readString(chunk, 'utf8')) !== false) 1180 | data.subtype = 'attrs'; 1181 | } else if (data.subtype === 'attrs') { 1182 | if ((data.names[data.c].attrs = this._readAttrs(chunk)) !== false) { 1183 | if (++data.c < data.count) 1184 | data.subtype = 'filename'; 1185 | else { 1186 | data.type = 'discard'; 1187 | cb = this._requests[data.reqid].cb; 1188 | delete this._requests[data.reqid]; 1189 | cb(undefined, data.names); 1190 | } 1191 | } 1192 | } 1193 | } else if (data.type === RESPONSE.ATTRS) { 1194 | /* 1195 | uint32 id 1196 | ATTRS attrs 1197 | */ 1198 | if (!data.subtype) { 1199 | if ((data.reqid = this._readUInt32BE(chunk)) !== false) 1200 | data.subtype = 'attrs'; 1201 | } else if (data.subtype === 'attrs') { 1202 | if ((data.attrs = this._readAttrs(chunk)) !== false) { 1203 | data.type = 'discard'; 1204 | cb = this._requests[data.reqid].cb; 1205 | delete this._requests[data.reqid]; 1206 | cb(undefined, data.attrs); 1207 | } 1208 | } 1209 | } else if (data.type === RESPONSE.EXTENDED) { 1210 | /* 1211 | uint32 id 1212 | string extended-request 1213 | ... any request-specific data ... 1214 | */ 1215 | // TODO 1216 | --data.len; 1217 | data.type = 'discard'; 1218 | } 1219 | 1220 | if (data.len === 0 && this._field !== 'packet_length') 1221 | this._reset(); 1222 | ++chunk.i; 1223 | } 1224 | }; 1225 | 1226 | SFTP.prototype._readUInt32BE = function(chunk) { 1227 | this._value <<= 8; 1228 | this._value += chunk[chunk.i]; 1229 | --this._data.len; 1230 | if (++this._count === 4) { 1231 | var val = this._value; 1232 | this._count = 0; 1233 | this._value = 0; 1234 | return val; 1235 | } 1236 | return false; 1237 | }; 1238 | 1239 | SFTP.prototype._readUInt64BE = function(chunk) { 1240 | this._value *= 256; 1241 | this._value += chunk[chunk.i]; 1242 | --this._data.len; 1243 | if (++this._count === 8) { 1244 | var val = this._value; 1245 | this._count = 0; 1246 | this._value = 0; 1247 | return val; 1248 | } 1249 | return false; 1250 | }; 1251 | 1252 | SFTP.prototype._readString = function(chunk, encoding) { 1253 | if (this._count < 4 && this._string === undefined) { 1254 | this._value <<= 8; 1255 | this._value += chunk[chunk.i]; 1256 | if (++this._count === 4) { 1257 | this._data.len -= 4; 1258 | this._count = 0; 1259 | if (this._value === 0) { 1260 | if (!encoding) { 1261 | if (Buffer.isBuffer(this._requests[this._data.reqid].buffer)) 1262 | this._requests[this._data.reqid].nbytes = 0; 1263 | return new Buffer(0); 1264 | } else 1265 | return ''; 1266 | } 1267 | if (!encoding) { 1268 | if (Buffer.isBuffer(this._requests[this._data.reqid].buffer)) { 1269 | this._string = this._requests[this._data.reqid].buffer; 1270 | this._requests[this._data.reqid].nbytes = this._value; 1271 | } else 1272 | this._string = new Buffer(this._value); 1273 | } else 1274 | this._string = ''; 1275 | } 1276 | } else if (this._string !== undefined) { 1277 | if (this._value <= chunk.length - chunk.i) { 1278 | // rest of string is in the chunk 1279 | var str; 1280 | if (!encoding) { 1281 | chunk.copy(this._string, this._count, chunk.i, chunk.i + this._value); 1282 | str = this._string; 1283 | } else { 1284 | str = this._string + chunk.toString(encoding || 'ascii', chunk.i, 1285 | chunk.i + this._value); 1286 | } 1287 | chunk.i += this._value - 1; 1288 | this._data.len -= this._value; 1289 | this._string = undefined; 1290 | this._value = 0; 1291 | this._count = 0; 1292 | return str; 1293 | } else { 1294 | // only part or none of string in rest of chunk 1295 | var diff = chunk.length - chunk.i; 1296 | if (diff > 0) { 1297 | if (!encoding) { 1298 | chunk.copy(this._string, this._count, chunk.i); 1299 | this._count += diff; 1300 | } else 1301 | this._string += chunk.toString(encoding || 'ascii', chunk.i); 1302 | chunk.i = chunk.length; 1303 | this._data.len -= diff; 1304 | this._value -= diff; 1305 | } 1306 | } 1307 | } 1308 | 1309 | return false; 1310 | }; 1311 | 1312 | SFTP.prototype._readAttrs = function(chunk) { 1313 | /* 1314 | uint32 flags 1315 | uint64 size present only if flag SSH_FILEXFER_ATTR_SIZE 1316 | uint32 uid present only if flag SSH_FILEXFER_ATTR_UIDGID 1317 | uint32 gid present only if flag SSH_FILEXFER_ATTR_UIDGID 1318 | uint32 permissions present only if flag SSH_FILEXFER_ATTR_PERMISSIONS 1319 | uint32 atime present only if flag SSH_FILEXFER_ACMODTIME 1320 | uint32 mtime present only if flag SSH_FILEXFER_ACMODTIME 1321 | uint32 extended_count present only if flag SSH_FILEXFER_ATTR_EXTENDED 1322 | string extended_type 1323 | string extended_data 1324 | ... more extended data (extended_type - extended_data pairs), 1325 | so that number of pairs equals extended_count 1326 | */ 1327 | var data = this._data; 1328 | if (!data._attrs) 1329 | data._attrs = new Stats(); 1330 | 1331 | if (typeof data._flags !== 'number') 1332 | data._flags = this._readUInt32BE(chunk); 1333 | else if (data._flags & ATTR.SIZE) { 1334 | if ((data._attrs.size = this._readUInt64BE(chunk)) !== false) 1335 | data._flags &= ~ATTR.SIZE; 1336 | } else if (data._flags & ATTR.UIDGID) { 1337 | if (typeof data._attrs.uid !== 'number') 1338 | data._attrs.uid = this._readUInt32BE(chunk); 1339 | else if ((data._attrs.gid = this._readUInt32BE(chunk)) !== false) 1340 | data._flags &= ~ATTR.UIDGID; 1341 | } else if (data._flags & ATTR.PERMISSIONS) { 1342 | if ((data._attrs.mode = this._readUInt32BE(chunk)) !== false) { 1343 | data._flags &= ~ATTR.PERMISSIONS; 1344 | // backwards compatibility 1345 | data._attrs.permissions = data._attrs.mode; 1346 | } 1347 | } else if (data._flags & ATTR.ACMODTIME) { 1348 | if (typeof data._attrs.atime !== 'number') 1349 | data._attrs.atime = this._readUInt32BE(chunk); 1350 | else if ((data._attrs.mtime = this._readUInt32BE(chunk)) !== false) 1351 | data._flags &= ~ATTR.ACMODTIME; 1352 | } else if (data._flags & ATTR.EXTENDED) { 1353 | //data._flags &= ~ATTR.EXTENDED; 1354 | data._flags = 0; 1355 | /*if (typeof data._attrsnExt !== 'number') 1356 | data._attrsnExt = this._readUInt32BE(chunk);*/ 1357 | } 1358 | 1359 | if (data._flags === 0) { 1360 | var ret = data._attrs; 1361 | data._flags = undefined; 1362 | data._attrs = undefined; 1363 | return ret; 1364 | } 1365 | 1366 | return false; 1367 | }; 1368 | 1369 | SFTP.prototype._reset = function() { 1370 | this._count = 0; 1371 | this._value = 0; 1372 | this._string = undefined; 1373 | this._field = 'packet_length'; 1374 | 1375 | this._data.len = 0; 1376 | this._data.type = undefined; 1377 | this._data.subtype = undefined; 1378 | this._data.reqid = undefined; 1379 | this._data.version = undefined; 1380 | this._data.statusCode = undefined; 1381 | this._data.errMsg = undefined; 1382 | this._data.lang = undefined; 1383 | this._data.handle = undefined; 1384 | this._data.data = undefined; 1385 | this._data.count = undefined; 1386 | this._data.names = undefined; 1387 | this._data.c = undefined; 1388 | this._data.attrs = undefined; 1389 | this._data._attrs = undefined; 1390 | this._data._flags = undefined; 1391 | }; 1392 | 1393 | var ATTR = { 1394 | SIZE: 0x00000001, 1395 | UIDGID: 0x00000002, 1396 | PERMISSIONS: 0x00000004, 1397 | ACMODTIME: 0x00000008, 1398 | EXTENDED: 0x80000000 1399 | }; 1400 | 1401 | var STATUS_CODE = { 1402 | OK: 0, 1403 | EOF: 1, 1404 | NO_SUCH_FILE: 2, 1405 | PERMISSION_DENIED: 3, 1406 | FAILURE: 4, 1407 | BAD_MESSAGE: 5, 1408 | NO_CONNECTION: 6, 1409 | CONNECTION_LOST: 7, 1410 | OP_UNSUPPORTED: 8 1411 | }; 1412 | for (var i=0,keys=Object.keys(STATUS_CODE),len=keys.length; i= 0; --i) { 1468 | sizeBytes[i] = val & 0xFF; 1469 | val /= 256; 1470 | } 1471 | ret.push(sizeBytes); 1472 | } 1473 | if (typeof attrs.uid === 'number' && typeof attrs.gid === 'number') { 1474 | flags |= ATTR.UIDGID; 1475 | attrBytes += 8; 1476 | ret.push([(attrs.uid >> 24) & 0xFF, (attrs.uid >> 16) & 0xFF, 1477 | (attrs.uid >> 8) & 0xFF, attrs.uid & 0xFF]); 1478 | ret.push([(attrs.gid >> 24) & 0xFF, (attrs.gid >> 16) & 0xFF, 1479 | (attrs.gid >> 8) & 0xFF, attrs.gid & 0xFF]); 1480 | } 1481 | if (typeof attrs.permissions === 'number' 1482 | || typeof attrs.permissions === 'string' 1483 | || typeof attrs.mode === 'number' 1484 | || typeof attrs.mode === 'string') { 1485 | var mode = modeNum(attrs.mode || attrs.permissions); 1486 | flags |= ATTR.PERMISSIONS; 1487 | attrBytes += 4; 1488 | ret.push([(mode >> 24) & 0xFF, 1489 | (mode >> 16) & 0xFF, 1490 | (mode >> 8) & 0xFF, 1491 | mode & 0xFF]); 1492 | } 1493 | if ((typeof attrs.atime === 'number' || isDate(attrs.atime)) 1494 | && (typeof attrs.mtime === 'number' || isDate(attrs.mtime))) { 1495 | var atime = toUnixTimestamp(attrs.atime), 1496 | mtime = toUnixTimestamp(attrs.mtime); 1497 | flags |= ATTR.ACMODTIME; 1498 | attrBytes += 8; 1499 | ret.push([(atime >> 24) & 0xFF, (atime >> 16) & 0xFF, 1500 | (atime >> 8) & 0xFF, atime & 0xFF]); 1501 | ret.push([(mtime >> 24) & 0xFF, (mtime >> 16) & 0xFF, 1502 | (mtime >> 8) & 0xFF, mtime & 0xFF]); 1503 | } 1504 | // TODO: extended attributes 1505 | 1506 | return [flags, attrBytes, ret]; 1507 | } 1508 | 1509 | function toUnixTimestamp(time) { 1510 | if (typeof time === 'number' && !isNaN(time)) 1511 | return time; 1512 | else if (isDate(time)) 1513 | return parseInt(time.getTime() / 1000, 10); 1514 | throw new Error('Cannot parse time: ' + time); 1515 | } 1516 | 1517 | function modeNum(mode) { 1518 | if (typeof mode === 'number' && !isNaN(mode)) 1519 | return mode; 1520 | else if (typeof mode === 'string') 1521 | return modeNum(parseInt(mode, 8)); 1522 | throw new Error('Cannot parse mode: ' + mode); 1523 | } 1524 | 1525 | 1526 | // ReadStream-related 1527 | var kMinPoolSpace = 128, 1528 | pool; 1529 | function allocNewPool(poolSize) { 1530 | pool = new Buffer(poolSize); 1531 | pool.used = 0; 1532 | } 1533 | 1534 | function ReadStream(sftp, path, options) { 1535 | if (!(this instanceof ReadStream)) 1536 | return new ReadStream(sftp, path, options); 1537 | 1538 | var self = this, 1539 | socket = sftp._stream._channel._conn._sock; 1540 | 1541 | // a little bit bigger buffer and water marks by default 1542 | options = util._extend({ 1543 | highWaterMark: 64 * 1024 1544 | }, options || {}); 1545 | 1546 | ReadableStream.call(this, options); 1547 | 1548 | this.path = path; 1549 | this.handle = options.hasOwnProperty('handle') ? options.handle : null; 1550 | this.flags = options.hasOwnProperty('flags') ? options.flags : 'r'; 1551 | this.mode = options.hasOwnProperty('mode') ? options.mode : 438; /*=0666*/ 1552 | 1553 | this.start = options.hasOwnProperty('start') ? options.start : undefined; 1554 | this.end = options.hasOwnProperty('end') ? options.end : undefined; 1555 | this.autoClose = (options.hasOwnProperty('autoClose') 1556 | ? options.autoClose 1557 | : true); 1558 | this.pos = 0; 1559 | this.sftp = sftp; 1560 | 1561 | if (this.start !== undefined) { 1562 | if ('number' !== typeof this.start) 1563 | throw new TypeError('start must be a Number'); 1564 | if (this.end === undefined) 1565 | this.end = Infinity; 1566 | else if ('number' !== typeof this.end) 1567 | throw new TypeError('end must be a Number'); 1568 | 1569 | if (this.start > this.end) 1570 | throw new Error('start must be <= end'); 1571 | else if (this.start < 0) 1572 | throw new Error('start must be >= zero'); 1573 | 1574 | this.pos = this.start; 1575 | } 1576 | 1577 | this.on('end', function() { 1578 | socket.removeListener('close', onclose); 1579 | if (self.autoClose) { 1580 | self.destroy(); 1581 | } 1582 | }); 1583 | 1584 | function onclose() { 1585 | self.destroy(); 1586 | } 1587 | socket.once('close', onclose); 1588 | 1589 | if (!Buffer.isBuffer(this.handle)) 1590 | this.open(); 1591 | } 1592 | inherits(ReadStream, ReadableStream); 1593 | 1594 | ReadStream.prototype.open = function() { 1595 | var self = this; 1596 | this.sftp.open(this.path, this.flags, this.mode, function(er, handle) { 1597 | if (er) { 1598 | if (self.autoClose) 1599 | self.destroy(); 1600 | return self.emit('error', er); 1601 | } 1602 | 1603 | self.handle = handle; 1604 | self.emit('open', handle); 1605 | // start the flow of data. 1606 | self.read(); 1607 | }); 1608 | }; 1609 | 1610 | ReadStream.prototype._read = function(n) { 1611 | if (!Buffer.isBuffer(this.handle)) { 1612 | return this.once('open', function() { 1613 | this._read(n); 1614 | }); 1615 | } 1616 | 1617 | if (this.destroyed) 1618 | return; 1619 | 1620 | if (!pool || pool.length - pool.used < kMinPoolSpace) { 1621 | // discard the old pool. 1622 | pool = null; 1623 | allocNewPool(this._readableState.highWaterMark); 1624 | } 1625 | 1626 | // Grab another reference to the pool in the case that while we're 1627 | // in the thread pool another read() finishes up the pool, and 1628 | // allocates a new one. 1629 | var thisPool = pool; 1630 | var toRead = Math.min(pool.length - pool.used, n); 1631 | var start = pool.used; 1632 | 1633 | if (this.end !== undefined) 1634 | toRead = Math.min(this.end - this.pos + 1, toRead); 1635 | 1636 | // already read everything we were supposed to read! 1637 | // treat as EOF. 1638 | if (toRead <= 0) 1639 | return this.push(null); 1640 | 1641 | // the actual read. 1642 | var self = this; 1643 | this.sftp.read(this.handle, pool, pool.used, toRead, this.pos, onread); 1644 | 1645 | // move the pool positions, and internal position for reading. 1646 | this.pos += toRead; 1647 | pool.used += toRead; 1648 | 1649 | function onread(er, bytesRead) { 1650 | if (er) { 1651 | if (self.autoClose) 1652 | self.destroy(); 1653 | return self.emit('error', er); 1654 | } 1655 | var b = null; 1656 | if (bytesRead > 0) 1657 | b = thisPool.slice(start, start + bytesRead); 1658 | 1659 | self.push(b); 1660 | } 1661 | }; 1662 | 1663 | ReadStream.prototype.destroy = function() { 1664 | if (this.destroyed) 1665 | return; 1666 | this.destroyed = true; 1667 | 1668 | if (Buffer.isBuffer(this.handle)) 1669 | this.close(); 1670 | }; 1671 | 1672 | 1673 | ReadStream.prototype.close = function(cb) { 1674 | var self = this; 1675 | if (cb) 1676 | this.once('close', cb); 1677 | if (this.closed || !Buffer.isBuffer(this.handle)) { 1678 | if (!Buffer.isBuffer(this.handle)) { 1679 | this.once('open', close); 1680 | return; 1681 | } 1682 | return process.nextTick(this.emit.bind(this, 'close')); 1683 | } 1684 | this.closed = true; 1685 | close(); 1686 | 1687 | function close(handle) { 1688 | self.sftp.close(handle || self.handle, function(er) { 1689 | if (er) 1690 | self.emit('error', er); 1691 | else 1692 | self.emit('close'); 1693 | }); 1694 | self.handle = null; 1695 | } 1696 | }; 1697 | 1698 | 1699 | function WriteStream(sftp, path, options) { 1700 | if (!(this instanceof WriteStream)) 1701 | return new WriteStream(sftp, path, options); 1702 | 1703 | options = options || {}; 1704 | 1705 | WritableStream.call(this, options); 1706 | 1707 | this.path = path; 1708 | this.handle = options.hasOwnProperty('handle') ? options.handle : null; 1709 | this.flags = options.hasOwnProperty('flags') ? options.flags : 'w'; 1710 | this.mode = options.hasOwnProperty('mode') ? options.mode : 438; /*=0666*/ 1711 | 1712 | this.start = options.hasOwnProperty('start') ? options.start : undefined; 1713 | this.pos = 0; 1714 | this.bytesWritten = 0; 1715 | this.sftp = sftp; 1716 | 1717 | if (this.start !== undefined) { 1718 | if ('number' !== typeof this.start) 1719 | throw new TypeError('start must be a Number'); 1720 | if (this.start < 0) 1721 | throw new Error('start must be >= zero'); 1722 | else if (this.start < 0) 1723 | throw new Error('start must be >= zero'); 1724 | 1725 | this.pos = this.start; 1726 | } 1727 | 1728 | if (!Buffer.isBuffer(this.handle)) 1729 | this.open(); 1730 | 1731 | var self = this, 1732 | socket = sftp._stream._channel._conn._sock; 1733 | 1734 | // dispose on finish. 1735 | this.once('finish', onclose); 1736 | 1737 | function onclose() { 1738 | socket.removeListener('close', onclose); 1739 | self.close(); 1740 | } 1741 | socket.once('close', onclose); 1742 | } 1743 | inherits(WriteStream, WritableStream); 1744 | 1745 | WriteStream.prototype.open = function() { 1746 | var self = this; 1747 | this.sftp.open(this.path, this.flags, this.mode, function(er, handle) { 1748 | if (er) { 1749 | self.destroy(); 1750 | self.emit('error', er); 1751 | return; 1752 | } 1753 | 1754 | self.handle = handle; 1755 | // SFTPv3 requires absolute offsets, no matter the open flag used 1756 | if (self.flags[0] === 'a') { 1757 | self.sftp.fstat(handle, function(err, st) { 1758 | if (err) { 1759 | self.destroy(); 1760 | self.emit('error', err); 1761 | return; 1762 | } 1763 | 1764 | self.pos = st.size; 1765 | self.emit('open', handle); 1766 | }); 1767 | return; 1768 | } 1769 | self.emit('open', handle); 1770 | }); 1771 | }; 1772 | 1773 | WriteStream.prototype._write = function(data, encoding, cb) { 1774 | if (!Buffer.isBuffer(data)) 1775 | return this.emit('error', new Error('Invalid data')); 1776 | 1777 | if (!Buffer.isBuffer(this.handle)) { 1778 | return this.once('open', function() { 1779 | this._write(data, encoding, cb); 1780 | }); 1781 | } 1782 | 1783 | var self = this; 1784 | this.sftp.write(this.handle, data, 0, data.length, this.pos, function(er, bytes) { 1785 | if (er) { 1786 | self.destroy(); 1787 | return cb(er); 1788 | } 1789 | self.bytesWritten += bytes; 1790 | cb(); 1791 | }); 1792 | 1793 | this.pos += data.length; 1794 | }; 1795 | 1796 | WriteStream.prototype.destroy = ReadStream.prototype.destroy; 1797 | WriteStream.prototype.close = ReadStream.prototype.close; 1798 | 1799 | // There is no shutdown() for files. 1800 | WriteStream.prototype.destroySoon = WriteStream.prototype.end; 1801 | --------------------------------------------------------------------------------