之前经常有同学会问到怎么有些异常无法捕获到呢?

虽然 connect 已经在 handler 外层加了 try catch ,还是无法捕获异步调用中产生的异常。

现状

例如最简单的 helloworld.js 代码

var connect = require('connect');

var app = connect()
.use(function (req, res, next) {
  if (req.url === '/sync_error') {
    throw new Error('sync error');
  }
  if (req.url === '/async_error') {
    return process.nextTick(function () {
      mk2.haha();
    });
  }
  res.end('hello, ' + req.method + ' ' + req.url);
});

process.on('uncaughtException', function (err) {
  console.error(err);
});

app.listen(1985);

使用 curl 来简单测试一下

$ curl localhost:1985/foo
hello, GET /foo

$ curl localhost:1985/sync_error
Error: sync error
    at Object.handle ( .../app.js:33:11)
    ....

好像还挺正常的,异常也被捕获了。 继续测试

$ curl localhost:1985/async_error

没任何输出了吧?从服务器端控制台看到这样的输出: 证明触发了 uncaughtException 事件。

[ReferenceError: mk2 is not defined]

虽然我们能通过 uncaughtException 事件捕获到异步调用中产生的异常,但是我们没办法返回 HTTP 500 异常响应给调用者。 调用者也只能一直hold住,直到请求超时。

这是非常不好的做法,因为请求量很大,服务进程内存会暴涨,会导致进程超出内存限制,非正常退出。

改进

任何时候,如果服务器端出现异常,我们就应该返回HTTP 500 告诉调用者服务器异常了。

异步调用产生的异常也应该如此处理。

nodejs@0.8 开始,增加了 domain 模块来帮助我们更好地处理异常。

让我们使用 connect-domain 来改进一下之前的 helloworld.js:

var connect = require('connect');
var connectDomain = require('connect-domain');

var app = connect()
.use(connectDomain()) // just using connect-domian middleware
.use(function (req, res, next) {
  if (req.url === '/sync_error') {
    throw new Error('sync error');
  }
  if (req.url === '/async_error') {
    return process.nextTick(function () {
      mk2.haha();
    });
  }
  res.end('hello, ' + req.method + ' ' + req.url);
});

process.on('uncaughtException', function (err) {
  console.error(err);
});

app.listen(1984);

再看看 connect-domain 的实现,非常简单:

var domain = require('domain');

module.exports = function (handler) {
  return function domainMiddleware(req, res, next) {
    var reqDomain = domain.create();

    res.on('close', function () {
      reqDomain.dispose();
    });

    reqDomain.on('error', function (err) {
      if (typeof handler === 'function') {
        handler(err, req, res, next);
      } else {
        next(err);
      }
    });

    reqDomain.run(next);
  };
};

重复刚才的 curl 测试:

$ curl localhost:1984/foo
hello, GET /foo

$ curl localhost:1984/sync_error
Error: sync error
    at Object.handle ( .../app.js:33:11)
    ....

$ curl localhost:1984/async_error
ReferenceError: mk2 is not defined
    at .../app.js:24:7
    at process.startup.processNextTick.process._tickCallback (node.js:244:9)

所有请求都有响应了。

服务器进程的输出:

{ [ReferenceError: mk2 is not defined]
  domain_thrown: true,
  domain: 
   { domain: null,
     _events: { error: [Function] },
     _maxListeners: 10,
     members: [] } }

奇怪的连 uncaughtException 事件也触发了。这按我的思维来说,不应该触发才对的。

@isaacs 最新的提交已经修复此问题: https://github.com/joyent/node/issues/4375#issuecomment-11691069

总结

应该没人再说 nodejs 无法捕获异步调用中出现的异常了吧?

posted @ 2012-12-27 12:25 MK2 阅读(883) 评论(0) 编辑

问题

一篇#2011-003 multiple implementations denial-of-service via hash algorithm collision 文章,引起了 Web 应用安全领域的骚动。

这种hash算法冲突的原理到底是什么?可以看看一下两篇以PHP为例子的说明文章:

解决办法

既然是语言层面的hash算法冲突导致的,那么这种冲突就无法避免了。
但是我们可以限制Hash key的数量来避免。

所以可以进行一下设置解决此问题:

  • 限制请求大小 (只能指标不治本,因为PHP版本的话,700KB的数据就足以发起攻击)
  • 限制请求key的数量 (这可以基本解决问题,除非应用无法预知最大的key数量)
  • 只接受允许的key (这是终极方法,但需要保证开发记得配置新增的key)

在Nodejs防御此问题

虽然在目前为止还没看到对Nodejs造成攻击的具体方法,但是还是以防范于未然为原则,需要对此问题做好充分的防御措施。

Nodejs 的攻击方法已经出现,具体测试结果可以查看 Hash algorithm collision in Nodejs
hac-results

由于我个人一直使用的是 connect ,所以我以 connect 为示例说明吧 ^_^

使用 connect.limit 限制 request-body-size

Issue #446 已有同学提出此问题了,@TJ回复如下:

we have limit() for this, which works for any request body. Even without this specific issue you could exhaust resources reasonably easily without some form of limiting

好吧,直接上 connect.limit 模块解决

connect()
  .use(connect.limit('1mb'))
  .use(handleRequest)

修改 qs 模块,让其支持 keys-limit 和 allow-keys

querystring.js

PS: 提了pull request,但是估计在没有真实攻击示例放出来之前,是不会被接受的。

/**
 * Parse the given str.
 */

function parseString(str, options) {
  var limit = options && options.limit;
  var keys = options && options.keys;
  if (keys && Array.isArray(keys)) {
    keys = {};
    for (var i = 0, l = options.keys.length; i < l; i++) {
      keys[options.keys[i]] = 1;
    }
  }
  return String(str)
    .split('&', limit)
    .reduce(function(ret, pair){
      try{
        pair = decodeURIComponent(pair.replace(/\+/g, ' '));
      } catch(e) {
        // ignore
      }

      var eql = pair.indexOf('=')
        , brace = lastBraceInKey(pair)
        , key = pair.substr(0, brace || eql);
      if (keys && !keys[key]) {
        return ret;
      }
      var val = pair.substr(brace || eql, pair.length)
      val = val.substr(val.indexOf('=') + 1, val.length);

      // ?foo
      if ('' == key) key = pair, val = '';

      return merge(ret, key, val);
    }, { base: {} }).base;
}


/**
 * Parse the given query `str` or `obj`, returning an object.
 *
 * Options: (only effect on parse string)
 *
 *  - `limit` parse string split limit.
 *  - `keys`  which keys need to be parse.
 *
 * @param {String} str | {Object} obj
 * @param {Object} options
 * @return {Object}
 * @api public
 */

exports.parse = function(str, options) {
  if (null == str || '' == str) return {};
  return 'object' == typeof str
    ? parseObject(str)
    : parseString(str, options);
};

还需要让 connect.query 模块 传递options参数给 qs.parse()

module.exports = function query(options){
  return function query(req, res, next){
    req.query = ~req.url.indexOf('?')
      ? qs.parse(parse(req.url).query, options)
      : {};
    next();
  };
};

同样 connect.urlencoded 模块也需要将options参数传递给 qs.parse()

    req.on('end', function(){
      try {
        req.body = buf.length
          ? qs.parse(buf, options)
          : {};
        next();
      } catch (err){
        next(err);
      }
    });

全部组合起来

var qsOptions = { limit: 100 };
connect()
  .use(connect.limit('1mb'))
  .use(connect.query(qsOptions))
  .use(connect.bodyParser(qsOptions))
  .use(handleRequest)

防范 http header 攻击

请求的 http header 也会导致hash冲突,在V8层面未修复hash算法之前,可以通过简单的 http_patch.js 修复此问题:

var http = require('http');

var IncomingMessage = http.IncomingMessage;
var _addHeaderLine = IncomingMessage.prototype._addHeaderLine;

// limit http header number
IncomingMessage.prototype._addHeaderLine = function(field, val) {
  if (!this.__headerCount__) {
    this.__headerCount__ = 0;
  } else if (this.__headerCount__ >= 100) {
    return;
  }
  _addHeaderLine.apply(this, arguments);
  this.__headerCount__++;
};

最后

2011年末,苦逼的程序员还在为这个安全漏洞写补丁,打补丁,想各种解决方法。
2012年或许还会有各种各样的问题,我们一起勇敢面对吧。
Happy New Year!

posted @ 2012-01-01 00:49 MK2 阅读(495) 评论(4) 编辑

Node.js is Cancer show a wrong way to use nodejs.
But the test code Fibonacci is so funny.
I implement the fibonacci function in other Dynamic Languages for comparison testing.

Languages

Dynamic

Static

If you want to help add more dynamic languagues, please leave the implement code in comments.

Results

(^_^) c > go > luajit > nodejs > pypy > lua > python > php > perl > ruby1.9.3 > ruby1.8.5 (T_T)

LanguageTimesPosition
c0m1.606s#0
go0m1.769s#1
node + cpp module0m2.216s#2
luajit0m2.583s#3
nodejs0m5.124s#4
pypy0m7.562s#5
lua0m34.492s#6
python1m11.647s#7
php1m28.198s#8
perl2m34.658s#9
ruby 1.9.34m40.790s#10
ruby 1.8.54m41.942s#11

lua use local function will get better performance.

Test Codes

nodejs

function fibonacci(n) {
  if (n < 2) {
    return 1;
  }
  return fibonacci(n - 2) + fibonacci(n - 1);
}

console.log(fibonacci(40));

run

$ time node fibonacci.js
165580141

real  0m5.153s
user  0m5.124s
sys 0m0.012s

nodejs + cpp module

cppfibonacci.cpp

#include 
#include 

using namespace v8;

int fibonacci(int n) {
  if (n < 2) {
    return 1;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

Handle Fibonacci(const Arguments& args) {
    HandleScope scope;

    if (args.Length() < 1) {
        return ThrowException(Exception::TypeError(
            String::New("First argument must be a number")));
    }
    Local integer = args[0]->ToInteger();
    int r = fibonacci(integer->Value());

    return scope.Close(Integer::New(r));
}

void RegisterModule(v8::Handle target) {
    // Add properties to target
    NODE_SET_METHOD(target, "fibonacci", Fibonacci);
}

// Register the module with node.
NODE_MODULE(cppfibonacci, RegisterModule);

wscript

#!/usr/bin/env python

def set_options(ctx):
  ctx.tool_options('compiler_cxx')

def configure(ctx):
  ctx.check_tool('compiler_cxx')
  ctx.check_tool('node_addon')

def build(ctx):
  t = ctx.new_task_gen('cxx', 'shlib', 'node_addon')

  t.source = ['cppfibonacci.cpp']

  # Must be same as first parameter in NODE_MODULE.
  t.target = 'cppfibonacci'

cppfibonacci.js

var fibonacci = require('./build/default/cppfibonacci').fibonacci;
console.log(fibonacci(40));

run

$ node-waf configure
$ node-waf build
$ time node cppfibonacci.js
165580141

real  0m2.224s
user  0m2.216s
sys 0m0.008s

python2.4.3 && python2.6.7 && pypy1.7

def fibonacci(n):
    if n < 2:
        return 1
    return fibonacci(n - 2) + fibonacci(n - 1)

print fibonacci(40)

run

$ time python2.4.3 fibonacci.py
165580141

real  1m11.667s
user  1m11.647s
sys 0m0.002s

$ time python2.6.7 fibonacci.py
165580141

real  1m9.837s
user  1m9.792s
sys 0m0.006s

$ time ./pypy-1.7/bin/pypy fibonacci.py
165580141

real  0m7.608s
user  0m7.562s
sys 0m0.031s

perl

sub fibonacci {
    my $n = shift;
    if ($n < 2) {
      return 1;
    }
    return fibonacci($n - 2) + fibonacci($n - 1);
}

print fibonacci(40), "\n";

run

$ time perl fibonacci.pl
165580141

real  2m34.777s
user  2m34.658s
sys 0m0.004s

php


run

$ time php fibonacci.php

165580141
real  1m28.364s
user  1m28.198s
sys 0m0.039s

ruby1.8.5 && ruby1.9.3

def fibonacci(n)
  if n < 2
    return 1
  end
  return fibonacci(n - 2) + fibonacci(n - 1)
end

puts fibonacci(40)

run

$ time ruby1.8.5 fibonacci.rb
165580141

real  5m43.132s
user  4m41.942s
sys 1m0.653s

$ time ruby1.9.3 fibonacci.rb
165580141

real  5m41.714s
user  4m40.790s
sys 1m0.661s

lua && luajit

function fibonacci(n)
  if n < 2 then
    return 1
  end
  return fibonacci(n - 2) + fibonacci(n - 1)
end

io.write(fibonacci(40), "\n")

run

$ time ./lua-5.1.4/src/lua fibonacci.lua 
165580141

real  0m34.514s
user  0m34.492s
sys 0m0.004s

$ time ./LuaJIT-2.0.0-beta9/src/luajit fibonacci.lua 
165580141

real  0m2.598s
user  0m2.583s
sys 0m0.001s

local function should be faster:

local function fibonacci(n)
  if n < 2 then
    return 1
  end
  return fibonacci(n - 2) + fibonacci(n - 1)
end

io.write(fibonacci(40), "\n")

$ time ./lua-5.1.4/src/lua fibonacci.lua.local
165580141

real  0m31.737s
user  0m31.549s
sys 0m0.001s

$ time ./LuaJIT-2.0.0-beta9/src/luajit fibonacci.lua.local
165580141

real  0m2.227s
user  0m2.225s
sys 0m0.001s

c

#include 

int fibonacci(n) {
  if (n < 2) {
    return 1;
  }
  return fibonacci(n - 2) + fibonacci(n - 1);
}

int main() {
  printf("%d\n", fibonacci(40));
  return 0;
}

run

$ gcc fibonacci.c
$ time ./a.out 
165580141

real  0m3.434s
user  0m3.427s
sys 0m0.000s

Compilation with optimization:

$ gcc -O2 fibonacci.c
$ time ./a.out 
165580141

real  0m1.607s
user  0m1.606s
sys 0m0.001s

@fool: How about C++ meta programming, it’s a bit of cheating

#include 

template
struct fibonacci {
    enum { Result = fibonacci::Result + fibonacci::Result };
};

template<>
struct fibonacci<1> {
    enum { Result = 1 };
};

template<>
struct fibonacci<0> {
    enum { Result = 1 };
};

int main(int argc, char *argv[])
{
    printf("%d\n", fibonacci<40>::Result);
    return 0;
}

run

$ g++ fibonacci.template.cpp 
$ time ./a.out
165580141

real  0m0.002s
user  0m0.001s
sys 0m0.001s

go

package main

import "fmt"

func fibonacci(n int) int{
  if (n < 2) {
    return 1
  }
  return fibonacci(n - 2) + fibonacci(n - 1)
}

func main() {
  fmt.Println(fibonacci(10))
}

run

$ 6g fibonacci.go
$ 6l fibonacci.6
$ time ./6.out
165580141

real  0m1.770s
user  0m1.769s
sys 0m0.001s

Conclusion

nodejs is very FAST.
luajit 2X faster than nodejs, Shocking.

posted @ 2011-12-14 22:21 MK2 阅读(291) 评论(1) 编辑

forever

A simple CLI tool for ensuring that a given script runs continuously (i.e. forever).
一个非常简单的CLI工具,让你的程序持续运行。

安装forever: https://github.com/nodejitsu/forever

$ [sudo] npm install forever -g

Demo: 进程重启超过5次后将不再运行

$ forever examples/error-on-timer.js -m 5

forever-webui

A simple web UI for efficient nodejs administration
既然有了CLI工具帮忙保证程序的持续运行,那么有个简单的web监控就更加好了。
forever-webui 就是这样一个web应用程序。

安装并运行

$ npm install forever-webui && node node_modules/forever-webui/app.js

浏览器访问 http://127.0.0.1:8085

http://ww4.sinaimg.cn/large/61c56ebcjw1dnd6elhv81j.jpg

PS:如果你使用forever来启动forever-webui,还可以自己监控自己喔,不知道会不会死循环?!

有爱

^_^ 希望本文对你有用。

posted @ 2011-11-23 02:33 MK2 阅读(1147) 评论(0) 编辑

疑问

按nodejs官方的文档说明,使用Buffer操作字节流通常会比转化成String要高效。
实际情况全都是这样的吗?
本文通过一个简单的解析HTTP Request Header实例来解开此疑问。

HTTP Request Header Demo

POST /foo HTTP/1.1\r\n
Host: foo.example.com\r\n
Content-Length: 5\r\n
Connection:keep-alive\r\n
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Cookie:connect.sid=OY2nKGqI3obs5lYee0JKTjhf.FDtbY1Jz5Ngw5So9Jv3MUetI5ITvrIfwgCkRw%2FcXUCk\r\n
User-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.41 Safari/535.7
\r\n\r\n
q=bar

需求

获取Header中Host的值

Buffer版本

var SPACE = 0x20, // ' '
    COLON = 0x3a, // 58, :
    NEWLINE = 0x0a, // \n
    ENTER = 0x0d; // \r

exports.parse = function parse(data) {
    var line_start = 0, len = data.length;
    for(var i = 0 ; i < len; i++) {
        // Host: xxx.abc.com
        if(data[i] === COLON) {
            var key = data.toString('ascii', line_start, i).toLowerCase();
            i++; // skip ':'
            if(key === 'host') {
                var value_start = i;
                while(i < len) {
                    if(data[i] === ENTER) {
                        return data.toString('ascii', value_start, i).trim().toLowerCase();
                    }
                    i++;
                }
            }
        } else if(data[i] ===  ENTER && data[i+1] === NEWLINE) {
            i += 2;
            line_start = i;
            if(data[i] ===  ENTER && data[i+1] === NEWLINE) {
                // \r\n\r\n
                return 'Host header not found';
            }
        }
    }
    return null;
};

String版本

exports.parse = function parse(data) {
    var lines = data.toString('ascii').split("\n");
    var cut, name, host;
    for (var i = 0, len = lines.length; i < len; i++) {
        cut = lines[i].split(':');
        name = cut[0];
        if (name === 'Host') {
            if (cut[1] === undefined) {
                return 'Host header not found';
            }
            host = cut[1].trim().toLowerCase();
            return host;
        }
    }
    return null;
};

测试脚本

var buffer_parse = require('./string-buffer-benchmark-parse-header-buffer').parse
  , string_parse = require('./string-buffer-benchmark-parse-header-string').parse;

var data = new Buffer('POST /foo HTTP/1.1\r\nHost: foo.example.com\r\nContent-Length: 5\r\nConnection:keep-alive\r\nAccept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nCookie:connect.sid=OY2nKGqI3obs5lYee0JKTjhf.FDtbY1Jz5Ngw5So9Jv3MUetI5ITvrIfwgCkRw%2FcXUCk\r\nUser-Agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_2) AppleWebKit/535.7 (KHTML, like Gecko) Chrome/16.0.912.41 Safari/535.7\r\n\r\nq=bar');

//console.log(buffer_parse(data));
//console.log(string_parse(data), data.length);

var n = 1000000;
var start = new Date();
for(var i = 0; i < n; i++) {
    buffer_parse(data);
}
console.log('buffer_parse take: ' + (new Date() - start) + ' ms');

start = new Date();
for(var i = 0; i < n; i++) {
    string_parse(data);
}
console.log('string_parse take: ' + (new Date() - start) + ' ms');

测试结果

$ node string-buffer-benchmark.js
buffer_parse take: 1888 ms
string_parse take: 4948 ms

结论

Buffer比String快多了。

posted @ 2011-11-19 01:21 MK2 阅读(695) 评论(0) 编辑
摘要: 新版本 nodejs 性能 node0.6.0 已经发布了,性能提高如何呢?本文将记录 nodejs 历史更新中所有版本的hello world性能测试。 测试环境 $ uname -a Linux xxx 2.6.18-164.el5 #1 SMP Tue Aug 18 15:51:48 EDT 2009 x86_64 x86_64 x86_64 GNU/Linux $ cat /pro...阅读全文
posted @ 2011-11-17 00:08 MK2 阅读(423) 评论(0) 编辑
摘要: 使用方法 @mashihua 同学在cnode新闻组发布了他维护的npm镜像源,速度很快! 以下是邮件内容: 很高兴的通知大家。在周末的空闲时间里,我们搭建了一个镜像的npm资源库,服务器在日本的Linode上。大家可用下面的命令来安装npm的模块: $ npm --registry "http://npm.hacknodejs.com/" install express 或下面的命令...阅读全文
posted @ 2011-11-16 16:05 MK2 阅读(682) 评论(0) 编辑
摘要: Change logs 完全基于Connect,提高整体响应性能; 移除对express, libxml2的依赖; Session使用Mongodb存储; NodeBlog 介绍 A blog base on nodejs. Demo: http://nodeblog.org, http://nodeblog.cnodejs.net Features Write, Read, Li...阅读全文
posted @ 2011-11-15 00:54 MK2 阅读(286) 评论(0) 编辑
摘要: 实例化解析: function Foo() {}; var foo = new Foo(); foo.__proto__ === Foo.prototype; foo.__proto__.__proto__ === Object.prototype; foo.__proto__.__proto__.__proto__ === null; foo.prototype === undefined;...阅读全文
posted @ 2011-11-02 15:02 MK2 阅读(256) 评论(0) 编辑
摘要: @Cnodejs社区的NAE 目前正在邀请内测中,目前已经增加自定义域名绑定。 只需在package.json文件中指定customHost属性即可。 例如我需要绑定nae.nodeblog.org => nodeblog.cnodejs.net ,只需在package.json增加一行即可: "customHost": "nae.nodeblog.org"阅读全文
posted @ 2011-10-27 02:29 MK2 阅读(334) 评论(0) 编辑
摘要: 本文收集我个人编译的一些离线文档 nodedoc.0.5.4.zip阅读全文
posted @ 2011-08-17 11:35 MK2 阅读(481) 评论(0) 编辑
摘要: 内存限制 大文件读写操作,由于内存限制问题,不要直接使用fs.readFile 和 fs.writeFile。 必须使用fs.ReadStream 和 fs.WriteStream 来对文件进行读写操作。 fs.ReadStream:上传大文件 var urlparse = require('url').parse , http = require('http') , fs = require('...阅读全文
posted @ 2011-08-16 12:19 MK2 阅读(1185) 评论(0) 编辑
摘要: 问题 Nodejs原生的http.request 方法是不支持设置超时参数的,而网络请求经常会遇到超时的情况,特别是对于外部网络,如果不处理超时,发起的请求将会一直卡主,消耗的系统资源也不能及时被释放。 解决方案(旧) 定时器:通过定时器,当timeout事件触发的时候,主动调用req.abort() 终止请求,然后返回超时异常。 Request Timeout & Response Ti...阅读全文
posted @ 2011-08-09 21:30 MK2 阅读(1260) 评论(0) 编辑
摘要: XMLHttpRequest.upload 在Firefox, Google Chrome and Safari中,如果通过XMLHttpRequest上传文件,是可以通过监听XMLHttpRequest.upload对象的progress事件来查看进度的。 var xhr = new XMLHttpRequest(); xhr.upload.addEventListener("progress"...阅读全文
posted @ 2011-08-05 15:22 MK2 阅读(1067) 评论(2) 编辑
摘要: Buffer不会被GC? 为了看看这个问题,我写了一段测试代码 var http = require('http') , os = require('os'); function create(size, res) { var mb = 1024 * 1024; res.write('free ' + os.freemem() / mb + 'MB\r\n'); res.write('------...阅读全文
posted @ 2011-07-20 00:16 MK2 阅读(746) 评论(0) 编辑