node-smb-server
Version:
A Pure JavaScript SMB Server Implementation
439 lines (396 loc) • 12.9 kB
JavaScript
/*
* Copyright 2015 Adobe Systems Incorporated. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
'use strict';
var crypto = require('crypto');
var Path = require('path');
var Long = require('long');
var mime = require('mime');
var unorm = require('unorm');
// register (unofficial) mime type for InDesign files (.indd)
mime.define({ 'application/x-indesign': ['indd' ] });
/**
* Lookup a mime type based on file extension.
*
* @param {String} path resource path
* @param {String} [fallback] fallback mime type if none is found
* @return {String} mime type associated with the specified file type
*/
function lookupMimeType(path, fallback) {
return mime.lookup(path, fallback);
}
/**
* Number of milliseconds between Jan 1, 1601, 00:00:00 UTC and Jan 1, 1970, 00:00:00.0.
*/
var DELTA_EPOCH_MS = 11644473600000;
/**
* indicates that extended attribute (EA) support is required on this file
*/
var FILE_NEED_EA = 0x80;
/**
* Converts the system time (number of milliseconds since Jan 1, 1970, 00:00:00 UTC)
* to the SMB format time (number of 100ns since Jan 1, 1601, 00:00:00 UTC).
*
* @param {Number} ms number of milliseconds since Jan 1, 1970, 00:00:00 UTC.
* @return {Long} a 64bit signed integer representing the number of 100ns since Jan 1, 1601, 00:00:00 UTC.
*/
function systemToSMBTime(ms) {
var l = Long.fromNumber(ms);
return l.add(DELTA_EPOCH_MS).multiply(10000);
}
/**
* Converts the SMB format time (number of 100ns since Jan 1, 1601, 00:00:00 UTC)
* to the number of milliseconds since Jan 1, 1970, 00:00:00 UTC.
*
* @param {Long} l a 64bit signed integer representing the number of 100ns since Jan 1, 1601, 00:00:00 UTC.
* @return {Number} number of milliseconds since Jan 1, 1970, 00:00:00 UTC.
*/
function smbToSystemTime(l) {
return l.div(10000).subtract(DELTA_EPOCH_MS).toNumber();
}
/**
* Converts the system time (number of milliseconds since Jan 1, 1970, 00:00:00 UTC)
* to the legacy 16bit SMB_DATE representation.
*
* @param {Number} ms number of milliseconds since Jan 1, 1970, 00:00:00 UTC.
* @return {Number} a 16bit integer SMB_DATE value.
*/
function systemToLegacySMBDate(ms) {
var date = new Date(ms);
var result = date.getDate();
result += (date.getMonth() + 1) << 5;
result += (date.getFullYear() - 1980) << 9;
return result;
}
/**
* Converts the system time (number of milliseconds since Jan 1, 1970, 00:00:00 UTC)
* to the legacy 16bit SMB_TIME representation.
*
* @param {Number} ms number of milliseconds since Jan 1, 1970, 00:00:00 UTC.
* @return {Number} a 16bit integer SMB_TIME value.
*/
function systemToLegacySMBTime(ms) {
var date = new Date(ms);
var result = Math.floor(date.getSeconds() / 2);
result += date.getMinutes() << 5;
result += date.getHours() << 11;
return result;
}
/**
* Converts the legacy 16bit SMB_DATE and SMB_TIME representation to system time (number of milliseconds since Jan 1, 1970, 00:00:00 UTC).
*
* @param {Number} smbDate 16bit integer SMB_DATE
* @param {Number} smbTime 16bit integer SMB_TIME
* @return {Number} number of milliseconds since Jan 1, 1970, 00:00:00 UTC.
*/
function legacySMBToSystemDateTime(smbDate, smbTime) {
var year = ((smbDate & 0xfe00) >> 9) + 1980;
var month = ((smbDate & 0x01e0) >> 5) - 1;
var day = smbDate & 0x001f;
var hour = (smbTime & 0xf800) >> 11;
var minutes = (smbTime & 0x07e0) >> 5;
var seconds = (smbTime & 0x001f) * 2;
return new Date(year, month, day, hour, minutes, seconds).getTime();
}
/**
* Reads the 8 byte SMB format time and returns the
* milliseconds since 1 January 1970 00:00:00 UTC (Unix Epoch).
*
* The following rules apply:
* If the read 64bit value is -1 (0xFFFFFFFFFFFFFFFF) the return value is -1;
* If the read 64bit value is 0 (0x0000000000000000) the return value is 0;
* All other values are converted to the Unix Epoch.
*
* @param {Buffer} buf
* @param {Number} pos
* @return {Number} milliseconds since 1 January 1970 00:00:00 UTC (Unix Epoch)
*/
function readTimestamp(buf, pos) {
var timeLow = buf.readUInt32LE(pos);
var timeHigh = buf.readUInt32LE(pos + 4);
if (timeLow === 0xffffffff && timeHigh === 0xffffffff) {
return -1;
} else if (!timeHigh && !timeLow) {
return 0;
} else {
return new Date(smbToSystemTime(new Long(timeLow, timeHigh))).getTime();
}
}
/**
* Parses an encoded FEA (full extended attribute) list.
*
* @param {Buffer} buf
* @param {Number} pos
* @return {Object[]} array of objects representing the extended attributes
*/
function parseFEAList(buf, pos) {
if (buf.length - pos < 4) {
return null;
}
var result = [];
var sizeOfListInBytes = buf.readUInt32LE(pos);
if (sizeOfListInBytes <= 4) {
return result;
}
pos += 4;
buf = buf.slice(pos, pos + sizeOfListInBytes - 4);
pos = 0;
var flag, nameLength, valueLenght;
while (pos < buf.length - 5) {
var ea = {};
ea.offset = pos;
flag = buf.readUInt8(pos);
pos += 1;
nameLength = buf.readUInt8(pos);
pos += 1;
valueLenght = buf.readUInt32LE(pos);
pos += 2;
// zero-terminated ascii string
ea.name = buf.slice(pos, pos + nameLength).toString('ascii');
pos += nameLength + 1;
// non-zero-terminated ascii string
ea.value = buf.slice(pos, pos + valueLenght).toString('ascii');
pos += valueLenght;
ea.needEA = !!(flag & FILE_NEED_EA);
result.push(ea);
}
return result;
}
/**
* Extracts the bytes of an 0x0000-delimited utf16le encoded string (excluding the delimiter)
* @param buf
* @param pos
* @return {Buffer} bytes of a utf16le encoded string (excluding the 0x0000 delimiter)
*/
function extractUnicodeBytes(buf, pos) {
var off = pos;
while (buf.readUInt16LE(off)) {
off += 2;
}
return buf.slice(pos, off);
}
/**
* Extracts the bytes of an 0x00-delimited ascii encoded string (excluding the delimiter)
* @param buf
* @param pos
* @return {Buffer} bytes of an ascii encoded string (excluding the 0x00 delimiter)
*/
function extractAsciiBytes(buf, pos) {
var off = pos;
while (buf.readUInt8(off)) {
off += 1;
}
return buf.slice(pos, off);
}
function calculatePadLength(offset, alignment) {
var pad = alignment - (offset % alignment);
return pad < alignment ? pad : 0;
}
/**
* Normalize a SMB file path or pattern. Converts backslashes to slashes, makes sure
* the path name is absolute, and removes a trailing slash.
*
* @param {String} name name to normalize
* @returns {String} normalized name
*/
function normalizeSMBFileName(name) {
name = name.replace(/\\/g, '/');
if (!name.length || (name.length && name.charAt(0) !== '/')) {
name = '/' + name;
}
if (name.length > 1 && name.substr(-1) === '/') {
name = name.substr(0, name.length - 1);
}
return name;
}
function bufferEquals(a, b) {
if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b)) {
return undefined;
}
if (typeof a.equals === 'function') {
// node >= v0.12
return a.equals(b);
}
if (a.length !== b.length) {
return false;
}
for (var i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false;
}
}
return true;
}
/**
* Returns the last segment of a path
*
* @param {String} path
* @return {String} last segment of path
*/
function getPathName(path) {
var pos = path.lastIndexOf(Path.sep);
if (pos === -1) {
return path;
}
return path.slice(pos + 1);
}
/**
* Returns the file extension
*
* @param {String} name path or file name
* @returns {String} file extension
*/
function getFileExtension(name) {
var pos = name.lastIndexOf('.');
if (pos === -1) {
return name;
}
return name.slice(pos + 1);
}
/**
* Returns the parent path
*
* @param {String} path
* @return {String} parent path
*/
function getParentPath(path) {
var pos = path.lastIndexOf(Path.sep);
if (pos === -1) {
return null;
}
if (pos === 0) {
return Path.sep;
}
return path.slice(0, pos);
}
/**
* Strips the specified parent path
*
* @param {String} path
* @param {String} parentPath
* @return {String} stripped path
*/
function stripParentPath(path, parentPath) {
if (path === parentPath) {
return '';
}
if (path.indexOf(parentPath) === 0) {
path = path.substr(parentPath.length);
if (path.length > 1 && path.substr(-1) === Path.sep) {
// strip trailing slash
path = path.substr(0, path.length - 1);
}
}
return path;
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function generateRawUUID() {
var timeLow, timeMid, timeHiAndVersion, clockSeqHiAndReserved, clockSeqLow, node;
timeLow = getRandomInt(0, Math.pow(2, 32) - 1);
timeMid = getRandomInt(0, Math.pow(2, 16) - 1);
timeHiAndVersion = 0x4000 | getRandomInt(0, Math.pow(2, 12) - 1);
clockSeqHiAndReserved = 0x80 | getRandomInt(0, Math.pow(2, 6) - 1);
clockSeqLow = getRandomInt(0, Math.pow(2, 8) - 1);
node = crypto.randomBytes(6);
var buf = new Buffer(16);
var off = 0;
buf.writeUInt32LE(timeLow, off);
off += 4;
buf.writeUInt16LE(timeMid, off);
off += 2;
buf.writeUInt16LE(timeHiAndVersion, off);
off += 2;
buf.writeUInt8(clockSeqHiAndReserved, off);
off += 1;
buf.writeUInt8(clockSeqLow, off);
off += 1;
node.copy(buf, off);
return buf;
}
function rawUUIDToString(buf) {
var uuid = parseRawUUID(buf);
return decimalToHex(uuid.timeLow, 8)
+ '-' + decimalToHex(uuid.timeMid, 4)
+ '-' + decimalToHex(uuid.timeHiAndVersion, 4)
+ '-' + decimalToHex(uuid.clockSeqHiAndReserved, 2) + decimalToHex(uuid.clockSeqLow, 2)
+ '-' + uuid.node.toString('hex');
}
function parseRawUUID(buf) {
var uuid = {};
var off = 0;
uuid.timeLow = buf.readUInt32LE(off);
off += 4;
uuid.timeMid = buf.readUInt16LE(off);
off += 2;
uuid.timeHiAndVersion = buf.readUInt16LE(off);
off += 2;
uuid.clockSeqHiAndReserved = buf.readUInt8(off);
off += 1;
uuid.clockSeqLow = buf.readUInt8(off);
off += 1;
uuid.node = buf.slice(off, off + 6);
return uuid;
}
function rawUUIDFromString(str) {
// todo validation / error handling
var parts = str.split('-');
// parts.length === 5
var b1 = new Buffer(parts[0], 'hex');
var b2 = new Buffer(parts[1], 'hex');
var b3 = new Buffer(parts[2], 'hex');
var b4 = new Buffer(parts[3].substr(0, 2), 'hex');
var b5 = new Buffer(parts[3].substr(2), 'hex');
var b6 = new Buffer(parts[4], 'hex');
return Buffer.concat([ b1, b2, b3, b4, b5, b6 ]);
}
function decimalToHex(d, padding) {
var hex = Number(d).toString(16);
padding = typeof (padding) === 'undefined' || padding === null ? 2 : padding;
while (hex.length < padding) {
hex = '0' + hex;
}
return hex;
}
function unicodeNormalize(str) {
// need to normalize unicode strings in order to avoid issues related to different code points (e.g. 'caf\u00E9' vs 'cafe\u0301').
// note that using the builtin string.normalize('NFKD') does not work...
return unorm.nfkd(str);
}
function unicodeEquals(str1, str2) {
return unicodeNormalize(str1) === unicodeNormalize(str2);
}
module.exports.EMPTY_BUFFER = new Buffer(0);
module.exports.ZERO_GUID = new Buffer(16);
module.exports.ZERO_GUID.fill(0);
module.exports.lookupMimeType = lookupMimeType;
module.exports.systemToSMBTime = systemToSMBTime;
module.exports.smbToSystemTime = smbToSystemTime;
module.exports.systemToLegacySMBDate = systemToLegacySMBDate;
module.exports.systemToLegacySMBTime = systemToLegacySMBTime;
module.exports.legacySMBToSystemDateTime = legacySMBToSystemDateTime;
module.exports.readTimestamp = readTimestamp;
module.exports.parseFEAList = parseFEAList;
module.exports.extractUnicodeBytes = extractUnicodeBytes;
module.exports.extractAsciiBytes = extractAsciiBytes;
module.exports.calculatePadLength = calculatePadLength;
module.exports.normalizeSMBFileName = normalizeSMBFileName;
module.exports.bufferEquals = bufferEquals;
module.exports.getParentPath = getParentPath;
module.exports.stripParentPath = stripParentPath;
module.exports.getPathName = getPathName;
module.exports.getFileExtension = getFileExtension;
module.exports.generateRawUUID = generateRawUUID;
module.exports.rawUUIDToString = rawUUIDToString;
module.exports.rawUUIDFromString = rawUUIDFromString;
module.exports.unicodeNormalize = unicodeNormalize;
module.exports.unicodeEquals = unicodeEquals;