问题1:我们首先学习一下forwarded库,这个需要掌握
/*!
* forwarded
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
/**
* Module exports.
* @public
*/
module.exports = forwarded
/**
* Get all addresses in the request, using the `X-Forwarded-For` header.
* @param {object} req
* @return {array}
* @public
*/
function forwarded(req) {
if (!req) {
throw new TypeError('argument req is required')
}
//获取请求中的x-forwarded-for请求参数
// simple header parsing
var proxyAddrs = (req.headers['x-forwarded-for'] || '')
.split(/ *, */) //切割成为一个数组了
.filter(Boolean)//去除其中的"",undefined等
.reverse()
var socketAddr = req.connection.remoteAddress
//通过req.connection.remoteAddress获取远程主机的地址
var addrs = [socketAddr].concat(proxyAddrs)
//最后返回的第一个是主机名,其余都是代理服务器的地址
// return all addresses
return addrs
}
功能很简单,主要通过x-forwarded-for和req.connection.remoteAddress来获取远程客户端的实际地址和具体的通过的代理服务器的地址!
问题2:我们学一学ipaddr库,这个库也需要掌握
我们看看Node.js核心库os.networkInterfaces返回的网卡信息时怎么样的:
{ '无线网络连接 3':
[ { address: 'fe80::5885:cf5d:2bab:ea29',
netmask: 'ffff:ffff:ffff:ffff::',
family: 'IPv6',
mac: 'f0:b4:29:30:ef:41',
scopeid: 19,
internal: false },
{ address: '192.168.16.94',
netmask: '255.255.252.0',
family: 'IPv4',
mac: 'f0:b4:29:30:ef:41',
internal: false } ],
'Loopback Pseudo-Interface 1':
[ { address: '::1',
netmask: 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff',
family: 'IPv6',
mac: '00:00:00:00:00:00',
scopeid: 0,
internal: true },
{ address: '127.0.0.1',
netmask: '255.0.0.0',
family: 'IPv4',
mac: '00:00:00:00:00:00',
internal: true } ] }
这个库是如何获取当前的ip地址的:
exports.getCurrentIp = function(){
/**
* 系统模块
* @type {Object}
*/
var os = require('os');
/**
* 获得系统的网卡列表
* @type {Object}
*/
var ipObj = os.networkInterfaces();
//获取系统的网卡
var result = [];
for(var i in ipObj) {
// 获得某个网卡下面的ip列表
var ipList = ipObj[i];
for(var j = 0; j < ipList.length; j += 1) {
// 某个网卡的某个ip
var ip = ipList[j];
//result中保存的是IPv4的地址
if(ip.family === 'IPv4') {
result.push(ip.address);
}
}
}
return result;
};
如何获取一个ip真实的地址来源:
exports.getAddress = function (ip, callback) {
http.get('http://ip.taobao.com/service/getIpInfo.php?ip=' + ip, function (res) {
//bl是一个Node中buffer对象的存储对象。这是一个全双工的Buffer,因此你可以从给一个流中获取buffer,然后消费他们或者把收集到的Buffer交给其他流消费
res.pipe(bl(function (err, data) {
if (err) {
console.log('error:', err);
return;
}
//其中data参数保存的就是res这个流中的所有的数据,然后获取这个数据的data域,其中包含了contry,region,isp等信息
data = JSON.parse(data).data;
if (callback) {
callback(data.country + ' ' + data.region + ' ' + data.isp);
} else {
console.log(ip, data.country || '', data.region || '', data.isp || '');
}
}));
}).on('error', function(e) { //如果报错那么就打印错误信息
if (callback) {
callback('Unknow');
} else {
console.log("Got error: " + e.message);
}
});
};
如果你想看懂上面的代码,最好看一下
bl这个包,这个包是对Node中的Buffer进行的处理
var getAddressPromise = function (ip) {
// console.log('get', ip);
return new Promise((resolve, reject) => {
exports.getAddress(ip, function (address) {
// console.log('addr', address, ip);
resolve(address);
});
});
};
var IP_REG = /((?:\d+\.){3}\d+)\s+\(\1\)/;
exports.traceRoute = function (domain) {
var trace = exec('traceroute ' + domain);
var seq = Promise.resolve();
trace.stdout.on('data', function (data) {
// 220.181.17.90 (220.181.17.90)
if (IP_REG.test(data)) {
seq = seq.then(() => {
return getAddressPromise(RegExp.$1).then((addr) => {
data = data.replace(IP_REG, '$1 (' + addr + ')');
process.stdout.write(data);
});
});
} else {
seq = seq.then(() => process.stdout.write(data));
}
});
trace.stderr.on('data', function (data) {
process.stdout.write(data);
});
};
这部分代码还需要仔细斟酌,可以参考
ipaddr
我们现在看看ipaddr的源代码是怎么样的?
/*!
* proxy-addr
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
'use strict'
module.exports = proxyaddr;
module.exports.all = alladdrs;
module.exports.compile = compile;
/**
* Module dependencies.
*/
var forwarded = require('forwarded');
var ipaddr = require('ipaddr.js');
//引入两个库forwarded和ipaddr
/**
* Variables.
*/
var digitre = /^[0-9]+$/;
var isip = ipaddr.isValid;
var parseip = ipaddr.parse;
/**
* Pre-defined IP ranges.
*/
//预定义的ip的范围
var ipranges = {
linklocal: ['169.254.0.0/16', 'fe80::/10'],
loopback: ['127.0.0.1/8', '::1/128'],
uniquelocal: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7']
};
//子网掩码常用点分十进制表示,我们还可以用CIDR的网络前缀法表示掩码,即“/<网络地址位数>;”。如138.96.0.0/16表示B类网络138.96.0.0的子网掩码为255.255.0.0。
//http://baike.baidu.com/link?url=mYHj2i69-z8qJNERGUejZmwXSWPDYk-eHDgnifqgaE8RNiqWEiI5CaMgfRN_egM9JdWeaKxWKnl0gDTr7ZV_I_
/**
* Get all addresses in the request, optionally stopping
* at the first untrusted.
* @param {Object} request
* @param {Function|Array|String} [trust]
* @api public
*/
function alladdrs(req, trust) {
// get addresses
var addrs = forwarded(req);
//得到所有的经过的ip地址,格式为[client,proxy1,proxy2........proxyN]
if (!trust) {
// Return all addresses
return addrs;
}
//如果没有指定trust那么表示没有指定信任列表那么返回所有的地址
if (typeof trust !== 'function') {
trust = compile(trust);
}
//如果指定的trust不是函数,那么调用compile,也就是把参数编译成为一个信任函数,参数可以是Array或者String!
for (var i = 0; i < addrs.length - 1; i++) {
if (trust(addrs[i], i)) continue;
//我们看到传入回调函数的参数其实是每一个地址和地址的下标是多少!
addrs.length = i + 1;
//如果你信任他那么就直接不对addrs进行任何处理,如若不信任也就是trust函数返回false那么这时候直接截断了
//所有如果有一个IP地址不信任那么直接截断!如[client,proxy1,proxy2........proxyN]如果proxy2不信任那么直接返回[client,proxy1,proxy2]
}
return addrs;
}
/**
* Compile argument into trust function.
*
* @param {Array|String} val
* @api private
*/
//调用方式为: proxyaddr.compile('localhost')
//或者为:proxyaddr(req, ['127.0.0.0/8', '10.0.0.0/8'])也就是里面的这个数组
//如果其中传入的参数在linklocal,loopback,uniquelocal中那么替换成为IP数组,否则不尽心任何处理,交给compileRangeSubnets和compileTrust处理
//其中compileRangeSubnets传入的参数是已经把上面的linklocal等都替换了以后的数组
function compile(val) {
if (!val) {
throw new TypeError('argument is required');
}
//如果参数是string那么修改为数组
var trust = typeof val === 'string'
? [val]
: val;
if (!Array.isArray(trust)) {
throw new TypeError('unsupported trust argument');
}
//如trust为['linklocal','loopback','uniquelocal','qinliang']
for (var i = 0; i < trust.length; i++) {
val = trust[i];
//如果数组对象不是ipranges中那么不进行任何处理
if (!ipranges.hasOwnProperty(val)) {
continue;
}
// Splice in pre-defined range
val = ipranges[val];
//如果获取到 linklocal: ['169.254.0.0/16', 'fe80::/10']或者获取到 loopback: ['127.0.0.1/8', '::1/128']
//或者获取到 uniquelocal: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7']
trust.splice.apply(trust, [i, 1].concat(val));
//替换这个数组['linklocal','loopback','uniquelocal'])为:['linklocal','loopback','uniquelocal'])其中linklocal等都被替换为上面的ipRange!
i += val.length - 1;
}
//把最后的trust传入函数compileRangeSubnets,函数compileRangeSubnets返回的都是这种结构类型的:[[ip, range],[ip, range],[ip, range]]
//接着用这种数据类型调用compileTrust函数!
return compileTrust(compileRangeSubnets(trust));
}
/**
* Compile `arr` elements into range subnets.
*
* @param {Array} arr
* @api private
*/
//如果我们传入到compile的数组为['linklocal','loopback'],这时候就是[ '169.254.0.0/16', 'fe80::/10', '127.0.0.1/8', '::1/128' ]
function compileRangeSubnets(arr) {
var rangeSubnets = new Array(arr.length);
for (var i = 0; i < arr.length; i++) {
//对每一个其中的ip地址全部进行解析调用parseipNotation保存到rangeSubnets中
rangeSubnets[i] = parseipNotation(arr[i]);
}
return rangeSubnets;
}
/**
* Compile range subnet array into trust function.
*
* @param {Array} rangeSubnets
* @api private
*/
function compileTrust(rangeSubnets) {
// Return optimized function based on length
//根据上面数组的长度调用不同的处理方法
var len = rangeSubnets.length;
return len === 0
? trustNone
: len === 1
? trustSingle(rangeSubnets[0])
: trustMulti(rangeSubnets);
}
/**
* Parse IP notation string into range subnet.
*
* @param {String} note
* @api private
*/
//对数组[ '169.254.0.0/16', 'fe80::/10', '127.0.0.1/8', '::1/128' ]中每一个IP进行解析
function parseipNotation(note) {
var pos = note.lastIndexOf('/');
//获取最后一个/,这个字符用来表示范围的
var str = pos !== -1
? note.substring(0, pos)
: note;
//获取没有含有/的前面的一部分IP地址
if (!isip(str)) {
throw new TypeError('invalid IP address: ' + str);
}
//如果前面一部分不是IP那么抛出错误
var ip = parseip(str)
//从字符串解析出来IP
if (pos === -1 && ip.kind() === 'ipv6' && ip.isIPv4MappedAddress()) {
// Store as IPv4
ip = ip.toIPv4Address();
}
//如果IP地址没有/符号,同时是ipv6的地址,而且isIPv4MappedAddress判断为true,这时候我们转化为IPV4地址
//上面任何一个条件不满足就执行这里的逻辑
var max = ip.kind() === 'ipv6'
? 128
: 32;
//如果地址为ipv6那么就是128为否则为32位!
var range = pos !== -1
? note.substring(pos + 1, note.length)
: null;
//如果地址中有/符号,那么就会对传入的数组[ '169.254.0.0/16', 'fe80::/10', '127.0.0.1/8', '::1/128' ]进行处理
//获取range部分,也就是/后面的部分!
if (range === null) {
range = max;
//如果没有/那么range就会是null,这时候range就是128或者32!
} else if (digitre.test(range)) {
//var digitre = /^[0-9]+$/;
//如果range全部是数字那么解析为10进制的range!
range = parseInt(range, 10);
} else if (ip.kind() === 'ipv4' && isip(range)) {
//如果是IPV4同时也是ip那么就调用parseNetmask
range = parseNetmask(range);
} else {
range = null;
}
//判断range是否是有效的数据
if (range <= 0 || range > max) {
throw new TypeError('invalid range on address: ' + note);
}
//第一个参数是真实的IP,第二个参数是range,是对/后面部分解析后获取到的值
return [ip, range];
}
/**
* Parse netmask string into CIDR range.
*
* @param {String} netmask
* @api private
*/
function parseNetmask(netmask) {
var ip = parseip(netmask);
var kind = ip.kind();
return kind === 'ipv4'
? ip.prefixLengthFromSubnetMask()
: null;
}
/**
* Determine address of proxied request.
*
* @param {Object} request
* @param {Function|Array|String} trust
* @api public
*/
//调用方式为:proxyaddr(req, function(addr){ return addr === '127.0.0.1' })
function proxyaddr(req, trust) {
if (!req) {
throw new TypeError('req argument is required');
}
if (!trust) {
throw new TypeError('trust argument is required');
}
var addrs = alladdrs(req, trust);
//继续调用alladdrs方法,把参数原封不动传递进去
var addr = addrs[addrs.length - 1];
//我们直接返回的是信任的那个地址
return addr;
}
/**
* Static trust function to trust nothing.
*
* @api private
*/
function trustNone() {
return false;
}
/**
* Compile trust function for multiple subnets.
*
* @param {Array} subnets
* @api private
*/
//如果有多个值得信任的ip就会调用这里的逻辑,参数为[[ip, range],[ip, range],[ip, range]]
function trustMulti(subnets) {
//返回一个高阶函数,这个高阶函数必须重新传入另外一个参数addr!
return function trust(addr) {
if (!isip(addr)) return false;
//如果传入的addr不是IP返回false
var ip = parseip(addr);
//解析addr参数为实际的ip!
var ipconv;
var kind = ip.kind();
for (var i = 0; i < subnets.length; i++) {
var subnet = subnets[i];
//获取一个子网
var subnetip = subnet[0];
//子网的ip
var subnetkind = subnetip.kind();
//子网ip的类型,是IPV4还是IPV6
var subnetrange = subnet[1];
//获取子网的范围
var trusted = ip;
//trusted就是我们传入的addr解析成为的IP地址
if (kind !== subnetkind) {
//如果传入的IP的类型和子网的类型不一样,同时子网是IPV4而传入的IP不是IPV4那么循环下一个子网!
if (subnetkind === 'ipv4' && !ip.isIPv4MappedAddress()) {
// Incompatible IP addresses
continue;
}
// Convert IP to match subnet IP kind
ipconv = ipconv || subnetkind === 'ipv4'
? ip.toIPv4Address()
: ip.toIPv4MappedAddress();
//把传入的IP转化为子网的IP类型
trusted = ipconv;
//trusted修改为我们转化后的IPV4地址
}
//如果传入的IP地址满足子网的类型,那么返回true!
if (trusted.match(subnetip, subnetrange)) {
return true;
}
}
//如果循环过后还是没有找到和子网匹配那么返回false
return false;
};
}
/**
* Compile trust function for single subnet.
* @param {Object} subnet
* @api private
*/
function trustSingle(subnet) {
var subnetip = subnet[0];
//获取子网的IP
var subnetkind = subnetip.kind();
//获取子网IP的类型
var subnetisipv4 = subnetkind === 'ipv4';
//查看子网的IP类型是否为IPV4
var subnetrange = subnet[1];
//获取子网的范围
return function trust(addr) {
if (!isip(addr)) return false;
//传入如果不是IP那么返回为false
var ip = parseip(addr);
var kind = ip.kind();
if (kind !== subnetkind) {
//如果参数的IP类型和子网的IP类型不一致,那么会继续判断
if (subnetisipv4 && !ip.isIPv4MappedAddress()) {
// Incompatible IP addresses
return false;
//如果子网类型是IPV4,但是传入的IP不是IPV4那么返回false
}
// Convert IP to match subnet IP kind
//否则如果子网是IPV4那么把我们的IP转化为IPV4地址!
ip = subnetisipv4
? ip.toIPv4Address()
: ip.toIPv4MappedAddress();
}
//成了判断子网的ip是否则子网的一个范围之内了
return ip.match(subnetip, subnetrange);
};
}
注意:这里面的内容还是挺多的,特别是当用户通过如下的方式进行调用的时候需要构建自己的过滤函数:
proxyaddr(req, 'loopback')
proxyaddr(req, ['loopback', 'fc00:ac:1ab5:fff::1/64'])
这部分的内容还是以后好好研究吧,既然是学习Express,我们还是从Express的角度来说吧。
最后,首先我们来看看应用级的settings中都包含什么内容:
settings:
{ 'x-powered-by': true,
//启用HTTP的头字段"X-Powered-By: Express,默认为true
etag: 'weak',
//设置http的Etag请求头,默认情况下是weak。etag设置的时候可以使用一些选项,但是这些选项只能用于动态文件而非静态文件。express.static会忽略这些设置
//这个etag的功能是使用etag包来完成的。
'etag fn': [Function: wetag],
env: 'development',
//表示环境模式,如果在生产环境下要设置为production
'query parser': 'extended',
'query parser fn': [Function],
'subdomain offset': 2,
'trust proxy': false,
'trust proxy fn': [Function: trustNone],
view: [Function: View],
views: 'C:\\Users\\Administrator\\Desktop\\WEB\\N-blog\\express\\views',
//默认情况下express的view层资源全部放在当前目录下的/views目录
'jsonp callback name': 'callback'
//对于express来说默认的回调函数是callback!当然可以通过app.set('jsonp callback name', 'cb')来修改
}
"query parser":
如果把这个设置false那么表示不进行请求参数的处理,当然也可以设置为"simple","extended"或者一个自定义的请求参数解析函数"simple"解析是基于Node.js的querystring模块来完成的,而extended是基于qs来完成的。而自定义的函数会接受到完整的请求内容,同时必须返回一个键值对
"Trust Proxy:"
指示这个应用运行在一个前置的代理后面,用X-Forwarded-*去获取连接和客户端的IP地址。这个头很容易伪造,于是获取到的客户端的IP地址是不可靠的。如果设置为true,Express就会通过前置代理或者一系列的代理来获取客户端的IP地址。这时候req.ips就会存在一系列的IP地址数组,包含客户端真实的地址以及代理服务器的地址
(1)如果设置为true那么就会包含X-Forwarded-*的头部,而且客户端的IP在最左边。如果设置为false,这时候就不会包含X-Forwarded-*头,如果要获取用户的IP地址那么英爱用req.connection.remoteAddress头,默认为false
(2)可以设置子网,或者一个IP数组用于信任。内置了如下的子网名称:
loopback - 127.0.0.1/8, ::1/128
linklocal - 169.254.0.0/16, fe80::/10
uniquelocal - 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
可以按照如下的方式设置信任的子网:
app.set('trust proxy', 'loopback') // specify a single subnet
app.set('trust proxy', 'loopback, 123.123.123.123') // specify a subnet and an address
app.set('trust proxy', 'loopback, linklocal, uniquelocal') // specify multiple subnets as CSV
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']) // specify multiple subnets as an array
如果设置了这个值,那么里面的IP地址或者子网就不会被监测。同时最接近服务器的不信任的地址就是客户端的IP
(3)信任代理服务器前n跳的客户端
(4)自己定义一个信任的实现,但是不建议这么做,除非你知道你做什么:
app.set('trust proxy', function (ip) {
if (ip === '127.0.0.1' || ip === '123.123.123.123') return true; // trusted IPs
else return false;
});
"views":
用于设置一个路径或者一个路径数组,这个路径用于存放应用的view层的资源,如果指定了一个数组那么会按顺序查找。默认值为process.cwd() + '/views'。其中process.cwd返回的是当前线程的工作目录
"env":
表示环境模式,如果在生产环境下要设置为production。默认是process.env.NODE_ENV,如果没有设置NODE_ENV那么就是"development"。NODE_ENV这个变量指定了一个应用工作的环境,通常为“development”或者"production"。一个很简单的提升性能的方式就是把他设置为”production“。这时候会有以下效果:缓存view template;缓存从后缀.css产生的CSS文件;不会产生冗长的错误信息。建议看一下最佳实践
"json spaces":
这个参数是指定JSON.stringify用于对结果进行缩进的空格数,没有默认值。
JSON.stringify({ uno: 1, dos: 2 }, null, '\t');
// returns the string:
// '{
// "uno": 1,
// "dos": 2
// }'
"strict routing":
是否开启严格路由,如果开启那么/foo和/foo/是不同的路由,否则两个路径是不同的
"view cache":
表示是否开启视图模板编译的缓存功能,在production模式下是true,否则为false
"view engine":
默认使用模板引擎
"x-powered-by":
是否开启HTTP头"X-Powered-By:Express"视图
"json replacer":
指定JSON.stringify的占位符。可以是一个函数或者数组,如果是函数那么有两个参数,被字符串化的key和value值。见下面的 例子就可以了:
function replacer(key, value) {
if (typeof value === "string") {
return undefined;
}
return value;
}
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, replacer);
console.log(jsonString);
//打印{"week":45,"month":7}
var arr=JSON.stringify(foo, ['week', 'month']);
console.log(arr);
// '{"week":45,"month":7}', only keep "week" and "month" properties
我们再来举一个例子:
var express=require('express');
var app=express();
function replacer(key, value) {
if (key === "car") {
//不要出现如if(key==='name')这样的判断
return undefined;
}
return value;
}
app.set('json replacer', replacer);
app.set('json spaces',5)
app.set('x-powered-by',false);
//返回一个http.Server对象,然后继续调用其listen方法
var admin = express();
app.get('/',function(req,res){
res.status(200).json({foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7})
});
这时候结果如下:
{
"foundation": "Mozilla",
"model": "box",
"week": 45,
"transport": "car",
"month": 7
}
很好奇的看看在express中的response.js中的res.json是如何实现的:
res.json = function json(obj) {
var val = obj;
// allow status / body
if (arguments.length === 2) {
// res.json(body, status) backwards compat
if (typeof arguments[1] === 'number') {
deprecate('res.json(obj, status): Use res.status(status).json(obj) instead');
this.statusCode = arguments[1];
} else {
deprecate('res.json(status, obj): Use res.status(status).json(obj) instead');
this.statusCode = arguments[0];
val = arguments[1];
}
}
// settings
var app = this.app;
var replacer = app.get('json replacer');
var spaces = app.get('json spaces');
var body = JSON.stringify(val, replacer, spaces);
// content-type
if (!this.get('Content-Type')) {
this.set('Content-Type', 'application/json');
}
return this.send(body);
};
"subdomain offset":
如果要获取子域名,那么要移除的点的个数!