diff --git a/.gitignore b/.gitignore index 79595d190..88ddd7288 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,15 @@ -/package.json npm-debug.log sinopia-*.tgz .DS_Store ### -bin/** !bin/sinopia test-storage* node_modules + +# Visual Studio Code +.vscode/* +.jscsrc +.jshintrc +jsconfig.json diff --git a/.npmignore b/.npmignore index 17d7a085f..fa9fd06d7 100644 --- a/.npmignore +++ b/.npmignore @@ -1,11 +1,5 @@ node_modules -package.json npm-debug.log -sinopia-*.tgz - -### -bin/** -!bin/sinopia +verdaccio-*.tgz test-storage* - /.* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..e8cfa8866 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## 2.1.0 (October 11, 2016) + +- Use __dirname to resolve local plugins ([@aledbf](https://github.com/aledbf) in [#25](https://github.com/verdaccio/verdaccio/pull/25)) +- Fix npm cli logout ([@plitex](https://github.com/plitex) in [#47](https://github.com/verdaccio/verdaccio/pull/47)) +- Add log format: pretty-timestamped ([@jachstet-sea](https://github.com/jachstet-sea) in [#68](https://github.com/verdaccio/verdaccio/pull/68)) +- Allow adding/overriding HTTP headers of uplinks via config ([@jachstet-sea](https://github.com/jachstet-sea) in [#67](https://github.com/verdaccio/verdaccio/pull/67)) +- Update Dockerfile to fix failed start ([@denisbabineau](https://github.com/denisbabineau) in [#62](https://github.com/verdaccio/verdaccio/pull/62)) +- Update the configs to fully support proxying scoped packages ([@ChadKillingsworth](https://github.com/ChadKillingsworth) in [#60](https://github.com/verdaccio/verdaccio/pull/60)) +- Prevent the server from crashing if a repo is accessed that the user does not have access to ([@crowebird](https://github.com/crowebird) in [#58](https://github.com/verdaccio/verdaccio/pull/58)) +- Hook system, for integration into things like slack +- Register entry partial even if custom template is provided ([@plitex](https://github.com/plitex) in [#46](https://github.com/verdaccio/verdaccio/pull/46)) +- Rename process to verdaccio ([@juanpicado](https://github.com/juanpicado) in [#57](https://github.com/verdaccio/verdaccio/pull/57)) + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b248b3a32 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM node:6.1.0-onbuild + +RUN mkdir -p /verdaccio/storage /verdaccio/conf + +WORKDIR /verdaccio + +ADD conf/docker.yaml /verdaccio/conf/config.yaml + +EXPOSE 4873 + +VOLUME ["/verdaccio/conf", "/verdaccio/storage"] + +CMD ["/usr/src/app/bin/verdaccio", "--config", "/verdaccio/conf/config.yaml", "--listen", "0.0.0.0:4873"] diff --git a/Gruntfile.js b/Gruntfile.js index dafdaed66..2bf41a705 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,6 +1,6 @@ module.exports = function(grunt) { grunt.initConfig({ - pkg: grunt.file.readYAML('package.yaml'), + pkg: grunt.file.readJSON('package.json'), browserify: { dist: { files: { diff --git a/README.md b/README.md index df7f24d74..544d7b796 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,9 @@ `sinopia` - a private/caching npm repository server -[![npm version badge](https://img.shields.io/npm/v/sinopia.svg)](https://www.npmjs.org/package/sinopia) -[![travis badge](http://img.shields.io/travis/rlidwka/sinopia.svg)](https://travis-ci.org/rlidwka/sinopia) -[![downloads badge](http://img.shields.io/npm/dm/sinopia.svg)](https://www.npmjs.org/package/sinopia) +[![travis badge](http://img.shields.io/travis/verdaccio/verdaccio.svg)](https://travis-ci.org/verdaccio/verdaccio) -It allows you to have a local npm registry with zero configuration. You don't have to install and replicate an entire CouchDB database. Sinopia keeps its own small database and, if a package doesn't exist there, it asks npmjs.org for it keeping only those packages you use. +It allows you to have a local npm registry with zero configuration. You don't have to install and replicate an entire CouchDB database. Verdaccio keeps its own small database and, if a package doesn't exist there, it asks npmjs.org for it keeping only those packages you use.

@@ -36,8 +34,8 @@ It allows you to have a local npm registry with zero configuration. You don't ha ```bash # installation and starting (application will create default # config in config.yaml you can edit later) -$ npm install -g sinopia -$ sinopia +$ npm install -g verdaccio +$ verdaccio # npm configuration $ npm set registry http://localhost:4873/ @@ -51,7 +49,15 @@ Now you can navigate to [http://localhost:4873/](http://localhost:4873/) where y ### Docker -A Sinopia docker image [is available](https://registry.hub.docker.com/u/keyvanfatehi/sinopia/) +`docker build -t verdaccio .` + +``` +docker run -it --rm --name verdaccio -p 4873:4873 \ + -v //conf:/verdaccio/conf \ + -v //storage:/verdaccio/storage \ + -v //local_storage:/verdaccio/local_storage \ + verdaccio +``` ### Chef @@ -71,7 +77,7 @@ When you start a server, it auto-creates a config file. npm adduser --registry http://localhost:4873/ ``` -This will prompt you for user credentials which will be saved on the Sinopia server. +This will prompt you for user credentials which will be saved on the Verdaccio server. ## Using private packages @@ -81,7 +87,7 @@ It is recommended that you define a prefix for your private packages, for exampl ## Using public packages from npmjs.org -If some package doesn't exist in the storage, server will try to fetch it from npmjs.org. If npmjs.org is down, it serves packages from cache pretending that no other packages exist. Sinopia will download only what's needed (= requested by clients), and this information will be cached, so if client will ask the same thing second time, it can be served without asking npmjs.org for it. +If some package doesn't exist in the storage, server will try to fetch it from npmjs.org. If npmjs.org is down, it serves packages from cache pretending that no other packages exist. Verdaccio will download only what's needed (= requested by clients), and this information will be cached, so if client will ask the same thing second time, it can be served without asking npmjs.org for it. Example: if you successfully request express@3.0.1 from this server once, you'll able to do that again (with all it's dependencies) anytime even if npmjs.org is down. But say express@3.0.0 will not be downloaded until it's actually needed by somebody. And if npmjs.org is offline, this server would say that only express@3.0.1 (= only what's in the cache) is published, but nothing else. @@ -93,7 +99,7 @@ There's two options here: 1. You want to create a separate fork and stop synchronizing with public version. - If you want to do that, you should modify your configuration file so sinopia won't make requests regarding this package to npmjs anymore. Add a separate entry for this package to *config.yaml* and remove `npmjs` from `proxy_access` list and restart the server. + If you want to do that, you should modify your configuration file so verdaccio won't make requests regarding this package to npmjs anymore. Add a separate entry for this package to *config.yaml* and remove `npmjs` from `proxy_access` list and restart the server. When you publish your package locally, you should probably start with version string higher than existing one, so it won't conflict with existing package in the cache. @@ -103,7 +109,7 @@ There's two options here: ## Compatibility -Sinopia aims to support all features of a standard npm client that make sense to support in private repository. Unfortunately, it isn't always possible. +Verdaccio aims to support all features of a standard npm client that make sense to support in private repository. Unfortunately, it isn't always possible. Basic features: @@ -119,7 +125,7 @@ Advanced package control: User management: - Registering new users (npm adduser {newuser}) - supported -- Transferring ownership (npm owner add {user} {pkg}) - not supported, sinopia uses its own acl management system +- Transferring ownership (npm owner add {user} {pkg}) - not supported, verdaccio uses its own acl management system Misc stuff: diff --git a/SERVER.md b/SERVER.md index c09a66855..90f1495c1 100644 --- a/SERVER.md +++ b/SERVER.md @@ -1,18 +1,18 @@ -This is mostly basic linux server configuration stuff but I felt it important to document and share the steps I took to get sinopia running permanently on my server. You will need root (or sudo) permissions for the following. +This is mostly basic linux server configuration stuff but I felt it important to document and share the steps I took to get verdaccio running permanently on my server. You will need root (or sudo) permissions for the following. ## Running as a separate user -First create the sinopia user: +First create the verdaccio user: ```bash -$ sudo adduser --disabled-login --gecos 'Sinopia NPM mirror' sinopia +$ sudo adduser --disabled-login --gecos 'Verdaccio NPM mirror' verdaccio ``` -You create a shell as the sinopia user using the following command: +You create a shell as the verdaccio user using the following command: ```bash -$ sudo su sinopia +$ sudo su verdaccio $ cd ~ ``` -The 'cd ~' command send you to the home directory of the sinopia user. Make sure you run sinopia at least once to generate the config file. Edit it according to your needs. +The 'cd ~' command send you to the home directory of the verdaccio user. Make sure you run verdaccio at least once to generate the config file. Edit it according to your needs. ## Listening on all addresses If you want to listen to every external address set the listen directive in the config to: @@ -21,8 +21,8 @@ If you want to listen to every external address set the listen directive in the listen: 0.0.0.0:4873 ``` -## Keeping sinopia running forever -We can use the node package called 'forever' to keep sinopia running all the time. +## Keeping verdaccio running forever +We can use the node package called 'forever' to keep verdaccio running all the time. https://github.com/nodejitsu/forever First install forever globally: @@ -30,16 +30,16 @@ First install forever globally: $ sudo npm install -g forever ``` -Make sure you've started sinopia at least once to generate the config file and write down the created admin user. You can then use the following command to start sinopia: +Make sure you've started verdaccio at least once to generate the config file and write down the created admin user. You can then use the following command to start verdaccio: ```bash -$ forever start `which sinopia` +$ forever start `which verdaccio` ``` You can check the documentation for more information on how to use forever. ## Surviving server restarts -We can use crontab and forever together to restart sinopia after a server reboot. -When you're logged in as the sinopia user do the following: +We can use crontab and forever together to restart verdaccio after a server reboot. +When you're logged in as the verdaccio user do the following: ```bash $ crontab -e @@ -48,11 +48,11 @@ $ crontab -e This might ask you to choose an editor. Pick your favorite and proceed. Add the following entry to the file: ``` -@reboot /usr/bin/forever start /usr/lib/node_modules/sinopia/bin/sinopia +@reboot /usr/bin/forever start /usr/lib/node_modules/verdaccio/bin/verdaccio ``` The locations may vary depending on your server setup. If you want to know where your files are you can use the 'which' command: ```bash $ which forever -$ which sinopia -``` \ No newline at end of file +$ which verdaccio +``` diff --git a/bin/sinopia b/bin/verdaccio similarity index 97% rename from bin/sinopia rename to bin/verdaccio index 32a78deaa..87170d755 100755 --- a/bin/sinopia +++ b/bin/verdaccio @@ -1,4 +1,3 @@ #!/usr/bin/env node require('../lib/cli') - diff --git a/conf/default.yaml b/conf/default.yaml index f777708e6..43e071c49 100644 --- a/conf/default.yaml +++ b/conf/default.yaml @@ -3,7 +3,7 @@ # so don't use it on production systems. # # Look here for more config file examples: -# https://github.com/rlidwka/sinopia/tree/master/conf +# https://github.com/verdaccio/verdaccio/tree/master/conf # # path to a directory with all packages @@ -26,8 +26,9 @@ packages: # scoped packages access: $all publish: $authenticated + proxy: npmjs - '*': + '**': # allow all users (including non-authenticated users) to read and # publish all packages # @@ -45,5 +46,5 @@ packages: # log settings logs: - {type: stdout, format: pretty, level: http} - #- {type: file, path: sinopia.log, level: info} + #- {type: file, path: verdaccio.log, level: info} diff --git a/conf/docker.yaml b/conf/docker.yaml new file mode 100644 index 000000000..b55be010d --- /dev/null +++ b/conf/docker.yaml @@ -0,0 +1,50 @@ +# +# This is the default config file. It allows all users to do anything, +# so don't use it on production systems. +# +# Look here for more config file examples: +# https://github.com/rlidwka/sinopia/tree/master/conf +# + +# path to a directory with all packages +storage: /verdaccio/storage + +auth: + htpasswd: + file: /verdaccio/config/htpasswd + # Maximum amount of users allowed to register, defaults to "+inf". + # You can set this to -1 to disable registration. + #max_users: 1000 + +# a list of other known repositories we can talk to +uplinks: + npmjs: + url: https://registry.npmjs.org/ + +packages: + '@*/*': + # scoped packages + access: $all + publish: $all + proxy: npmjs + + '**': + # allow all users (including non-authenticated users) to read and + # publish all packages + # + # you can specify usernames/groupnames (depending on your auth plugin) + # and three keywords: "$all", "$anonymous", "$authenticated" + access: $all + + # allow all known users to publish packages + # (anyone can register by default, remember?) + publish: $all + + # if package is not available locally, proxy requests to 'npmjs' registry + proxy: npmjs + +# log settings +logs: + - {type: stdout, format: pretty, level: http} + #- {type: file, path: sinopia.log, level: info} + diff --git a/conf/full.yaml b/conf/full.yaml index b8c344181..965594767 100644 --- a/conf/full.yaml +++ b/conf/full.yaml @@ -17,7 +17,7 @@ web: # this has a lot of issues, e.g. no auth yet, so use at your own risk #enable: true - title: Sinopia + title: Verdaccio # logo: logo.png # template: custom.hbs @@ -51,6 +51,11 @@ uplinks: # timeouts are defined in the same way as nginx, see: # http://wiki.nginx.org/ConfigNotation + # add/override HTTP headers sent to the uplink server + # this allows for HTTP Basic auth for example: + #headers: + # authorization: "Basic YourBase64EncodedCredentials==" + packages: # uncomment this for packages with "local-" prefix to be available # for admin only, it's a recommended way of handling private packages @@ -60,7 +65,7 @@ packages: # # you can override storage directory for a group of packages this way: # storage: 'local_storage' - '*': + '**': # allow all users to read packages (including non-authenticated users) # # you can specify usernames/groupnames (depending on your auth plugin) @@ -78,10 +83,10 @@ packages: ##################################################################### # if you use nginx with custom path, use this to override links -#url_prefix: https://dev.company.local/sinopia/ +#url_prefix: https://dev.company.local/verdaccio/ # You can specify listen address (or simply a port). -# If you add multiple values, sinopia will listen on all of them. +# If you add multiple values, verdaccio will listen on all of them. # # Examples: # @@ -91,7 +96,7 @@ packages: # - 0.0.0.0:4873 # listen on all addresses (INADDR_ANY) # - https://example.org:4873 # if you want to use https # - [::1]:4873 # ipv6 -# - unix:/tmp/sinopia.sock # unix socket +# - unix:/tmp/verdaccio.sock # unix socket # Configure HTTPS, it is required if you use "https" protocol above. #https: @@ -102,13 +107,13 @@ packages: # level: trace | debug | info | http (default) | warn | error | fatal # # parameters for file: name is filename -# {type: 'file', path: 'sinopia.log', level: 'debug'}, +# {type: 'file', path: 'verdaccio.log', level: 'debug'}, # -# parameters for stdout and stderr: format: json | pretty +# parameters for stdout and stderr: format: json | pretty | pretty-timestamped # {type: 'stdout', format: 'pretty', level: 'debug'}, logs: - {type: stdout, format: pretty, level: http} - #- {type: file, path: sinopia.log, level: info} + #- {type: file, path: verdaccio.log, level: info} # you can specify proxy used with all requests in wget-like manner here # (or set up ENV variables with the same name) @@ -120,8 +125,24 @@ logs: # increase it if you have "request entity too large" errors #max_body_size: 1mb -# Workaround for countless npm bugs. Must have for npm <1.14.x, but expect -# it to be turned off in future versions. If `true`, latest tag is ignored, -# and the highest semver is placed instead. -#ignore_latest_tag: false - +# Notify Settings +# Notify was built primarily to use with Slack's Incoming +# webhooks, but will also deliver a simple payload to +# any endpoint. Currently only active for publish / create +# commands. +notify: + # Choose a method. Technically this will accept any HTTP + # request method, but probably stick to GET or POST + method: POST + # If this endpoint requires specific headers, set them here + # as an array of key: value objects. + headers: [{'Content-type': 'application/x-www-form-urlencoded'}] + # set the URL endpoint for this call + endpoint: https://hooks.slack.com/... + # Finally, the content you will be sending in the body. + # This data will first be run through Handlebars to parse + # any Handlebar expressions. All data housed in the metadata object + # is available for use within the expressions. + content: ' {{ handlebar-expression }}' + # For Slack, follow the following format: + # content: '{ "text": "Package *{{ name }}* published to version *{{ dist-tags.latest }}*", "username": "Verdaccio", "icon_emoji": ":package:" }' diff --git a/lib/cli.js b/lib/cli.js index e78926de8..3e3566b01 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -6,7 +6,7 @@ if (process.getuid && process.getuid() === 0) { global.console.error("Sinopia doesn't need superuser privileges. Don't run it under root.") } -process.title = 'sinopia' +process.title = 'verdaccio' require('es6-shim') try { @@ -28,13 +28,14 @@ var Path = require('path') var URL = require('url') var server = require('./index') var Utils = require('./utils') -var pkg_file = '../package.yaml' -var pkg = YAML.safeLoad(fs.readFileSync(__dirname+'/'+ pkg_file, 'utf8')) +var pkginfo = require('pkginfo')(module); // eslint-disable-line no-unused-vars +var pkgVersion = module.exports.version +var pkgName = module.exports.name commander .option('-l, --listen <[host:]port>', 'host:port number to listen on (default: localhost:4873)') .option('-c, --config ', 'use this configuration file (default: ./config.yaml)') - .version(pkg.version) + .version(pkgVersion) .parse(process.argv) if (commander.args.length == 1 && !commander.config) { @@ -161,7 +162,7 @@ function afterConfigLoad() { pathname: '/', }) ), - version: 'Sinopia/'+pkg.version, + version: pkgName + '/' + pkgVersion, }, 'http address - @{addr}') }) diff --git a/lib/config-path.js b/lib/config-path.js index 692ba8ec2..6cb7a2236 100644 --- a/lib/config-path.js +++ b/lib/config-path.js @@ -25,7 +25,7 @@ function create_config_file(config_path) { var data_dir = process.env.XDG_DATA_HOME || Path.join(process.env.HOME, '.local', 'share') if (folder_exists(data_dir)) { - data_dir = Path.resolve(Path.join(data_dir, 'sinopia', 'storage')) + data_dir = Path.resolve(Path.join(data_dir, 'verdaccio', 'storage')) created_config = created_config.replace(/^storage: .\/storage$/m, 'storage: ' + data_dir) } } @@ -39,7 +39,7 @@ function get_paths() { || process.env.HOME && Path.join(process.env.HOME, '.config') if (xdg_config && folder_exists(xdg_config)) { try_paths.push({ - path: Path.join(xdg_config, 'sinopia', 'config.yaml'), + path: Path.join(xdg_config, 'verdaccio', 'config.yaml'), type: 'xdg', }) } @@ -48,13 +48,13 @@ function get_paths() { && process.env.APPDATA && folder_exists(process.env.APPDATA)) { try_paths.push({ - path: Path.resolve(Path.join(process.env.APPDATA, 'sinopia', 'config.yaml')), + path: Path.resolve(Path.join(process.env.APPDATA, 'verdaccio', 'config.yaml')), type: 'win', }) } try_paths.push({ - path: Path.resolve(Path.join('.', 'sinopia', 'config.yaml')), + path: Path.resolve(Path.join('.', 'verdaccio', 'config.yaml')), type: 'def', }) diff --git a/lib/config.js b/lib/config.js index e9add9ec7..6386a0cac 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,14 +1,14 @@ var assert = require('assert') var Crypto = require('crypto') -var fs = require('fs') -var YAML = require('js-yaml') var Error = require('http-errors') var minimatch = require('minimatch') var Path = require('path') var LocalData = require('./local-data') var Utils = require('./utils') -var pkg_file = '../package.yaml' -var pkg = YAML.safeLoad(fs.readFileSync(__dirname+'/'+pkg_file, 'utf8')) +var Utils = require('./utils') +var pkginfo = require('pkginfo')(module) // eslint-disable-line no-unused-vars +var pkgVersion = module.exports.version +var pkgName = module.exports.name // [[a, [b, c]], d] -> [a, b, c, d] function flatten(array) { @@ -28,7 +28,7 @@ function Config(config) { for (var i in config) { if (self[i] == null) self[i] = config[i] } - if (!self.user_agent) self.user_agent = 'Sinopia/'+pkg.version + if (!self.user_agent) self.user_agent = pkgName + '/' + pkgVersion // some weird shell scripts are valid yaml files parsed as string assert.equal(typeof(config), 'object', 'CONFIG: it doesn\'t look like a valid config file') @@ -36,7 +36,7 @@ function Config(config) { assert(self.storage, 'CONFIG: storage path not defined') self.localList = LocalData( Path.join( - Path.resolve(Path.dirname(self.self_path), self.storage), + Path.resolve(Path.dirname(self.self_path || ''), self.storage), '.sinopia-db.json' ) ) @@ -142,8 +142,6 @@ function Config(config) { self.server_id = Crypto.pseudoRandomBytes(6).toString('hex') } - if (self.ignore_latest_tag == null) self.ignore_latest_tag = false - return self } diff --git a/lib/file-locking.js b/lib/file-locking.js new file mode 100644 index 000000000..7b19e2e25 --- /dev/null +++ b/lib/file-locking.js @@ -0,0 +1,152 @@ +/** + * file-locking.js - file system locking (replaces fs-ext) + */ + +var async = require('async'), + locker = require('lockfile'), + fs = require('fs'), + path = require('path') + +// locks a file by creating a lock file +function lockFile(name, next) { + var lockFileName = name + '.lock', + lockOpts = { + wait: 1000, // time (ms) to wait when checking for stale locks + pollPeriod: 100, // how often (ms) to re-check stale locks + + stale: 5 * 60 * 1000, // locks are considered stale after 5 minutes + + retries: 100, // number of times to attempt to create a lock + retryWait: 100 // time (ms) between tries + } + + async.series({ + + statdir: function (callback) { + // test to see if the directory exists + fs.stat(path.dirname(name), function (err, stats) { + if (err) { + callback(err) + } else if (!stats.isDirectory()) { + callback(new Error(path.dirname(name) + ' is not a directory')) + } else { + callback(null) + } + }) + }, + + statfile: function (callback) { + // test to see if the file to lock exists + fs.stat(name, function (err, stats) { + if (err) { + callback(err) + } else if (!stats.isFile()) { + callback(new Error(path.dirname(name) + ' is not a file')) + } else { + callback(null) + } + }); + }, + + lockfile: function (callback) { + // try to lock the file + locker.lock(lockFileName, lockOpts, callback) + } + + }, function (err) { + if (err) { + // lock failed + return next(err) + } + + // lock succeeded + return next(null); + }) + +} + +// unlocks file by removing existing lock file +function unlockFile(name, next) { + var lockFileName = name + '.lock' + + locker.unlock(lockFileName, function (err) { + if (err) { + return next(err) + } + + return next(null) + }) +} + +/** + * reads a local file, which involves + * optionally taking a lock + * reading the file contents + * optionally parsing JSON contents + */ +function readFile(name, options, next) { + if (typeof options === 'function' && next === null) { + next = options; + options = {} + } + + options = options || {} + options.lock = options.lock || false + options.parse = options.parse || false + + function lock(callback) { + if (!options.lock) { + return callback(null) + } + + lockFile(name, function (err) { + if (err) { + return callback(err) + } + return callback(null) + }) + } + + function read(callback) { + fs.readFile(name, 'utf8', function (err, contents) { + if (err) { + return callback(err) + } + + callback(null, contents) + + }) + } + + function parseJSON(contents, callback) { + if (!options.parse) { + return callback(null, contents) + } + + try { + contents = JSON.parse(contents) + return callback(null, contents) + } catch (err) { + return callback(err) + } + } + + async.waterfall([ + lock, + read, + parseJSON + ], + + function (err, result) { + if (err) { + return next(err) + } else { + return next(null, result) + } + }) +} + +exports.lockFile = lockFile; +exports.unlockFile = unlockFile; + +exports.readFile = readFile; diff --git a/lib/index-api.js b/lib/index-api.js index f91b62450..224573fb7 100644 --- a/lib/index-api.js +++ b/lib/index-api.js @@ -1,9 +1,10 @@ var Cookies = require('cookies') var express = require('express') -var expressJson5 = require('express-json5') +var bodyParser = require('body-parser') var Error = require('http-errors') var Path = require('path') var Middleware = require('./middleware') +var Notify = require('./notify') var Utils = require('./utils') var expect_json = Middleware.expect_json var match = Middleware.match @@ -14,6 +15,7 @@ var validate_pkg = Middleware.validate_package module.exports = function(config, auth, storage) { var app = express.Router() var can = Middleware.allow(auth) + var notify = Notify.notify; // validate all of these params as a package name // this might be too harsh, so ask if it causes trouble @@ -22,6 +24,7 @@ module.exports = function(config, auth, storage) { app.param('tag', validate_name) app.param('version', validate_name) app.param('revision', validate_name) + app.param('token', validate_name) // these can't be safely put into express url for some reason app.param('_rev', match(/^-rev$/)) @@ -30,7 +33,7 @@ module.exports = function(config, auth, storage) { app.use(auth.basic_middleware()) //app.use(auth.bearer_middleware()) - app.use(expressJson5({ strict: false, limit: config.max_body_size || '10mb' })) + app.use(bodyParser.json({ strict: false, limit: config.max_body_size || '10mb' })) app.use(Middleware.anti_loop(config)) // encode / in a scoped package name to be matched as a single parameter in routes @@ -202,6 +205,13 @@ module.exports = function(config, auth, storage) { } }) + app.delete('/-/user/token/*', function(req, res, next) { + res.status(200) + next({ + ok: 'Logged out', + }) + }) + function tag_package_version(req, res, next) { if (typeof(req.body) !== 'string') return next('route') @@ -336,7 +346,7 @@ module.exports = function(config, auth, storage) { add_tags(metadata['dist-tags'], function(err) { if (err) return next(err) - + notify(metadata, config) res.status(201) return next({ ok: ok_message }) }) diff --git a/lib/index-web.js b/lib/index-web.js index d1a15a9ce..a62a4005f 100644 --- a/lib/index-web.js +++ b/lib/index-web.js @@ -33,11 +33,12 @@ module.exports = function(config, auth, storage) { Search.configureStorage(storage) + Handlebars.registerPartial('entry', fs.readFileSync(require.resolve('./GUI/entry.hbs'), 'utf8')) + if(config.web && config.web.template) { var template = Handlebars.compile(fs.readFileSync(config.web.template, 'utf8')); } else { - Handlebars.registerPartial('entry', fs.readFileSync(require.resolve('./GUI/entry.hbs'), 'utf8')) var template = Handlebars.compile(fs.readFileSync(require.resolve('./GUI/index.hbs'), 'utf8')) } app.get('/', function(req, res, next) { @@ -51,10 +52,15 @@ module.exports = function(config, auth, storage) { async.filterSeries(packages, function(package, cb) { auth.allow_access(package.name, req.remote_user, function(err, allowed) { setImmediate(function () { - cb(!err && allowed) + if (err) { + cb(null, false); + } else { + cb(err, allowed) + } }) }) - }, function(packages) { + }, function(err, packages) { + if (err) throw err packages.sort(function(p1, p2) { if (p1.name < p2.name) { return -1; @@ -65,7 +71,7 @@ module.exports = function(config, auth, storage) { }); next(template({ - name: config.web && config.web.title ? config.web.title : 'Sinopia', + name: config.web && config.web.title ? config.web.title : 'Verdaccio', packages: packages, baseUrl: base, username: req.remote_user.name, @@ -152,4 +158,3 @@ module.exports = function(config, auth, storage) { }) return app } - diff --git a/lib/local-fs.js b/lib/local-fs.js index db85b0006..8d7a66719 100644 --- a/lib/local-fs.js +++ b/lib/local-fs.js @@ -10,15 +10,7 @@ function FSError(code) { return err } -try { - var fsExt = require('fs-ext') -} catch (e) { - fsExt = { - flock: function() { - arguments[arguments.length-1]() - } - } -} +var locker = require('./file-locking') function tempFile(str) { return str + '.tmp' + String(Math.random()).substr(2) @@ -134,7 +126,7 @@ function read_stream(name, stream, callback) { }) }) - var stream = MyStreams.ReadTarballStream() + stream = MyStreams.ReadTarballStream() stream.abort = function() { rstream.close() } @@ -159,64 +151,6 @@ function read(name, callback) { fs.readFile(name, callback) } -// open and flock with exponential backoff -function open_flock(name, opmod, flmod, tries, backoff, cb) { - fs.open(name, opmod, function(err, fd) { - if (err) return cb(err, fd) - - fsExt.flock(fd, flmod, function(err) { - if (err) { - if (!tries) { - fs.close(fd, function() { - cb(err) - }) - } else { - fs.close(fd, function() { - setTimeout(function() { - open_flock(name, opmod, flmod, tries-1, backoff*2, cb) - }, backoff) - }) - } - } else { - cb(null, fd) - } - }) - }) -} - -// this function neither unlocks file nor closes it -// it'll have to be done manually later -function lock_and_read(name, _callback) { - open_flock(name, 'r', 'exnb', 4, 10, function(err, fd) { - function callback(err) { - if (err && fd) { - fs.close(fd, function(err2) { - _callback(err) - }) - } else { - _callback.apply(null, arguments) - } - } - - if (err) return callback(err, fd) - - fs.fstat(fd, function(err, st) { - if (err) return callback(err, fd) - - var buffer = Buffer(st.size) - if (st.size === 0) return onRead(null, 0, buffer) - fs.read(fd, buffer, 0, st.size, null, onRead) - - function onRead(err, bytesRead, buffer) { - if (err) return callback(err, fd) - if (bytesRead != st.size) return callback(Error('st.size != bytesRead'), fd) - - callback(null, fd, buffer) - } - }) - }) -} - module.exports.read = read module.exports.read_json = function(name, cb) { @@ -233,22 +167,24 @@ module.exports.read_json = function(name, cb) { }) } -module.exports.lock_and_read = lock_and_read +module.exports.lock_and_read = function(name, cb) { + locker.readFile(name, {lock: true}, function(err, res) { + if (err) return cb(err) + return cb(null, res) + }) +} module.exports.lock_and_read_json = function(name, cb) { - lock_and_read(name, function(err, fd, res) { - if (err) return cb(err, fd) - - var args = [] - try { - args = [ null, fd, JSON.parse(res.toString('utf8')) ] - } catch(err) { - args = [ err, fd ] - } - cb.apply(null, args) + locker.readFile(name, {lock: true, parse: true}, function(err, res) { + if (err) return cb(err) + return cb(null, res); }) } +module.exports.unlock_file = function (name, cb) { + locker.unlockFile(name, cb) +} + module.exports.create = create module.exports.create_json = function(name, value, cb) { diff --git a/lib/local-storage.js b/lib/local-storage.js index 52a03210d..bccc1f268 100644 --- a/lib/local-storage.js +++ b/lib/local-storage.js @@ -176,19 +176,7 @@ Storage.prototype.update_versions = function(name, newdata, callback) { } } for (var tag in newdata['dist-tags']) { - if (!Array.isArray(data['dist-tags'][tag]) || data['dist-tags'][tag].length != newdata['dist-tags'][tag].length) { - // backward compat - var need_change = true - } else { - for (var i=0; i= self._maxusers) { + err = Error('maximum amount of users reached') + } + if (err) err.status = 403 + return err + } + + // preliminary checks, just to ensure that file won't be reloaded if it's not needed + var s_err = sanity_check() + if (s_err) return real_cb(s_err, false) + + utils.lock_and_read(self._path, function (err, res) { + var locked = false + + // callback that cleans up lock first + function cb(err) { + if (locked) { + utils.unlock_file(self._path, function () { + // ignore any error from the unlock + real_cb(err, !err) + }) + } else { + real_cb(err, !err) + } + } + + if (!err) { + locked = true + } + + // ignore ENOENT errors, we'll just create .htpasswd in that case + if (err && err.code !== 'ENOENT') return cb(err) + + var body = (res || '').toString('utf8') + self._users = utils.parse_htpasswd(body) + + // real checks, to prevent race conditions + var s_err = sanity_check() + if (s_err) return cb(s_err) + + try { + console.log('body = utils.add_user_to_htpasswd(body, user, password)') + console.log(user, password) + body = utils.add_user_to_htpasswd(body, user, password) + } catch (err) { + return cb(err) + } + fs.writeFile(self._path, body, function (err) { + if (err) return cb(err) + self._reload(function () { + cb(null, true) + }) + }) + }) +} + +HTPasswd.prototype._reload = function (_callback) { + var self = this + + fs.stat(self._path, function (err, stats) { + if (err) return _callback(err) + + if (self._last_time === stats.mtime) return _callback() + self._last_time = stats.mtime + + fs.readFile(self._path, 'utf8', function (err, buffer) { + if (err) return _callback(err) + + self._users = utils.parse_htpasswd(buffer) + + _callback() + + }); + + }); + +} diff --git a/lib/plugins/htpasswd/utils.js b/lib/plugins/htpasswd/utils.js new file mode 100644 index 000000000..229662f9f --- /dev/null +++ b/lib/plugins/htpasswd/utils.js @@ -0,0 +1,65 @@ +var crypto = require('crypto') +var crypt3 = require('./crypt3') +var locker = require('../../file-locking') + +// this function neither unlocks file nor closes it +// it'll have to be done manually later +function lock_and_read(name, cb) { + locker.readFile(name, {lock: true}, function (err, res) { + if (err) { + return cb(err) + } + return cb(null, res) + }) +} + +// close and unlock file +function unlock_file(name, cb) { + locker.unlockFile(name, cb) +} + +function parse_htpasswd(input) { + var result = {} + input.split('\n').forEach(function(line) { + var args = line.split(':', 3) + if (args.length > 1) result[args[0]] = args[1] + }) + return result +} + +function verify_password(user, passwd, hash) { + if (hash.indexOf('{PLAIN}') === 0) { + return passwd === hash.substr(7) + } else if (hash.indexOf('{SHA}') === 0) { + return crypto.createHash('sha1').update(passwd, 'binary').digest('base64') === hash.substr(5) + } else if (crypt3) { + return crypt3(passwd, hash) === hash + } else { + return false + } +} + +function add_user_to_htpasswd(body, user, passwd) { + if (user !== encodeURIComponent(user)) { + var err = Error('username should not contain non-uri-safe characters') + err.status = 409 + throw err + } + + if (crypt3) { + passwd = crypt3(passwd) + } else { + passwd = '{SHA}' + crypto.createHash('sha1').update(passwd, 'binary').digest('base64') + } + var comment = 'autocreated ' + (new Date()).toJSON() + + var newline = user + ':' + passwd + ':' + comment + '\n' + if (body.length && body[body.length-1] !== '\n') newline = '\n' + newline + return body + newline +} + +module.exports.parse_htpasswd = parse_htpasswd +module.exports.verify_password = verify_password +module.exports.add_user_to_htpasswd = add_user_to_htpasswd +module.exports.lock_and_read = lock_and_read +module.exports.unlock_file = unlock_file diff --git a/lib/storage.js b/lib/storage.js index 3daace978..14bf349ee 100644 --- a/lib/storage.js +++ b/lib/storage.js @@ -307,16 +307,7 @@ Storage.prototype.get_package = function(name, options, callback) { if (whitelist.indexOf(i) === -1) delete result[i] } - if (self.config.ignore_latest_tag || !result['dist-tags'].latest) { - result['dist-tags'].latest = Utils.semver_sort(Object.keys(result.versions)) - } - - for (var i in result['dist-tags']) { - if (Array.isArray(result['dist-tags'][i])) { - result['dist-tags'][i] = result['dist-tags'][i][result['dist-tags'][i].length-1] - if (result['dist-tags'][i] == null) delete result['dist-tags'][i] - } - } + Utils.normalize_dist_tags(result) // npm can throw if this field doesn't exist result._attachments = {} @@ -382,10 +373,8 @@ Storage.prototype.get_local = function(callback) { var getPackage = function(i) { self.local.get_package(locals[i], function(err, info) { if (!err) { - var latest = Array.isArray(info['dist-tags'].latest) - ? Utils.semver_sort(info['dist-tags'].latest).pop() - : info['dist-tags'].latest - if (info.versions[latest]) { + var latest = info['dist-tags'].latest + if (latest && info.versions[latest]) { packages.push(info.versions[latest]) } else { self.logger.warn( { package: locals[i] } @@ -521,10 +510,12 @@ Storage._merge_versions = function(local, up, config) { // refresh dist-tags for (var i in up['dist-tags']) { - var added = Utils.tag_version(local, up['dist-tags'][i], i, config || {}) - if (i === 'latest' && added) { - // if remote has more fresh package, we should borrow its readme - local.readme = up.readme + if (local['dist-tags'][i] !== up['dist-tags'][i]) { + local['dist-tags'][i] = up['dist-tags'][i] + if (i === 'latest') { + // if remote has more fresh package, we should borrow its readme + local.readme = up.readme + } } } } diff --git a/lib/up-storage.js b/lib/up-storage.js index 83ae61ec7..392c52b1b 100644 --- a/lib/up-storage.js +++ b/lib/up-storage.js @@ -116,6 +116,11 @@ Storage.prototype.request = function(options, cb) { headers['User-Agent'] = headers['User-Agent'] || this.userAgent this._add_proxy_headers(options.req, headers) + // add/override headers specified in the config + for (var key in this.config.headers) { + headers[key] = this.config.headers[key] + } + var method = options.method || 'GET' var uri = options.uri_full || (this.config.url + options.uri) diff --git a/lib/utils.js b/lib/utils.js index bf558115f..303998c34 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -80,28 +80,19 @@ module.exports.filter_tarball_urls = function(pkg, req, config) { return pkg } -function can_add_tag(tag, config) { - if (!tag) return false - if (tag === 'latest' && config.ignore_latest_tag) return false - return true -} - -module.exports.tag_version = function(data, version, tag, config) { - if (!can_add_tag(tag, config)) return false - - switch (typeof(data['dist-tags'][tag])) { - case 'string': - data['dist-tags'][tag] = [ data['dist-tags'][tag] ] - break - case 'object': // array - break - default: - data['dist-tags'][tag] = [] - } - if (data['dist-tags'][tag].indexOf(version) === -1) { - data['dist-tags'][tag].push(version) - data['dist-tags'][tag] = module.exports.semver_sort(data['dist-tags'][tag]) - return data['dist-tags'][tag][data['dist-tags'][tag].length - 1] === version +module.exports.tag_version = function(data, version, tag) { + if (tag) { + if (data['dist-tags'][tag] !== version) { + if (Semver.parse(version, true)) { + // valid version - store + data['dist-tags'][tag] = version + return true + } + } + Logger.logger.warn({ver: version, tag: tag}, 'ignoring bad version @{ver} in @{tag}') + if (tag && data['dist-tags'][tag]) { + delete data['dist-tags'][tag] + } } return false } @@ -170,3 +161,36 @@ module.exports.semver_sort = function semver_sort(array) { .map(String) } +// flatten arrays of tags +module.exports.normalize_dist_tags = function (data) { + var sorted + + if (!data['dist-tags'].latest) { + // overwrite latest with highest known version based on semver sort + sorted = module.exports.semver_sort(Object.keys(data.versions)) + if (sorted && sorted.length) { + data['dist-tags'].latest = sorted.pop() + } + } + + for (var tag in data['dist-tags']) { + if (Array.isArray(data['dist-tags'][tag])) { + if (data['dist-tags'][tag].length) { + // sort array + sorted = module.exports.semver_sort(data['dist-tags'][tag]) + if (sorted.length) { + // use highest version based on semver sort + data['dist-tags'][tag] = sorted.pop() + } + + } else { + delete data['dist-tags'][tag] + } + } else if (typeof data['dist-tags'][tag] === 'string') { + if (!Semver.parse(data['dist-tags'][tag], true)) { + // if the version is invalid, delete the dist-tag entry + delete data['dist-tags'][tag] + } + } + } +} diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json deleted file mode 100644 index c2784c0c3..000000000 --- a/npm-shrinkwrap.json +++ /dev/null @@ -1,726 +0,0 @@ -{ - "name": "sinopia", - "version": "1.4.0", - "dependencies": { - "JSONStream": { - "version": "1.0.6", - "dependencies": { - "jsonparse": { - "version": "1.1.0" - }, - "through": { - "version": "2.3.8" - } - } - }, - "async": { - "version": "0.9.2" - }, - "body-parser": { - "version": "1.14.1", - "dependencies": { - "bytes": { - "version": "2.1.0" - }, - "content-type": { - "version": "1.0.1" - }, - "debug": { - "version": "2.2.0", - "dependencies": { - "ms": { - "version": "0.7.1" - } - } - }, - "depd": { - "version": "1.1.0" - }, - "iconv-lite": { - "version": "0.4.12" - }, - "on-finished": { - "version": "2.3.0", - "dependencies": { - "ee-first": { - "version": "1.1.1" - } - } - }, - "qs": { - "version": "5.1.0" - }, - "raw-body": { - "version": "2.1.4", - "dependencies": { - "unpipe": { - "version": "1.0.0" - } - } - }, - "type-is": { - "version": "1.6.9", - "dependencies": { - "media-typer": { - "version": "0.3.0" - }, - "mime-types": { - "version": "2.1.7", - "dependencies": { - "mime-db": { - "version": "1.19.0" - } - } - } - } - } - } - }, - "bunyan": { - "version": "1.5.1", - "dependencies": { - "dtrace-provider": { - "version": "0.6.0", - "dependencies": { - "nan": { - "version": "2.0.9" - } - } - }, - "mv": { - "version": "2.1.1", - "dependencies": { - "ncp": { - "version": "2.0.0" - } - } - }, - "safe-json-stringify": { - "version": "1.0.3" - } - } - }, - "commander": { - "version": "2.8.1", - "dependencies": { - "graceful-readlink": { - "version": "1.0.1" - } - } - }, - "compression": { - "version": "1.6.0", - "dependencies": { - "accepts": { - "version": "1.3.0", - "dependencies": { - "mime-types": { - "version": "2.1.7", - "dependencies": { - "mime-db": { - "version": "1.19.0" - } - } - }, - "negotiator": { - "version": "0.6.0" - } - } - }, - "bytes": { - "version": "2.1.0" - }, - "compressible": { - "version": "2.0.6", - "dependencies": { - "mime-db": { - "version": "1.19.0" - } - } - }, - "debug": { - "version": "2.2.0", - "dependencies": { - "ms": { - "version": "0.7.1" - } - } - }, - "on-headers": { - "version": "1.0.1" - }, - "vary": { - "version": "1.1.0" - } - } - }, - "cookies": { - "version": "0.5.1", - "dependencies": { - "keygrip": { - "version": "1.0.1" - } - } - }, - "crypt3": { - "version": "0.1.8", - "dependencies": { - "nan": { - "version": "1.9.0" - } - } - }, - "es6-shim": { - "version": "0.21.1" - }, - "express": { - "version": "5.0.0-alpha.2", - "dependencies": { - "accepts": { - "version": "1.2.13", - "dependencies": { - "mime-types": { - "version": "2.1.7", - "dependencies": { - "mime-db": { - "version": "1.19.0" - } - } - }, - "negotiator": { - "version": "0.5.3" - } - } - }, - "array-flatten": { - "version": "1.1.0" - }, - "content-disposition": { - "version": "0.5.0" - }, - "content-type": { - "version": "1.0.1" - }, - "cookie": { - "version": "0.1.3" - }, - "cookie-signature": { - "version": "1.0.6" - }, - "debug": { - "version": "2.2.0", - "dependencies": { - "ms": { - "version": "0.7.1" - } - } - }, - "depd": { - "version": "1.0.1" - }, - "escape-html": { - "version": "1.0.2" - }, - "etag": { - "version": "1.7.0" - }, - "finalhandler": { - "version": "0.4.0", - "dependencies": { - "unpipe": { - "version": "1.0.0" - } - } - }, - "fresh": { - "version": "0.3.0" - }, - "merge-descriptors": { - "version": "1.0.0" - }, - "methods": { - "version": "1.1.1" - }, - "on-finished": { - "version": "2.3.0", - "dependencies": { - "ee-first": { - "version": "1.1.1" - } - } - }, - "parseurl": { - "version": "1.3.0" - }, - "path-is-absolute": { - "version": "1.0.0" - }, - "path-to-regexp": { - "version": "0.1.6" - }, - "proxy-addr": { - "version": "1.0.8", - "dependencies": { - "forwarded": { - "version": "0.1.0" - }, - "ipaddr.js": { - "version": "1.0.1" - } - } - }, - "qs": { - "version": "4.0.0" - }, - "range-parser": { - "version": "1.0.2" - }, - "router": { - "version": "1.1.3", - "dependencies": { - "array-flatten": { - "version": "1.1.1" - }, - "path-to-regexp": { - "version": "0.1.7" - }, - "setprototypeof": { - "version": "1.0.0" - } - } - }, - "send": { - "version": "0.13.0", - "dependencies": { - "destroy": { - "version": "1.0.3" - }, - "mime": { - "version": "1.3.4" - }, - "ms": { - "version": "0.7.1" - }, - "statuses": { - "version": "1.2.1" - } - } - }, - "serve-static": { - "version": "1.10.0" - }, - "type-is": { - "version": "1.6.9", - "dependencies": { - "media-typer": { - "version": "0.3.0" - }, - "mime-types": { - "version": "2.1.7", - "dependencies": { - "mime-db": { - "version": "1.19.0" - } - } - } - } - }, - "vary": { - "version": "1.0.1" - }, - "utils-merge": { - "version": "1.0.0" - } - } - }, - "express-json5": { - "version": "0.1.0", - "dependencies": { - "raw-body": { - "version": "1.3.4", - "dependencies": { - "bytes": { - "version": "1.0.0" - }, - "iconv-lite": { - "version": "0.4.8" - } - } - } - } - }, - "fs-ext": { - "version": "0.4.6", - "dependencies": { - "nan": { - "version": "1.9.0" - } - } - }, - "handlebars": { - "version": "2.0.0", - "dependencies": { - "optimist": { - "version": "0.3.7", - "dependencies": { - "wordwrap": { - "version": "0.0.3" - } - } - }, - "uglify-js": { - "version": "2.3.6", - "dependencies": { - "async": { - "version": "0.2.10" - }, - "source-map": { - "version": "0.1.43", - "dependencies": { - "amdefine": { - "version": "1.0.0" - } - } - } - } - } - } - }, - "highlight.js": { - "version": "8.8.0" - }, - "http-errors": { - "version": "1.3.1", - "dependencies": { - "inherits": { - "version": "2.0.1" - }, - "statuses": { - "version": "1.2.1" - } - } - }, - "jju": { - "version": "1.2.1" - }, - "js-yaml": { - "version": "3.4.2", - "dependencies": { - "argparse": { - "version": "1.0.2", - "dependencies": { - "lodash": { - "version": "3.10.1" - }, - "sprintf-js": { - "version": "1.0.3" - } - } - }, - "esprima": { - "version": "2.2.0" - } - } - }, - "lunr": { - "version": "0.5.12" - }, - "minimatch": { - "version": "1.0.0", - "dependencies": { - "lru-cache": { - "version": "2.7.0" - }, - "sigmund": { - "version": "1.0.1" - } - } - }, - "mkdirp": { - "version": "0.5.1", - "dependencies": { - "minimist": { - "version": "0.0.8" - } - } - }, - "readable-stream": { - "version": "1.1.13", - "dependencies": { - "core-util-is": { - "version": "1.0.1" - }, - "isarray": { - "version": "0.0.1" - }, - "string_decoder": { - "version": "0.10.31" - }, - "inherits": { - "version": "2.0.1" - } - } - }, - "render-readme": { - "version": "1.3.1", - "dependencies": { - "markdown-it": { - "version": "4.4.0", - "dependencies": { - "argparse": { - "version": "1.0.2", - "dependencies": { - "lodash": { - "version": "3.10.1" - }, - "sprintf-js": { - "version": "1.0.3" - } - } - }, - "entities": { - "version": "1.1.1" - }, - "linkify-it": { - "version": "1.2.0" - }, - "mdurl": { - "version": "1.0.1" - }, - "uc.micro": { - "version": "1.0.0" - } - } - }, - "sanitize-html": { - "version": "1.10.1", - "dependencies": { - "htmlparser2": { - "version": "3.8.3", - "dependencies": { - "domhandler": { - "version": "2.3.0" - }, - "domutils": { - "version": "1.5.1", - "dependencies": { - "dom-serializer": { - "version": "0.1.0", - "dependencies": { - "domelementtype": { - "version": "1.1.3" - }, - "entities": { - "version": "1.1.1" - } - } - } - } - }, - "domelementtype": { - "version": "1.3.0" - }, - "entities": { - "version": "1.0.0" - } - } - }, - "regexp-quote": { - "version": "0.0.0" - }, - "xtend": { - "version": "4.0.0" - } - } - } - } - }, - "request": { - "version": "2.64.0", - "dependencies": { - "bl": { - "version": "1.0.0", - "dependencies": { - "readable-stream": { - "version": "2.0.2", - "dependencies": { - "core-util-is": { - "version": "1.0.1" - }, - "inherits": { - "version": "2.0.1" - }, - "isarray": { - "version": "0.0.1" - }, - "process-nextick-args": { - "version": "1.0.3" - }, - "string_decoder": { - "version": "0.10.31" - }, - "util-deprecate": { - "version": "1.0.1" - } - } - } - } - }, - "caseless": { - "version": "0.11.0" - }, - "extend": { - "version": "3.0.0" - }, - "forever-agent": { - "version": "0.6.1" - }, - "form-data": { - "version": "1.0.0-rc3", - "dependencies": { - "async": { - "version": "1.4.2" - } - } - }, - "json-stringify-safe": { - "version": "5.0.1" - }, - "mime-types": { - "version": "2.1.7", - "dependencies": { - "mime-db": { - "version": "1.19.0" - } - } - }, - "node-uuid": { - "version": "1.4.3" - }, - "qs": { - "version": "5.1.0" - }, - "tunnel-agent": { - "version": "0.4.1" - }, - "tough-cookie": { - "version": "2.1.0" - }, - "http-signature": { - "version": "0.11.0", - "dependencies": { - "assert-plus": { - "version": "0.1.5" - }, - "asn1": { - "version": "0.1.11" - }, - "ctype": { - "version": "0.5.3" - } - } - }, - "oauth-sign": { - "version": "0.8.0" - }, - "hawk": { - "version": "3.1.0", - "dependencies": { - "hoek": { - "version": "2.16.3" - }, - "boom": { - "version": "2.9.0" - }, - "cryptiles": { - "version": "2.0.5" - }, - "sntp": { - "version": "1.0.9" - } - } - }, - "aws-sign2": { - "version": "0.5.0" - }, - "stringstream": { - "version": "0.0.4" - }, - "combined-stream": { - "version": "1.0.5", - "dependencies": { - "delayed-stream": { - "version": "1.0.0" - } - } - }, - "isstream": { - "version": "0.1.2" - }, - "har-validator": { - "version": "1.8.0", - "dependencies": { - "chalk": { - "version": "1.1.1", - "dependencies": { - "ansi-styles": { - "version": "2.1.0" - }, - "escape-string-regexp": { - "version": "1.0.3" - }, - "has-ansi": { - "version": "2.0.0", - "dependencies": { - "ansi-regex": { - "version": "2.0.0" - } - } - }, - "strip-ansi": { - "version": "3.0.0", - "dependencies": { - "ansi-regex": { - "version": "2.0.0" - } - } - }, - "supports-color": { - "version": "2.0.0" - } - } - }, - "is-my-json-valid": { - "version": "2.12.2", - "dependencies": { - "generate-function": { - "version": "2.0.0" - }, - "generate-object-property": { - "version": "1.2.0", - "dependencies": { - "is-property": { - "version": "1.0.2" - } - } - }, - "jsonpointer": { - "version": "2.0.0" - }, - "xtend": { - "version": "4.0.0" - } - } - } - } - } - } - }, - "semver": { - "version": "4.3.6" - }, - "sinopia-htpasswd": { - "version": "0.4.5" - } - } -} diff --git a/package.json b/package.json new file mode 100644 index 000000000..1a7e20f4d --- /dev/null +++ b/package.json @@ -0,0 +1,83 @@ +{ + "name": "verdaccio", + "version": "2.1.0", + "description": "Private npm repository server", + "author": { + "name": "Alex Kocharin", + "email": "alex@kocharin.ru" + }, + "repository": { + "type": "git", + "url": "git://github.com/verdaccio/verdaccio" + }, + "main": "index.js", + "bin": { + "verdaccio": "./bin/verdaccio" + }, + "dependencies": { + "JSONStream": "^1.1.1", + "async": "^2.0.1", + "body-parser": "^1.15.0", + "bunyan": "^1.8.0", + "commander": "^2.9.0", + "compression": "^1.6.1", + "cookies": "^0.6.1", + "es6-shim": "^0.35.0", + "express": "^4.13.4", + "handlebars": "^4.0.5", + "highlight.js": "^9.3.0", + "http-errors": "^1.4.0", + "jju": "^1.3.0", + "js-yaml": "^3.6.0", + "lockfile": "^1.0.1", + "lunr": "^0.7.0", + "minimatch": "^3.0.0", + "mkdirp": "^0.5.1", + "pkginfo": "^0.4.0", + "readable-stream": "^2.1.2", + "render-readme": "^1.3.1", + "request": "^2.72.0", + "semver": "^5.1.0", + "symbol": "^0.2.1", + "unix-crypt-td-js": "^1.0.0" + }, + "devDependencies": { + "rimraf": "^2.5.2", + "bluebird": "^3.3.5", + "mocha": "^2.4.5", + "eslint": "^2.9.0", + "browserify": "^13.0.0", + "browserify-handlebars": "^1.0.0", + "grunt": "^1.0.1", + "grunt-cli": "^1.2.0", + "grunt-browserify": "^5.0.0", + "grunt-contrib-less": "^1.3.0", + "grunt-contrib-watch": "^1.0.0", + "unopinionate": "^0.0.4", + "onclick": "^0.1.0", + "transition-complete": "^0.0.2" + }, + "keywords": [ + "private", + "package", + "repository", + "registry", + "modules", + "proxy", + "server" + ], + "scripts": { + "test": "eslint . && mocha ./test/functional ./test/unit", + "test-travis": "eslint . && mocha -R spec ./test/functional ./test/unit", + "test-only": "mocha ./test/functional ./test/unit", + "lint": "eslint ." + }, + "engines": { + "node": ">=0.10" + }, + "preferGlobal": true, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "license": "WTFPL" +} diff --git a/package.yaml b/package.yaml deleted file mode 100644 index 0e4b939b5..000000000 --- a/package.yaml +++ /dev/null @@ -1,135 +0,0 @@ -# use "yapm install ." if you're installing this from git repository - -name: sinopia -version: 1.4.0 -description: Private npm repository server - -author: - name: Alex Kocharin - email: alex@kocharin.ru - -repository: - type: git - url: git://github.com/rlidwka/sinopia - -main: index.js - -bin: - sinopia: ./bin/sinopia - -dependencies: - express: '>=5.0.0-0 <6.0.0-0' - - # express middlewares - express-json5: '>=0.1.0 <1.0.0-0' - body-parser: '>=1.9.2 <2.0.0-0' - compression: '>=1.2.0 <2.0.0-0' - - commander: '>=2.3.0 <3.0.0-0' - js-yaml: '>=3.0.1 <4.0.0-0' - cookies: '>=0.5.0 <1.0.0-0' - request: '>=2.31.0 <3.0.0-0' - async: '>=0.9.0 <1.0.0-0' - es6-shim: '0.21.x' - semver: '>=2.2.1 <5.0.0-0' - minimatch: '>=0.2.14 <2.0.0-0' - bunyan: '>=0.22.1 <2.0.0-0' - handlebars: '2.x' - highlight.js: '8.x' - lunr: '>=0.5.2 <1.0.0-0' - render-readme: '>=0.2.1' - jju: '1.x' - JSONStream: '1.x' - - mkdirp: '>=0.3.5 <1.0.0-0' - sinopia-htpasswd: '>= 0.4.3' - http-errors: '>=1.2.0' - - # node 0.10 compatibility, should go away soon - readable-stream: '~1.1.0' - -optionalDependencies: - # those are native modules that could fail to compile - # and unavailable on windows - fs-ext: '>=0.4.1 <1.0.0-0' - crypt3: '>=0.1.6 <1.0.0-0' # for sinopia-htpasswd - -devDependencies: - # - # Tools required for testing - # - rimraf: '>=2.2.5 <3.0.0-0' - bluebird: '2 >=2.9' - - mocha: '2 >=2.2.3' - - # - # Linting tools - # - eslint: '1 >=1.1.0' - - # for debugging memory leaks, it'll be require()'d if - # installed, but I don't want it to be installed everytime - #heapdump: '*' - - # - # Tools required to build static files - # - browserify: '7.x' - browserify-handlebars: '1.x' - grunt: '>=0.4.4 <1.0.0-0' - grunt-cli: '*' - grunt-browserify: '>=2.0.8 <3.0.0-0' - grunt-contrib-less: '>=0.11.0 <1.0.0-0' - grunt-contrib-watch: '>=0.6.1 <1.0.0-0' - - # for building static/main.js, - # not required in runtime - unopinionate: '>=0.0.4 <1.0.0-0' - onclick: '>=0.1.0 <1.0.0-0' - transition-complete: '>=0.0.2 <1.0.0-0' - -keywords: - - private - - package - - repository - - registry - - modules - - proxy - - server - -scripts: - test: eslint . && mocha ./test/functional ./test/unit - test-travis: eslint . && mocha -R spec ./test/functional ./test/unit - test-only: mocha ./test/functional ./test/unit - lint: eslint . - prepublish: js-yaml package.yaml > package.json - clean-shrinkwrap: | - node -e ' - function clean(j) { - if (!j) return - for (var k in j) { - delete j[k].from - delete j[k].resolved - if (j[k].dependencies) clean(j[k].dependencies) - } - } - x = JSON.parse(require("fs").readFileSync("./npm-shrinkwrap.json")) - clean(x.dependencies) - x = JSON.stringify(x, null, " ") - require("fs").writeFileSync("./npm-shrinkwrap.json", x + "\n") - ' - -# we depend on streams2 stuff -# it can be replaced with isaacs/readable-stream, ask if you need to use 0.8 -engines: - node: '>=0.10' - -preferGlobal: true - -publishConfig: - registry: https://registry.npmjs.org/ - -license: - type: WTFPL - url: http://www.wtfpl.net/txt/copying/ diff --git a/test/functional/basic.js b/test/functional/basic.js index 785113406..542c5fa9c 100644 --- a/test/functional/basic.js +++ b/test/functional/basic.js @@ -43,7 +43,7 @@ module.exports = function () { return server.get_tarball('testpkg', 'blahblah') .status(200) .then(function (body) { - assert.deepEqual(body, readfile('fixtures/binary').toString('utf8')) + assert.deepEqual(body, readfile('fixtures/binary')) }) }) diff --git a/test/functional/gh29.js b/test/functional/gh29.js index b26e97f1a..f130c84c4 100644 --- a/test/functional/gh29.js +++ b/test/functional/gh29.js @@ -54,7 +54,7 @@ module.exports = function() { return server2.get_tarball('testpkg-gh29', 'blahblah') .status(200) .then(function (body) { - assert.deepEqual(body, readfile('fixtures/binary').toString('utf8')) + assert.deepEqual(body, readfile('fixtures/binary')) }) }) }) diff --git a/test/functional/index.js b/test/functional/index.js index c424eaa2c..34df7fd1c 100644 --- a/test/functional/index.js +++ b/test/functional/index.js @@ -59,6 +59,7 @@ describe('Func', function() { require('./scoped')() require('./security')() require('./adduser')() + require('./logout')() require('./addtag')() require('./plugins')() @@ -85,4 +86,3 @@ process.on('unhandledRejection', function (err) { throw err }) }) - diff --git a/test/functional/lib/server.js b/test/functional/lib/server.js index 3e33a9f30..4a1af0ef7 100644 --- a/test/functional/lib/server.js +++ b/test/functional/lib/server.js @@ -43,6 +43,13 @@ Server.prototype.auth = function(user, pass) { }) } +Server.prototype.logout = function(token) { + return this.request({ + uri: '/-/user/token/'+encodeURIComponent(token), + method: 'DELETE' + }) +} + Server.prototype.get_package = function(name) { return this.request({ uri: '/'+encodeURIComponent(name), @@ -76,6 +83,7 @@ Server.prototype.get_tarball = function(name, filename) { return this.request({ uri: '/'+encodeURIComponent(name)+'/-/'+encodeURIComponent(filename), method: 'GET', + encoding: null }) } diff --git a/test/functional/lib/smart_request.js b/test/functional/lib/smart_request.js index 9ca777c77..872fe176a 100644 --- a/test/functional/lib/smart_request.js +++ b/test/functional/lib/smart_request.js @@ -2,9 +2,8 @@ var assert = require('assert') var request = require('request') var Promise = require('bluebird') -var sym = typeof Symbol !== 'undefined' - ? Symbol('smart_request_data') - : '__why_does_node_0.10_support_suck_so_much' +var Symbol = require('symbol') +var sym = Symbol('smart_request_data') function smart_request(options) { var self = {} diff --git a/test/functional/lib/startup.js b/test/functional/lib/startup.js index b056346ff..fa0d868a1 100644 --- a/test/functional/lib/startup.js +++ b/test/functional/lib/startup.js @@ -17,7 +17,7 @@ module.exports.start = function start(dir, conf, cb) { return x !== '--debug-brk' }) - var f = fork(__dirname + '/../../../bin/sinopia' + var f = fork(__dirname + '/../../../bin/verdaccio' , ['-c', __dirname + '/../' + conf] , {silent: !process.env.TRAVIS} ) diff --git a/test/functional/logout.js b/test/functional/logout.js new file mode 100644 index 000000000..3729e70c9 --- /dev/null +++ b/test/functional/logout.js @@ -0,0 +1,11 @@ +module.exports = function() { + var server = process.server + + describe('logout', function() { + it('should log out', function () { + return server.logout('some-token') + .status(200) + .body_ok(/Logged out/) + }) + }) +} diff --git a/test/functional/mirror.js b/test/functional/mirror.js index 9fdd97c96..d1105ca1c 100644 --- a/test/functional/mirror.js +++ b/test/functional/mirror.js @@ -53,7 +53,7 @@ module.exports = function() { return server.get_tarball(pkg, pkg+'.file') .status(200) .then(function (body) { - assert.deepEqual(body, readfile('fixtures/binary').toString('utf8')) + assert.deepEqual(body, readfile('fixtures/binary')) }) }) }) diff --git a/test/functional/newnpmreg.js b/test/functional/newnpmreg.js index 2565950d0..ee9311420 100644 --- a/test/functional/newnpmreg.js +++ b/test/functional/newnpmreg.js @@ -32,7 +32,7 @@ module.exports = function() { .status(200) .then(function (body) { // not real sha due to utf8 conversion - assert.strictEqual(sha(body), '789ca61e3426ce55c4983451b58e62b04abceaf6') + assert.strictEqual(sha(body), '8ee7331cbc641581b1a8cecd9d38d744a8feb863') }) }) @@ -41,7 +41,7 @@ module.exports = function() { .status(200) .then(function (body) { // not real sha due to utf8 conversion - assert.strictEqual(sha(body), '789ca61e3426ce55c4983451b58e62b04abceaf6') + assert.strictEqual(sha(body), '8ee7331cbc641581b1a8cecd9d38d744a8feb863') }) }) diff --git a/test/functional/nullstorage.js b/test/functional/nullstorage.js index 330820c30..11857a422 100644 --- a/test/functional/nullstorage.js +++ b/test/functional/nullstorage.js @@ -51,7 +51,7 @@ module.exports = function() { return server.get_tarball('test-nullstorage2', 'blahblah') .status(200) .then(function (body) { - assert.deepEqual(body, readfile('fixtures/binary').toString('utf8')) + assert.deepEqual(body, readfile('fixtures/binary')) }) }) diff --git a/test/functional/scoped.js b/test/functional/scoped.js index 250853c7b..f25f16260 100644 --- a/test/functional/scoped.js +++ b/test/functional/scoped.js @@ -31,7 +31,7 @@ module.exports = function() { .status(200) .then(function (body) { // not real sha due to utf8 conversion - assert.strictEqual(sha(body), 'c59298948907d077c3b42f091554bdeea9208964') + assert.strictEqual(sha(body), '6e67b14e2c0e450b942e2bc8086b49e90f594790') }) }) @@ -40,7 +40,7 @@ module.exports = function() { .status(200) .then(function (body) { // not real sha due to utf8 conversion - assert.strictEqual(sha(body), 'c59298948907d077c3b42f091554bdeea9208964') + assert.strictEqual(sha(body), '6e67b14e2c0e450b942e2bc8086b49e90f594790') }) }) diff --git a/test/unit/st_merge.js b/test/unit/st_merge.js index d07988fd1..8c98ba453 100644 --- a/test/unit/st_merge.js +++ b/test/unit/st_merge.js @@ -20,24 +20,12 @@ describe('Merge', function() { it('dist-tags - compat', function() { var x = { versions: {}, - 'dist-tags': {q:'1.1.1',w:['2.2.2']}, + 'dist-tags': {q:'1.1.1',w:'2.2.2'}, } merge(x, {'dist-tags':{q:'2.2.2',w:'3.3.3',t:'4.4.4'}}) assert.deepEqual(x, { versions: {}, - 'dist-tags': {q:['1.1.1','2.2.2'],w:['2.2.2','3.3.3'],t:['4.4.4']}, - }) - }) - - it('dist-tags - sort', function() { - var x = { - versions: {}, - 'dist-tags': {w:['2.2.2','1.1.1','12.2.2','2.2.2-rc2']}, - } - merge(x, {'dist-tags':{w:'3.3.3'}}) - assert.deepEqual(x, { - versions: {}, - 'dist-tags': {w:["1.1.1","2.2.2-rc2","2.2.2","3.3.3","12.2.2"]}, + 'dist-tags': {q:'2.2.2',w:'3.3.3',t:'4.4.4'}, }) }) diff --git a/test/unit/tag_version.js b/test/unit/tag_version.js index b8f2f1c46..036881d80 100644 --- a/test/unit/tag_version.js +++ b/test/unit/tag_version.js @@ -12,7 +12,7 @@ describe('tag_version', function() { assert(tag_version(x, '1.1.1', 'foo', {})) assert.deepEqual(x, { versions: {}, - 'dist-tags': {foo: ['1.1.1']}, + 'dist-tags': {foo: '1.1.1'}, }) }) @@ -21,35 +21,24 @@ describe('tag_version', function() { versions: {}, 'dist-tags': {foo: '1.1.0'}, } - assert(tag_version(x, '1.1.1', 'foo', {})) + assert(tag_version(x, '1.1.1', 'foo')) assert.deepEqual(x, { versions: {}, - 'dist-tags': {foo: ['1.1.0', '1.1.1']}, + 'dist-tags': {foo: '1.1.1'}, }) }) it('add fresh tag', function() { var x = { versions: {}, - 'dist-tags': {foo: ['1.1.0']}, + 'dist-tags': {foo: '1.1.0'}, } - assert(tag_version(x, '1.1.1', 'foo', {})) + assert(tag_version(x, '1.1.1', 'foo')) assert.deepEqual(x, { versions: {}, - 'dist-tags': {foo: ['1.1.0', '1.1.1']}, + 'dist-tags': {foo: '1.1.1'}, }) }) - it('add stale tag', function() { - var x = { - versions: {}, - 'dist-tags': {foo: ['1.1.2']}, - } - assert(!tag_version(x, '1.1.1', 'foo', {})) - assert.deepEqual(x, { - versions: {}, - 'dist-tags': {foo: ['1.1.1', '1.1.2']}, - }) - }) }) diff --git a/test/unit/toplevel.js b/test/unit/toplevel.js index 4a2e3c3fe..abe05dd7a 100644 --- a/test/unit/toplevel.js +++ b/test/unit/toplevel.js @@ -39,7 +39,7 @@ describe('toplevel', function() { url: 'http://localhost:' + port + '/', }, function(err, res, body) { assert.equal(err, null) - assert(body.match(/Sinopia<\/title>/)) + assert(body.match(/<title>Verdaccio<\/title>/)) done() }) })