diff --git a/.gitignore b/.gitignore index 0c86496..790d57b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ *.pyc *.pyo +/tools/zerobinpaste.js +/tools/zerobinpaste.min +/tools/zerobinpaste + # files generated by setuptools *.egg-info diff --git a/README.rst b/README.rst index 4ed7b20..e1bbb3a 100644 --- a/README.rst +++ b/README.rst @@ -57,7 +57,8 @@ Other features - copy paste to clipboard in a click; - get paste short URL in a click; - own previous pastes history; -- visual hash of a paste to easily tell it apart from others in a list. +- visual hash of a paste to easily tell it apart from others in a list; +- optional command-line tool to encrypt and paste data from shell or scripts. Technologies used ================== @@ -69,6 +70,7 @@ Technologies used - Bootstrap_, the Twitter HTML5/CSS3 framework - VizHash.js_ to create visual hashes from pastes - Cherrypy_ (server only) +- `node.js`_ (for optional command-line tool only) Known issues @@ -96,5 +98,6 @@ What does 0bin not implement? .. _Bootstrap: http://twitter.github.com/bootstrap/ .. _VizHash.js: https://github.com/sametmax/VizHash.js .. _Cherrypy: http://www.cherrypy.org/ (server only) +.. _node.js: http://nodejs.org/ .. _is not worth it: http://stackoverflow.com/questions/201705/how-many-random-elements-before-md5-produces-collisions .. _WTF licence: http://en.wikipedia.org/wiki/WTFPL diff --git a/docs/en/intro.rst b/docs/en/intro.rst index 7b45cae..9653a63 100644 --- a/docs/en/intro.rst +++ b/docs/en/intro.rst @@ -45,7 +45,8 @@ Other features - copy paste to clipboard in a click; - get paste short URL in a click; - own previous pastes history; -- visual hash of a paste to easily tell it apart from others in a list. +- visual hash of a paste to easily tell it apart from others in a list; +- `optional command-line tool`_ to encrypt and paste data from shell or scripts. Technologies used ================== @@ -57,6 +58,7 @@ Technologies used - Bootstrap_, the Twitter HTML5/CSS3 framework - VizHash.js_ to create visual hashes from pastes - Cherrypy_ (server only) +- `node.js`_ (for optional command-line tool only) Known issues @@ -84,4 +86,6 @@ What does 0bin not implement? .. _Bootstrap: http://twitter.github.com/bootstrap/ .. _VizHash.js: https://github.com/sametmax/VizHash.js .. _Cherrypy: http://www.cherrypy.org/ (server only) +.. _node.js: http://nodejs.org/ +.. _optional command-line tool: ./zerobinpaste_tool .. _is not worth it: http://stackoverflow.com/questions/201705/how-many-random-elements-before-md5-produces-collisions diff --git a/docs/en/zerobinpaste_tool.rst b/docs/en/zerobinpaste_tool.rst new file mode 100644 index 0000000..ba3d410 --- /dev/null +++ b/docs/en/zerobinpaste_tool.rst @@ -0,0 +1,100 @@ +============================== +zerobinpaste command-line tool +============================== + +zerobinpaste is a simple CLI tool (analogous to pastebinit or wgetpaste) to use +with files or shell redirection in terminal or simple scripts. + +Example use-cases might look like:: + + % zerobinpaste README.rst + http://some.0bin.site/paste/0cc3d8a8... + + % grep error /var/log/syslog | zerobinpaste + http://some.0bin.site/paste/81fd1324... + + % zerobinpaste docs/en/*.rst + easy_install.rst http://some.0bin.site/paste/9adc576a... + apache_install.rst http://some.0bin.site/paste/01408cbd... + options.rst http://some.0bin.site/paste/921b2768... + ... + + + % ps axlf | zerobinpaste | mail -s "Process tree on $(date)" root + +Produced links can then be copy-pasted to some IRC channel or used in whatever +other conceivable way. + +Tool does encryption by itself on the client machine and key (after hashmark) is +never sent to server or anywhere but the tool's stdout stream (e.g. terminal). + +Tool has to be built with `node.js`_ separately (see below). + + +Usage +===== + +At least the pastebin site (main URL where you'd paste stuff with the browser) +has to be specified for the tool to use either via -u (--url) option (can be +simplified with shell alias - e.g. ``alias zp='zerobinpaste -u http://some.0bin.site``) +or in the "~/.zerobinpasterc" configuration file (json format). + +| Non-option arguments are interpreted as files to upload/paste contents of. +| If no arguments are specified, data to paste will be read from stdin stream. + +Simple configuration file may look like this: + + {"url": "http://some.0bin.site"} + +Any options (in the long form, e.g. "url" for --url above) that are allowed on +the command-line can be specified there. + +Run the tool with -h or --help option to see full list of supported parameters. + + +Build / Installation +==================== + +In essence: + + 0bin% cd tools + 0bin/tools% make + ... + 0bin/tools% cp zerobinpaste ~/bin # install to PATH + +"npm" binary (packaged and installed with node.js) is required to pull in build +dependencies, if necessary, and "node" binary is required for produced binary to +run. + +Use "make" in "tools" path to produce non-minified runnable "zerobinpaste" +script there. + +``make ugly`` command can be used instead of ``make`` to create "minified" +version (using/installing uglifyjs_, about 25% smaller in size). + +Resulting "zerobinpaste" script requires only node.js ("node" binary) installed +to run and can be placed in any of the PATH dirs (e.g. "~/bin", +"/usr/local/bin") to be run just as "zerobinpaste". + + +Why node.js and not python +========================== + +Unfortunately, it's fairly complex and unreliable to replicate non-trivial and +undocumented encryption protocol that SJCL_ convenience methods employ, and any +mistake in encryption is guaranteed to produce unreadable paste. + +Current implementation uses same JavaScript code (and V8 node.js engine) that +browsers do, hence can be fairly simple and robust. + +Future development plans include supporting configurable, less complex and more +widespread encryption schemas, allowing for simplier non-javascript client as +well. + +See `related pull request`_ for more details. + + +.. _node.js: http://nodejs.org/ +.. _uglifyjs: https://github.com/mishoo/UglifyJS +.. _SJCL: http://crypto.stanford.edu/sjcl/ +.. _related pull request: https://github.com/sametmax/0bin/pull/39 diff --git a/docs/index.rst b/docs/index.rst index 9c65518..50c8992 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -22,10 +22,11 @@ | en/using_supervisor | fr/using_supervisor | | en/theming | fr/theming | | en/options | fr/options | +| en/zerobinpaste_tool | | | | | |`Report a bug`_ |`Signaler un bug`_ | +-------------------------+--------------------------------+ .. _Signaler un bug: https://github.com/sametmax/0bin/issues -.. _Report a bug: https://github.com/sametmax/0bin/issues \ No newline at end of file +.. _Report a bug: https://github.com/sametmax/0bin/issues diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000..b7dc3e5 --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,25 @@ + +zerobinpaste: zerobinpaste.js commander.js ../zerobin/static/js/sjcl.js ../zerobin/static/js/lzw.js + echo '#!/usr/bin/env node' > zerobinpaste + cat commander.js ../zerobin/static/js/sjcl.js ../zerobin/static/js/lzw.js >> zerobinpaste + cat zerobinpaste.js >> zerobinpaste + chmod +x zerobinpaste + +ugly: zerobinpaste + uglifyjs=$$(PATH="$$(npm bin):$$PATH" which uglifyjs 2>/dev/null) \ + || { npm install uglify-js; uglifyjs=$$(PATH="$$(npm bin):$$PATH" which uglifyjs); } \ + && sed -i 1d zerobinpaste \ + && $${uglifyjs} -o zerobinpaste.min zerobinpaste \ + && echo '#!/usr/bin/env node' > zerobinpaste \ + && cat zerobinpaste.min >> zerobinpaste \ + && chmod +x zerobinpaste + +clean: + rm -f zerobinpaste{,.js,.min} + +zerobinpaste.js: zerobinpaste.coffee + coffee=$$(PATH="$$(npm bin):$$PATH" which coffee 2>/dev/null) \ + || { npm install coffee-script; coffee=$$(PATH="$$(npm bin):$$PATH" which coffee); } \ + && $$coffee -c zerobinpaste.coffee + +.PHONY: uglify diff --git a/tools/commander.js b/tools/commander.js new file mode 100644 index 0000000..03d4f33 --- /dev/null +++ b/tools/commander.js @@ -0,0 +1,1118 @@ +/*! + * commander + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var EventEmitter = require('events').EventEmitter + , spawn = require('child_process').spawn + , fs = require('fs') + , exists = fs.existsSync + , path = require('path') + , tty = require('tty') + , dirname = path.dirname + , basename = path.basename; + +/** + * Expose the root command. + */ + +program = new Command; + +/** + * Initialize a new `Option` with the given `flags` and `description`. + * + * @param {String} flags + * @param {String} description + * @api public + */ + +function Option(flags, description) { + this.flags = flags; + this.required = ~flags.indexOf('<'); + this.optional = ~flags.indexOf('['); + this.bool = !~flags.indexOf('-no-'); + flags = flags.split(/[ ,|]+/); + if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift(); + this.long = flags.shift(); + this.description = description || ''; +} + +/** + * Return option name. + * + * @return {String} + * @api private + */ + +Option.prototype.name = function(){ + return this.long + .replace('--', '') + .replace('no-', ''); +}; + +/** + * Check if `arg` matches the short or long flag. + * + * @param {String} arg + * @return {Boolean} + * @api private + */ + +Option.prototype.is = function(arg){ + return arg == this.short + || arg == this.long; +}; + +/** + * Initialize a new `Command`. + * + * @param {String} name + * @api public + */ + +function Command(name) { + this.commands = []; + this.options = []; + this._args = []; + this._name = name; +} + +/** + * Inherit from `EventEmitter.prototype`. + */ + +Command.prototype.__proto__ = EventEmitter.prototype; + +/** + * Add command `name`. + * + * The `.action()` callback is invoked when the + * command `name` is specified via __ARGV__, + * and the remaining arguments are applied to the + * function for access. + * + * When the `name` is "*" an un-matched command + * will be passed as the first arg, followed by + * the rest of __ARGV__ remaining. + * + * Examples: + * + * program + * .version('0.0.1') + * .option('-C, --chdir ', 'change the working directory') + * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') + * .option('-T, --no-tests', 'ignore test hook') + * + * program + * .command('setup') + * .description('run remote setup commands') + * .action(function(){ + * console.log('setup'); + * }); + * + * program + * .command('exec ') + * .description('run the given remote command') + * .action(function(cmd){ + * console.log('exec "%s"', cmd); + * }); + * + * program + * .command('*') + * .description('deploy the given env') + * .action(function(env){ + * console.log('deploying "%s"', env); + * }); + * + * program.parse(process.argv); + * + * @param {String} name + * @param {String} [desc] + * @return {Command} the new command + * @api public + */ + +Command.prototype.command = function(name, desc){ + var args = name.split(/ +/); + var cmd = new Command(args.shift()); + if (desc) cmd.description(desc); + if (desc) this.executables = true; + this.commands.push(cmd); + cmd.parseExpectedArgs(args); + cmd.parent = this; + if (desc) return this; + return cmd; +}; + +/** + * Add an implicit `help [cmd]` subcommand + * which invokes `--help` for the given command. + * + * @api private + */ + +Command.prototype.addImplicitHelpCommand = function() { + this.command('help [cmd]', 'display help for [cmd]'); +}; + +/** + * Parse expected `args`. + * + * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. + * + * @param {Array} args + * @return {Command} for chaining + * @api public + */ + +Command.prototype.parseExpectedArgs = function(args){ + if (!args.length) return; + var self = this; + args.forEach(function(arg){ + switch (arg[0]) { + case '<': + self._args.push({ required: true, name: arg.slice(1, -1) }); + break; + case '[': + self._args.push({ required: false, name: arg.slice(1, -1) }); + break; + } + }); + return this; +}; + +/** + * Register callback `fn` for the command. + * + * Examples: + * + * program + * .command('help') + * .description('display verbose help') + * .action(function(){ + * // output help here + * }); + * + * @param {Function} fn + * @return {Command} for chaining + * @api public + */ + +Command.prototype.action = function(fn){ + var self = this; + this.parent.on(this._name, function(args, unknown){ + // Parse any so-far unknown options + unknown = unknown || []; + var parsed = self.parseOptions(unknown); + + // Output help if necessary + outputHelpIfNecessary(self, parsed.unknown); + + // If there are still any unknown options, then we simply + // die, unless someone asked for help, in which case we give it + // to them, and then we die. + if (parsed.unknown.length > 0) { + self.unknownOption(parsed.unknown[0]); + } + + // Leftover arguments need to be pushed back. Fixes issue #56 + if (parsed.args.length) args = parsed.args.concat(args); + + self._args.forEach(function(arg, i){ + if (arg.required && null == args[i]) { + self.missingArgument(arg.name); + } + }); + + // Always append ourselves to the end of the arguments, + // to make sure we match the number of arguments the user + // expects + if (self._args.length) { + args[self._args.length] = self; + } else { + args.push(self); + } + + fn.apply(this, args); + }); + return this; +}; + +/** + * Define option with `flags`, `description` and optional + * coercion `fn`. + * + * The `flags` string should contain both the short and long flags, + * separated by comma, a pipe or space. The following are all valid + * all will output this way when `--help` is used. + * + * "-p, --pepper" + * "-p|--pepper" + * "-p --pepper" + * + * Examples: + * + * // simple boolean defaulting to false + * program.option('-p, --pepper', 'add pepper'); + * + * --pepper + * program.pepper + * // => Boolean + * + * // simple boolean defaulting to false + * program.option('-C, --no-cheese', 'remove cheese'); + * + * program.cheese + * // => true + * + * --no-cheese + * program.cheese + * // => true + * + * // required argument + * program.option('-C, --chdir ', 'change the working directory'); + * + * --chdir /tmp + * program.chdir + * // => "/tmp" + * + * // optional argument + * program.option('-c, --cheese [type]', 'add cheese [marble]'); + * + * @param {String} flags + * @param {String} description + * @param {Function|Mixed} fn or default + * @param {Mixed} defaultValue + * @return {Command} for chaining + * @api public + */ + +Command.prototype.option = function(flags, description, fn, defaultValue){ + var self = this + , option = new Option(flags, description) + , oname = option.name() + , name = camelcase(oname); + + // default as 3rd arg + if ('function' != typeof fn) defaultValue = fn, fn = null; + + // preassign default value only for --no-*, [optional], or + if (false == option.bool || option.optional || option.required) { + // when --no-* we make sure default is true + if (false == option.bool) defaultValue = true; + // preassign only if we have a default + if (undefined !== defaultValue) self[name] = defaultValue; + } + + // register the option + this.options.push(option); + + // when it's passed assign the value + // and conditionally invoke the callback + this.on(oname, function(val){ + // coercion + if (null != val && fn) val = fn(val); + + // unassigned or bool + if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) { + // if no value, bool true, and we have a default, then use it! + if (null == val) { + self[name] = option.bool + ? defaultValue || true + : false; + } else { + self[name] = val; + } + } else if (null !== val) { + // reassign + self[name] = val; + } + }); + + return this; +}; + +/** + * Parse `argv`, settings options and invoking commands when defined. + * + * @param {Array} argv + * @return {Command} for chaining + * @api public + */ + +Command.prototype.parse = function(argv){ + // implicit help + if (this.executables) this.addImplicitHelpCommand(); + + // store raw args + this.rawArgs = argv; + + // guess name + this._name = this._name || basename(argv[1]); + + // process argv + var parsed = this.parseOptions(this.normalize(argv.slice(2))); + var args = this.args = parsed.args; + + // executable sub-commands, skip .parseArgs() + if (this.executables) return this.executeSubCommand(argv, args, parsed.unknown); + + return this.parseArgs(this.args, parsed.unknown); +}; + +/** + * Execute a sub-command executable. + * + * @param {Array} argv + * @param {Array} args + * @param {Array} unknown + * @api private + */ + +Command.prototype.executeSubCommand = function(argv, args, unknown) { + args = args.concat(unknown); + + if (!args.length) this.help(); + if ('help' == args[0] && 1 == args.length) this.help(); + + // --help + if ('help' == args[0]) { + args[0] = args[1]; + args[1] = '--help'; + } + + // executable + var dir = dirname(argv[1]); + var bin = basename(argv[1]) + '-' + args[0]; + + // check for ./ first + var local = path.join(dir, bin); + if (exists(local)) bin = local; + + // run it + args = args.slice(1); + var proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] }); + proc.on('exit', function(code){ + if (code == 127) { + console.error('\n %s(1) does not exist\n', bin); + } + }); +}; + +/** + * Normalize `args`, splitting joined short flags. For example + * the arg "-abc" is equivalent to "-a -b -c". + * This also normalizes equal sign and splits "--abc=def" into "--abc def". + * + * @param {Array} args + * @return {Array} + * @api private + */ + +Command.prototype.normalize = function(args){ + var ret = [] + , arg + , index; + + for (var i = 0, len = args.length; i < len; ++i) { + arg = args[i]; + if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) { + arg.slice(1).split('').forEach(function(c){ + ret.push('-' + c); + }); + } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) { + ret.push(arg.slice(0, index), arg.slice(index + 1)); + } else { + ret.push(arg); + } + } + + return ret; +}; + +/** + * Parse command `args`. + * + * When listener(s) are available those + * callbacks are invoked, otherwise the "*" + * event is emitted and those actions are invoked. + * + * @param {Array} args + * @return {Command} for chaining + * @api private + */ + +Command.prototype.parseArgs = function(args, unknown){ + var cmds = this.commands + , len = cmds.length + , name; + + if (args.length) { + name = args[0]; + if (this.listeners(name).length) { + this.emit(args.shift(), args, unknown); + } else { + this.emit('*', args); + } + } else { + outputHelpIfNecessary(this, unknown); + + // If there were no args and we have unknown options, + // then they are extraneous and we need to error. + if (unknown.length > 0) { + this.unknownOption(unknown[0]); + } + } + + return this; +}; + +/** + * Return an option matching `arg` if any. + * + * @param {String} arg + * @return {Option} + * @api private + */ + +Command.prototype.optionFor = function(arg){ + for (var i = 0, len = this.options.length; i < len; ++i) { + if (this.options[i].is(arg)) { + return this.options[i]; + } + } +}; + +/** + * Parse options from `argv` returning `argv` + * void of these options. + * + * @param {Array} argv + * @return {Array} + * @api public + */ + +Command.prototype.parseOptions = function(argv){ + var args = [] + , len = argv.length + , literal + , option + , arg; + + var unknownOptions = []; + + // parse options + for (var i = 0; i < len; ++i) { + arg = argv[i]; + + // literal args after -- + if ('--' == arg) { + literal = true; + continue; + } + + if (literal) { + args.push(arg); + continue; + } + + // find matching Option + option = this.optionFor(arg); + + // option is defined + if (option) { + // requires arg + if (option.required) { + arg = argv[++i]; + if (null == arg) return this.optionMissingArgument(option); + if ('-' == arg[0]) return this.optionMissingArgument(option, arg); + this.emit(option.name(), arg); + // optional arg + } else if (option.optional) { + arg = argv[i+1]; + if (null == arg || '-' == arg[0]) { + arg = null; + } else { + ++i; + } + this.emit(option.name(), arg); + // bool + } else { + this.emit(option.name()); + } + continue; + } + + // looks like an option + if (arg.length > 1 && '-' == arg[0]) { + unknownOptions.push(arg); + + // If the next argument looks like it might be + // an argument for this option, we pass it on. + // If it isn't, then it'll simply be ignored + if (argv[i+1] && '-' != argv[i+1][0]) { + unknownOptions.push(argv[++i]); + } + continue; + } + + // arg + args.push(arg); + } + + return { args: args, unknown: unknownOptions }; +}; + +/** + * Argument `name` is missing. + * + * @param {String} name + * @api private + */ + +Command.prototype.missingArgument = function(name){ + console.error(); + console.error(" error: missing required argument `%s'", name); + console.error(); + process.exit(1); +}; + +/** + * `Option` is missing an argument, but received `flag` or nothing. + * + * @param {String} option + * @param {String} flag + * @api private + */ + +Command.prototype.optionMissingArgument = function(option, flag){ + console.error(); + if (flag) { + console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag); + } else { + console.error(" error: option `%s' argument missing", option.flags); + } + console.error(); + process.exit(1); +}; + +/** + * Unknown option `flag`. + * + * @param {String} flag + * @api private + */ + +Command.prototype.unknownOption = function(flag){ + console.error(); + console.error(" error: unknown option `%s'", flag); + console.error(); + process.exit(1); +}; + + +/** + * Set the program version to `str`. + * + * This method auto-registers the "-V, --version" flag + * which will print the version number when passed. + * + * @param {String} str + * @param {String} flags + * @return {Command} for chaining + * @api public + */ + +Command.prototype.version = function(str, flags){ + if (0 == arguments.length) return this._version; + this._version = str; + flags = flags || '-V, --version'; + this.option(flags, 'output the version number'); + this.on('version', function(){ + console.log(str); + process.exit(0); + }); + return this; +}; + +/** + * Set the description `str`. + * + * @param {String} str + * @return {String|Command} + * @api public + */ + +Command.prototype.description = function(str){ + if (0 == arguments.length) return this._description; + this._description = str; + return this; +}; + +/** + * Set / get the command usage `str`. + * + * @param {String} str + * @return {String|Command} + * @api public + */ + +Command.prototype.usage = function(str){ + var args = this._args.map(function(arg){ + return arg.required + ? '<' + arg.name + '>' + : '[' + arg.name + ']'; + }); + + var usage = '[options' + + (this.commands.length ? '] [command' : '') + + ']' + + (this._args.length ? ' ' + args : ''); + + if (0 == arguments.length) return this._usage || usage; + this._usage = str; + + return this; +}; + +/** + * Return the largest option length. + * + * @return {Number} + * @api private + */ + +Command.prototype.largestOptionLength = function(){ + return this.options.reduce(function(max, option){ + return Math.max(max, option.flags.length); + }, 0); +}; + +/** + * Return help for options. + * + * @return {String} + * @api private + */ + +Command.prototype.optionHelp = function(){ + var width = this.largestOptionLength(); + + // Prepend the help information + return [pad('-h, --help', width) + ' ' + 'output usage information'] + .concat(this.options.map(function(option){ + return pad(option.flags, width) + + ' ' + option.description; + })) + .join('\n'); +}; + +/** + * Return command help documentation. + * + * @return {String} + * @api private + */ + +Command.prototype.commandHelp = function(){ + if (!this.commands.length) return ''; + return [ + '' + , ' Commands:' + , '' + , this.commands.map(function(cmd){ + var args = cmd._args.map(function(arg){ + return arg.required + ? '<' + arg.name + '>' + : '[' + arg.name + ']'; + }).join(' '); + + return pad(cmd._name + + (cmd.options.length + ? ' [options]' + : '') + ' ' + args, 22) + + (cmd.description() + ? ' ' + cmd.description() + : ''); + }).join('\n').replace(/^/gm, ' ') + , '' + ].join('\n'); +}; + +/** + * Return program help documentation. + * + * @return {String} + * @api private + */ + +Command.prototype.helpInformation = function(){ + return [ + '' + , ' Usage: ' + this._name + ' ' + this.usage() + , '' + this.commandHelp() + , ' Options:' + , '' + , '' + this.optionHelp().replace(/^/gm, ' ') + , '' + , '' + ].join('\n'); +}; + +/** + * Prompt for a `Number`. + * + * @param {String} str + * @param {Function} fn + * @api private + */ + +Command.prototype.promptForNumber = function(str, fn){ + var self = this; + this.promptSingleLine(str, function parseNumber(val){ + val = Number(val); + if (isNaN(val)) return self.promptSingleLine(str + '(must be a number) ', parseNumber); + fn(val); + }); +}; + +/** + * Prompt for a `Date`. + * + * @param {String} str + * @param {Function} fn + * @api private + */ + +Command.prototype.promptForDate = function(str, fn){ + var self = this; + this.promptSingleLine(str, function parseDate(val){ + val = new Date(val); + if (isNaN(val.getTime())) return self.promptSingleLine(str + '(must be a date) ', parseDate); + fn(val); + }); +}; + +/** + * Single-line prompt. + * + * @param {String} str + * @param {Function} fn + * @api private + */ + +Command.prototype.promptSingleLine = function(str, fn){ + if ('function' == typeof arguments[2]) { + return this['promptFor' + (fn.name || fn)](str, arguments[2]); + } + + process.stdout.write(str); + process.stdin.setEncoding('utf8'); + process.stdin.once('data', function(val){ + fn(val.trim()); + }).resume(); +}; + +/** + * Multi-line prompt. + * + * @param {String} str + * @param {Function} fn + * @api private + */ + +Command.prototype.promptMultiLine = function(str, fn){ + var buf = []; + console.log(str); + process.stdin.setEncoding('utf8'); + process.stdin.on('data', function(val){ + if ('\n' == val || '\r\n' == val) { + process.stdin.removeAllListeners('data'); + fn(buf.join('\n')); + } else { + buf.push(val.trimRight()); + } + }).resume(); +}; + +/** + * Prompt `str` and callback `fn(val)` + * + * Commander supports single-line and multi-line prompts. + * To issue a single-line prompt simply add white-space + * to the end of `str`, something like "name: ", whereas + * for a multi-line prompt omit this "description:". + * + * + * Examples: + * + * program.prompt('Username: ', function(name){ + * console.log('hi %s', name); + * }); + * + * program.prompt('Description:', function(desc){ + * console.log('description was "%s"', desc.trim()); + * }); + * + * @param {String|Object} str + * @param {Function} fn + * @api public + */ + +Command.prototype.prompt = function(str, fn){ + var self = this; + + if ('string' == typeof str) { + if (/ $/.test(str)) return this.promptSingleLine.apply(this, arguments); + this.promptMultiLine(str, fn); + } else { + var keys = Object.keys(str) + , obj = {}; + + function next() { + var key = keys.shift() + , label = str[key]; + + if (!key) return fn(obj); + self.prompt(label, function(val){ + obj[key] = val; + next(); + }); + } + + next(); + } +}; + +/** + * Prompt for password with `str`, `mask` char and callback `fn(val)`. + * + * The mask string defaults to '', aka no output is + * written while typing, you may want to use "*" etc. + * + * Examples: + * + * program.password('Password: ', function(pass){ + * console.log('got "%s"', pass); + * process.stdin.destroy(); + * }); + * + * program.password('Password: ', '*', function(pass){ + * console.log('got "%s"', pass); + * process.stdin.destroy(); + * }); + * + * @param {String} str + * @param {String} mask + * @param {Function} fn + * @api public + */ + +Command.prototype.password = function(str, mask, fn){ + var self = this + , buf = ''; + + // default mask + if ('function' == typeof mask) { + fn = mask; + mask = ''; + } + + keypress(process.stdin); + + function setRawMode(mode) { + if (process.stdin.setRawMode) { + process.stdin.setRawMode(mode); + } else { + tty.setRawMode(mode); + } + }; + setRawMode(true); + process.stdout.write(str); + + // keypress + process.stdin.on('keypress', function(c, key){ + if (key && 'enter' == key.name) { + console.log(); + process.stdin.pause(); + process.stdin.removeAllListeners('keypress'); + setRawMode(false); + if (!buf.trim().length) return self.password(str, mask, fn); + fn(buf); + return; + } + + if (key && key.ctrl && 'c' == key.name) { + console.log('%s', buf); + process.exit(); + } + + process.stdout.write(mask); + buf += c; + }).resume(); +}; + +/** + * Confirmation prompt with `str` and callback `fn(bool)` + * + * Examples: + * + * program.confirm('continue? ', function(ok){ + * console.log(' got %j', ok); + * process.stdin.destroy(); + * }); + * + * @param {String} str + * @param {Function} fn + * @api public + */ + + +Command.prototype.confirm = function(str, fn, verbose){ + var self = this; + this.prompt(str, function(ok){ + if (!ok.trim()) { + if (!verbose) str += '(yes or no) '; + return self.confirm(str, fn, true); + } + fn(parseBool(ok)); + }); +}; + +/** + * Choice prompt with `list` of items and callback `fn(index, item)` + * + * Examples: + * + * var list = ['tobi', 'loki', 'jane', 'manny', 'luna']; + * + * console.log('Choose the coolest pet:'); + * program.choose(list, function(i){ + * console.log('you chose %d "%s"', i, list[i]); + * process.stdin.destroy(); + * }); + * + * @param {Array} list + * @param {Number|Function} index or fn + * @param {Function} fn + * @api public + */ + +Command.prototype.choose = function(list, index, fn){ + var self = this + , hasDefault = 'number' == typeof index; + + if (!hasDefault) { + fn = index; + index = null; + } + + list.forEach(function(item, i){ + if (hasDefault && i == index) { + console.log('* %d) %s', i + 1, item); + } else { + console.log(' %d) %s', i + 1, item); + } + }); + + function again() { + self.prompt(' : ', function(val){ + val = parseInt(val, 10) - 1; + if (hasDefault && isNaN(val)) val = index; + + if (null == list[val]) { + again(); + } else { + fn(val, list[val]); + } + }); + } + + again(); +}; + + +/** + * Output help information for this command + * + * @api public + */ + +Command.prototype.outputHelp = function(){ + process.stdout.write(this.helpInformation()); + this.emit('--help'); +}; + +/** + * Output help information and exit. + * + * @api public + */ + +Command.prototype.help = function(){ + this.outputHelp(); + process.exit(); +}; + +/** + * Camel-case the given `flag` + * + * @param {String} flag + * @return {String} + * @api private + */ + +function camelcase(flag) { + return flag.split('-').reduce(function(str, word){ + return str + word[0].toUpperCase() + word.slice(1); + }); +} + +/** + * Parse a boolean `str`. + * + * @param {String} str + * @return {Boolean} + * @api private + */ + +function parseBool(str) { + return /^y|yes|ok|true$/i.test(str); +} + +/** + * Pad `str` to `width`. + * + * @param {String} str + * @param {Number} width + * @return {String} + * @api private + */ + +function pad(str, width) { + var len = Math.max(0, width - str.length); + return str + Array(len + 1).join(' '); +} + +/** + * Output help information if necessary + * + * @param {Command} command to output help for + * @param {Array} array of options to search for -h or --help + * @api private + */ + +function outputHelpIfNecessary(cmd, options) { + options = options || []; + for (var i = 0; i < options.length; i++) { + if (options[i] == '--help' || options[i] == '-h') { + cmd.outputHelp(); + process.exit(0); + } + } +} diff --git a/tools/zerobinpaste.coffee b/tools/zerobinpaste.coffee new file mode 100644 index 0000000..85871ad --- /dev/null +++ b/tools/zerobinpaste.coffee @@ -0,0 +1,103 @@ + +program + .version('0.0.1') + .usage('[options] [ file ... ]\n\n' + ' Paste contents of file(s) or stdin to 0bin site.') + .option('-u, --url [url]', 'URL of a 0bin site.') + .option('-e, --expire [period]', + 'Expiration period - one of: 1_view, 1_day (default), 1_month, never.', '1_day') + .option('-c, --config [path]', 'Path to zerobin configuration file (default: ~/.zerobinpasterc).\n'\ + + ' Should be json-file with the same keys as can be specified on the command line.\n'\ + + ' Example contents: {"url": "http://some-0bin.com"}', '~/.zerobinpasterc') + .parse(process.argv); + + +[http, url, qs, fs, path] = ['http', 'url', 'querystring', 'fs', 'path'].map(require) + + +# Parse config file, if any +config = program.config.replace(/^~\/+/, '') +config = path.resolve(process.env.HOME, config) + +try + if fs.statSync(config).isFile() + config = JSON.parse(fs.readFileSync(config)) + (program[k] = v) for own k, v of config + + +# Sanity checks and option processing +if not program.url + console.error('ERROR: URL option must be specified.') + process.exit(1) + +if program.expire == '1_view' + # "burn_after_reading" is too damn long for cli + program.expire = 'burn_after_reading' + +expire_opts = ['burn_after_reading', '1_day', '1_month', 'never'] +if program.expire not in expire_opts + console.error( + "ERROR: --expire value (provided: '#{program.expire}')"\ + + ' must be one of: ' + expire_opts.join(', ') + "." ) + process.exit(1) + + +# Paste one dump and print URL, optionally prefixed with name +paste_file = (content, name) -> + + content = sjcl.codec.utf8String.toBits(content) + content = sjcl.codec.base64.fromBits(content) + # content = lzw.compress(content) + + key = sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0) + content = sjcl.encrypt(key, content) + content = qs.stringify + content: content + expiration: program.expire + + # host.com -> http://host.com + if not program.url.match(/^https?:\/\//) + program.url = 'http://' + program.url.replace(/^\/+/, '') + + req_opts = url.parse(program.url) + req_opts.method = 'POST' + req_opts.headers = + 'Content-Type': 'application/x-www-form-urlencoded' + 'Content-Length': content.length + + req_url_base = req_opts.path + .replace(/\/paste\/create\/?$/, '').replace(/\/+$/, '') + req_opts.path = req_url_base + '/paste/create' + + req = http.request req_opts, (res) -> + req_reply = '' + res.setEncoding('utf8') + res.on 'data', (chunk) -> req_reply += chunk + res.on 'end', -> + req_reply = JSON.parse(req_reply) + if req_reply.status != 'ok' + console.error("ERROR: failure posting #{name} - " + req_reply.message) + return + + req_opts.pathname = req_url_base + '/paste/' + req_reply.paste + req_opts.hash = key + paste = url.format(req_opts) + + console.log(if name then "#{name} #{paste}" else paste) + + req.write(content) + req.end() + + +# Loop over file args or read stdin +if not program.args or not program.args.length + process.stdin.resume() + process.stdin.setEncoding('utf8') + + stdin_data = '' + process.stdin.on 'data', (chunk) -> stdin_data += chunk + process.stdin.on 'end', -> paste_file(stdin_data) + +else + for file in program.args + paste_file( fs.readFileSync(file, 'utf8'), + if program.args.length > 1 then path.basename(file) else null ) diff --git a/zerobin/static/js/lzw.js b/zerobin/static/js/lzw.js index b1f3e9a..02516b5 100644 --- a/zerobin/static/js/lzw.js +++ b/zerobin/static/js/lzw.js @@ -107,5 +107,4 @@ var lzw = { } return result; } -} - +};