diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ac17d84 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +File containning the license of your package. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6723274 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Keeweb for YunoHost # + +Web client for reading and editing Keepass files locally. It can also sync with Dropbox. + +**Current version: 1.0.4** + +## Usage ## +- Add application source files into `sources` subfolder. + + +**More information on the documentation page:** +https://keeweb.info/ + +**Live demo:** +https://app.keeweb.info/ diff --git a/conf/nginx.conf b/conf/nginx.conf new file mode 100644 index 0000000..600e47f --- /dev/null +++ b/conf/nginx.conf @@ -0,0 +1,10 @@ +location YNH_WWW_PATH { + # Path to source + alias YNH_WWW_ALIAS ; + + index index.html; + default_type text/html; + + # Include SSOWAT user panel. + include conf.d/yunohost_panel.conf.inc; +} diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..be5350f --- /dev/null +++ b/manifest.json @@ -0,0 +1,60 @@ +{ + "name": "Keeweb", + "id": "keeweb", + "description": { + "en": "Password manager compatible with KeePass.", + "fr": "Gestionnaire de mots de passe compatible avec KeePass." + }, + "url": "https://keeweb.info/", + "license": "free", + "maintainer": { + "name": "Scith", + "url": "https://github.com/scith" + }, + "multi_instance": "false", + "services": [ + "nginx", + "php5-fpm" + ], + "arguments": { + "install" : [ + { + "name": "domain", + "type": "domain", + "ask": { + "en": "Choose a domain name for Keeweb", + "fr": "Choisissez un nom de domaine pour Keeweb" + }, + "example": "example.com" + }, + { + "name": "path", + "type": "path", + "ask": { + "en": "Choose a path for Keeweb", + "fr": "Choisissez un chemin pour Keeweb" + }, + "example": "/passwords", + "default": "/passwords" + }, + { + "name": "dropbox", + "type": "user", + "ask": { + "en": "To sync with Dropbox, create a Dropbox app and enter here your app key (Settings/App key)", + "fr": "Pour synchroniser avec Dropbox, créez une app Dropbox et entrez ici votre clé (Settings/App key)" + }, + "example": "qp7ctun6qt5n9d6" + }, + { + "name": "is_public", + "ask": { + "en": "Is it a public application?", + "fr": "Est-ce une application publique ?" + }, + "choices": ["Yes", "No"], + "default": "Yes" + } + ] + } +} diff --git a/scripts/backup b/scripts/backup new file mode 100644 index 0000000..a38128d --- /dev/null +++ b/scripts/backup @@ -0,0 +1,18 @@ +#!/bin/bash + +# causes the shell to exit if any subcommand or pipeline returns a non-zero status +set -e + +app=keeweb + +# The parameter $1 is the backup directory location which will be compressed afterward + backup_dir=$1/apps/$app + sudo mkdir -p $backup_dir + +# Backup sources & data + sudo cp -a /var/www/$app/. $backup_dir/sources + +# Copy Nginx and YunoHost parameters to make the script "standalone" + sudo cp -a /etc/yunohost/apps/$app/. $backup_dir/yunohost + domain=$(sudo yunohost app setting $app domain) + sudo cp -a /etc/nginx/conf.d/$domain.d/$app.conf $backup_dir/nginx.conf \ No newline at end of file diff --git a/scripts/install b/scripts/install new file mode 100644 index 0000000..357572c --- /dev/null +++ b/scripts/install @@ -0,0 +1,50 @@ +#!/bin/bash + +# causes the shell to exit if any subcommand or pipeline returns a non-zero status +set -e + +app=keeweb + +# Retrieve arguments + domain=$1 + path=$2 + dropbox=$3 + is_public=$4 + +# Save app settings + sudo yunohost app setting $app dropbox -v "$dropbox" + sudo yunohost app setting $app is_public -v "$is_public" + +# Check domain/path availability + sudo yunohost app checkurl $domain$path -a $app \ + || (echo "Path not available: $domain$path" && exit 1) + +# If a Dropbox app key is present, add it + if [ -n "$dropbox" ]; + then + sudo sed -i.bak s/qp7ctun6qt5n9d6/$dropbox/g ../sources/index.html + fi + +# Copy source files + final_path=/var/www/$app + sudo mkdir -p $final_path + sudo cp -a ../sources/. $final_path + +# Set permissions to app files + sudo chown -R root:root $final_path + +# Modify Nginx configuration file and copy it to Nginx conf directory + sed -i "s@YNH_WWW_PATH@$path@g" ../conf/nginx.conf + sed -i "s@YNH_WWW_ALIAS@$final_path/@g" ../conf/nginx.conf + sudo cp ../conf/nginx.conf /etc/nginx/conf.d/$domain.d/$app.conf + +# If app is public, add url to SSOWat conf as skipped_uris + if [ "$is_public" = "Yes" ]; + then + # unprotected_uris allows SSO credentials to be passed anyway. + sudo yunohost app setting $app unprotected_uris -v "/" + fi + +# Restart services + sudo service nginx reload + sudo yunohost app ssowatconf diff --git a/scripts/remove b/scripts/remove new file mode 100644 index 0000000..f8d619c --- /dev/null +++ b/scripts/remove @@ -0,0 +1,18 @@ +#!/bin/bash +app=keeweb + +# Retrieve arguments + domain=$(sudo yunohost app setting $app domain) + path=$(sudo yunohost app setting $app path) + dropbox=$(sudo yunohost app setting $app dropbox) + is_public=$(sudo yunohost app setting $app is_public) + +# Remove sources + sudo rm -rf /var/www/$app + +# Remove configuration files + sudo rm -f /etc/nginx/conf.d/$domain.d/$app.conf + +# Restart services + sudo service nginx reload + sudo yunohost app ssowatconf diff --git a/scripts/restore b/scripts/restore new file mode 100644 index 0000000..2afd736 --- /dev/null +++ b/scripts/restore @@ -0,0 +1,23 @@ +#!/bin/bash + +# causes the shell to exit if any subcommand or pipeline returns a non-zero status +set -e + +app=keeweb + +# The parameter $1 is the uncompressed restore directory location + backup_dir=$1/apps/$app + +# Restore sources & data + sudo cp -a $backup_dir/sources/. /var/www/$app + +# Restore permissions to app files + sudo chown -R root:root $final_path + +# Restore Nginx and YunoHost parameters + sudo cp -a $backup_dir/yunohost/. /etc/yunohost/apps/$app + domain=$(sudo yunohost app setting $app domain) + sudo cp -a $backup_dir/nginx.conf /etc/nginx/conf.d/$domain.d/$app.conf + +# Restart webserver + sudo service nginx reload diff --git a/scripts/upgrade b/scripts/upgrade new file mode 100644 index 0000000..7f01178 --- /dev/null +++ b/scripts/upgrade @@ -0,0 +1,47 @@ +#!/bin/bash + +# causes the shell to exit if any subcommand or pipeline returns a non-zero status +set -e + +app=keeweb + +# Retrieve arguments + domain=$(sudo yunohost app setting $app domain) + path=$(sudo yunohost app setting $app path) + dropbox=$(sudo yunohost app setting $app dropbox) + is_public=$(sudo yunohost app setting $app is_public) + +# Remove trailing "/" for next commands + path=${path%/} + +# If a Dropbox app key is present, add it + if [ -n "$dropbox" ]; + then + sudo sed -i.bak s/qp7ctun6qt5n9d6/$dropbox/g ../sources/index.html + fi + +# Copy source files + final_path=/var/www/$app + sudo mkdir -p $final_path + sudo cp -a ../sources/. $final_path + +# Set permissions to app files + sudo chown -R root:root $final_path + +# Modify Nginx configuration file and copy it to Nginx conf directory + sed -i "s@YNH_WWW_PATH@$path@g" ../conf/nginx.conf + sed -i "s@YNH_WWW_ALIAS@$final_path/@g" ../conf/nginx.conf + sudo cp ../conf/nginx.conf /etc/nginx/conf.d/$domain.d/$app.conf + +# If app is public, add url to SSOWat conf as skipped_uris + if [ "$is_public" = "Yes" ]; + then + # See install script + sudo yunohost app setting $app unprotected_uris -v "/" + # Remove old settings + sudo yunohost app setting $app skipped_uris -d + fi + +# Restart services + sudo service nginx reload + sudo yunohost app ssowatconf diff --git a/sources/app.js b/sources/app.js new file mode 100644 index 0000000..d24eda0 --- /dev/null +++ b/sources/app.js @@ -0,0 +1,166 @@ +'use strict'; + +/* jshint node:true */ +/* jshint browser:false */ + +var app = require('app'), + path = require('path'), + BrowserWindow = require('browser-window'), + Menu = require('menu'), + Tray = require('tray'); + +var mainWindow = null, + appIcon = null, + openFile = process.argv.filter(function(arg) { return /\.kdbx$/i.test(arg); })[0], + ready = false, + restartPending = false, + htmlPath = path.join(__dirname, 'index.html'); + +process.argv.forEach(function(arg) { + if (arg.lastIndexOf('--htmlpath=', 0) === 0) { + htmlPath = path.resolve(arg.replace('--htmlpath=', ''), 'index.html'); + } +}); + +app.on('window-all-closed', function() { + if (restartPending) { + // unbind all handlers, load new app.js module and pass control to it + app.removeAllListeners('window-all-closed'); + app.removeAllListeners('ready'); + app.removeAllListeners('open-file'); + app.removeAllListeners('activate'); + var userDataAppFile = path.join(app.getPath('userData'), 'app.js'); + delete require.cache[require.resolve('./app.js')]; + require(userDataAppFile); + app.emit('ready'); + } else { + if (process.platform !== 'darwin') { + app.quit(); + } + } +}); +app.on('ready', function() { + createMainWindow(); +}); +app.on('open-file', function(e, path) { + e.preventDefault(); + openFile = path; + notifyOpenFile(); +}); +app.on('activate', function() { + if (process.platform === 'darwin') { + if (!mainWindow) { + createMainWindow(); + } + } +}); +app.restartApp = function() { + restartPending = true; + mainWindow.close(); + setTimeout(function() { restartPending = false; }, 1000); +}; +app.openWindow = function(opts) { + return new BrowserWindow(opts); +}; +app.minimizeApp = function() { + if (process.platform === 'win32') { + mainWindow.minimize(); + mainWindow.setSkipTaskbar(true); + appIcon = new Tray(path.join(__dirname, 'icon.png')); + appIcon.on('click', restoreMainWindow); + var contextMenu = Menu.buildFromTemplate([ + { label: 'Open KeeWeb', click: restoreMainWindow }, + { label: 'Quit KeeWeb', click: closeMainWindow } + ]); + appIcon.setContextMenu(contextMenu); + appIcon.setToolTip('KeeWeb'); + } +}; +app.getMainWindow = function() { + return mainWindow; +}; + +function createMainWindow() { + mainWindow = new BrowserWindow({ + show: false, + width: 1000, height: 700, 'min-width': 600, 'min-height': 300, + icon: path.join(__dirname, 'icon.png') + }); + setMenu(); + mainWindow.loadURL('file://' + htmlPath); + mainWindow.webContents.on('dom-ready', function() { + setTimeout(function() { + mainWindow.show(); + ready = true; + notifyOpenFile(); + }, 50); + }); + mainWindow.on('closed', function() { + mainWindow = null; + }); + mainWindow.on('minimize', function() { + emitBackboneEvent('launcher-minimize'); + }); +} + +function restoreMainWindow() { + appIcon.destroy(); + appIcon = null; + mainWindow.restore(); + mainWindow.setSkipTaskbar(false); +} + +function closeMainWindow() { + appIcon.destroy(); + appIcon = null; + emitBackboneEvent('launcher-exit-request'); +} + +function emitBackboneEvent(e) { + mainWindow.webContents.executeJavaScript('Backbone.trigger("' + e + '");'); +} + +function setMenu() { + if (process.platform === 'darwin') { + var name = require('app').getName(); + var template = [ + { + label: name, + submenu: [ + { label: 'About ' + name, role: 'about' }, + { type: 'separator' }, + { label: 'Services', role: 'services', submenu: [] }, + { type: 'separator' }, + { label: 'Hide ' + name, accelerator: 'Command+H', role: 'hide' }, + { label: 'Hide Others', accelerator: 'Command+Shift+H', role: 'hideothers' }, + { label: 'Show All', role: 'unhide' }, + { type: 'separator' }, + { label: 'Quit', accelerator: 'Command+Q', click: function() { app.quit(); } } + ] + }, + { + label: 'Edit', + submenu: [ + { label: 'Undo', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, + { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, + { type: 'separator' }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', role: 'cut' }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', role: 'copy' }, + { label: 'Paste', accelerator: 'CmdOrCtrl+V', role: 'paste' }, + { label: 'Select All', accelerator: 'CmdOrCtrl+A', role: 'selectall' } + ] + } + ]; + var menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); + } +} + +function notifyOpenFile() { + if (ready && openFile && mainWindow) { + openFile = openFile.replace(/"/g, '\\"').replace(/\\/g, '\\\\'); + mainWindow.webContents.executeJavaScript('if (window.launcherOpen) { window.launcherOpen("' + openFile + '"); } ' + + ' else { window.launcherOpenedFile="' + openFile + '"; }'); + openFile = null; + } +} diff --git a/sources/icon.png b/sources/icon.png new file mode 100644 index 0000000..0e2774a Binary files /dev/null and b/sources/icon.png differ diff --git a/sources/index.html b/sources/index.html new file mode 100644 index 0000000..243fd34 --- /dev/null +++ b/sources/index.html @@ -0,0 +1,21 @@ +KeeWeb \ No newline at end of file diff --git a/sources/main.js b/sources/main.js new file mode 100644 index 0000000..84015d2 --- /dev/null +++ b/sources/main.js @@ -0,0 +1,43 @@ +// KeeWeb launcher script + +// This script is distributed with the app and is its entry point +// It checks whether the app is available in userData folder and if its version is higher than local, launches it +// This script is the only part which will be updated only with the app itself, auto-update will not change it + +// (C) Antelle 2015, MIT license https://github.com/antelle/keeweb + +'use strict'; + +/* jshint node:true */ +/* jshint browser:false */ + +var app = require('app'), + path = require('path'), + fs = require('fs'); + +var userDataDir = app.getPath('userData'), + appPathUserData = path.join(userDataDir, 'app.js'), + appPath = path.join(__dirname, 'app.js'); + +if (fs.existsSync(appPathUserData)) { + var versionLocal = require('./package.json').version; + try { + var versionUserData = require(path.join(userDataDir, 'package.json')).version; + versionLocal = versionLocal.split('.'); + versionUserData = versionUserData.split('.'); + for (var i = 0; i < versionLocal.length; i++) { + if (+versionUserData[i] > +versionLocal[i]) { + appPath = appPathUserData; + break; + } + if (+versionUserData[i] < +versionLocal[i]) { + break; + } + } + } + catch (e) { + console.error('Error reading user file version', e); + } +} + +require(appPath); diff --git a/sources/node_modules/node-stream-zip/README.md b/sources/node_modules/node-stream-zip/README.md new file mode 100644 index 0000000..f612305 --- /dev/null +++ b/sources/node_modules/node-stream-zip/README.md @@ -0,0 +1,70 @@ +# node-stream-zip + +node.js library for reading and extraction of ZIP archives. +Features: + +- it never loads entire archive into memory, everything is read by chunks +- large archives support +- all operations are non-blocking, no sync i/o +- fast initialization +- no dependencies, no binary addons +- decompression with built-in zlib module +- deflate, deflate64, sfx, macosx/windows built-in archives + +# Installation + +`$ npm install node-stream-zip` + +# Usage + +```javascript +var StreamZip = require('node-stream-zip'); +var zip = new StreamZip({ + file: 'archive.zip', + storeEntries: true +}); +zip.on('error', function(err) { /*handle*/ }); +zip.on('ready', function() { + console.log('Entries read: ' + zip.entriesCount); + // stream to stdout + zip.stream('node/benchmark/net/tcp-raw-c2s.js', function(err, stm) { + stm.pipe(process.stdout); + }); + // extract file + zip.extract('node/benchmark/net/tcp-raw-c2s.js', './temp/', function(err) { + console.log('Entry extracted'); + }); + // extract folder + zip.extract('node/benchmark/', './temp/', function(err, count) { + console.log('Extracted ' + count + ' entries'); + }); + // extract all + zip.extract(null, './temp/', function(err, count) { + console.log('Extracted ' + count + ' entries'); + }); + // read file as buffer in sync way + var data = zip.entryDataSync('README.md'); +}); +zip.on('extract', function(entry, file) { + console.log('Extracted ' + entry.name + ' to ' + file); +}); +zip.on('entry', function(entry) { + // called on load, when entry description has been read + // you can already stream this entry, without waiting until all entry descriptions are read (suitable for very large archives) + console.log('Read entry ', entry.name); +}); +``` + +# Building + +The project doesn't require building. To run unit tests with [nodeunit](https://github.com/caolan/nodeunit): +`$ npm test` + +# Known issues + +- [utf8](https://github.com/rubyzip/rubyzip/wiki/Files-with-non-ascii-filenames) file names +- AES encrypted files + +# Contributors + +ZIP parsing code has been partially forked from [cthackers/adm-zip](https://github.com/cthackers/adm-zip) (MIT license). \ No newline at end of file diff --git a/sources/node_modules/node-stream-zip/node_stream_zip.js b/sources/node_modules/node-stream-zip/node_stream_zip.js new file mode 100644 index 0000000..5c39f48 --- /dev/null +++ b/sources/node_modules/node-stream-zip/node_stream_zip.js @@ -0,0 +1,849 @@ +/** + * @license node-stream-zip | (c) 2015 Antelle | https://github.com/antelle/node-stream-zip/blob/master/MIT-LICENSE.txt + * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/MIT-LICENSE.txt + */ + +// region Deps + +var + util = require('util'), + fs = require('fs'), + path = require('path'), + events = require('events'), + zlib = require('zlib'), + stream = require('stream'); + +// endregion + +// region Constants + +var consts = { + /* The local file header */ + LOCHDR : 30, // LOC header size + LOCSIG : 0x04034b50, // "PK\003\004" + LOCVER : 4, // version needed to extract + LOCFLG : 6, // general purpose bit flag + LOCHOW : 8, // compression method + LOCTIM : 10, // modification time (2 bytes time, 2 bytes date) + LOCCRC : 14, // uncompressed file crc-32 value + LOCSIZ : 18, // compressed size + LOCLEN : 22, // uncompressed size + LOCNAM : 26, // filename length + LOCEXT : 28, // extra field length + + /* The Data descriptor */ + EXTSIG : 0x08074b50, // "PK\007\008" + EXTHDR : 16, // EXT header size + EXTCRC : 4, // uncompressed file crc-32 value + EXTSIZ : 8, // compressed size + EXTLEN : 12, // uncompressed size + + /* The central directory file header */ + CENHDR : 46, // CEN header size + CENSIG : 0x02014b50, // "PK\001\002" + CENVEM : 4, // version made by + CENVER : 6, // version needed to extract + CENFLG : 8, // encrypt, decrypt flags + CENHOW : 10, // compression method + CENTIM : 12, // modification time (2 bytes time, 2 bytes date) + CENCRC : 16, // uncompressed file crc-32 value + CENSIZ : 20, // compressed size + CENLEN : 24, // uncompressed size + CENNAM : 28, // filename length + CENEXT : 30, // extra field length + CENCOM : 32, // file comment length + CENDSK : 34, // volume number start + CENATT : 36, // internal file attributes + CENATX : 38, // external file attributes (host system dependent) + CENOFF : 42, // LOC header offset + + /* The entries in the end of central directory */ + ENDHDR : 22, // END header size + ENDSIG : 0x06054b50, // "PK\005\006" + ENDSUB : 8, // number of entries on this disk + ENDTOT : 10, // total number of entries + ENDSIZ : 12, // central directory size in bytes + ENDOFF : 16, // offset of first CEN header + ENDCOM : 20, // zip file comment length + MAXFILECOMMENT : 0xFFFF, + + /* Compression methods */ + STORED : 0, // no compression + SHRUNK : 1, // shrunk + REDUCED1 : 2, // reduced with compression factor 1 + REDUCED2 : 3, // reduced with compression factor 2 + REDUCED3 : 4, // reduced with compression factor 3 + REDUCED4 : 5, // reduced with compression factor 4 + IMPLODED : 6, // imploded + // 7 reserved + DEFLATED : 8, // deflated + ENHANCED_DEFLATED: 9, // enhanced deflated + PKWARE : 10,// PKWare DCL imploded + // 11 reserved + BZIP2 : 12, // compressed using BZIP2 + // 13 reserved + LZMA : 14, // LZMA + // 15-17 reserved + IBM_TERSE : 18, // compressed using IBM TERSE + IBM_LZ77 : 19, //IBM LZ77 z + + /* General purpose bit flag */ + FLG_ENC : 0, // encrypted file + FLG_COMP1 : 1, // compression option + FLG_COMP2 : 2, // compression option + FLG_DESC : 4, // data descriptor + FLG_ENH : 8, // enhanced deflation + FLG_STR : 16, // strong encryption + FLG_LNG : 1024, // language encoding + FLG_MSK : 4096, // mask header values + FLG_ENTRY_ENC : 1, + + /* 4.5 Extensible data fields */ + EF_ID : 0, + EF_SIZE : 2, + + /* Header IDs */ + ID_ZIP64 : 0x0001, + ID_AVINFO : 0x0007, + ID_PFS : 0x0008, + ID_OS2 : 0x0009, + ID_NTFS : 0x000a, + ID_OPENVMS : 0x000c, + ID_UNIX : 0x000d, + ID_FORK : 0x000e, + ID_PATCH : 0x000f, + ID_X509_PKCS7 : 0x0014, + ID_X509_CERTID_F : 0x0015, + ID_X509_CERTID_C : 0x0016, + ID_STRONGENC : 0x0017, + ID_RECORD_MGT : 0x0018, + ID_X509_PKCS7_RL : 0x0019, + ID_IBM1 : 0x0065, + ID_IBM2 : 0x0066, + ID_POSZIP : 0x4690, + + EF_ZIP64_OR_32 : 0xffffffff, + EF_ZIP64_OR_16 : 0xffff, + EF_ZIP64_SUNCOMP : 0, + EF_ZIP64_SCOMP : 8, + EF_ZIP64_RHO : 16, + EF_ZIP64_DSN : 24 +}; + +// endregion + +// region StreamZip + +var StreamZip = function(config) { + var + fd, + fileSize, + chunkSize, + ready = false, + that = this, + op, + centralDirectory, + + entries = config.storeEntries !== false ? {} : null, + fileName = config.file; + + open(); + + function open() { + fs.open(fileName, 'r', function(err, f) { + if (err) + return that.emit('error', err); + fd = f; + fs.fstat(fd, function(err, stat) { + if (err) + return that.emit('error', err); + fileSize = stat.size; + chunkSize = config.chunkSize || Math.round(fileSize / 1000); + chunkSize = Math.max(Math.min(chunkSize, Math.min(128*1024, fileSize)), Math.min(1024, fileSize)); + readCentralDirectory(); + }); + }); + } + + function readCentralDirectory() { + var totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, fileSize); + op = { + win: new FileWindowBuffer(fd), + totalReadLength: totalReadLength, + minPos: fileSize - totalReadLength, + lastPos: fileSize, + chunkSize: 1024 + }; + op.win.read(fileSize - op.chunkSize, op.chunkSize, readCentralDirectoryCallback); + } + + function readCentralDirectoryCallback(err, bytesRead) { + if (err || !bytesRead) + return that.emit('error', err || 'Archive read error'); + var + buffer = op.win.buffer, + pos = op.lastPos, + bufferPosition = pos - op.win.position, + minPos = Math.max(op.minPos, bufferPosition); + while (--pos >= minPos && --bufferPosition >= 0) { + if (buffer[bufferPosition] === 0x50) { // quick check that the byte is 'P' + if (buffer.readUInt32LE(bufferPosition) === consts.ENDSIG) { // "PK\005\006" + op = {}; + try { + centralDirectory = new CentralDirectoryHeader(); + centralDirectory.read(buffer.slice(bufferPosition, bufferPosition + consts.ENDHDR)); + centralDirectory.headerOffset = pos; + if (centralDirectory.commentLength) + that.comment = buffer.slice(bufferPosition + consts.ENDHDR, + bufferPosition + consts.ENDHDR + centralDirectory.commentLength).toString(); + else + that.comment = null; + that.entriesCount = centralDirectory.volumeEntries; + that.centralDirectory = centralDirectory; + readEntries(); + } catch (err) { + that.emit('error', err); + } + return; + } + } + } + if (pos === minPos) { + return that.emit('error', 'Bad archive format'); + } + op.lastPos = pos + 1; + op.chunkSize *= 2; + if (pos <= minPos) + return that.emit('error', 'Bad archive'); + var expandLength = Math.min(op.chunkSize, pos - minPos); + op.win.expandLeft(expandLength, readCentralDirectoryCallback); + } + + function readEntries() { + op = { + win: new FileWindowBuffer(fd), + pos: centralDirectory.offset, + chunkSize: chunkSize, + entriesLeft: centralDirectory.volumeEntries + }; + op.win.read(op.pos, Math.min(chunkSize, fileSize - op.pos), readEntriesCallback); + } + + function readEntriesCallback(err, bytesRead) { + if (err || !bytesRead) + return that.emit('error', err || 'Entries read error'); + var + buffer = op.win.buffer, + bufferPos = op.pos - op.win.position, + bufferLength = buffer.length, + entry = op.entry; + try { + while (op.entriesLeft > 0) { + if (!entry) { + entry = new ZipEntry(); + entry.readHeader(buffer, bufferPos); + entry.headerOffset = op.pos; + op.entry = entry; + op.pos += consts.CENHDR; + bufferPos += consts.CENHDR; + } + var entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen; + var advanceBytes = entryHeaderSize + (op.entriesLeft > 1 ? consts.CENHDR : 0); + if (bufferLength - bufferPos < advanceBytes) { + op.win.moveRight(chunkSize, readEntriesCallback, bufferPos); + op.move = true; + return; + } + entry.read(buffer, bufferPos); + if (entries) + entries[entry.name] = entry; + that.emit('entry', entry); + op.entry = entry = null; + op.entriesLeft--; + op.pos += entryHeaderSize; + bufferPos += entryHeaderSize; + } + that.emit('ready'); + } catch (err) { + that.emit('error', err); + } + } + + function checkEntriesExist() { + if (!entries) + throw 'storeEntries disabled'; + } + + Object.defineProperty(this, 'ready', { get: function() { return ready; } }); + + this.entry = function(name) { + checkEntriesExist(); + return entries[name]; + }; + + this.entries = function() { + checkEntriesExist(); + return entries; + }; + + this.stream = function(entry, callback) { + return openEntry(entry, function(err, entry) { + if (err) + return callback(err); + var offset = dataOffset(entry); + var entryStream = new EntryDataReaderStream(fd, offset, entry.compressedSize); + if (entry.method === consts.STORED) { + } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) { + entryStream = entryStream.pipe(zlib.createInflateRaw()); + } else { + return callback('Unknown compression method: ' + entry.method); + } + if (canVerifyCrc(entry)) + entryStream = entryStream.pipe(new EntryVerifyStream(entryStream, entry.crc, entry.size)); + callback(null, entryStream); + }, false); + }; + + this.entryDataSync = function(entry) { + var err = null; + openEntry(entry, function(e, en) { + err = e; + entry = en; + }, true); + if (err) + throw err; + var + data = new Buffer(entry.compressedSize), + bytesRead; + new FsRead(fd, data, 0, entry.compressedSize, dataOffset(entry), function(e, br) { + err = e; + bytesRead = br; + }).read(true); + if (err) + throw err; + if (entry.method === consts.STORED) { + } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) { + data = zlib.inflateRawSync(data); + } else { + throw 'Unknown compression method: ' + entry.method; + } + if (data.length !== entry.size) + throw 'Invalid size'; + if (canVerifyCrc(entry)) { + var verify = new CrcVerify(entry.crc, entry.size); + verify.data(data); + } + return data; + }; + + function openEntry(entry, callback, sync) { + if (typeof entry === 'string') { + checkEntriesExist(); + entry = entries[entry]; + if (!entry) + return callback('Entry not found'); + } + if (!entry.isFile) + return callback('Entry is not file'); + if (!fd) + return callback('Archive closed'); + var buffer = new Buffer(consts.LOCHDR); + new FsRead(fd, buffer, 0, buffer.length, entry.offset, function(err) { + if (err) + return callback(err); + try { + entry.readDataHeader(buffer); + if (entry.encrypted) { + return callback('Entry encrypted'); + } + callback(null, entry); + } catch (ex) { + callback(ex); + } + }).read(sync); + } + + function dataOffset(entry) { + return entry.offset + consts.LOCHDR + entry.fnameLen + entry.extraLen; + } + + function canVerifyCrc(entry) { + // if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written + return (entry.flags & 0x8) != 0x8 + } + + function extract(entry, outPath, callback) { + that.stream(entry, function (err, stm) { + if (err) { + callback(err); + } else { + var fsStm, errThrown; + stm.on('error', function(err) { + errThrown = err; + if (fsStm) { + stm.unpipe(fsStm); + fsStm.close(function () { + callback(err); + }); + } + }); + fs.open(outPath, 'w', function(err, fdFile) { + if (err) + return callback(err || errThrown); + if (errThrown) { + fs.close(fd, function() { + callback(errThrown); + }); + return; + } + fsStm = fs.createWriteStream(outPath, { fd: fdFile }); + fsStm.on('finish', function() { + that.emit('extract', entry, outPath); + if (!errThrown) + callback(); + }); + stm.pipe(fsStm); + }); + } + }); + } + + function createDirectories(baseDir, dirs, callback) { + if (!dirs.length) + return callback(); + var dir = dirs.shift(); + dir = path.join(baseDir, path.join.apply(path, dir)); + fs.mkdir(dir, function(err) { + if (err && err.code !== 'EEXIST') + return callback(err); + createDirectories(baseDir, dirs, callback); + }); + } + + function extractFiles(baseDir, baseRelPath, files, callback, extractedCount) { + if (!files.length) + return callback(null, extractedCount); + var file = files.shift(); + var targetPath = path.join(baseDir, file.name.replace(baseRelPath, '')); + extract(file, targetPath, function (err) { + if (err) + return callback(err, extractedCount); + extractFiles(baseDir, baseRelPath, files, callback, extractedCount + 1); + }); + } + + this.extract = function(entry, outPath, callback) { + var entryName = entry || ''; + if (typeof entry === 'string') { + entry = this.entry(entry); + if (entry) { + entryName = entry.name; + } else { + if (entryName.length && entryName[entryName.length - 1] !== '/') + entryName += '/'; + } + } + if (!entry || entry.isDirectory) { + var files = [], dirs = [], allDirs = {}; + for (var e in entries) { + if (Object.prototype.hasOwnProperty.call(entries, e) && e.lastIndexOf(entryName, 0) === 0) { + var relPath = e.replace(entryName, ''); + var childEntry = entries[e]; + if (childEntry.isFile) { + files.push(childEntry); + relPath = path.dirname(relPath); + } + if (relPath && !allDirs[relPath] && relPath[0] !== '.') { + allDirs[relPath] = true; + var parts = relPath.split('/').filter(function (f) { return f; }); + if (parts.length) + dirs.push(parts); + } + } + } + dirs.sort(function(x, y) { return x.length - y.length; }); + if (dirs.length) { + createDirectories(outPath, dirs, function (err) { + if (err) + callback(err); + else + extractFiles(outPath, entryName, files, callback, 0); + }); + } else { + extractFiles(outPath, entryName, files, callback, 0); + } + } else { + fs.stat(outPath, function(err, stat) { + if (stat && stat.isDirectory()) + extract(entry, path.join(outPath, path.basename(entry.name)), callback); + else + extract(entry, outPath, callback); + }); + } + }; + + this.close = function() { + if (fd) { + fs.close(fd, function() { + fd = null; + }); + } + }; +}; + +util.inherits(StreamZip, events.EventEmitter); + +// endregion + +// region CentralDirectoryHeader + +var CentralDirectoryHeader = function() { +}; + +CentralDirectoryHeader.prototype.read = function(data) { + if (data.length != consts.ENDHDR || data.readUInt32LE(0) != consts.ENDSIG) + throw 'Invalid central directoty'; + // number of entries on this volume + this.volumeEntries = data.readUInt16LE(consts.ENDSUB); + // total number of entries + this.totalEntries = data.readUInt16LE(consts.ENDTOT); + // central directory size in bytes + this.size = data.readUInt32LE(consts.ENDSIZ); + // offset of first CEN header + this.offset = data.readUInt32LE(consts.ENDOFF); + // zip file comment length + this.commentLength = data.readUInt16LE(consts.ENDCOM); +}; + +// endregion + +// region ZipEntry + +var ZipEntry = function() { +}; + +ZipEntry.prototype.readHeader = function(data, offset) { + // data should be 46 bytes and start with "PK 01 02" + if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) != consts.CENSIG) { + throw 'Invalid entry header'; + } + // version made by + this.verMade = data.readUInt16LE(offset + consts.CENVEM); + // version needed to extract + this.version = data.readUInt16LE(offset + consts.CENVER); + // encrypt, decrypt flags + this.flags = data.readUInt16LE(offset + consts.CENFLG); + // compression method + this.method = data.readUInt16LE(offset + consts.CENHOW); + // modification time (2 bytes time, 2 bytes date) + this.time = data.readUInt32LE(offset + consts.CENTIM); + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(offset + consts.CENCRC); + // compressed size + this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ); + // uncompressed size + this.size = data.readUInt32LE(offset + consts.CENLEN); + // filename length + this.fnameLen = data.readUInt16LE(offset + consts.CENNAM); + // extra field length + this.extraLen = data.readUInt16LE(offset + consts.CENEXT); + // file comment length + this.comLen = data.readUInt16LE(offset + consts.CENCOM); + // volume number start + this.diskStart = data.readUInt16LE(offset + consts.CENDSK); + // internal file attributes + this.inattr = data.readUInt16LE(offset + consts.CENATT); + // external file attributes + this.attr = data.readUInt32LE(offset + consts.CENATX); + // LOC header offset + this.offset = data.readUInt32LE(offset + consts.CENOFF); +}; + +ZipEntry.prototype.readDataHeader = function(data) { + // 30 bytes and should start with "PK\003\004" + if (data.readUInt32LE(0) != consts.LOCSIG) { + throw 'Invalid local header'; + } + // version needed to extract + this.version = data.readUInt16LE(consts.LOCVER); + // general purpose bit flag + this.flags = data.readUInt16LE(consts.LOCFLG); + // compression method + this.method = data.readUInt16LE(consts.LOCHOW); + // modification time (2 bytes time ; 2 bytes date) + this.time = data.readUInt32LE(consts.LOCTIM); + // uncompressed file crc-32 value + this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc; + // compressed size + this.compressedSize = data.readUInt32LE(consts.LOCSIZ) || this.compressedSize; + // uncompressed size + this.size = data.readUInt32LE(consts.LOCLEN) || this.size; + // filename length + this.fnameLen = data.readUInt16LE(consts.LOCNAM); + // extra field length + this.extraLen = data.readUInt16LE(consts.LOCEXT); +}; + +ZipEntry.prototype.read = function(data, offset) { + this.name = data.slice(offset, offset += this.fnameLen).toString(); + var lastChar = data[offset - 1]; + this.isDirectory = (lastChar == 47) || (lastChar == 92); + + if (this.extraLen) { + this.readExtra(data, offset); + offset += this.extraLen; + } + this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null; +}; + +ZipEntry.prototype.readExtra = function(data, offset) { + var signature, size, maxPos = offset + this.extraLen; + while (offset < maxPos) { + signature = data.readUInt16LE(offset); + offset += 2; + size = data.readUInt16LE(offset); + offset += 2; + if (consts.ID_ZIP64 === signature) { + this.parseZip64Extra(data, offset, size); + } + offset += size; + } +}; + +ZipEntry.prototype.parseZip64Extra = function(data, offset, length) { + if (length >= consts.EF_ZIP64_SCOMP && this.size === consts.EF_ZIP64_OR_32) + this.size = this.readUInt64LE(data, offset + consts.EF_ZIP64_SUNCOMP); + if (length >= consts.EF_ZIP64_RHO && this.compressedSize === consts.EF_ZIP64_OR_32) + this.compressedSize = this.readUInt64LE(data, offset + consts.EF_ZIP64_SCOMP); + if (length >= consts.EF_ZIP64_DSN && this.offset === consts.EF_ZIP64_OR_32) + this.offset = this.readUInt64LE(data, offset + consts.EF_ZIP64_RHO); + if (length >= consts.EF_ZIP64_DSN + 4 && this.diskStart === consts.EF_ZIP64_OR_16) + this.diskStart = data.readUInt32LE(consts.EF_ZIP64_DSN); +}; + +ZipEntry.prototype.readUInt64LE = function(buffer, offset) { + return (buffer.readUInt32LE(offset + 4) << 4) + buffer.readUInt32LE(offset); +}; + +Object.defineProperty(ZipEntry.prototype, 'encrypted', { + get: function() { return (this.flags & consts.FLG_ENTRY_ENC) == consts.FLG_ENTRY_ENC; } +}); + +Object.defineProperty(ZipEntry.prototype, 'isFile', { + get: function() { return !this.isDirectory; } +}); + +// endregion + +// region FsRead + +var FsRead = function(fd, buffer, offset, length, position, callback) { + this.fd = fd; + this.buffer = buffer; + this.offset = offset; + this.length = length; + this.position = position; + this.callback = callback; + this.bytesRead = 0; + this.waiting = false; +}; + +FsRead.prototype.read = function(sync) { + //console.log('read', this.position, this.bytesRead, this.length, this.offset); + this.waiting = true; + if (sync) { + try { + var bytesRead = fs.readSync(this.fd, this.buffer, this.offset + this.bytesRead, + this.length - this.bytesRead, this.position + this.bytesRead); + this.readCallback(sync, null, bytesRead); + } catch (err) { + this.readCallback(sync, err, null); + } + } else { + fs.read(this.fd, this.buffer, this.offset + this.bytesRead, + this.length - this.bytesRead, this.position + this.bytesRead, + this.readCallback.bind(this, sync)); + } +}; + +FsRead.prototype.readCallback = function(sync, err, bytesRead) { + if (typeof bytesRead === 'number') + this.bytesRead += bytesRead; + if (err || !bytesRead || this.bytesRead === this.length) { + this.waiting = false; + return this.callback(err, this.bytesRead); + } else { + this.read(sync); + } +}; + +// endregion + +// region FileWindowBuffer + +var FileWindowBuffer = function(fd) { + this.position = 0; + this.buffer = new Buffer(0); + + var fsOp = null; + + this.checkOp = function() { + if (fsOp && fsOp.waiting) + throw 'Operation in progress'; + }; + + this.read = function(pos, length, callback) { + this.checkOp(); + if (this.buffer.length < length) + this.buffer = new Buffer(length); + this.position = pos; + fsOp = new FsRead(fd, this.buffer, 0, length, this.position, callback).read(); + }; + + this.expandLeft = function(length, callback) { + this.checkOp(); + this.buffer = Buffer.concat([new Buffer(length), this.buffer]); + this.position -= length; + if (this.position < 0) + this.position = 0; + fsOp = new FsRead(fd, this.buffer, 0, length, this.position, callback).read(); + }; + + this.expandRight = function(length, callback) { + this.checkOp(); + var offset = this.buffer.length; + this.buffer = Buffer.concat([this.buffer, new Buffer(length)]); + fsOp = new FsRead(fd, this.buffer, offset, length, this.position + offset, callback).read(); + }; + + this.moveRight = function(length, callback, shift) { + this.checkOp(); + if (shift) { + this.buffer.copy(this.buffer, 0, shift); + } else { + shift = 0; + } + this.position += shift; + fsOp = new FsRead(fd, this.buffer, this.buffer.length - shift, shift, this.position + this.buffer.length - shift, callback).read(); + }; +}; + +// endregion + +// region EntryDataReaderStream + +var EntryDataReaderStream = function(fd, offset, length) { + stream.Readable.prototype.constructor.call(this); + this.fd = fd; + this.offset = offset; + this.length = length; + this.pos = 0; + this.readCallback = this.readCallback.bind(this); +}; + +util.inherits(EntryDataReaderStream, stream.Readable); + +EntryDataReaderStream.prototype._read = function(n) { + var buffer = new Buffer(Math.min(n, this.length - this.pos)); + if (buffer.length) { + fs.read(this.fd, buffer, 0, buffer.length, this.offset + this.pos, this.readCallback); + } else { + this.push(null); + } +}; + +EntryDataReaderStream.prototype.readCallback = function(err, bytesRead, buffer) { + this.pos += bytesRead; + if (err) { + this.emit('error', err); + this.push(null); + } else if (!bytesRead) { + this.push(null); + } else { + if (bytesRead !== buffer.length) + buffer = buffer.slice(0, bytesRead); + this.push(buffer); + } +}; + +// endregion + +// region EntryVerifyStream + +var EntryVerifyStream = function(baseStm, crc, size) { + stream.Transform.prototype.constructor.call(this); + this.verify = new CrcVerify(crc, size); + var that = this; + baseStm.on('error', function(e) { + that.emit('error', e); + }); +}; + +util.inherits(EntryVerifyStream, stream.Transform); + +EntryVerifyStream.prototype._transform = function(data, encoding, callback) { + try { + this.verify.data(data); + callback(null, data); + } catch (err) { + callback(err, data); + } +}; + +// endregion + +// region CrcVerify + +var CrcVerify = function(crc, size) { + this.crc = crc; + this.size = size; + this.state = { + crc: ~0, + size: 0 + }; +}; + +CrcVerify.prototype.data = function(data) { + var crcTable = CrcVerify.getCrcTable(); + var crc = this.state.crc, off = 0, len = data.length; + while (--len >= 0) + crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8); + this.state.crc = crc; + this.state.size += data.length; + if (this.state.size >= this.size) { + var buf = new Buffer(4); + buf.writeInt32LE(~this.state.crc & 0xffffffff, 0); + crc = buf.readUInt32LE(0); + if (crc !== this.crc) + throw 'Invalid CRC'; + if (this.state.size !== this.size) + throw 'Invalid size'; + } +}; + +CrcVerify.getCrcTable = function() { + var crcTable = CrcVerify.crcTable; + if (!crcTable) { + CrcVerify.crcTable = crcTable = []; + var b = new Buffer(4); + for (var n = 0; n < 256; n++) { + var c = n; + for (var k = 8; --k >= 0; ) + if ((c & 1) != 0) { c = 0xedb88320 ^ (c >>> 1); } else { c = c >>> 1; } + if (c < 0) { + b.writeInt32LE(c, 0); + c = b.readUInt32LE(0); + } + crcTable[n] = c; + } + } + return crcTable; +}; + +// endregion + +// region exports + +module.exports = StreamZip; + +// endregion diff --git a/sources/node_modules/node-stream-zip/package.json b/sources/node_modules/node-stream-zip/package.json new file mode 100644 index 0000000..52bb90a --- /dev/null +++ b/sources/node_modules/node-stream-zip/package.json @@ -0,0 +1,65 @@ +{ + "name": "node-stream-zip", + "version": "1.2.1", + "description": "node.js library for reading and extraction of ZIP archives", + "keywords": [ + "zip", + "archive", + "unzip", + "stream" + ], + "homepage": "https://github.com/antelle/node-stream-zip", + "author": { + "name": "Antelle", + "email": "antelle.net@gmail.com", + "url": "https://github.com/antelle" + }, + "bugs": { + "url": "https://github.com/antelle/node-stream-zip/issues", + "email": "antelle.net@gmail.com" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/antelle/node-stream-zip/blob/master/MIT-LICENSE.txt" + } + ], + "files": [ + "node_stream_zip.js" + ], + "scripts": { + "test": "nodeunit test/tests.js" + }, + "main": "node_stream_zip.js", + "repository": { + "type": "git", + "url": "git+https://github.com/antelle/node-stream-zip.git" + }, + "engines": { + "node": ">=0.10.0" + }, + "devDependencies": {}, + "gitHead": "527b59025358dd69ee8d71c4e34798deff3375d3", + "_id": "node-stream-zip@1.2.1", + "_shasum": "98882b14651694added2c5a567ec150d59a30985", + "_from": "node-stream-zip@>=1.2.1 <2.0.0", + "_npmVersion": "2.5.1", + "_nodeVersion": "0.12.0", + "_npmUser": { + "name": "antelle", + "email": "antelle.net@gmail.com" + }, + "maintainers": [ + { + "name": "antelle", + "email": "antelle.net@gmail.com" + } + ], + "dist": { + "shasum": "98882b14651694added2c5a567ec150d59a30985", + "tarball": "http://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.2.1.tgz" + }, + "directories": {}, + "_resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.2.1.tgz", + "readme": "ERROR: No README data found!" +} diff --git a/sources/package.json b/sources/package.json new file mode 100644 index 0000000..1e9d37f --- /dev/null +++ b/sources/package.json @@ -0,0 +1,13 @@ +{ + "name": "KeeWeb", + "version": "1.0.4", + "description": "KeePass web app", + "main": "main.js", + "repository": "https://github.com/antelle/keeweb", + "author": "Antelle", + "license": "MIT", + "readme": "../README.md", + "dependencies": { + "node-stream-zip": "^1.2.1" + } +}