diff --git a/.gitignore b/.gitignore index d7109ad28..b4681b0a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Logs logs *.log +.DS_Store tmp # Runtime data diff --git a/lib/https/index.js b/lib/https/index.js index 94a869f19..ed44e0a5b 100644 --- a/lib/https/index.js +++ b/lib/https/index.js @@ -98,12 +98,14 @@ function handleWebsocket(socket, clientIp, callback) { options = url.parse(fullUrl); resolveHost(fullUrl, function(ip) { var isWss = options.protocol == 'wss'; - reqSocket = (isWss ? tls : net).connect({ - rejectUnauthorized: false, - host: ip, - port: options.port || (isWss ? 443 : 80) - }, pipeData); - abortIfUnavailable(reqSocket); + util.domain.run(function() { + reqSocket = (isWss ? tls : net).connect({ + rejectUnauthorized: false, + host: ip, + port: options.port || (isWss ? 443 : 80) + }, pipeData); + abortIfUnavailable(reqSocket); + }); }); } diff --git a/lib/rules/storage.js b/lib/rules/storage.js new file mode 100644 index 000000000..434cbacd5 --- /dev/null +++ b/lib/rules/storage.js @@ -0,0 +1,284 @@ +var fs = require('fs'); +var fse = require('fs-extra'); +var path = require('path'); +var RETRY_INTERVAL = 16000; + +function readFileSafe(file) { + try { + file = fs.readFileSync(file, {encoding: 'utf8'}); + } catch(e) { + file = null; + } + + return file || ''; +} + +function readJsonSafe(file) { + try { + file = fse.readJsonSync(file); + } catch(e) { + file = null; + } + + return file || {}; +} + +function noop() {} + +function Storage(dir) { + var self = this; + if (!(self instanceof Storage)) { + return new Storage(dir); + } + + self._dir = dir; + fse.ensureDirSync(dir); + + self._files = path.join(dir, 'files'); + self._meta = path.join(dir, 'meta'); + self._properties = path.join(dir, 'properties'); + fse.ensureDirSync(self._files); + fse.ensureFileSync(self._meta); + fse.ensureFileSync(self._properties); + + var maxIndex = -1; + var files = {}; + var meta = readJsonSafe(self._meta); + var marks = meta.marks || []; + fs.readdirSync(self._files) + .forEach(function(file) { + var name = file.split('.'); + if (name.length != 2 || !/^\d+$/.test(name[0])) { + return; + } + var index = parseInt(name[0], 10); + var filename = decodeURIComponent(name[1]); + if (index > maxIndex) { + maxIndex = index; + } + files[filename] = { + index: index, + name: filename, + mark: marks.indexOf(filename) == -1 ? false : true, + data: readFileSafe(path.join(dir, file)) + }; + }); + + self._cache = { + maxIndex: maxIndex, + files: files, + meta: meta, + properties: readJsonSafe(self._properties) + }; + +} + +var proto = Storage.prototype; + +proto._writeMeta = function _writeMeta() { + var self = this; + if (self._writeMetaPending) { + self._writeMetaWaiting = true; + return; + } + clearTimeout(self._writeMetaTimeout); + self._writeMetaPending = true; + fse.outputJson(self._meta, self._cache.meta, function(err) { + self._writeMetaPending = false; + if (self._writeMetaWaiting) { + self._writeMetaWaiting = false; + self._writeMeta(); + } else if (err) { + self._writeMetaTimeout = setTimeout(self._writeMeta.bind(self), RETRY_INTERVAL); + } + }); +}; + +proto._writeProperties = function _writeProperties() { + var self = this; + if (self._writePropertiesPending) { + self._writePropertiesWaiting = true; + return; + } + clearTimeout(self._writePropertiesTimeout); + self._writePropertiesPending = true; + fse.outputJson(self._properties, self._cache.properties, function(err) { + self._writePropertiesPending = false; + if (self._writePropertiesWaiting) { + self._writePropertiesWaiting = false; + self._writeProperties(); + } else if (err) { + self._writePropertiesTimeout = setTimeout(self._writeProperties.bind(self), RETRY_INTERVAL); + } + }); +}; + +proto._writeFile = function _writeFile(file) { + var self = this; + if (!(file = self._cache.files[file])) { + return; + } + if (file._pending) { + file._waiting = true; + return; + } + clearTimeout(file._timeout); + file._pending = true; + fs.writeFile(self._getFilePath(file), file.data, function(err) { + file._pending = false; + if (file._waiting) { + file._waiting = false; + self._writeFile(file.name); + } else if (err) { + file._timeout = setTimeout(function() { + self._writeFile(file.name); + }, RETRY_INTERVAL); + } + }); +}; + +proto._getFilePath = function _getFilePath(file) { + file = typeof file == 'string' ? this._cache.files[file] : file; + return file && path.join(this._files, file.index + '.' + encodeURIComponent(file.name)); +}; + +proto.count = function count() { + return Object.keys(this._cache.files).length; +}; + +proto.isMarked = function isMarked(file) { + if (!file) { + return; + } + + file = this._cache.file[file]; + return !!(file && file.mark); +}; + +proto.markFile = function markFile(file) { + if (!file) { + return; + } + + var self = this; + var cache = self._cache; + file = cache.files[file]; + if (!file) { + return false; + } + file.mark = true; + var marks = cache.meta.marks = cache.meta.marks || []; + if (marks.indexOf(file.name) == -1) { + marks.push(file.name); + self._writeMeta(); + } + return true; +}; + +proto.unmarkFile = function unmarkFile(file) { + if (!file) { + return; + } + var self = this; + var marks = self._cache.meta.marks; + var index = marks && marks.indexOf(file); + if (index > -1) { + marks.splice(index, 1); + self._writeMeta(); + } +}; + +proto.getMarkFileList = function getMarkFileList() { + + return this._cache.meta.marks || []; +}; + +proto.existsFile = function existsFile(file) { + + return this._cache.files[file]; +}; + +proto.getFileList = function getFileList() { + var cache = this._cache; + var list = []; + Object.keys(cache.files) + .forEach(function(file) { + list.push(cache.files[file]); + }); + list.sort(function(prev, next) { + return prev.index > next.index ? 1 : -1; + }); + return list; +}; + +proto.writeFile = function writeFile(file, data) { + if (!file) { + return; + } + + var self = this; + var cache = self._cache; + var fileData = cache.files[file]; + if (!fileData) { + fileData = cache.files[file] = { + index: ++cache.maxIndex, + name: file + }; + } + fileData.data = data == null ? '' : data; + self._writeFile(file); + return fileData; +}; + +proto.readFile = function(file) { + return file && this._cache.files[file]; +}; + +proto.removeFile = function removeFile(file) { + var files = this._cache.files; + file = file && files[file]; + if (!file) { + return; + } + + delete files[file.name]; + fs.unlink(this._getFilePath(file), noop); +}; + +proto.renameFile = function renameFile(file, newFile) { + var cache = this._cache; + if (!newFile || !(file = cache.files[file]) + || cache.files[newFile]) { + return; + } + + var path = this._getFilePath(file); + delete cache.files[file.name]; + file.name = newFile; + cache.files[newFile] = file; + fs.rename(path, this._getFilePath(file), noop); //不考虑并发 + return true; +}; + +proto.setProperty = function setProperty(name, value) { + this._cache.properties[name] = value; + this._writeProperties(); +}; + +proto.setProperties = function setProperties(obj) { + if (!obj) { + return; + } + + var props = this._cache.properties; + Object.keys(obj).forEach(function(key) { + props[key] = obj[key]; + }); + this._writeProperties(); +}; + +proto.getProperty = function getProperty(name) { + return this._cache.properties[name]; +}; + +module.exports = Storage; \ No newline at end of file diff --git a/lib/util/connect-http.js b/lib/util/connect-http.js index f63dbe87c..122d99284 100644 --- a/lib/util/connect-http.js +++ b/lib/util/connect-http.js @@ -29,10 +29,12 @@ function connect(options, callback) { var client = http.request(options) .on('connect', function (res, socket, head) { socket.on('error', util.noop); - execCallback(null, options.isHttps ? tls.connect({ - rejectUnauthorized: false, - socket: socket - }).on('error', util.noop) : socket); + util.domain.run(function() { + execCallback(null, options.isHttps ? tls.connect({ + rejectUnauthorized: false, + socket: socket + }).on('error', util.noop) : socket); + }); }).on('error', execCallback); client.end(); return client; diff --git a/lib/util/connect-socks.js b/lib/util/connect-socks.js index c9f4cee6a..b42c9875e 100644 --- a/lib/util/connect-socks.js +++ b/lib/util/connect-socks.js @@ -37,11 +37,13 @@ module.exports = function connect(proxyOptions, callback) { auths: auths }, function(socket) { socket.on('error', util.noop); - execCallback(null, isHttps ? tls.connect({ - rejectUnauthorized: false, - socket: socket - }).on('error', util.noop) : socket); - }).on('error', execCallback); + util.domain.run(function() { + execCallback(null, isHttps ? tls.connect({ + rejectUnauthorized: false, + socket: socket + }).on('error', util.noop) : socket); + }).on('error', execCallback); + }); } catch(err) {//socksv5会限制一些参数的格式 execCallback(err); } diff --git a/lib/util/index.js b/lib/util/index.js index 16d8f792b..4b49e8245 100644 --- a/lib/util/index.js +++ b/lib/util/index.js @@ -12,6 +12,13 @@ var zlib = require('zlib'); var PipeStream = require('pipestream'); var config = require('../../package.json'); var now = Date.now(); +/** + * 如果https请求放长时间了,会抛以下异常 + * see: https://github.com/joyent/node/issues/9272 + */ +var domain = exports.domain = require('domain').create(); +domain.on('error', noop); + var OLD_LOCAL_DATA_PATH = exports.OLD_LOCAL_DATA_PATH = path.join(__dirname, '../../../.whistle data'); var LOCAL_DATA_PATH = exports.LOCAL_DATA_PATH = path.join(__dirname, '../../../' + config.dataDirname); diff --git a/package.json b/package.json index 83f6b7419..2e2df9ec0 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "pipestream": "*", "starting": ">=0.0.9", "node-forge": "0.6.34", - "socksv5": "0.0.6" + "socksv5": "0.0.6", + "fs-extra": "0.22.1" }, "scripts": { "start": "node app.js"