├── .gitignore ├── AUTHORS ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── TODO.txt ├── lib ├── cache.js └── ldapauth.js ├── package.json └── tools └── jsstyle /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /tmp 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Trent Mick (http://trentm.com) 2 | Jacques Marneweck (https://github.com/jacques) 3 | Vesa Poikajärvi (https://github.com/vesse) 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # node-ldapauth Changelog 2 | 3 | ## not yet released 4 | 5 | (nothing yet) 6 | 7 | 8 | ## 2.3.1 9 | 10 | - [#29] Treat "UnwillingToPerformError" (LDAP error code 53) the same as 11 | "InvalidCredentialsError" as a failure that should stop connection retries. 12 | (By github.com/melloc). 13 | 14 | 15 | ## 2.3.0 16 | 17 | - [pull #19] Update deps for node 0.12 support (by https://github.com/ricardohbin). 18 | 19 | - Rewrite ldapjs connection handling. We now do retries. We now bind 20 | up front. The connect/bind check to verify a found user's password 21 | (in `.authenticate()`) creates a new connection each time. A significant 22 | usage change here is that one should wait for the 'connect' event 23 | from the `LdapAuth` instance before using it: 24 | 25 | var LdapAuth = require('ldapauth'); 26 | var auth = new LdapAuth({url: 'ldaps://ldap.example.com:663', ...}); 27 | 28 | // If you want to be lazier you can skip waiting for 'connect'. :) 29 | // It just means that a quick `.authenticate()` call will likely fail 30 | // while the LDAP connect and bind is still being done. 31 | auth.once('connect', function () { 32 | ... 33 | auth.authenticate(username, password, function (err, user) { ... }); 34 | ... 35 | auth.close(function (err) { ... }) 36 | }); 37 | 38 | There is a lot new here, so caveat usor. 39 | 40 | - Drop log4js support in favour of Bunyan. 41 | 42 | - 4-space code indents. Should be no functional change. 43 | 44 | 45 | ## 2.2.4 46 | 47 | - [pull #12] Add `tlsOptions`, `timeout` and `connectTimeout` options in `LdapAuth` 48 | constructor (by github.com/vesse). 49 | 50 | ## 2.2.3 51 | 52 | - [pull #11] Update to latest ldapjs, v0.6.3 (by github.com/Esya). 53 | 54 | 55 | ## 2.2.2 56 | 57 | - [issue #5] update to bcrypt 0.7.5 (0.7.3 fixes potential mem issues) 58 | 59 | 60 | ## 2.2.1 61 | 62 | - Fix a bug where ldapauth `authenticate()` would raise an example on an empty 63 | username. 64 | 65 | 66 | ## 2.2.0 67 | 68 | - Update to latest ldapjs (0.5.6) and other deps. 69 | Note: This makes ldapauth only work with node >=0.8 (because of internal dep 70 | in ldapjs 0.5). 71 | 72 | 73 | ## 2.1.0 74 | 75 | - Update to ldapjs 0.4 (from 0.3). Crossing fingers that this doesn't cause breakage. 76 | 77 | 78 | ## 2.0.0 79 | 80 | - Add `make check` for checking jsstyle. 81 | - [issue #1] Update to bcrypt 0.5. This means increasing the base node from 0.4 82 | to 0.6, hence the major version bump. 83 | 84 | 85 | ## 1.0.2 86 | 87 | First working version. 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011 Trent Mick. 2 | All rights reserved. 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Trent Mick 3 | # Copyright 2019 Joyent, Inc. 4 | # 5 | # node-ldapauth Makefile 6 | # 7 | 8 | #---- Files 9 | 10 | JSSTYLE_FILES := $(shell find lib -name *.js) 11 | 12 | 13 | 14 | #---- Targets 15 | 16 | all: 17 | npm install 18 | 19 | .PHONY: check-jsstyle 20 | check-jsstyle: $(JSSTYLE_FILES) 21 | ./tools/jsstyle -o indent=2,doxygen,unparenthesized-return=0,blank-after-start-comment=0 $(JSSTYLE_FILES) 22 | 23 | # Ensure CHANGES.md and package.json have the same version. 24 | .PHONY: check-version 25 | check-version: 26 | @echo version is: $(shell cat package.json | json version) 27 | [[ `cat package.json | json version` == `grep '^## ' CHANGES.md | head -2 | tail -1 | awk '{print $$2}'` ]] 28 | 29 | .PHONY: check 30 | check: check-version check-jsstyle 31 | @echo "Check ok." 32 | 33 | .PHONY: cutarelease 34 | cutarelease: check-version 35 | [[ -z `git status --short` ]] # If this fails, the working dir is dirty. 36 | @which json 2>/dev/null 1>/dev/null && \ 37 | ver=$(shell json -f package.json version) && \ 38 | name=$(shell json -f package.json name) && \ 39 | publishedVer=$(shell npm view -j $(shell json -f package.json name)@$(shell json -f package.json version) version 2>/dev/null) && \ 40 | if [[ -n "$$publishedVer" ]]; then \ 41 | echo "error: $$name@$$ver is already published to npm"; \ 42 | exit 1; \ 43 | fi && \ 44 | echo "** Are you sure you want to tag and publish $$name@$$ver to npm?" && \ 45 | echo "** Enter to continue, Ctrl+C to abort." && \ 46 | read 47 | ver=$(shell cat package.json | json version) && \ 48 | date=$(shell date -u "+%Y-%m-%d") && \ 49 | git tag -a "$$ver" -m "version $$ver ($$date)" && \ 50 | git push --tags origin && \ 51 | npm publish 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Note: This repo is unmaintained and has been for a while. If you are 2 | interested in taking over this repo, then please let me know (trentm at 3 | google's email thing).** 4 | 5 | * * * 6 | 7 | A simple node.js lib to authenticate against an LDAP server. 8 | 9 | 10 | # Usage 11 | 12 | var LdapAuth = require('ldapauth'); 13 | var options = { 14 | url: 'ldaps://ldap.example.com:663', 15 | ... 16 | }; 17 | var auth = new LdapAuth(options); 18 | ... 19 | auth.authenticate(username, password, function(err, user) { ... }); 20 | ... 21 | auth.close(function(err) { ... }) 22 | 23 | 24 | # Install 25 | 26 | npm install ldapauth 27 | 28 | 29 | # License 30 | 31 | MIT. See "LICENSE" file. 32 | 33 | 34 | # `LdapAuth` Config Options 35 | 36 | [Use the source Luke](https://github.com/trentm/node-ldapauth/blob/master/lib/ldapauth.js#L25-53) 37 | 38 | 39 | # express/connect basicAuth example 40 | 41 | var connect = require('connect'); 42 | var LdapAuth = require('ldapauth'); 43 | 44 | // Config from a .json or .ini file or whatever. 45 | var config = { 46 | ldap: { 47 | url: "ldaps://ldap.example.com:636", 48 | adminDn: "uid=myadminusername,ou=users,o=example.com", 49 | adminPassword: "mypassword", 50 | searchBase: "ou=users,o=example.com", 51 | searchFilter: "(uid={{username}})" 52 | } 53 | }; 54 | 55 | var ldap = new LdapAuth({ 56 | url: config.ldap.url, 57 | adminDn: config.ldap.adminDn, 58 | adminPassword: config.ldap.adminPassword, 59 | searchBase: config.ldap.searchBase, 60 | searchFilter: config.ldap.searchFilter, 61 | //log4js: require('log4js'), 62 | cache: true 63 | }); 64 | 65 | var basicAuthMiddleware = connect.basicAuth(function (username, password, callback) { 66 | ldap.authenticate(username, password, function (err, user) { 67 | if (err) { 68 | console.log("LDAP auth error: %s", err); 69 | } 70 | callback(err, user) 71 | }); 72 | }); 73 | 74 | 75 | # Development 76 | 77 | Check coding style before commit: 78 | 79 | make check 80 | 81 | To cut a release (tagging, npm publish, etc., see 82 | for details): 83 | 84 | make cutarelease 85 | -------------------------------------------------------------------------------- /TODO.txt: -------------------------------------------------------------------------------- 1 | - change to bunyan logging 2 | - lib/cache.js -> https://github.com/trentm/node-expiring-lru-cache 3 | -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 Joyent, Inc. All rights reserved. 3 | * 4 | * An expiring LRU cache. 5 | * 6 | * Usage: 7 | * var Cache = require('./cache').Cache; 8 | * // size, expiry, log, name 9 | * this.accountCache = new Cache( 100, 300, log, 'account'); 10 | * this.accountCache.set('hamish', {...}); 11 | * ... 12 | * this.accountCache.get('hamish') // -> {...} 13 | */ 14 | 15 | var debug = console.warn; 16 | var assert = require('assert'); 17 | var LRU = require('lru-cache'); 18 | 19 | 20 | /** 21 | * A LRU and expiring cache. 22 | * 23 | * @param size {Number} Max number of entries to cache. 24 | * @param expiry {Number} Number of seconds after which to expire entries. 25 | * @param log {log4js Logger} Optional. 26 | * All logging is at the Trace level. 27 | * @param name {string} Optional name for this cache. Just used for logging. 28 | */ 29 | function Cache(size, expiry, log, name) { 30 | assert.ok(size !== undefined); 31 | assert.ok(expiry !== undefined); 32 | this.size = size; 33 | this.expiry = expiry * 1000; 34 | this.log = log; 35 | this.name = (name ? name + ' ' : ''); 36 | this.items = LRU(this.size); 37 | } 38 | 39 | // Debugging stuff: .getAll isn't in official lru-cache 40 | //Cache.prototype.getAll = function getAll() { 41 | // return this.items.getAll(); 42 | //} 43 | 44 | Cache.prototype.reset = function reset() { 45 | if (this.log) { 46 | this.log.trace('%scache reset', this.name); 47 | } 48 | this.items.reset(); 49 | } 50 | 51 | Cache.prototype.get = function get(key) { 52 | assert.ok(key !== undefined); 53 | var cached = this.items.get(key); 54 | if (cached) { 55 | if (((new Date()).getTime() - cached.ctime) <= this.expiry) { 56 | if (this.log) { 57 | this.log.trace('%scache hit: key="%s": %o', this.name, key, cached); 58 | } 59 | return cached.value; 60 | } 61 | } 62 | if (this.log) { 63 | this.log.trace('%scache miss: key="%s"', this.name, key); 64 | } 65 | return null; 66 | } 67 | 68 | Cache.prototype.set = function set(key, value) { 69 | assert.ok(key !== undefined); 70 | var item = { 71 | value: value, 72 | ctime: new Date().getTime() 73 | }; 74 | if (this.log) { 75 | this.log.trace('%scache set: key="%s": %o', this.name, key, item); 76 | } 77 | this.items.set(key, item); 78 | return item; 79 | } 80 | 81 | Cache.prototype.del = function del(key) { 82 | if (this.log) { 83 | this.log.trace('%scache del: key="%s"', this.name, key); 84 | } 85 | this.items.del(key); 86 | } 87 | 88 | 89 | module.exports = Cache; 90 | -------------------------------------------------------------------------------- /lib/ldapauth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2013 (c) Trent Mick. All rights reserved. 3 | * Copyright 2013 (c) Joyent Inc. All rights reserved. 4 | * 5 | * LDAP auth. 6 | * 7 | * Usage: 8 | * var LdapAuth = require('ldapauth'); 9 | * var auth = new LdapAuth({url: 'ldaps://ldap.example.com:663', ...}); 10 | * 11 | * // If you want to be lazier you can skip waiting for 'connect'. :) 12 | * // It just means that a quick `.authenticate()` call will likely fail 13 | * // while the LDAP connect and bind is still being done. 14 | * auth.once('connect', function () { 15 | * ... 16 | * auth.authenticate(username, password, function (err, user) { ... }); 17 | * ... 18 | * auth.close(function (err) { ... }) 19 | * }); 20 | */ 21 | 22 | var p = console.warn; 23 | var EventEmitter = require('events').EventEmitter; 24 | var util = require('util'), 25 | format = util.format; 26 | 27 | var assert = require('assert-plus'); 28 | var backoff = require('backoff'); 29 | var bcrypt = require('bcrypt'); 30 | var ldap = require('ldapjs'); 31 | var once = require('once'); 32 | 33 | 34 | 35 | //---- internal support stuff 36 | 37 | function objCopy(obj) { 38 | var copy = {}; 39 | Object.keys(obj).forEach(function (k) { 40 | copy[k] = obj[k]; 41 | }); 42 | return copy; 43 | } 44 | 45 | // Other ldapjs client events are handled here or in `createClient`. 46 | var LDAP_PROXY_EVENTS = [ 47 | 'timeout', 48 | 'socketTimeout' 49 | ]; 50 | 51 | 52 | //---- LdapAuth exported class 53 | 54 | /** 55 | * Create an LDAP auth class. Primary usage is the `.authenticate` method. 56 | * 57 | * @param opts {Object} Config options. Keys (required, unless says 58 | * otherwise) are: 59 | * url {String} E.g. 'ldaps://ldap.example.com:663' 60 | * adminDn {String} E.g. 'uid=myapp,ou=users,o=example.com' 61 | * adminPassword {String} Password for adminDn. 62 | * searchBase {String} The base DN from which to search for users by 63 | * username. E.g. 'ou=users,o=example.com' 64 | * searchFilter {String} LDAP search filter with which to find a user by 65 | * username, e.g. '(uid={{username}})'. Use the literal '{{username}}' 66 | * to have the given username be interpolated in for the LDAP 67 | * search. 68 | * log {Bunyan Logger} Optional. If given this will result in TRACE-level 69 | * logging for component:ldapauth. 70 | * verbose {Boolean} Optional, default false. If `log` is also given, 71 | * this will add TRACE-level logging for ldapjs (quite verbose). 72 | * cache {Boolean} Optional, default false. If true, then up to 100 73 | * credentials at a time will be cached for 5 minutes. 74 | * timeout {Integer} Optional, default Infinity. How long the client should 75 | * let operations live for before timing out. 76 | * connectTimeout {Integer} Optional, default is up to the OS. How long the 77 | * client should wait before timing out on TCP connections. 78 | * tlsOptions {Object} Additional options passed to the TLS connection layer 79 | * when connecting via ldaps://. See 80 | * http://nodejs.org/api/tls.html#tls_tls_connect_options_callback 81 | * for available options 82 | * retry {Object} Optional: 83 | * - maxDelay {Number} maximum amount of time between retries 84 | * - retries {Number} maximum # of retries 85 | */ 86 | function LdapAuth(opts) { 87 | assert.string(opts.url, 'opts.url'); 88 | assert.ok(opts.adminDn, 'opts.adminDn'); 89 | assert.ok(opts.searchBase, 'opts.searchBase'); 90 | assert.ok(opts.searchFilter, 'opts.searchFilter'); 91 | 92 | var self = this; 93 | EventEmitter.call(this); 94 | 95 | this.opts = opts; 96 | this.log = opts.log && opts.log.child({component: 'ldapauth'}, true); 97 | if (opts.cache) { 98 | var Cache = require('./cache'); 99 | this.userCache = new Cache(100, 300, this.log, 'user'); 100 | } 101 | this._salt = bcrypt.genSaltSync(); 102 | 103 | this._adminOpts = { 104 | connectTimeout: opts.connectTimeout, 105 | credentials: { 106 | dn: opts.adminDn, 107 | passwd: opts.adminPassword 108 | }, 109 | log: opts.verbose ? self.log : undefined, 110 | retry: opts.retry || {}, 111 | tlsOptions: opts.tlsOptions, 112 | timeout: opts.timeout, 113 | url: opts.url 114 | }; 115 | (function adminConnect() { 116 | self._adminConnecting = self._createClient(self._adminOpts, function (err, client) { 117 | self._adminConnecting = false; 118 | 119 | // We only get error if credentials are invalid 120 | if (err) { 121 | self.emit('error', err); 122 | return; 123 | } 124 | 125 | if (self.closed && client) { 126 | client.unbind(); 127 | return; 128 | } 129 | 130 | function handleClose() { 131 | if (self._adminClient && !self._adminConnecting && !self.closed) { 132 | self.log && self.log.warn(err, 'admin LDAP client disconnected'); 133 | self._adminClient = null; 134 | adminConnect(); 135 | } 136 | } 137 | 138 | client.once('error', handleClose); 139 | client.once('close', handleClose); 140 | LDAP_PROXY_EVENTS.forEach(function reEmit(event) { 141 | client.on(event, self.emit.bind(self, event)); 142 | }); 143 | 144 | self._adminClient = client; 145 | self.emit('connect'); 146 | }); 147 | })(); 148 | } 149 | util.inherits(LdapAuth, EventEmitter); 150 | 151 | 152 | // TODO: change all this to pull bind OUT of the retry section 153 | LdapAuth.prototype._createClient = function _createClient(opts, cb) { 154 | assert.object(opts, 'options'); 155 | assert.func(cb, 'callback'); 156 | var self = this; 157 | 158 | cb = once(cb); 159 | 160 | var dn = opts.credentials.dn; 161 | var log = opts.log; 162 | var passwd = opts.credentials.passwd; 163 | var retryOpts = objCopy(opts.retry || {}); 164 | retryOpts.maxDelay = retryOpts.maxDelay || retryOpts.maxTimeout || 30000; 165 | retryOpts.retries = retryOpts.retries || Infinity; 166 | 167 | function _createClientAttempt(_, _cb) { 168 | function onConnect() { 169 | client.removeListener('error', onError); 170 | log && log.trace('connected'); 171 | if (self.closed) { 172 | client.socket.end(); 173 | _cb(); 174 | return; 175 | } 176 | client.bind(dn, passwd, function (err) { 177 | if (self.closed) { 178 | client.socket.end(); 179 | _cb(); 180 | return; 181 | } 182 | if (err) { 183 | if (err.name === 'InvalidCredentialsError' || 184 | err.name === 'UnwillingToPerformError') { 185 | log && log.trace({bindDn: dn, err: err}, 186 | 'invalid credentials; aborting retries'); 187 | cb(err); 188 | client.socket.end(); 189 | retry.abort(); 190 | } else { 191 | log && log.trace({bindDn: dn, err: err}, 192 | 'unexpected bind error'); 193 | _cb(err); 194 | } 195 | return; 196 | } 197 | 198 | log && log.trace({bindDn: dn}, 'connected and bound'); 199 | client.socket.setKeepAlive(true); 200 | _cb(null, client); 201 | }); 202 | } 203 | 204 | function onError(err) { 205 | client.removeListener('connect', onConnect); 206 | _cb(err); 207 | } 208 | 209 | var client = ldap.createClient(opts); 210 | client.once('connect', onConnect); 211 | client.once('error', onError); 212 | client.once('connectTimeout', function () { 213 | onError(new Error('connect timeout')); 214 | }); 215 | } 216 | 217 | var retry = backoff.call(_createClientAttempt, null, cb); 218 | retry.setStrategy(new backoff.ExponentialStrategy(retryOpts)); 219 | retry.failAfter(retryOpts.retries); 220 | 221 | retry.on('backoff', function (number, delay) { 222 | var level; 223 | if (number === 0) { 224 | level = 'info'; 225 | } else if (number < 5) { 226 | level = 'warn'; 227 | } else { 228 | level = 'error'; 229 | } 230 | log && log[level]({attempt: number, delay: delay}, 231 | 'connection attempt failed'); 232 | }); 233 | 234 | retry.start(); 235 | return (retry); 236 | } 237 | 238 | 239 | 240 | LdapAuth.prototype.close = function close(cb) { 241 | assert.func(cb, 'callback'); 242 | var self = this; 243 | cb = once(cb); 244 | 245 | this.closed = true; 246 | if (!this._adminClient) { 247 | if (this._adminConnecting) { 248 | this._adminConnecting.abort(); 249 | } 250 | cb(); 251 | return; 252 | } 253 | 254 | LDAP_PROXY_EVENTS.forEach(function reEmit(event) { 255 | self._adminClient.removeAllListeners(event); 256 | }); 257 | 258 | this._adminClient.unbind(function (err) { 259 | if (err) { 260 | cb(err); 261 | } else { 262 | process.nextTick(self.emit.bind(self, 'close')); 263 | cb(); 264 | } 265 | }); 266 | }; 267 | 268 | 269 | /** 270 | * Find the user record for the given username. 271 | * 272 | * @param username {String} 273 | * @param callback {Function} `function (err, user)`. If no such user is 274 | * found but no error processing, then `user` is undefined. 275 | * 276 | */ 277 | LdapAuth.prototype._findUser = function (username, callback) { 278 | var self = this; 279 | if (!username) { 280 | return callback(new Error("empty username")); 281 | } 282 | 283 | if (!this._adminClient) { 284 | return callback(new Error("LDAP connection is not yet bound")); 285 | } 286 | 287 | var searchFilter = self.opts.searchFilter.replace('{{username}}', username); 288 | var opts = { 289 | filter: searchFilter, 290 | scope: 'sub' 291 | }; 292 | self._adminClient.search(self.opts.searchBase, opts, 293 | function (err, result) { 294 | if (err) { 295 | self.log && self.log.trace(err, 'ldap authenticate: search error'); 296 | return callback(err); 297 | } 298 | var items = []; 299 | result.on('searchEntry', function (entry) { 300 | items.push(entry.object); 301 | }); 302 | result.on('error', function (err) { 303 | self.log && self.log.trace(err, 304 | 'ldap authenticate: search error event'); 305 | return callback(err); 306 | }); 307 | result.on('end', function (result) { 308 | if (result.status !== 0) { 309 | var err = 'non-zero status from LDAP search: ' + result.status; 310 | self.log && self.log.trace(err, 'ldap authenticate'); 311 | return callback(err); 312 | } 313 | switch (items.length) { 314 | case 0: 315 | return callback(); 316 | case 1: 317 | return callback(null, items[0]) 318 | default: 319 | return callback(format( 320 | 'unexpected number of matches (%s) for "%s" username', 321 | items.length, username)); 322 | } 323 | }); 324 | }); 325 | } 326 | 327 | 328 | /** 329 | * 330 | */ 331 | LdapAuth.prototype.authenticate = function (username, password, callback) { 332 | var self = this; 333 | var opts = self.opts; 334 | var log = self.log; 335 | 336 | if (self.opts.cache) { 337 | // Check cache. 'cached' is `{password: , user: }`. 338 | var cached = self.userCache.get(username); 339 | if (cached && bcrypt.compareSync(password, cached.password)) { 340 | return callback(null, cached.user) 341 | } 342 | } 343 | 344 | // 1. Find the user DN in question. 345 | self._findUser(username, function (err, user) { 346 | if (err) 347 | return callback(err); 348 | if (!user) 349 | return callback(format('no such user: "%s"', username)); 350 | 351 | // 2. Attempt to bind as that user to check password. 352 | var userOpts = { 353 | connectTimeout: opts.connectTimeout, 354 | credentials: { 355 | dn: user.dn, 356 | passwd: password 357 | }, 358 | log: opts.verbose ? log : undefined, 359 | retry: opts.retry || {}, 360 | tlsOptions: opts.tlsOptions, 361 | timeout: opts.timeout, 362 | url: opts.url 363 | }; 364 | self._createClient(userOpts, function (err, client) { 365 | // We only get error if credentials are invalid. 366 | if (err) { 367 | log && log.trace('ldap authenticate: bind error: %s', err); 368 | return callback(err); 369 | } 370 | client.unbind(function (unbindErr) { 371 | log && log.trace(unbindErr, 'error unbinding user client (ignoring)'); 372 | if (self.opts.cache) { 373 | bcrypt.hash(password, self._salt, function (err, hash) { 374 | self.userCache.set(username, { 375 | password: hash, 376 | user: user 377 | }); 378 | return callback(null, user); 379 | }); 380 | } else { 381 | return callback(null, user); 382 | } 383 | }); 384 | }); 385 | }); 386 | } 387 | 388 | 389 | 390 | module.exports = LdapAuth; 391 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ldapauth", 3 | "version": "2.3.1", 4 | "main": "./lib/ldapauth.js", 5 | "description": "Authenticate against an LDAP server", 6 | "author": "Trent Mick (http://trentm.com)", 7 | "license": { 8 | "type": "MIT", 9 | "url": "https://github.com/trentm/node-ldapauth/raw/master/LICENSE" 10 | }, 11 | "keywords": ["authenticate", "ldap", "authentication", "auth"], 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/trentm/node-ldapauth.git" 15 | }, 16 | "engines": ["node >=0.8.0"], 17 | "dependencies": { 18 | "assert-plus": "0.1.4", 19 | "backoff": "2.3.0", 20 | "bcrypt": "~0.8.1", 21 | "ldapjs": "~0.7.1", 22 | "lru-cache": "2.0.4", 23 | "once": "1.3.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tools/jsstyle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env perl 2 | # 3 | # CDDL HEADER START 4 | # 5 | # The contents of this file are subject to the terms of the 6 | # Common Development and Distribution License (the "License"). 7 | # You may not use this file except in compliance with the License. 8 | # 9 | # You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE 10 | # or http://www.opensolaris.org/os/licensing. 11 | # See the License for the specific language governing permissions 12 | # and limitations under the License. 13 | # 14 | # When distributing Covered Code, include this CDDL HEADER in each 15 | # file and include the License file at usr/src/OPENSOLARIS.LICENSE. 16 | # If applicable, add the following below this CDDL HEADER, with the 17 | # fields enclosed by brackets "[]" replaced with your own identifying 18 | # information: Portions Copyright [yyyy] [name of copyright owner] 19 | # 20 | # CDDL HEADER END 21 | # 22 | # 23 | # Copyright 2008 Sun Microsystems, Inc. All rights reserved. 24 | # Use is subject to license terms. 25 | # 26 | # Copyright 2011 Joyent, Inc. All rights reserved. 27 | # 28 | # jsstyle - check for some common stylistic errors. 29 | # 30 | # jsstyle is a sort of "lint" for Javascript coding style. This tool is 31 | # derived from the cstyle tool, used to check for the style used in the 32 | # Solaris kernel, sometimes known as "Bill Joy Normal Form". 33 | # 34 | # There's a lot this can't check for, like proper indentation of code 35 | # blocks. There's also a lot more this could check for. 36 | # 37 | # A note to the non perl literate: 38 | # 39 | # perl regular expressions are pretty much like egrep 40 | # regular expressions, with the following special symbols 41 | # 42 | # \s any space character 43 | # \S any non-space character 44 | # \w any "word" character [a-zA-Z0-9_] 45 | # \W any non-word character 46 | # \d a digit [0-9] 47 | # \D a non-digit 48 | # \b word boundary (between \w and \W) 49 | # \B non-word boundary 50 | # 51 | 52 | require 5.0; 53 | use IO::File; 54 | use Getopt::Std; 55 | use strict; 56 | 57 | my $usage = 58 | "Usage: jsstyle [-h?vcC] [-t ] [-f ] [-o ] file ... 59 | 60 | Check your JavaScript file for style. 61 | See for details on config options. 62 | Report bugs to . 63 | 64 | Options: 65 | -h print this help and exit 66 | -v verbose 67 | 68 | -c check continuation indentation inside functions 69 | -t specify tab width for line length calculation 70 | -C don't check anything in header block comments 71 | 72 | -f PATH 73 | path to a jsstyle config file 74 | -o OPTION1,OPTION2 75 | set config options, e.g. '-o doxygen,indent=2' 76 | 77 | "; 78 | 79 | my %opts; 80 | 81 | if (!getopts("ch?o:t:f:vC", \%opts)) { 82 | print $usage; 83 | exit 2; 84 | } 85 | 86 | if (defined($opts{'h'}) || defined($opts{'?'})) { 87 | print $usage; 88 | exit; 89 | } 90 | 91 | my $check_continuation = $opts{'c'}; 92 | my $verbose = $opts{'v'}; 93 | my $ignore_hdr_comment = $opts{'C'}; 94 | my $tab_width = $opts{'t'}; 95 | 96 | # By default, tabs are 8 characters wide 97 | if (! defined($opts{'t'})) { 98 | $tab_width = 8; 99 | } 100 | 101 | 102 | # Load config 103 | my %config = ( 104 | indent => "tab", 105 | doxygen => 0, # doxygen comments: /** ... */ 106 | splint => 0, # splint comments. Needed? 107 | "unparenthesized-return" => 1, 108 | "literal-string-quote" => "single", # 'single' or 'double' 109 | "blank-after-start-comment" => 1, 110 | ); 111 | sub add_config_var ($$) { 112 | my ($scope, $str) = @_; 113 | 114 | if ($str !~ /^([\w-]+)(?:\s*=\s*(.*?))?$/) { 115 | die "$scope: invalid option: '$str'"; 116 | } 117 | my $name = $1; 118 | my $value = ($2 eq '' ? 1 : $2); 119 | #print "scope: '$scope', str: '$str', name: '$name', value: '$value'\n"; 120 | 121 | # Validate config var. 122 | if ($name eq "indent") { 123 | # A number of spaces or "tab". 124 | if ($value !~ /^\d+$/ && $value ne "tab") { 125 | die "$scope: invalid '$name': must be a number (of ". 126 | "spaces) or 'tab'"; 127 | } 128 | } elsif ($name eq "doxygen" || # boolean vars 129 | $name eq "splint" || 130 | $name eq "unparenthesized-return" || 131 | $name eq "blank-after-start-comment") { 132 | if ($value != 1 && $value != 0) { 133 | die "$scope: invalid '$name': don't give a value"; 134 | } 135 | } elsif ($name eq "literal-string-quote") { 136 | if ($value !~ /single|double/) { 137 | die "$scope: invalid '$name': must be 'single' ". 138 | "or 'double'"; 139 | } 140 | } else { 141 | die "$scope: unknown config var: $name"; 142 | } 143 | $config{$name} = $value; 144 | } 145 | 146 | if (defined($opts{'f'})) { 147 | my $path = $opts{'f'}; 148 | my $fh = new IO::File $path, "r"; 149 | if (!defined($fh)) { 150 | die "cannot open config path '$path'"; 151 | } 152 | my $line = 0; 153 | while (<$fh>) { 154 | $line++; 155 | s/^\s*//; # drop leading space 156 | s/\s*$//; # drop trailing space 157 | next if ! $_; # skip empty line 158 | next if /^#/; # skip comments 159 | add_config_var "$path:$line", $_; 160 | } 161 | } 162 | 163 | if (defined($opts{'o'})) { 164 | for my $x (split /,/, $opts{'o'}) { 165 | add_config_var "'-o' option", $x; 166 | } 167 | } 168 | 169 | 170 | my ($filename, $line, $prev); # shared globals 171 | 172 | my $fmt; 173 | my $hdr_comment_start; 174 | 175 | if ($verbose) { 176 | $fmt = "%s: %d: %s\n%s\n"; 177 | } else { 178 | $fmt = "%s: %d: %s\n"; 179 | } 180 | 181 | if ($config{"doxygen"}) { 182 | # doxygen comments look like "/*!" or "/**"; allow them. 183 | $hdr_comment_start = qr/^\s*\/\*[\!\*]?$/; 184 | } else { 185 | $hdr_comment_start = qr/^\s*\/\*$/; 186 | } 187 | 188 | # Note, following must be in single quotes so that \s and \w work right. 189 | my $lint_re = qr/\/\*(?: 190 | jsl:\w+?|ARGSUSED[0-9]*|NOTREACHED|LINTLIBRARY|VARARGS[0-9]*| 191 | CONSTCOND|CONSTANTCOND|CONSTANTCONDITION|EMPTY| 192 | FALLTHRU|FALLTHROUGH|LINTED.*?|PRINTFLIKE[0-9]*| 193 | PROTOLIB[0-9]*|SCANFLIKE[0-9]*|JSSTYLED.*? 194 | )\*\//x; 195 | 196 | my $splint_re = qr/\/\*@.*?@\*\//x; 197 | 198 | my $err_stat = 0; # exit status 199 | 200 | if ($#ARGV >= 0) { 201 | foreach my $arg (@ARGV) { 202 | my $fh = new IO::File $arg, "r"; 203 | if (!defined($fh)) { 204 | printf "%s: cannot open\n", $arg; 205 | } else { 206 | &jsstyle($arg, $fh); 207 | close $fh; 208 | } 209 | } 210 | } else { 211 | &jsstyle("", *STDIN); 212 | } 213 | exit $err_stat; 214 | 215 | my $no_errs = 0; # set for JSSTYLED-protected lines 216 | 217 | sub err($) { 218 | my ($error) = @_; 219 | unless ($no_errs) { 220 | printf $fmt, $filename, $., $error, $line; 221 | $err_stat = 1; 222 | } 223 | } 224 | 225 | sub err_prefix($$) { 226 | my ($prevline, $error) = @_; 227 | my $out = $prevline."\n".$line; 228 | unless ($no_errs) { 229 | printf $fmt, $filename, $., $error, $out; 230 | $err_stat = 1; 231 | } 232 | } 233 | 234 | sub err_prev($) { 235 | my ($error) = @_; 236 | unless ($no_errs) { 237 | printf $fmt, $filename, $. - 1, $error, $prev; 238 | $err_stat = 1; 239 | } 240 | } 241 | 242 | sub jsstyle($$) { 243 | 244 | my ($fn, $filehandle) = @_; 245 | $filename = $fn; # share it globally 246 | 247 | my $in_cpp = 0; 248 | my $next_in_cpp = 0; 249 | 250 | my $in_comment = 0; 251 | my $in_header_comment = 0; 252 | my $comment_done = 0; 253 | my $in_function = 0; 254 | my $in_function_header = 0; 255 | my $in_declaration = 0; 256 | my $note_level = 0; 257 | my $nextok = 0; 258 | my $nocheck = 0; 259 | 260 | my $in_string = 0; 261 | 262 | my ($okmsg, $comment_prefix); 263 | 264 | $line = ''; 265 | $prev = ''; 266 | reset_indent(); 267 | 268 | line: while (<$filehandle>) { 269 | s/\r?\n$//; # strip return and newline 270 | 271 | # save the original line, then remove all text from within 272 | # double or single quotes, we do not want to check such text. 273 | 274 | $line = $_; 275 | 276 | # 277 | # C allows strings to be continued with a backslash at the end of 278 | # the line. We translate that into a quoted string on the previous 279 | # line followed by an initial quote on the next line. 280 | # 281 | # (we assume that no-one will use backslash-continuation with character 282 | # constants) 283 | # 284 | $_ = '"' . $_ if ($in_string && !$nocheck && !$in_comment); 285 | 286 | # 287 | # normal strings and characters 288 | # 289 | s/'([^\\']|\\.)*'/\'\'/g; 290 | s/"([^\\"]|\\.)*"/\"\"/g; 291 | 292 | # 293 | # detect string continuation 294 | # 295 | if ($nocheck || $in_comment) { 296 | $in_string = 0; 297 | } else { 298 | # 299 | # Now that all full strings are replaced with "", we check 300 | # for unfinished strings continuing onto the next line. 301 | # 302 | $in_string = 303 | (s/([^"](?:"")*)"([^\\"]|\\.)*\\$/$1""/ || 304 | s/^("")*"([^\\"]|\\.)*\\$/""/); 305 | } 306 | 307 | # 308 | # figure out if we are in a cpp directive 309 | # 310 | $in_cpp = $next_in_cpp || /^\s*#/; # continued or started 311 | $next_in_cpp = $in_cpp && /\\$/; # only if continued 312 | 313 | # strip off trailing backslashes, which appear in long macros 314 | s/\s*\\$//; 315 | 316 | # an /* END JSSTYLED */ comment ends a no-check block. 317 | if ($nocheck) { 318 | if (/\/\* *END *JSSTYLED *\*\//) { 319 | $nocheck = 0; 320 | } else { 321 | reset_indent(); 322 | next line; 323 | } 324 | } 325 | 326 | # a /*JSSTYLED*/ comment indicates that the next line is ok. 327 | if ($nextok) { 328 | if ($okmsg) { 329 | err($okmsg); 330 | } 331 | $nextok = 0; 332 | $okmsg = 0; 333 | if (/\/\* *JSSTYLED.*\*\//) { 334 | /^.*\/\* *JSSTYLED *(.*) *\*\/.*$/; 335 | $okmsg = $1; 336 | $nextok = 1; 337 | } 338 | $no_errs = 1; 339 | } elsif ($no_errs) { 340 | $no_errs = 0; 341 | } 342 | 343 | # check length of line. 344 | # first, a quick check to see if there is any chance of being too long. 345 | if ((($line =~ tr/\t/\t/) * ($tab_width - 1)) + length($line) > 80) { 346 | # yes, there is a chance. 347 | # replace tabs with spaces and check again. 348 | my $eline = $line; 349 | 1 while $eline =~ 350 | s/\t+/' ' x 351 | (length($&) * $tab_width - length($`) % $tab_width)/e; 352 | if (length($eline) > 80) { 353 | err("line > 80 characters"); 354 | } 355 | } 356 | 357 | # ignore NOTE(...) annotations (assumes NOTE is on lines by itself). 358 | if ($note_level || /\b_?NOTE\s*\(/) { # if in NOTE or this is NOTE 359 | s/[^()]//g; # eliminate all non-parens 360 | $note_level += s/\(//g - length; # update paren nest level 361 | next; 362 | } 363 | 364 | # a /* BEGIN JSSTYLED */ comment starts a no-check block. 365 | if (/\/\* *BEGIN *JSSTYLED *\*\//) { 366 | $nocheck = 1; 367 | } 368 | 369 | # a /*JSSTYLED*/ comment indicates that the next line is ok. 370 | if (/\/\* *JSSTYLED.*\*\//) { 371 | /^.*\/\* *JSSTYLED *(.*) *\*\/.*$/; 372 | $okmsg = $1; 373 | $nextok = 1; 374 | } 375 | if (/\/\/ *JSSTYLED/) { 376 | /^.*\/\/ *JSSTYLED *(.*)$/; 377 | $okmsg = $1; 378 | $nextok = 1; 379 | } 380 | 381 | # universal checks; apply to everything 382 | if (/\t +\t/) { 383 | err("spaces between tabs"); 384 | } 385 | if (/ \t+ /) { 386 | err("tabs between spaces"); 387 | } 388 | if (/\s$/) { 389 | err("space or tab at end of line"); 390 | } 391 | if (/[^ \t(]\/\*/ && !/\w\(\/\*.*\*\/\);/) { 392 | err("comment preceded by non-blank"); 393 | } 394 | 395 | # is this the beginning or ending of a function? 396 | # (not if "struct foo\n{\n") 397 | if (/^{$/ && $prev =~ /\)\s*(const\s*)?(\/\*.*\*\/\s*)?\\?$/) { 398 | $in_function = 1; 399 | $in_declaration = 1; 400 | $in_function_header = 0; 401 | $prev = $line; 402 | next line; 403 | } 404 | if (/^}\s*(\/\*.*\*\/\s*)*$/) { 405 | if ($prev =~ /^\s*return\s*;/) { 406 | err_prev("unneeded return at end of function"); 407 | } 408 | $in_function = 0; 409 | reset_indent(); # we don't check between functions 410 | $prev = $line; 411 | next line; 412 | } 413 | if (/^\w*\($/) { 414 | $in_function_header = 1; 415 | } 416 | 417 | # a blank line terminates the declarations within a function. 418 | # XXX - but still a problem in sub-blocks. 419 | if ($in_declaration && /^$/) { 420 | $in_declaration = 0; 421 | } 422 | 423 | if ($comment_done) { 424 | $in_comment = 0; 425 | $in_header_comment = 0; 426 | $comment_done = 0; 427 | } 428 | # does this looks like the start of a block comment? 429 | if (/$hdr_comment_start/) { 430 | if ($config{"indent"} eq "tab") { 431 | if (!/^\t*\/\*/) { 432 | err("block comment not indented by tabs"); 433 | } 434 | } elsif (!/^ *\/\*/) { 435 | err("block comment not indented by spaces"); 436 | } 437 | $in_comment = 1; 438 | /^(\s*)\//; 439 | $comment_prefix = $1; 440 | if ($comment_prefix eq "") { 441 | $in_header_comment = 1; 442 | } 443 | $prev = $line; 444 | next line; 445 | } 446 | # are we still in the block comment? 447 | if ($in_comment) { 448 | if (/^$comment_prefix \*\/$/) { 449 | $comment_done = 1; 450 | } elsif (/\*\//) { 451 | $comment_done = 1; 452 | err("improper block comment close") 453 | unless ($ignore_hdr_comment && $in_header_comment); 454 | } elsif (!/^$comment_prefix \*[ \t]/ && 455 | !/^$comment_prefix \*$/) { 456 | err("improper block comment") 457 | unless ($ignore_hdr_comment && $in_header_comment); 458 | } 459 | } 460 | 461 | if ($in_header_comment && $ignore_hdr_comment) { 462 | $prev = $line; 463 | next line; 464 | } 465 | 466 | # check for errors that might occur in comments and in code. 467 | 468 | # allow spaces to be used to draw pictures in header comments. 469 | #if (/[^ ] / && !/".* .*"/ && !$in_header_comment) { 470 | # err("spaces instead of tabs"); 471 | #} 472 | #if (/^ / && !/^ \*[ \t\/]/ && !/^ \*$/ && 473 | # (!/^ \w/ || $in_function != 0)) { 474 | # err("indent by spaces instead of tabs"); 475 | #} 476 | if ($config{"indent"} eq "tab") { 477 | if (/^ {2,}/ && !/^ [^ ]/) { 478 | err("indent by spaces instead of tabs"); 479 | } 480 | } elsif (/^\t/) { 481 | err("indent by tabs instead of spaces") 482 | } elsif (/^( +)/ && !$in_comment) { 483 | my $indent = $1; 484 | if (length($indent) < $config{"indent"}) { 485 | err("indent of " . length($indent) . 486 | " space(s) instead of " . $config{'indent'}); 487 | } 488 | } 489 | if (/^\t+ [^ \t\*]/ || /^\t+ \S/ || /^\t+ \S/) { 490 | err("continuation line not indented by 4 spaces"); 491 | } 492 | 493 | # A multi-line block comment must not have content on the first line. 494 | if (/^\s*\/\*./ && !/^\s*\/\*.*\*\// && !/$hdr_comment_start/) { 495 | err("improper first line of block comment"); 496 | } 497 | 498 | if ($in_comment) { # still in comment, don't do further checks 499 | $prev = $line; 500 | next line; 501 | } 502 | 503 | if ((/[^(]\/\*\S/ || /^\/\*\S/) && 504 | !(/$lint_re/ || ($config{"splint"} && /$splint_re/))) { 505 | err("missing blank after open comment"); 506 | } 507 | if (/\S\*\/[^)]|\S\*\/$/ && 508 | !(/$lint_re/ || ($config{"splint"} && /$splint_re/))) { 509 | err("missing blank before close comment"); 510 | } 511 | if ($config{"blank-after-start-comment"} && /(?\s][!<>=]=/ || /[^<>!=][!<>=]==?[^\s,=]/ || 550 | (/[^->]>[^,=>\s]/ && !/[^->]>$/) || 551 | (/[^<]<[^,=<\s]/ && !/[^<]<$/) || 552 | /[^<\s]<[^<]/ || /[^->\s]>[^>]/) { 553 | err("missing space around relational operator"); 554 | } 555 | if (/\S>>=/ || /\S<<=/ || />>=\S/ || /<<=\S/ || /\S[-+*\/&|^%]=/ || 556 | (/[^-+*\/&|^%!<>=\s]=[^=]/ && !/[^-+*\/&|^%!<>=\s]=$/) || 557 | (/[^!<>=]=[^=\s]/ && !/[^!<>=]=$/)) { 558 | # XXX - should only check this for C++ code 559 | # XXX - there are probably other forms that should be allowed 560 | if (!/\soperator=/) { 561 | err("missing space around assignment operator"); 562 | } 563 | } 564 | if (/[,;]\S/ && !/\bfor \(;;\)/ && 565 | # Allow a comma in a regex quantifier. 566 | !/\/.*?\{\d+,?\d*\}.*?\//) { 567 | err("comma or semicolon followed by non-blank"); 568 | } 569 | # allow "for" statements to have empty "while" clauses 570 | if (/\s[,;]/ && !/^[\t]+;$/ && !/^\s*for \([^;]*; ;[^;]*\)/) { 571 | err("comma or semicolon preceded by blank"); 572 | } 573 | if (/^\s*(&&|\|\|)/) { 574 | err("improper boolean continuation"); 575 | } 576 | if (/\S *(&&|\|\|)/ || /(&&|\|\|) *\S/) { 577 | err("more than one space around boolean operator"); 578 | } 579 | if (/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\(/) { 580 | err("missing space between keyword and paren"); 581 | } 582 | if (/(\b(catch|for|if|with|while|switch|return)\b.*){2,}/) { 583 | # multiple "case" and "sizeof" allowed 584 | err("more than one keyword on line"); 585 | } 586 | if (/\b(delete|typeof|instanceOf|with|throw|catch|new|function|in|for|if|while|switch|return|case)\s\s+\(/ && 587 | !/^#if\s+\(/) { 588 | err("extra space between keyword and paren"); 589 | } 590 | # try to detect "func (x)" but not "if (x)" or 591 | # "#define foo (x)" or "int (*func)();" 592 | if (/\w\s\(/) { 593 | my $s = $_; 594 | # strip off all keywords on the line 595 | s/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\s\(/XXX(/g; 596 | s/#elif\s\(/XXX(/g; 597 | s/^#define\s+\w+\s+\(/XXX(/; 598 | # do not match things like "void (*f)();" 599 | # or "typedef void (func_t)();" 600 | s/\w\s\(+\*/XXX(*/g; 601 | s/\b(void)\s+\(+/XXX(/og; 602 | if (/\w\s\(/) { 603 | err("extra space between function name and left paren"); 604 | } 605 | $_ = $s; 606 | } 607 | 608 | if ($config{"unparenthesized-return"} && 609 | /^\s*return\W[^;]*;/ && !/^\s*return\s*\(.*\);/) { 610 | err("unparenthesized return expression"); 611 | } 612 | if (/\btypeof\b/ && !/\btypeof\s*\(.*\)/) { 613 | err("unparenthesized typeof expression"); 614 | } 615 | if (/\(\s/) { 616 | err("whitespace after left paren"); 617 | } 618 | # allow "for" statements to have empty "continue" clauses 619 | if (/\s\)/ && !/^\s*for \([^;]*;[^;]*; \)/) { 620 | err("whitespace before right paren"); 621 | } 622 | if (/^\s*\(void\)[^ ]/) { 623 | err("missing space after (void) cast"); 624 | } 625 | if (/\S{/ && !/({|\(){/ && 626 | # Allow a brace in a regex quantifier. 627 | !/\/.*?\{\d+,?\d*\}.*?\//) { 628 | err("missing space before left brace"); 629 | } 630 | if ($in_function && /^\s+{/ && 631 | ($prev =~ /\)\s*$/ || $prev =~ /\bstruct\s+\w+$/)) { 632 | err("left brace starting a line"); 633 | } 634 | if (/}(else|while)/) { 635 | err("missing space after right brace"); 636 | } 637 | if (/}\s\s+(else|while)/) { 638 | err("extra space after right brace"); 639 | } 640 | if (/^\s+#/) { 641 | err("preprocessor statement not in column 1"); 642 | } 643 | if (/^#\s/) { 644 | err("blank after preprocessor #"); 645 | } 646 | 647 | # 648 | # We completely ignore, for purposes of indentation: 649 | # * lines outside of functions 650 | # * preprocessor lines 651 | # 652 | if ($check_continuation && $in_function && !$in_cpp) { 653 | process_indent($_); 654 | } 655 | 656 | if (/^\s*else\W/) { 657 | if ($prev =~ /^\s*}$/) { 658 | err_prefix($prev, 659 | "else and right brace should be on same line"); 660 | } 661 | } 662 | $prev = $line; 663 | } 664 | 665 | if ($prev eq "") { 666 | err("last line in file is blank"); 667 | } 668 | 669 | } 670 | 671 | # 672 | # Continuation-line checking 673 | # 674 | # The rest of this file contains the code for the continuation checking 675 | # engine. It's a pretty simple state machine which tracks the expression 676 | # depth (unmatched '('s and '['s). 677 | # 678 | # Keep in mind that the argument to process_indent() has already been heavily 679 | # processed; all comments have been replaced by control-A, and the contents of 680 | # strings and character constants have been elided. 681 | # 682 | 683 | my $cont_in; # currently inside of a continuation 684 | my $cont_off; # skipping an initializer or definition 685 | my $cont_noerr; # suppress cascading errors 686 | my $cont_start; # the line being continued 687 | my $cont_base; # the base indentation 688 | my $cont_first; # this is the first line of a statement 689 | my $cont_multiseg; # this continuation has multiple segments 690 | 691 | my $cont_special; # this is a C statement (if, for, etc.) 692 | my $cont_macro; # this is a macro 693 | my $cont_case; # this is a multi-line case 694 | 695 | my @cont_paren; # the stack of unmatched ( and [s we've seen 696 | 697 | sub 698 | reset_indent() 699 | { 700 | $cont_in = 0; 701 | $cont_off = 0; 702 | } 703 | 704 | sub 705 | delabel($) 706 | { 707 | # 708 | # replace labels with tabs. Note that there may be multiple 709 | # labels on a line. 710 | # 711 | local $_ = $_[0]; 712 | 713 | while (/^(\t*)( *(?:(?:\w+\s*)|(?:case\b[^:]*)): *)(.*)$/) { 714 | my ($pre_tabs, $label, $rest) = ($1, $2, $3); 715 | $_ = $pre_tabs; 716 | while ($label =~ s/^([^\t]*)(\t+)//) { 717 | $_ .= "\t" x (length($2) + length($1) / 8); 718 | } 719 | $_ .= ("\t" x (length($label) / 8)).$rest; 720 | } 721 | 722 | return ($_); 723 | } 724 | 725 | sub 726 | process_indent($) 727 | { 728 | require strict; 729 | local $_ = $_[0]; # preserve the global $_ 730 | 731 | s///g; # No comments 732 | s/\s+$//; # Strip trailing whitespace 733 | 734 | return if (/^$/); # skip empty lines 735 | 736 | # regexps used below; keywords taking (), macros, and continued cases 737 | my $special = '(?:(?:\}\s*)?else\s+)?(?:if|for|while|switch)\b'; 738 | my $macro = '[A-Z_][A-Z_0-9]*\('; 739 | my $case = 'case\b[^:]*$'; 740 | 741 | # skip over enumerations, array definitions, initializers, etc. 742 | if ($cont_off <= 0 && !/^\s*$special/ && 743 | (/(?:(?:\b(?:enum|struct|union)\s*[^\{]*)|(?:\s+=\s*)){/ || 744 | (/^\s*{/ && $prev =~ /=\s*(?:\/\*.*\*\/\s*)*$/))) { 745 | $cont_in = 0; 746 | $cont_off = tr/{/{/ - tr/}/}/; 747 | return; 748 | } 749 | if ($cont_off) { 750 | $cont_off += tr/{/{/ - tr/}/}/; 751 | return; 752 | } 753 | 754 | if (!$cont_in) { 755 | $cont_start = $line; 756 | 757 | if (/^\t* /) { 758 | err("non-continuation indented 4 spaces"); 759 | $cont_noerr = 1; # stop reporting 760 | } 761 | $_ = delabel($_); # replace labels with tabs 762 | 763 | # check if the statement is complete 764 | return if (/^\s*\}?$/); 765 | return if (/^\s*\}?\s*else\s*\{?$/); 766 | return if (/^\s*do\s*\{?$/); 767 | return if (/{$/); 768 | return if (/}[,;]?$/); 769 | 770 | # Allow macros on their own lines 771 | return if (/^\s*[A-Z_][A-Z_0-9]*$/); 772 | 773 | # cases we don't deal with, generally non-kosher 774 | if (/{/) { 775 | err("stuff after {"); 776 | return; 777 | } 778 | 779 | # Get the base line, and set up the state machine 780 | /^(\t*)/; 781 | $cont_base = $1; 782 | $cont_in = 1; 783 | @cont_paren = (); 784 | $cont_first = 1; 785 | $cont_multiseg = 0; 786 | 787 | # certain things need special processing 788 | $cont_special = /^\s*$special/? 1 : 0; 789 | $cont_macro = /^\s*$macro/? 1 : 0; 790 | $cont_case = /^\s*$case/? 1 : 0; 791 | } else { 792 | $cont_first = 0; 793 | 794 | # Strings may be pulled back to an earlier (half-)tabstop 795 | unless ($cont_noerr || /^$cont_base / || 796 | (/^\t*(?: )?(?:gettext\()?\"/ && !/^$cont_base\t/)) { 797 | err_prefix($cont_start, 798 | "continuation should be indented 4 spaces"); 799 | } 800 | } 801 | 802 | my $rest = $_; # keeps the remainder of the line 803 | 804 | # 805 | # The split matches 0 characters, so that each 'special' character 806 | # is processed separately. Parens and brackets are pushed and 807 | # popped off the @cont_paren stack. For normal processing, we wait 808 | # until a ; or { terminates the statement. "special" processing 809 | # (if/for/while/switch) is allowed to stop when the stack empties, 810 | # as is macro processing. Case statements are terminated with a : 811 | # and an empty paren stack. 812 | # 813 | foreach $_ (split /[^\(\)\[\]\{\}\;\:]*/) { 814 | next if (length($_) == 0); 815 | 816 | # rest contains the remainder of the line 817 | my $rxp = "[^\Q$_\E]*\Q$_\E"; 818 | $rest =~ s/^$rxp//; 819 | 820 | if (/\(/ || /\[/) { 821 | push @cont_paren, $_; 822 | } elsif (/\)/ || /\]/) { 823 | my $cur = $_; 824 | tr/\)\]/\(\[/; 825 | 826 | my $old = (pop @cont_paren); 827 | if (!defined($old)) { 828 | err("unexpected '$cur'"); 829 | $cont_in = 0; 830 | last; 831 | } elsif ($old ne $_) { 832 | err("'$cur' mismatched with '$old'"); 833 | $cont_in = 0; 834 | last; 835 | } 836 | 837 | # 838 | # If the stack is now empty, do special processing 839 | # for if/for/while/switch and macro statements. 840 | # 841 | next if (@cont_paren != 0); 842 | if ($cont_special) { 843 | if ($rest =~ /^\s*{?$/) { 844 | $cont_in = 0; 845 | last; 846 | } 847 | if ($rest =~ /^\s*;$/) { 848 | err("empty if/for/while body ". 849 | "not on its own line"); 850 | $cont_in = 0; 851 | last; 852 | } 853 | if (!$cont_first && $cont_multiseg == 1) { 854 | err_prefix($cont_start, 855 | "multiple statements continued ". 856 | "over multiple lines"); 857 | $cont_multiseg = 2; 858 | } elsif ($cont_multiseg == 0) { 859 | $cont_multiseg = 1; 860 | } 861 | # We've finished this section, start 862 | # processing the next. 863 | goto section_ended; 864 | } 865 | if ($cont_macro) { 866 | if ($rest =~ /^$/) { 867 | $cont_in = 0; 868 | last; 869 | } 870 | } 871 | } elsif (/\;/) { 872 | if ($cont_case) { 873 | err("unexpected ;"); 874 | } elsif (!$cont_special) { 875 | err("unexpected ;") if (@cont_paren != 0); 876 | if (!$cont_first && $cont_multiseg == 1) { 877 | err_prefix($cont_start, 878 | "multiple statements continued ". 879 | "over multiple lines"); 880 | $cont_multiseg = 2; 881 | } elsif ($cont_multiseg == 0) { 882 | $cont_multiseg = 1; 883 | } 884 | if ($rest =~ /^$/) { 885 | $cont_in = 0; 886 | last; 887 | } 888 | if ($rest =~ /^\s*special/) { 889 | err("if/for/while/switch not started ". 890 | "on its own line"); 891 | } 892 | goto section_ended; 893 | } 894 | } elsif (/\{/) { 895 | err("{ while in parens/brackets") if (@cont_paren != 0); 896 | err("stuff after {") if ($rest =~ /[^\s}]/); 897 | $cont_in = 0; 898 | last; 899 | } elsif (/\}/) { 900 | err("} while in parens/brackets") if (@cont_paren != 0); 901 | if (!$cont_special && $rest !~ /^\s*(while|else)\b/) { 902 | if ($rest =~ /^$/) { 903 | err("unexpected }"); 904 | } else { 905 | err("stuff after }"); 906 | } 907 | $cont_in = 0; 908 | last; 909 | } 910 | } elsif (/\:/ && $cont_case && @cont_paren == 0) { 911 | err("stuff after multi-line case") if ($rest !~ /$^/); 912 | $cont_in = 0; 913 | last; 914 | } 915 | next; 916 | section_ended: 917 | # End of a statement or if/while/for loop. Reset 918 | # cont_special and cont_macro based on the rest of the 919 | # line. 920 | $cont_special = ($rest =~ /^\s*$special/)? 1 : 0; 921 | $cont_macro = ($rest =~ /^\s*$macro/)? 1 : 0; 922 | $cont_case = 0; 923 | next; 924 | } 925 | $cont_noerr = 0 if (!$cont_in); 926 | } 927 | --------------------------------------------------------------------------------