Browse Source

Merge remote-tracking branch 'ry/v0.6' into v0.6-merge

Conflicts:
	AUTHORS
	ChangeLog
	Makefile
	doc/about/index.html
	doc/api/tls.markdown
	doc/community/index.html
	doc/index.html
	doc/logos/index.html
	doc/template.html
	lib/http.js
	lib/tls.js
	src/node_version.h
	src/platform_win32.cc
	test/simple/test-tls-connect-given-socket.js
v0.9.1-release
isaacs 13 years ago
parent
commit
31721da4b1
  1. 3
      AUTHORS
  2. 42
      ChangeLog
  3. 4
      deps/uv/src/unix/tty.c
  4. 3
      deps/uv/src/win/error.c
  5. 2
      doc/about/index.html
  6. 97
      doc/api/tls.markdown
  7. 6
      doc/community/index.html
  8. 6
      doc/index.html
  9. 23
      doc/logos/index.html
  10. 2
      lib/dgram.js
  11. 2
      lib/http.js
  12. 8
      lib/path.js
  13. 7
      lib/repl.js
  14. 58
      lib/tls.js
  15. 1
      node.gyp
  16. 227
      src/node.cc
  17. 16
      src/node_crypto.cc
  18. 2
      src/node_crypto.h
  19. 41
      src/node_main.cc
  20. 5
      src/pipe_wrap.cc
  21. 5
      src/tcp_wrap.cc
  22. 100
      test/pummel/test-tls-ci-reneg-attack.js
  23. 77
      test/simple/test-cluster-bind-twice.js
  24. 35
      test/simple/test-dgram-close.js
  25. 9
      test/simple/test-http-server-multiheaders.js
  26. 9
      test/simple/test-repl-tab-complete.js
  27. 156
      test/simple/test-tls-over-http-tunnel.js
  28. 1
      vcbuild.bat

3
AUTHORS

@ -263,3 +263,6 @@ Dan VerWeire <dverweire@gmail.com>
Matthew Fitzsimmons <matt@joyent.com>
Philip Tellis <philip.tellis@gmail.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
Paddy Byers <paddy.byers@gmail.com>
Seth Fitzsimmons <seth@mojodna.net>
Einar Otto Stangvik <einaros@gmail.com>

42
ChangeLog

@ -97,7 +97,47 @@
* Bug fixes
2012.02.02, Version 0.6.10 (stable)
2012.02.17 Version 0.6.11 (stable), 1eb1fe32250fc88cb5b0a97cddf3e02be02e3f4a
* http: allow multiple WebSocket RFC6455 headers (Einar Otto Stangvik)
* http: allow multiple WWW-Authenticate headers (Ben Noordhuis)
* windows: support unicode argv and environment variables (Bert Belder)
* tls: mitigate session renegotiation attacks (Ben Noordhuis)
* tcp, pipe: don't assert on uv_accept() errors (Ben Noordhuis)
* tls: Allow establishing secure connection on the existing socket (koichik)
* dgram: handle close of dgram socket before DNS lookup completes (Seth Fitzsimmons)
* windows: Support half-duplex pipes (Igor Zinkovsky)
* build: disable omit-frame-pointer on solaris systems (Dave Pacheco)
* debugger: fix --debug-brk (Ben Noordhuis)
* net: fix large file downloads failing (koichik)
* fs: fix ReadStream failure to read from existing fd (Christopher Jeffrey)
* net: destroy socket on DNS error (Stefan Rusu)
* dtrace: add missing translator (Dave Pacheco)
* unix: don't flush tty on switch to raw mode (Ben Noordhuis)
* windows: reset brightness when reverting to default text color (Bert Belder)
* npm: update to 1.1.1
- Update which, fstream, mkdirp, request, and rimraf
- Fix #2123 Set path properly for lifecycle scripts on windows
- Mark the root as seen, so we don't recurse into it. Fixes #1838. (Martin Cooper)
2012.02.02, Version 0.6.10 (stable), 051908e023f87894fa68f5b64d0b99a19a7db01e
* Update V8 to 3.6.6.20

4
deps/uv/src/unix/tty.c

@ -76,8 +76,8 @@ int uv_tty_set_mode(uv_tty_t* tty, int mode) {
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
/* Put terminal in raw mode after flushing */
if (tcsetattr(fd, TCSAFLUSH, &raw)) {
/* Put terminal in raw mode after draining */
if (tcsetattr(fd, TCSADRAIN, &raw)) {
goto fatal;
}

3
deps/uv/src/win/error.c

@ -108,6 +108,9 @@ uv_err_code uv_translate_sys_error(int sys_errno) {
case ERROR_INVALID_PARAMETER: return UV_EINVAL;
case ERROR_NO_UNICODE_TRANSLATION: return UV_ECHARSET;
case ERROR_BROKEN_PIPE: return UV_EOF;
case ERROR_BAD_PIPE: return UV_EPIPE;
case ERROR_NO_DATA: return UV_EPIPE;
case ERROR_PIPE_NOT_CONNECTED: return UV_EPIPE;
case ERROR_PIPE_BUSY: return UV_EBUSY;
case ERROR_SEM_TIMEOUT: return UV_ETIMEDOUT;
case WSAETIMEDOUT: return UV_ETIMEDOUT;

2
doc/about/index.html

@ -1,5 +1,5 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>

97
doc/api/tls.markdown

@ -26,8 +26,40 @@ Alternatively you can send the CSR to a Certificate Authority for signing.
(TODO: docs on creating a CA, for now interested users should just look at
`test/fixtures/keys/Makefile` in the Node source code)
### Client-initiated renegotiation attack mitigation
#### tls.createServer(options, [secureConnectionListener])
The TLS protocol lets the client renegotiate certain aspects of the TLS session.
Unfortunately, session renegotiation requires a disproportional amount of
server-side resources, which makes it a potential vector for denial-of-service
attacks.
To mitigate this, renegotiations are limited to three times every 10 minutes. An
error is emitted on the [CleartextStream](#tls.CleartextStream) instance when
the threshold is exceeded. The limits are configurable:
- `tls.CLIENT_RENEG_LIMIT`: renegotiation limit, default is 3.
- `tls.CLIENT_RENEG_WINDOW`: renegotiation window in seconds, default is
10 minutes.
Don't change the defaults unless you know what you are doing.
To test your server, connect to it with `openssl s_client -connect address:port`
and tap `R<CR>` (that's the letter `R` followed by a carriage return) a few
times.
### NPN and SNI
NPN (Next Protocol Negotiation) and SNI (Server Name Indication) are TLS
handshake extensions allowing you:
* NPN - to use one TLS server for multiple protocols (HTTP, SPDY)
* SNI - to use one TLS server for multiple hostnames with different SSL
certificates.
## tls.createServer(options, [secureConnectionListener])
Creates a new [tls.Server](#tls.Server).
The `connectionListener` argument is automatically set as a listener for the
@ -144,6 +176,11 @@ Creates a new client connection to the given `port` and `host` (old API) or
- `servername`: Servername for SNI (Server Name Indication) TLS extension.
- `socket`: Establish secure connection on a given socket rather than
creating a new socket. If this option is specified, `host` and `port`
are ignored. This is intended FOR INTERNAL USE ONLY. As with all
undocumented APIs in Node, they should not be used.
The `secureConnectListener` parameter will be added as a listener for the
['secureConnect'](#event_secureConnect_) event.
@ -178,27 +215,7 @@ Here is an example of a client of echo server as described previously:
});
### STARTTLS
In the v0.4 branch no function exists for starting a TLS session on an
already existing TCP connection. This is possible it just requires a bit of
work. The technique is to use `tls.createSecurePair()` which returns two
streams: an encrypted stream and a cleartext stream. The encrypted stream is
then piped to the socket, the cleartext stream is what the user interacts with
thereafter.
[Here is some code that does it.](http://gist.github.com/848444)
### NPN and SNI
NPN (Next Protocol Negotiation) and SNI (Server Name Indication) are TLS
handshake extensions allowing you:
* NPN - to use one TLS server for multiple protocols (HTTP, SPDY)
* SNI - to use one TLS server for multiple hostnames with different SSL
certificates.
### pair = tls.createSecurePair([credentials], [isServer], [requestCert], [rejectUnauthorized])
## tls.createSecurePair([credentials], [isServer], [requestCert], [rejectUnauthorized])
Creates a new secure pair object with two streams, one of which reads/writes
encrypted data, and one reads/writes cleartext data.
@ -220,7 +237,7 @@ and the cleartext one is used as a replacement for the initial encrypted stream.
`tls.createSecurePair()` returns a SecurePair object with
[cleartext](#tls.CleartextStream) and `encrypted` stream properties.
#### Event: 'secure'
### Event: 'secure'
The event is emitted from the SecurePair once the pair has successfully
established a secure connection.
@ -229,13 +246,13 @@ Similarly to the checking for the server 'secureConnection' event,
pair.cleartext.authorized should be checked to confirm whether the certificate
used properly authorized.
### tls.Server
## tls.Server
This class is a subclass of `net.Server` and has the same methods on it.
Instead of accepting just raw TCP connections, this accepts encrypted
connections using TLS or SSL.
#### Event: 'secureConnection'
### Event: 'secureConnection'
`function (cleartextStream) {}`
@ -255,7 +272,7 @@ server, you unauthorized connections may be accepted.
SNI.
#### Event: 'clientError'
### Event: 'clientError'
`function (exception) { }`
@ -263,7 +280,7 @@ When a client connection emits an 'error' event before secure connection is
established - it will be forwarded here.
#### server.listen(port, [host], [callback])
### server.listen(port, [host], [callback])
Begin accepting connections on the specified `port` and `host`. If the
`host` is omitted, the server will accept connections directed to any
@ -275,35 +292,35 @@ when the server has been bound.
See `net.Server` for more information.
#### server.close()
### server.close()
Stops the server from accepting new connections. This function is
asynchronous, the server is finally closed when the server emits a `'close'`
event.
#### server.address()
### server.address()
Returns the bound address and port of the server as reported by the operating
system.
See [net.Server.address()](net.html#server.address) for more information.
#### server.addContext(hostname, credentials)
### server.addContext(hostname, credentials)
Add secure context that will be used if client request's SNI hostname is
matching passed `hostname` (wildcards can be used). `credentials` can contain
`key`, `cert` and `ca`.
#### server.maxConnections
### server.maxConnections
Set this property to reject connections when the server's connection count
gets high.
#### server.connections
### server.connections
The number of concurrent connections on the server.
### tls.CleartextStream
## tls.CleartextStream
This is a stream on top of the *Encrypted* stream that makes it possible to
read/write an encrypted data as a cleartext data.
@ -311,7 +328,7 @@ read/write an encrypted data as a cleartext data.
This instance implements a duplex [Stream](streams.html#streams) interfaces.
It has all the common stream methods and events.
#### Event: 'secureConnect'
### Event: 'secureConnect'
`function () {}`
@ -323,17 +340,17 @@ If `cleartextStream.authorized === false` then the error can be found in
`cleartextStream.authorizationError`. Also if NPN was used - you can check
`cleartextStream.npnProtocol` for negotiated protocol.
#### cleartextStream.authorized
### cleartextStream.authorized
A boolean that is `true` if the peer certificate was signed by one of the
specified CAs, otherwise `false`
#### cleartextStream.authorizationError
### cleartextStream.authorizationError
The reason why the peer's certificate has not been verified. This property
becomes available only when `cleartextStream.authorized === false`.
#### cleartextStream.getPeerCertificate()
### cleartextStream.getPeerCertificate()
Returns an object representing the peer's certificate. The returned object has
some properties corresponding to the field of the certificate.
@ -361,17 +378,17 @@ Example:
If the peer does not provide a certificate, it returns `null` or an empty
object.
#### cleartextStream.address()
### cleartextStream.address()
Returns the bound address and port of the underlying socket as reported by the
operating system. Returns an object with two properties, e.g.
`{"address":"192.168.57.1", "port":62053}`
#### cleartextStream.remoteAddress
### cleartextStream.remoteAddress
The string representation of the remote IP address. For example,
`'74.125.127.100'` or `'2001:4860:a005::68'`.
#### cleartextStream.remotePort
### cleartextStream.remotePort
The numeric representation of the remote port. For example, `443`.

6
doc/community/index.html

@ -1,5 +1,5 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<style>
@ -12,8 +12,8 @@
<script src="../sh_javascript.min.js"></script>
<link type="image/x-icon" rel="icon" href="../favicon.ico">
<link type="image/x-icon" rel="shortcut icon" href="../favicon.ico">
<link type="text/css" rel="stylesheet" href="../pipe.css">
<link type="text/css" rel="stylesheet" href="../sh_vim-dark.css">
<link rel="stylesheet" href="../pipe.css">
<link rel="stylesheet" href="../sh_vim-dark.css">
<link rel="alternate"
type="application/rss+xml"
title="node blog"

6
doc/index.html

@ -157,7 +157,7 @@ var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, "127.0.0.1");
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');</pre>
<p>To run the server, put the code into a file <code>example.js</code> and execute it with the <code>node</code> program:</p>
@ -171,11 +171,11 @@ Server running at http://127.0.0.1:1337/</pre>
var net = require('net');
var server = net.createServer(function (socket) {
socket.write("Echo server\r\n");
socket.write('Echo server\r\n');
socket.pipe(socket);
});
server.listen(1337, "127.0.0.1");</pre>
server.listen(1337, '127.0.0.1');</pre>
<!-- <p>Ready to dig in? <a href="">Download the latest version</a> of node.js or learn how other organizations are <a href="">using the technology</a>.</p> -->
</div>

23
doc/logos/index.html

@ -1,24 +1,21 @@
<!doctype html>
<html>
<html lang="en">
<head>
<style type="text/css">
<meta charset="utf-8">
<style>
ul {
padding: 0;
margin: 0;
}
</style>
<script type='text/javascript' src='http://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js?ver=3.1.3'></script>
<script type="text/javascript" src="../sh_main.js"></script>
<script type="text/javascript" src="../sh_javascript.min.js"></script>
<link type="image/x-icon" rel="icon" href="../favicon.ico">
<link type="image/x-icon" rel="shortcut icon" href="../favicon.ico">
<link type="text/css" rel="stylesheet" href="../pipe.css">
<link type="text/css" rel="stylesheet" href="../sh_vim-dark.css">
<link rel="stylesheet" href="../pipe.css">
<link rel="stylesheet" href="../sh_vim-dark.css">
<link rel="alternate"
type="application/rss+xml"
title="node blog"
href="http://feeds.feedburner.com/nodejs/123123123">
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<title>node.js</title>
</head>
<body class="int" id="logos">
@ -88,10 +85,7 @@
<p>Copyright <a href="http://joyent.com">Joyent, Inc</a>., Node.js is a <a href="/trademark-policy.pdf">trademark of Joyent, Inc</a>., <a href="https://raw.github.com/joyent/node/v0.7.4/LICENSE">View License</a></p>
</div>
<script type="text/javascript">
<script>
var gaJsHost = (("https:" == document.location.protocol) ?
"https://ssl." : "http://www.");
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
@ -102,6 +96,5 @@
pageTracker._trackPageview();
} catch(err) {}
</script>
</body></html>
</body>
</html>

2
lib/dgram.js

@ -175,7 +175,7 @@ Socket.prototype.send = function(buffer,
if (callback) callback(err);
self.emit('error', err);
}
else {
else if (self._handle) {
var req = self._handle.send(buffer, offset, length, port, ip);
if (req) {
req.oncomplete = afterSend;

2
lib/http.js

@ -359,6 +359,8 @@ IncomingMessage.prototype._addHeaderLine = function(field, value) {
case 'pragma':
case 'link':
case 'www-authenticate':
case 'sec-websocket-extensions':
case 'sec-websocket-protocol':
if (field in dest) {
dest[field] += ', ' + value;
} else {

8
lib/path.js

@ -97,7 +97,13 @@ if (isWindows) {
// directories. If we've resolved a drive letter but not yet an
// absolute path, get cwd for that drive. We're sure the device is not
// an unc path at this points, because unc paths are always absolute.
path = process._cwdForDrive(resolvedDevice[0]);
path = process.env['=' + resolvedDevice];
// Verify that a drive-local cwd was found and that it actually points
// to our drive. If not, default to the drive's root.
if (!path || path.slice(0, 3).toLowerCase() !==
resolvedDevice.toLowerCase() + '\\') {
path = resolvedDevice + '\\';
}
}
// Skip empty and invalid entries

7
lib/repl.js

@ -525,8 +525,13 @@ REPLServer.prototype.complete = function(line, callback) {
}
// works for non-objects
try {
var p = Object.getPrototypeOf(obj);
var sentinel = 5;
var p;
if (typeof obj == 'object') {
p = Object.getPrototypeOf(obj);
} else {
p = obj.constructor ? obj.constructor.prototype : null;
}
while (p !== null) {
memberGroups.push(Object.getOwnPropertyNames(p));
p = Object.getPrototypeOf(p);

58
lib/tls.js

@ -27,6 +27,14 @@ var stream = require('stream');
var END_OF_FILE = 42;
var assert = require('assert').ok;
// Allow {CLIENT_RENEG_LIMIT} client-initiated session renegotiations
// every {CLIENT_RENEG_WINDOW} seconds. An error event is emitted if more
// renegotations are seen. The settings are applied to all remote client
// connections.
exports.CLIENT_RENEG_LIMIT = 3;
exports.CLIENT_RENEG_WINDOW = 600;
var debug;
if (process.env.NODE_DEBUG && /tls/.test(process.env.NODE_DEBUG)) {
debug = function(a) { console.error('TLS:', a); };
@ -539,6 +547,37 @@ EncryptedStream.prototype._pusher = function(pool, offset, length) {
};
function onhandshakestart() {
debug('onhandshakestart');
var self = this, ssl = this.ssl;
ssl.handshakes++;
if (ssl.handshakes === 1) {
function timeout() {
ssl.handshakes = 0;
ssl.timer = null;
}
ssl.timer = setTimeout(timeout, exports.CLIENT_RENEG_WINDOW * 1000);
}
else if (ssl.handshakes >= exports.CLIENT_RENEG_LIMIT) {
// Defer the error event to the next tick. We're being called from OpenSSL's
// state machine and OpenSSL is not re-entrant. We cannot allow the user's
// callback to destroy the connection right now, it would crash and burn.
process.nextTick(function() {
var err = new Error('TLS session renegotiation attack detected.');
if (self.cleartext) self.cleartext.emit('error', err);
});
}
}
function onhandshakedone() {
// for future use
debug('onhandshakedone');
}
/**
* Provides a pair of streams to do encrypted communication.
*/
@ -585,6 +624,13 @@ function SecurePair(credentials, isServer, requestCert, rejectUnauthorized,
this._isServer ? this._requestCert : options.servername,
this._rejectUnauthorized);
if (this._isServer) {
this.ssl.onhandshakestart = onhandshakestart.bind(this);
this.ssl.onhandshakedone = onhandshakedone.bind(this);
this.ssl.handshakes = 0;
this.ssl.timer = null;
}
if (process.features.tls_sni) {
if (this._isServer && options.SNICallback) {
this.ssl.setSNICallback(options.SNICallback);
@ -720,13 +766,16 @@ SecurePair.prototype.maybeInitFinished = function() {
SecurePair.prototype.destroy = function() {
if (this._doneFlag) {
return;
}
var self = this;
if (!this._doneFlag) {
this._doneFlag = true;
if (this.ssl.timer) {
clearTimeout(this.ssl.timer);
this.ssl.timer = null;
}
this.ssl.error = null;
this.ssl.close();
this.ssl = null;
@ -739,6 +788,7 @@ SecurePair.prototype.destroy = function() {
self.encrypted.emit('close');
self.cleartext.emit('close');
});
}
};

1
node.gyp

@ -155,6 +155,7 @@
'FD_SETSIZE=1024',
# we need to use node's preferred "win32" rather than gyp's preferred "win"
'PLATFORM="win32"',
'_UNICODE=1',
],
'libraries': [ '-lpsapi.lib' ]
},{ # POSIX

227
src/node.cc

@ -1290,76 +1290,6 @@ static Handle<Value> Cwd(const Arguments& args) {
}
#ifdef _WIN32
static Handle<Value> CwdForDrive(const Arguments& args) {
HandleScope scope;
if (args.Length() < 1) {
Local<Value> exception = Exception::Error(
String::New("process._cwdForDrive takes exactly 1 argument."));
return ThrowException(exception);
}
Local<String> driveLetter = args[0]->ToString();
if (driveLetter->Length() != 1) {
Local<Value> exception = Exception::Error(
String::New("Drive name should be 1 character."));
return ThrowException(exception);
}
char drive;
driveLetter->WriteAscii(&drive, 0, 1, 0);
if (drive >= 'a' && drive <= 'z') {
// Convert to uppercase
drive += 'A' - 'a';
} else if (drive < 'A' || drive > 'Z') {
// Not a letter
Local<Value> exception = Exception::Error(
String::New("Drive name should be a letter."));
return ThrowException(exception);
}
WCHAR env_key[] = L"=X:";
env_key[1] = (WCHAR) drive;
DWORD len = GetEnvironmentVariableW(env_key, NULL, 0);
if (len == 0 && GetLastError() == ERROR_ENVVAR_NOT_FOUND) {
// There is no current directory for that drive. Default to drive + ":\".
Local<String> cwd = String::Concat(String::New(&drive, 1),
String::New(":\\"));
return scope.Close(cwd);
} else if (len == 0) {
// Error
Local<Value> exception = Exception::Error(
String::New(winapi_strerror(GetLastError())));
return ThrowException(exception);
}
WCHAR* buffer = new WCHAR[len];
if (buffer == NULL) {
Local<Value> exception = Exception::Error(
String::New("Out of memory."));
return ThrowException(exception);
}
DWORD len2 = GetEnvironmentVariableW(env_key, buffer, len);
if (len2 == 0 || len2 >= len) {
// Error
delete[] buffer;
Local<Value> exception = Exception::Error(
String::New(winapi_strerror(GetLastError())));
return ThrowException(exception);
}
Local<String> cwd = String::New(reinterpret_cast<uint16_t*>(buffer), len2);
delete[] buffer;
return scope.Close(cwd);
}
#endif
static Handle<Value> Umask(const Arguments& args) {
HandleScope scope;
unsigned int old;
@ -1883,12 +1813,28 @@ static void ProcessTitleSetter(Local<String> property,
static Handle<Value> EnvGetter(Local<String> property,
const AccessorInfo& info) {
HandleScope scope;
#ifdef __POSIX__
String::Utf8Value key(property);
const char* val = getenv(*key);
if (val) {
HandleScope scope;
return scope.Close(String::New(val));
}
#else // _WIN32
String::Value key(property);
WCHAR buffer[32767]; // The maximum size allowed for environment variables.
DWORD result = GetEnvironmentVariableW(reinterpret_cast<WCHAR*>(*key),
buffer,
ARRAY_SIZE(buffer));
// If result >= sizeof buffer the buffer was too small. That should never
// happen. If result == 0 and result != ERROR_SUCCESS the variable was not
// not found.
if ((result > 0 || GetLastError() == ERROR_SUCCESS) &&
result < ARRAY_SIZE(buffer)) {
return scope.Close(String::New(reinterpret_cast<uint16_t*>(buffer), result));
}
#endif
// Not found
return Undefined();
}
@ -1897,66 +1843,82 @@ static Handle<Value> EnvSetter(Local<String> property,
Local<Value> value,
const AccessorInfo& info) {
HandleScope scope;
#ifdef __POSIX__
String::Utf8Value key(property);
String::Utf8Value val(value);
#ifdef __POSIX__
setenv(*key, *val, 1);
#else // __WIN32__
int n = key.length() + val.length() + 2;
char* pair = new char[n];
snprintf(pair, n, "%s=%s", *key, *val);
int r = _putenv(pair);
if (r) {
fprintf(stderr, "error putenv: '%s'\n", pair);
#else // _WIN32
String::Value key(property);
String::Value val(value);
WCHAR* key_ptr = reinterpret_cast<WCHAR*>(*key);
// Environment variables that start with '=' are read-only.
if (key_ptr[0] != L'=') {
SetEnvironmentVariableW(key_ptr, reinterpret_cast<WCHAR*>(*val));
}
delete [] pair;
#endif
return value;
// Whether it worked or not, always return rval.
return scope.Close(value);
}
static Handle<Integer> EnvQuery(Local<String> property,
const AccessorInfo& info) {
HandleScope scope;
#ifdef __POSIX__
String::Utf8Value key(property);
if (getenv(*key)) {
HandleScope scope;
return scope.Close(Integer::New(None));
}
return Handle<Integer>();
#else // _WIN32
String::Value key(property);
WCHAR* key_ptr = reinterpret_cast<WCHAR*>(*key);
if (GetEnvironmentVariableW(key_ptr, NULL, 0) > 0 ||
GetLastError() == ERROR_SUCCESS) {
if (key_ptr[0] == L'=') {
// Environment variables that start with '=' are hidden and read-only.
return scope.Close(Integer::New(v8::ReadOnly ||
v8::DontDelete ||
v8::DontEnum));
} else {
return scope.Close(Integer::New(None));
}
}
#endif
// Not found
return scope.Close(Handle<Integer>());
}
static Handle<Boolean> EnvDeleter(Local<String> property,
const AccessorInfo& info) {
HandleScope scope;
String::Utf8Value key(property);
if (getenv(*key)) {
#ifdef __POSIX__
unsetenv(*key); // prototyped as `void unsetenv(const char*)` on some platforms
String::Utf8Value key(property);
// prototyped as `void unsetenv(const char*)` on some platforms
if (unsetenv(*key) < 0) {
// Deletion failed. Return true if the key wasn't there in the first place,
// false if it is still there.
return scope.Close(Boolean::New(getenv(*key) == NULL));
};
#else
int n = key.length() + 2;
char* pair = new char[n];
snprintf(pair, n, "%s=", *key);
int r = _putenv(pair);
if (r) {
fprintf(stderr, "error unsetenv: '%s'\n", pair);
String::Value key(property);
WCHAR* key_ptr = reinterpret_cast<WCHAR*>(*key);
if (key_ptr[0] == L'=' || !SetEnvironmentVariableW(key_ptr, NULL)) {
// Deletion failed. Return true if the key wasn't there in the first place,
// false if it is still there.
bool rv = GetEnvironmentVariableW(key_ptr, NULL, NULL) == 0 &&
GetLastError() != ERROR_SUCCESS;
return scope.Close(Boolean::New(rv));
}
delete [] pair;
#endif
return True();
}
return False();
// It worked
return v8::True();
}
static Handle<Array> EnvEnumerator(const AccessorInfo& info) {
HandleScope scope;
#ifdef __POSIX__
int size = 0;
while (environ[size]) size++;
@ -1968,7 +1930,32 @@ static Handle<Array> EnvEnumerator(const AccessorInfo& info) {
const int length = s ? s - var : strlen(var);
env->Set(i, String::New(var, length));
}
#else // _WIN32
WCHAR* environment = GetEnvironmentStringsW();
if (environment == NULL) {
// This should not happen.
return scope.Close(Handle<Array>());
}
Local<Array> env = Array::New();
WCHAR* p = environment;
int i = 0;
while (*p != NULL) {
WCHAR *s;
if (*p == L'=') {
// If the key starts with '=' it is a hidden environment variable.
p += wcslen(p) + 1;
continue;
} else {
s = wcschr(p, L'=');
}
if (!s) {
s = p + wcslen(p);
}
env->Set(i++, String::New(reinterpret_cast<uint16_t*>(p), s - p));
p = s + wcslen(s) + 1;
}
FreeEnvironmentStringsW(environment);
#endif
return scope.Close(env);
}
@ -2127,10 +2114,6 @@ Handle<Object> SetupProcessObject(int argc, char *argv[]) {
NODE_SET_METHOD(process, "chdir", Chdir);
NODE_SET_METHOD(process, "cwd", Cwd);
#ifdef _WIN32
NODE_SET_METHOD(process, "_cwdForDrive", CwdForDrive);
#endif
NODE_SET_METHOD(process, "umask", Umask);
#ifdef __POSIX__
@ -2416,13 +2399,14 @@ DWORD WINAPI EnableDebugThreadProc(void* arg) {
}
static int GetDebugSignalHandlerMappingName(DWORD pid, char* buf, size_t buf_len) {
return snprintf(buf, buf_len, "node-debug-handler-%u", pid);
static int GetDebugSignalHandlerMappingName(DWORD pid, wchar_t* buf,
size_t buf_len) {
return _snwprintf(buf, buf_len, L"node-debug-handler-%u", pid);
}
static int RegisterDebugSignalHandler() {
char mapping_name[32];
wchar_t mapping_name[32];
HANDLE mapping_handle;
DWORD pid;
LPTHREAD_START_ROUTINE* handler;
@ -2431,11 +2415,11 @@ static int RegisterDebugSignalHandler() {
if (GetDebugSignalHandlerMappingName(pid,
mapping_name,
sizeof mapping_name) < 0) {
ARRAY_SIZE(mapping_name)) < 0) {
return -1;
}
mapping_handle = CreateFileMappingA(INVALID_HANDLE_VALUE,
mapping_handle = CreateFileMappingW(INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
@ -2445,11 +2429,12 @@ static int RegisterDebugSignalHandler() {
return -1;
}
handler = (LPTHREAD_START_ROUTINE*) MapViewOfFile(mapping_handle,
handler = reinterpret_cast<LPTHREAD_START_ROUTINE*>(
MapViewOfFile(mapping_handle,
FILE_MAP_ALL_ACCESS,
0,
0,
sizeof *handler);
sizeof *handler));
if (handler == NULL) {
CloseHandle(mapping_handle);
return -1;
@ -2470,7 +2455,7 @@ static Handle<Value> DebugProcess(const Arguments& args) {
HANDLE process = NULL;
HANDLE thread = NULL;
HANDLE mapping = NULL;
char mapping_name[32];
wchar_t mapping_name[32];
LPTHREAD_START_ROUTINE* handler = NULL;
if (args.Length() != 1) {
@ -2492,22 +2477,24 @@ static Handle<Value> DebugProcess(const Arguments& args) {
if (GetDebugSignalHandlerMappingName(pid,
mapping_name,
sizeof mapping_name) < 0) {
ARRAY_SIZE(mapping_name)) < 0) {
rv = ThrowException(ErrnoException(errno, "sprintf"));
goto out;
}
mapping = OpenFileMapping(FILE_MAP_READ, FALSE, mapping_name);
mapping = OpenFileMappingW(FILE_MAP_READ, FALSE, mapping_name);
if (mapping == NULL) {
rv = ThrowException(WinapiErrnoException(GetLastError(), "sprintf"));
rv = ThrowException(WinapiErrnoException(GetLastError(),
"OpenFileMappingW"));
goto out;
}
handler = (LPTHREAD_START_ROUTINE*) MapViewOfFile(mapping,
handler = reinterpret_cast<LPTHREAD_START_ROUTINE*>(
MapViewOfFile(mapping,
FILE_MAP_READ,
0,
0,
sizeof *handler);
sizeof *handler));
if (handler == NULL || *handler == NULL) {
rv = ThrowException(WinapiErrnoException(GetLastError(), "MapViewOfFile"));
goto out;

16
src/node_crypto.cc

@ -908,6 +908,8 @@ Handle<Value> Connection::New(const Arguments& args) {
SSL_set_app_data(p->ssl_, p);
if (is_server) SSL_set_info_callback(p->ssl_, SSLInfoCallback);
#ifdef OPENSSL_NPN_NEGOTIATED
if (is_server) {
// Server should advertise NPN protocols
@ -970,6 +972,20 @@ Handle<Value> Connection::New(const Arguments& args) {
}
void Connection::SSLInfoCallback(const SSL *ssl, int where, int ret) {
if (where & SSL_CB_HANDSHAKE_START) {
HandleScope scope;
Connection* c = static_cast<Connection*>(SSL_get_app_data(ssl));
MakeCallback(c->handle_, "onhandshakestart", 0, NULL);
}
if (where & SSL_CB_HANDSHAKE_DONE) {
HandleScope scope;
Connection* c = static_cast<Connection*>(SSL_get_app_data(ssl));
MakeCallback(c->handle_, "onhandshakedone", 0, NULL);
}
}
Handle<Value> Connection::EncIn(const Arguments& args) {
HandleScope scope;

2
src/node_crypto.h

@ -190,6 +190,8 @@ class Connection : ObjectWrap {
}
private:
static void SSLInfoCallback(const SSL *ssl, int where, int ret);
BIO *bio_read_;
BIO *bio_write_;
SSL *ssl_;

41
src/node_main.cc

@ -21,6 +21,47 @@
#include <node.h>
#ifdef _WIN32
int wmain(int argc, wchar_t *wargv[]) {
// Convert argv to to UTF8
char** argv = new char*[argc];
for (int i = 0; i < argc; i++) {
// Compute the size of the required buffer
DWORD size = WideCharToMultiByte(CP_UTF8,
0,
wargv[i],
-1,
NULL,
0,
NULL,
NULL);
if (size == 0) {
// This should never happen.
fprintf(stderr, "Could not convert arguments to utf8.");
exit(1);
}
// Do the actual conversion
argv[i] = new char[size];
DWORD result = WideCharToMultiByte(CP_UTF8,
0,
wargv[i],
-1,
argv[i],
size,
NULL,
NULL);
if (result == 0) {
// This should never happen.
fprintf(stderr, "Could not convert arguments to utf8.");
exit(1);
}
}
// Now that conversion is done, we can finally start.
return node::Start(argc, argv);
}
#else
// UNIX
int main(int argc, char *argv[]) {
return node::Start(argc, argv);
}
#endif

5
src/pipe_wrap.cc

@ -204,10 +204,7 @@ void PipeWrap::OnConnection(uv_stream_t* handle, int status) {
PipeWrap* client_wrap =
static_cast<PipeWrap*>(client_obj->GetPointerFromInternalField(0));
int r = uv_accept(handle, (uv_stream_t*)&client_wrap->handle_);
// uv_accept should always work.
assert(r == 0);
if (uv_accept(handle, (uv_stream_t*)&client_wrap->handle_)) return;
// Successful accept. Call the onconnection callback in JavaScript land.
Local<Value> argv[1] = { client_obj };

5
src/tcp_wrap.cc

@ -366,10 +366,7 @@ void TCPWrap::OnConnection(uv_stream_t* handle, int status) {
TCPWrap* client_wrap =
static_cast<TCPWrap*>(client_obj->GetPointerFromInternalField(0));
int r = uv_accept(handle, (uv_stream_t*)&client_wrap->handle_);
// uv_accept should always work.
assert(r == 0);
if (uv_accept(handle, (uv_stream_t*)&client_wrap->handle_)) return;
// Successful accept. Call the onconnection callback in JavaScript land.
argv[0] = client_obj;

100
test/pummel/test-tls-ci-reneg-attack.js

@ -0,0 +1,100 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
var common = require('../common');
var assert = require('assert');
var spawn = require('child_process').spawn;
var tls = require('tls');
var fs = require('fs');
// renegotiation limits to test
var LIMITS = [0, 1, 2, 3, 5, 10, 16];
if (process.platform === 'win32') {
console.log("Skipping test, you probably don't have openssl installed.");
process.exit();
}
(function() {
var n = 0;
function next() {
if (n >= LIMITS.length) return;
tls.CLIENT_RENEG_LIMIT = LIMITS[n++];
test(next);
}
next();
})();
function test(next) {
var options = {
cert: fs.readFileSync(common.fixturesDir + '/test_cert.pem'),
key: fs.readFileSync(common.fixturesDir + '/test_key.pem')
};
var server = tls.createServer(options, function(conn) {
conn.on('error', function(err) {
console.error('Caught exception: ' + err);
assert(/TLS session renegotiation attack/.test(err));
conn.destroy();
});
conn.pipe(conn);
});
server.listen(common.PORT, function() {
var args = ('s_client -connect 127.0.0.1:' + common.PORT).split(' ');
var child = spawn('openssl', args);
child.stdout.pipe(process.stdout);
child.stderr.pipe(process.stderr);
// count handshakes, start the attack after the initial handshake is done
var handshakes = 0;
child.stderr.on('data', function(data) {
handshakes += (('' + data).match(/verify return:1/g) || []).length;
if (handshakes === 2) spam();
});
child.on('exit', function() {
// with a renegotiation limit <= 1, we always see 4 handshake markers:
// two for the initial handshake and another two for the attempted
// renegotiation
assert.equal(handshakes, 2 * Math.max(2, tls.CLIENT_RENEG_LIMIT));
server.close();
process.nextTick(next);
});
var closed = false;
child.stdin.on('error', function(err) {
assert.equal(err.code, 'EPIPE');
closed = true;
});
child.stdin.on('close', function() {
closed = true;
});
// simulate renegotiation attack
function spam() {
if (closed) return;
child.stdin.write("R\n");
setTimeout(spam, 250);
}
});
}

77
test/simple/test-cluster-bind-twice.js

@ -1,77 +0,0 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// This test starts two clustered HTTP servers on the same port. It expects the
// first cluster to succeed and the second cluster to fail with EADDRINUSE.
var common = require('../common');
var assert = require('assert');
var cluster = require('cluster');
var fork = require('child_process').fork;
var http = require('http');
var id = process.argv[2];
if (!id) {
var a = fork(__filename, ['one']);
var b = fork(__filename, ['two']);
a.on('message', function(m) {
assert.equal(m, 'READY');
b.send('START');
});
var ok = false;
b.on('message', function(m) {
assert.equal(m, 'EADDRINUSE');
a.kill();
b.kill();
ok = true;
});
process.on('exit', function() {
a.kill();
b.kill();
assert(ok);
});
}
else if (id === 'one') {
if (cluster.isMaster) cluster.fork();
http.createServer(assert.fail).listen(common.PORT, function() {
process.send('READY');
});
}
else if (id === 'two') {
if (cluster.isMaster) cluster.fork();
process.on('message', function(m) {
assert.equal(m, 'START');
var server = http.createServer(assert.fail);
server.listen(common.PORT, assert.fail);
server.on('error', function(e) {
assert.equal(e.code, 'EADDRINUSE');
process.send(e.code);
});
});
}
else {
assert(0); // bad command line argument
}

35
test/simple/test-dgram-close.js

@ -0,0 +1,35 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// Ensure that if a dgram socket is closed before the DNS lookup completes, it
// won't crash.
var assert = require('assert'),
common = require('../common'),
dgram = require('dgram');
var buf = new Buffer(1024);
buf.fill(42);
var socket = dgram.createSocket('udp4');
socket.send(buf, 0, buf.length, common.port, 'localhost');
socket.close();

9
test/simple/test-http-server-multiheaders.js

@ -33,6 +33,8 @@ var srv = http.createServer(function(req, res) {
assert.equal(req.headers['www-authenticate'], 'foo, bar, baz');
assert.equal(req.headers['x-foo'], 'bingo');
assert.equal(req.headers['x-bar'], 'banjo, bango');
assert.equal(req.headers['sec-websocket-protocol'], 'chat, share');
assert.equal(req.headers['sec-websocket-extensions'], 'foo; 1, bar; 2, baz');
res.writeHead(200, {'Content-Type' : 'text/plain'});
res.end('EOF');
@ -57,7 +59,12 @@ srv.listen(common.PORT, function() {
['WWW-AUTHENTICATE', 'baz'],
['x-foo', 'bingo'],
['x-bar', 'banjo'],
['x-bar', 'bango']
['x-bar', 'bango'],
['sec-websocket-protocol', 'chat'],
['sec-websocket-protocol', 'share'],
['sec-websocket-extensions', 'foo; 1'],
['sec-websocket-extensions', 'bar; 2'],
['sec-websocket-extensions', 'baz']
]
});
});

9
test/simple/test-repl-tab-complete.js

@ -180,3 +180,12 @@ testMe.complete('inner.o', function(error, data) {
assert.deepEqual(data, doesNotBreak);
});
putIn.run(['.clear']);
// make sure tab completion works on non-Objects
putIn.run([
'var str = "test";'
]);
testMe.complete('str.len', function(error, data) {
assert.deepEqual(data, [ [ 'str.length' ], 'str.len' ]);
});

156
test/simple/test-tls-over-http-tunnel.js

@ -0,0 +1,156 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
if (!process.versions.openssl) {
console.error('Skipping because node compiled without OpenSSL.');
process.exit(0);
}
var common = require('../common');
var assert = require('assert');
var fs = require('fs');
var net = require('net');
var http = require('http');
var https = require('https');
var proxyPort = common.PORT + 1;
var gotRequest = false;
var key = fs.readFileSync(common.fixturesDir + '/keys/agent1-key.pem');
var cert = fs.readFileSync(common.fixturesDir + '/keys/agent1-cert.pem');
var options = {
key: key,
cert: cert
};
var server = https.createServer(options, function(req, res) {
console.log('SERVER: got request');
res.writeHead(200, {
'content-type': 'text/plain',
});
console.log('SERVER: sending response');
res.end('hello world\n');
});
var proxy = net.createServer(function(clientSocket) {
console.log('PROXY: got a client connection');
var serverSocket = null;
clientSocket.on('data', function(chunk) {
if (!serverSocket) {
// Verify the CONNECT request
assert.equal('CONNECT localhost:' + common.PORT + ' HTTP/1.1\r\n' +
'Proxy-Connections: keep-alive\r\nContent-Length:' +
' 0\r\nHost: localhost:' + proxyPort + '\r\n\r\n',
chunk);
console.log('PROXY: got CONNECT request');
console.log('PROXY: creating a tunnel');
// create the tunnel
serverSocket = net.connect(common.PORT, function() {
console.log('PROXY: replying to client CONNECT request');
// Send the response
clientSocket.write('HTTP/1.1 200 OK\r\nProxy-Connections: keep' +
'-alive\r\nConnections: keep-alive\r\nVia: ' +
'localhost:' + proxyPort + '\r\n\r\n');
});
serverSocket.on('data', function(chunk) {
clientSocket.write(chunk);
});
serverSocket.on('end', function() {
clientSocket.destroy();
});
} else {
serverSocket.write(chunk);
}
});
clientSocket.on('end', function() {
serverSocket.destroy();
});
});
server.listen(common.PORT);
proxy.listen(proxyPort, function() {
console.log('CLIENT: Making CONNECT request');
http.request({
port: proxyPort,
method: 'CONNECT',
path: 'localhost:' + common.PORT,
headers: {
'Proxy-Connections': 'keep-alive',
'Content-Length': 0
}
}, function(res) {
assert.equal(200, res.statusCode);
console.log('CLIENT: got CONNECT response');
// detach the socket
res.socket.emit('agentRemove');
res.socket.removeAllListeners('data');
res.socket.removeAllListeners('close');
res.socket.removeAllListeners('error');
res.socket.removeAllListeners('drain');
res.socket.removeAllListeners('end');
res.socket.ondata = null;
res.socket.onend = null;
res.socket.ondrain = null;
console.log('CLIENT: Making HTTPS request');
https.get({
path: '/foo',
key: key,
cert: cert,
socket: res.socket, // reuse the socket
agent: false,
}, function(res) {
assert.equal(200, res.statusCode);
res.on('data', function(chunk) {
assert.equal('hello world\n', chunk);
console.log('CLIENT: got HTTPS response');
gotRequest = true;
});
res.on('end', function() {
proxy.close();
server.close();
});
}).end();
}).end();
});
process.on('exit', function() {
assert.ok(gotRequest);
});

1
vcbuild.bat

@ -46,6 +46,7 @@ if /i "%1"=="test" set test=test&goto arg-ok
if /i "%1"=="msi" set msi=1&goto arg-ok
if /i "%1"=="upload" set upload=1&goto arg-ok
echo Warning: ignoring invalid command line option `%1`.
:arg-ok
shift

Loading…
Cancel
Save