Compare commits

..

No commits in common. "master" and "v2.2.1" have entirely different histories.

161 changed files with 10925 additions and 4053 deletions

View File

@ -1,4 +0,0 @@
node_modules/
coverage/
.nyc_output/
docs/

View File

@ -1,20 +0,0 @@
module.exports = {
env: {
commonjs: true,
es2021: true,
node: true
},
parserOptions: {
ecmaVersion: 'latest'
},
extends: [
'standard'
],
rules: {
'no-shadow': 'error',
'no-unused-vars': ['error', {
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}]
}
}

View File

@ -1,30 +0,0 @@
name: 'Update Docs'
on:
push:
branches:
- master
jobs:
docs:
name: Update Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install Packages
run: npm install
- name: Build Docs
run: npm run docs
- name: Deploy 🚢
uses: cpina/github-action-push-to-another-repository@master
env:
API_TOKEN_GITHUB: ${{ secrets.API_TOKEN_GITHUB }}
with:
source-directory: 'public'
destination-github-username: 'ldapjs'
destination-repository-name: 'ldapjs.github.io'
user-email: 'bot@ldapjs.org'
target-branch: 'gh-pages'

View File

@ -4,34 +4,35 @@ name: 'Integration Tests'
# https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538
on:
push:
branches:
- master
- next
pull_request:
branches:
- master
- next
jobs:
baseline:
name: Baseline Tests
runs-on: ubuntu-latest
services:
openldap:
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30
ports:
- 389:389
- 636:636
options: >
--health-cmd "ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn"
# services:
# openldap:
# image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:1.0
# ports:
# - 389:389
# - 636:636
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
# Hack way to start service since GitHub doesn't integrate with its own services
- name: Docker login
run: docker login docker.pkg.github.com -u ${GITHUB_ACTOR} -p ${GITHUB_TOKEN}
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
- name: Pull Docker image
run: docker pull "docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest"
- name: Start OpenLDAP service
run: docker run -it -d --name openldap -p 389:389 -p 636:636 docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
- name: Install Packages
run: npm install

View File

@ -4,21 +4,17 @@ on:
push:
branches:
- master
- next
pull_request:
branches:
- master
- next
jobs:
lint:
name: Lint Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 'lts/*'
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- name: Install Packages
run: npm install
- name: Lint Code
@ -32,16 +28,27 @@ jobs:
- ubuntu-latest
- windows-latest
node:
- 16
- 18
- 20
- 10.13.0
- 10.x
- 12.x
- 14.x
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- name: Install Packages
run: npm install
- name: Run Tests
run: npm run test:ci
- name: Coveralls Parallel
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel: true
- name: Coveralls Finished
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
parallel-finished: true

3
.gitignore vendored
View File

@ -75,6 +75,3 @@ xcuserdata
*.pem
*.env.json
*.env
# built docs
/public

7
.npmignore Normal file
View File

@ -0,0 +1,7 @@
.gitmodules
Makefile
deps
docs
test
tools
coverage

3
.npmrc
View File

@ -1,3 +0,0 @@
# npm general settings
package-lock=false
legacy-peer-deps=true

6
.taprc Normal file
View File

@ -0,0 +1,6 @@
esm: false
jsx: false
ts: false
files:
- 'test/**/*.test.js'

View File

@ -1,10 +0,0 @@
# With PR #834 the code in this code base has been reduced significantly.
# As a result, the coverage percentages changed, and are much lower than
# previously. So we are reducing the requirements accordingly
branches: 50
functions: 50
lines: 50
statements: 50
files:
- 'test/**/*.test.js'

View File

@ -1,34 +1,54 @@
# Project Decomissioned
# LDAPjs
This project has been decomissioned. I, James Sumners, took it on when it was
languishing without any maintenance as it filled a need in the ecosystem and
I had built things at a prior organization that depended upon this project.
I spent a lot of time triaging issues and reworking things toward a path
that could be more easily maintained by a community of volunteers. But I have
not had the time to dedicate to this project in quite a while. There are
outstanding issues that would take me at least a week of dedicated development
time to solve, and I cannot afford to take time off of work to do that.
Particularly considering that the aforementioned organization was two
jobs ago, and it is extremely unlikely that I will transition to a role again
that will need this project.
[![Build Status](https://github.com/ldapjs/node-ldapjs/workflows/Lint%20And%20Test/badge.svg)](https://github.com/ldapjs/node-ldapjs/actions)
[![Coverage Status](https://coveralls.io/repos/github/ldapjs/node-ldapjs/badge.svg)](https://coveralls.io/github/ldapjs/node-ldapjs/)
So, why am I just now deciding to decomission this project? Because today,
2024-05-14, I received the following email:
LDAPjs makes the LDAP protocol a first class citizen in Node.js.
![Abusive email](dt.png)
## Usage
I will not tolerate abuse, and I especially will not tolerate tacit death
threats, over a hobby. You can thank the author of that email for the
decomissioning on this project.
For full docs, head on over to <http://ldapjs.org>.
My recommendation to you in regard to LDAP operations: write a gateway in a
language that is more suited to these types of operations. I'd suggest
[Go](https://go.dev).
```javascript
var ldap = require('ldapjs');
👋
var server = ldap.createServer();
P.S.: if I ever do need this project again, I might revive it. But I'd fight
hard for my suggestion above. Also, I will consider turning it over to an
interested party, but I will require at least one recommendation from a
Node.js core contributor that I can vet with the people that I know on that
team.
server.search('dc=example', function(req, res, next) {
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
};
if (req.filter.matches(obj.attributes))
res.send(obj);
res.end();
});
server.listen(1389, function() {
console.log('ldapjs listening at ' + server.url);
});
```
To run that, assuming you've got the [OpenLDAP](http://www.openldap.org/)
client on your system:
ldapsearch -H ldap://localhost:1389 -x -b dc=example objectclass=*
## Installation
npm install ldapjs
DTrace support is included in ldapjs. To enable it, `npm install dtrace-provider`.
## License
MIT.
## Bugs
See <https://github.com/ldapjs/node-ldapjs/issues>.

View File

@ -1,10 +1,8 @@
version: '3'
services:
openldap:
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30
image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
ports:
- 389:389
- 636:636
healthcheck:
start_period: 3s
test: >
/usr/bin/ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn 1>/dev/null

View File

@ -0,0 +1,49 @@
<script type="text/javascript" charset="utf-8">
$(function() {
var headerHeight = $("#header").height();
var offsets = [];
var current = -1;
function endpoint(scrollDistance) {
if (scrollDistance < offsets[0]) {
return -1;
} else {
for (var id = offsets.length; id > 0; id--) {
if (scrollDistance > offsets[id - 1]) {
return id - 1;
break;
}
}
}
}
$("h2").each(function(i) {
offsets.push($(this).offset().top - headerHeight)
});
$("#content").append('<h2 class="fixed" style="display: none"><span>&nbsp;</span></h2>');
var fixed_h2 = $("h2.fixed");
var fixed_span = $("h2.fixed span");
$("#content").scroll(function() {
var scrollDistance = $("#content").attr('scrollTop');
var now = endpoint(scrollDistance);
if (now !== current) {
$("#sidebar li").removeClass("current");
current = now;
if (current < 0) {
fixed_h2.hide();
} else if (current >= 0) {
var heading = $($("h2 span")[current]).text();
$("#sidebar a[href|=#" + heading.replace(' ', '-') + "]").parent().addClass("current");
fixed_span.text(heading);
fixed_h2.show();
}
}
});
});
</script>
</body>
</html>

View File

@ -3,8 +3,8 @@
<head>
<title>%(title)s</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="media/css/style.css">
<link rel="stylesheet" type="text/css" href="media/css/highlight.css">
<link rel="stylesheet" type="text/css" href="%(mediaroot)s/css/restdown.css">
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
</head>
<body>
<div id="header">
@ -30,10 +30,3 @@
</span>
%(toc_html)s
</div>
<div id="content">
%(content)s
</div><!-- end #content -->
</body>
</html>

View File

@ -7,7 +7,7 @@ body {
color: #4a3f2d;
}
:focus:not(:focus-visible) {
:focus {
outline: 0;
}
@ -83,6 +83,22 @@ h4 {
display: inline;
}
/* ---- custom classes */
pre.shell,
pre.shell code {
background:#444;
color:#fff;
border-width:0px;
}
pre.shell code::before {
content: '$ ';
display: inline;
}
/* ---- header and sidebar */
#header {
@ -134,6 +150,15 @@ h4 {
z-index:0;
}
#sidebar .vertical_divider {
background-color:#FFFFFF;
bottom:0px;
position:absolute;
top:0px;
right:0px;
width:1px;
}
#sidebar h1 {
font-size:1.2em;
padding:0px;
@ -162,6 +187,25 @@ h4 {
padding:1px 0px 1px 2px;
}
#sidebar li span.verb {
color:#aaa;
padding:2px 3px 0px;
width:30px;
display:block;
float:left;
font-size:9px;
font-family:verdana;
-moz-border-radius:3px;
-webkit-border-radius:3px;
margin-left:0px;
margin-right:5px;
}
#sidebar li.current {
background-repeat: no-repeat;
background-position:right;
}
/* ---- intro */
@ -175,6 +219,13 @@ h4 {
border-radius: 5px;
margin-bottom:40px;
}
.intro pre.base {
background:#444;
color:#29231A;
color:#fff;
border-color:#fff;
font-size:1.5em;
}
.intro h1 {
color: #1C313C;
}
@ -208,6 +259,14 @@ h1 + h2 {
margin-top: 0px;
}
h2.fixed {
position:fixed;
margin-top: 0;
border-top:none;
right:45px;
top:66px;
}
h2 span {
background: #979592;
float:right;

View File

@ -0,0 +1,406 @@
/* Trent Mick's blog's CSS. Based heavily (at least initially) on
* <http://html5boilerplate.com/> and on <http://lessframework.com/>.
*/
/* ---- reset
* html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline)
* v1.4 2009-07-27 | Authors: Eric Meyer & Richard Clark
* html5doctor.com/html-5-reset-stylesheet/
*/
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, figure, footer, header,
hgroup, menu, nav, section,
time, mark, audio, video {
margin:0;
padding:0;
border:0;
outline:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
article, aside, figure, footer, header,
hgroup, nav, section { display:block; }
nav ul { list-style:none; }
blockquote, q { quotes:none; }
blockquote:before, blockquote:after,
q:before, q:after { content:''; content:none; }
a { margin:0; padding:0; font-size:100%; vertical-align:baseline; background:transparent; }
ins { background-color:#ff9; color:#000; text-decoration:none; }
mark { background-color:#ff9; color:#000; font-style:italic; font-weight:bold; }
del { text-decoration: line-through; }
abbr[title], dfn[title] { border-bottom:1px dotted #000; cursor:help; }
sub { vertical-align: sub; }
sup { vertical-align: super; }
/* tables still need cellspacing="0" in the markup */
table { border-collapse:collapse; border-spacing:0; }
input, select { vertical-align:middle; }
/* ---- 24px vertical rhythm
* <http://webtypography.net/Rhythm_and_Proportion/Vertical_Motion/2.2.2/>
* All tags except 'pre' because 24px line-height is way too much, want 18px.
* Tough.
*/
body {
line-height: 24px;
font-family: helvetica, arial, freesans, clean, sans-serif;
font-size: 16px;
}
h1,h2,h3,h4,h5,h6 {
font-family: Georgia,serif;
font-weight: bold;
/* http://www.aestheticallyloyal.com/public/optimize-legibility/ */
text-rendering: optimizeLegibility;
}
h1 {
font-size: 28px;
line-height: 48px;
padding: 24px 0 24px 0;
}
h2 {
font-size: 21px;
line-height: 24px;
padding: 24px 0 24px 0;
}
h3 {
font-size: 16px;
line-height: 24px;
padding: 24px 0 0 0;
}
h4 {
font-size: 14px;
line-height: 24px;
padding: 24px 0 0 0;
}
h5 {
color: #666;
font-size: 14px;
line-height: 24px;
padding: 24px 0 0 0;
}
h6 {
color: #666;
font-size: 12px;
line-height: 24px;
padding: 0;
}
hr {
padding-bottom: 24px;
margin: 24px 0;
height: 0;
border: none;
text-align: center;
color: #333;
}
hr:after {
content: "\2767";
}
p + p {
padding-top: 24px;
}
dl {
margin: 24px 0;
}
dt {
font-weight: bold;
margin-top: 24px;
}
dd {
margin: 0 0 0 48px;
}
/* ---- layout */
body {
margin: 12px;
}
#wrapper {
margin: 0 auto;
max-width: 700px;
position: relative;
}
#main {
min-height: 300px;
}
#fadeout {
z-index: 50;
position: fixed;
left: 0;
top: 0;
right: 0;
height: 210px;
background: -webkit-gradient(linear, left top, left bottom, from(rgba(255, 255, 255, 255)), to(rgba(255, 255, 255, 0)));
background: -moz-linear-gradient(top, rgba(255, 255, 255, 255), rgba(255, 255, 255, 0));
}
#header {
position: fixed;
z-index: 100;
padding: 48px 0 24px 0;
top: 0;
left: auto;
right: auto;
width: 100%;
background: white;
}
#content {
position: relative;
top: 150px;
overflow: auto;
padding-bottom: 200px;
}
#footer {
position: relative;
padding-top: 60px;
}
/* ---- base styles */
blockquote {
font-family: Georgia,serif;
font-size: 18px;
color: #555;
margin: 24px 15% 24px 15%;
margin: 24px 10% 24px 10%;
}
blockquote cite:before {
content: "\2014 ";
}
blockquote p.hangquotes, blockquote.hangquotes {
text-indent: -0.5em;
}
code,
pre {
font-family: Consolas, Monaco, "Lucida Console", "Courier New", monospace;
font-size: 12px;
background-color: #f5f5f5;
border: 1px solid #ccc;
}
code {
padding: 0 0.2em;
}
pre {
font-size: 80%;
line-height: 18px;
padding: 5px;
margin: 18px 0;
/* "overflow:auto" does NOT yield scrollbars in mobile browsers. iPhone
* *does* support two-finger scrolling inside the pre-block, but that
* is undiscoverable. Let's pre-wrap.
*/
white-space: pre;
white-space: -moz-pre-wrap;
white-space: -hp-pre-wrap;
white-space: -o-pre-wrap;
white-space: pre-wrap;
word-wrap: break-word;
}
pre code {
border: medium none;
padding: 0;
}
a code {
text-decoration: underline;
}
h1 + pre,
h2 + pre,
h3 + pre,
h4 + pre,
h5 + pre,
h6 + pre {
margin-top: 0;
}
table {
margin: 24px 0;
}
th,
td {
border: solid #aaa;
border-width: 1px 0;
line-height: 23px;
padding: 0 12px;
text-align: left;
}
th {
border-collapse: separate;
}
tbody tr:nth-child(odd) {
background-color: #f6f6f6;
}
ol, ul {
padding: 12px 0;
margin: 0 0 0 24px;
}
ol ol, ul ul, ul ol, ol ul {
margin-left: 24px;
}
ol { list-style-type: decimal; }
ol ol { list-style-type: lower-alpha; }
ol ol ol { list-style-type: upper-roman; }
ol ol ol ol { list-style-type: lower-roman; }
ul { list-style-type: circle; }
ul ul { list-style-type: disc; }
ul ul ul { list-style-type: circle; }
:link { color: hsl(206, 100%, 23%); }
:visited { color: hsl(240, 20%, 50%); }
:link:hover, :visited:hover { color: hsl(206, 100%, 38%); }
/* ---- larger screens */
@media only screen and (min-width: 481px) {
pre {
overflow: auto;
white-space: pre;
word-wrap: normal;
}
}
@media only screen and (min-width: 700px) {
ol, ul {
margin: 0;
}
}
/* ---- custom classes */
pre.shell,
pre.shell code {
background:#444;
color:#fff;
border-width:0px;
}
pre.shell code::before {
content: '$ ';
display: inline;
}
.copy {
font-size: 90%;
text-align: center;
color: #777;
}
/* ----- top header */
a#homelink:link {
color: #008000;
text-decoration: none;
}
a#homelink:visited {
color: #008000;
text-decoration: none;
}
a#homelink:active {
color: #008000;
text-decoration: none;
}
a#homelink:hover {
color: #008000;
text-decoration: none;
}
#logo {
font-family: Georgia,serif;
font-weight: bold;
font-size: 60px;
top: 55px;
}
#apibox,
#tocbox {
position: relative;
}
#tocbox {
margin-left: 24px;
}
.navbutton {
border: 1px solid #ccc;
position: relative;
display: inline-block;
width: 120px;
text-align: center;
top: -7px;
font-size: 70%;
font-weight: bold;
text-transform: uppercase;
text-decoration: none;
color: black;
}
.navbutton:hover {
cursor: pointer;
background-color: #f3f3f3;
}
.popup {
border: 1px solid #ccc;
padding: 5px 10px;
background-color: white;
font-size: 80%;
line-height: 1.6em;
position: absolute;
top: 16px;
left: 0px;
width: 200px;
}
.popup a {
text-decoration: none;
}
.popup a:hover {
text-decoration: underline;
}
.popup ul {
margin: 0;
padding: 0px;
list-style-type: none;
}
.popup ul ul {
margin-left: 24px;
}
#githubfork {
position: fixed;
top: 0;
right: 0px;
}
#indextagline {
font-size: 24px;
font-style: italic;
text-align: center;
color: green;
}
a#indextaglink:link {
color: #008000;
text-decoration: none;
}
a#indextaglink:visited {
color: #008000;
text-decoration: none;
}
a#indextaglink:active {
color: #008000;
text-decoration: none;
}
a#indextaglink:hover {
color: #008000;
text-decoration: none;
}

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,154 @@
/*!
* jQuery JavaScript Library v1.4.2
* http://jquery.com/
*
* Copyright 2010, John Resig
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* Includes Sizzle.js
* http://sizzlejs.com/
* Copyright 2010, The Dojo Foundation
* Released under the MIT, BSD, and GPL Licenses.
*
* Date: Sat Feb 13 22:33:48 2010 -0500
*/
(function(A,w){function ma(){if(!c.isReady){try{s.documentElement.doScroll("left")}catch(a){setTimeout(ma,1);return}c.ready()}}function Qa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function X(a,b,d,f,e,j){var i=a.length;if(typeof b==="object"){for(var o in b)X(a,o,b[o],f,e,d);return a}if(d!==w){f=!j&&f&&c.isFunction(d);for(o=0;o<i;o++)e(a[o],b,f?d.call(a[o],o,e(a[o],b)):d,j);return a}return i?
e(a[0],b):w}function J(){return(new Date).getTime()}function Y(){return false}function Z(){return true}function na(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function oa(a){var b,d=[],f=[],e=arguments,j,i,o,k,n,r;i=c.data(this,"events");if(!(a.liveFired===this||!i||!i.live||a.button&&a.type==="click")){a.liveFired=this;var u=i.live.slice(0);for(k=0;k<u.length;k++){i=u[k];i.origType.replace(O,"")===a.type?f.push(i.selector):u.splice(k--,1)}j=c(a.target).closest(f,a.currentTarget);n=0;for(r=
j.length;n<r;n++)for(k=0;k<u.length;k++){i=u[k];if(j[n].selector===i.selector){o=j[n].elem;f=null;if(i.preType==="mouseenter"||i.preType==="mouseleave")f=c(a.relatedTarget).closest(i.selector)[0];if(!f||f!==o)d.push({elem:o,handleObj:i})}}n=0;for(r=d.length;n<r;n++){j=d[n];a.currentTarget=j.elem;a.data=j.handleObj.data;a.handleObj=j.handleObj;if(j.handleObj.origHandler.apply(j.elem,e)===false){b=false;break}}return b}}function pa(a,b){return"live."+(a&&a!=="*"?a+".":"")+b.replace(/\./g,"`").replace(/ /g,
"&")}function qa(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function ra(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var f=c.data(a[d++]),e=c.data(this,f);if(f=f&&f.events){delete e.handle;e.events={};for(var j in f)for(var i in f[j])c.event.add(this,j,f[j][i],f[j][i].data)}}})}function sa(a,b,d){var f,e,j;b=b&&b[0]?b[0].ownerDocument||b[0]:s;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===s&&!ta.test(a[0])&&(c.support.checkClone||!ua.test(a[0]))){e=
true;if(j=c.fragments[a[0]])if(j!==1)f=j}if(!f){f=b.createDocumentFragment();c.clean(a,b,f,d)}if(e)c.fragments[a[0]]=j?f:1;return{fragment:f,cacheable:e}}function K(a,b){var d={};c.each(va.concat.apply([],va.slice(0,b)),function(){d[this]=a});return d}function wa(a){return"scrollTo"in a&&a.document?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var c=function(a,b){return new c.fn.init(a,b)},Ra=A.jQuery,Sa=A.$,s=A.document,T,Ta=/^[^<]*(<[\w\W]+>)[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/,
Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&&
(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this,
a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b===
"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this,
function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(j in e){i=a[j];o=e[j];if(a!==o)if(f&&o&&(c.isPlainObject(o)||c.isArray(o))){i=i&&(c.isPlainObject(i)||
c.isArray(i))?i:c.isArray(o)?[]:{};a[j]=c.extend(f,i,o)}else if(o!==w)a[j]=o}return a};c.extend({noConflict:function(a){A.$=Sa;if(a)A.jQuery=Ra;return c},isReady:false,ready:function(){if(!c.isReady){if(!s.body)return setTimeout(c.ready,13);c.isReady=true;if(Q){for(var a,b=0;a=Q[b++];)a.call(s,c);Q=null}c.fn.triggerHandler&&c(s).triggerHandler("ready")}},bindReady:function(){if(!xa){xa=true;if(s.readyState==="complete")return c.ready();if(s.addEventListener){s.addEventListener("DOMContentLoaded",
L,false);A.addEventListener("load",c.ready,false)}else if(s.attachEvent){s.attachEvent("onreadystatechange",L);A.attachEvent("onload",c.ready);var a=false;try{a=A.frameElement==null}catch(b){}s.documentElement.doScroll&&a&&ma()}}},isFunction:function(a){return $.call(a)==="[object Function]"},isArray:function(a){return $.call(a)==="[object Array]"},isPlainObject:function(a){if(!a||$.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!aa.call(a,"constructor")&&!aa.call(a.constructor.prototype,
"isPrototypeOf"))return false;var b;for(b in a);return b===w||aa.call(a,b)},isEmptyObject:function(a){for(var b in a)return false;return true},error:function(a){throw a;},parseJSON:function(a){if(typeof a!=="string"||!a)return null;a=c.trim(a);if(/^[\],:{}\s]*$/.test(a.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return A.JSON&&A.JSON.parse?A.JSON.parse(a):(new Function("return "+
a))();else c.error("Invalid JSON: "+a)},noop:function(){},globalEval:function(a){if(a&&Va.test(a)){var b=s.getElementsByTagName("head")[0]||s.documentElement,d=s.createElement("script");d.type="text/javascript";if(c.support.scriptEval)d.appendChild(s.createTextNode(a));else d.text=a;b.insertBefore(d,b.firstChild);b.removeChild(d)}},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,b,d){var f,e=0,j=a.length,i=j===w||c.isFunction(a);if(d)if(i)for(f in a){if(b.apply(a[f],
d)===false)break}else for(;e<j;){if(b.apply(a[e++],d)===false)break}else if(i)for(f in a){if(b.call(a[f],f,a[f])===false)break}else for(d=a[0];e<j&&b.call(d,e,d)!==false;d=a[++e]);return a},trim:function(a){return(a||"").replace(Wa,"")},makeArray:function(a,b){b=b||[];if(a!=null)a.length==null||typeof a==="string"||c.isFunction(a)||typeof a!=="function"&&a.setInterval?ba.call(b,a):c.merge(b,a);return b},inArray:function(a,b){if(b.indexOf)return b.indexOf(a);for(var d=0,f=b.length;d<f;d++)if(b[d]===
a)return d;return-1},merge:function(a,b){var d=a.length,f=0;if(typeof b.length==="number")for(var e=b.length;f<e;f++)a[d++]=b[f];else for(;b[f]!==w;)a[d++]=b[f++];a.length=d;return a},grep:function(a,b,d){for(var f=[],e=0,j=a.length;e<j;e++)!d!==!b(a[e],e)&&f.push(a[e]);return f},map:function(a,b,d){for(var f=[],e,j=0,i=a.length;j<i;j++){e=b(a[j],j,d);if(e!=null)f[f.length]=e}return f.concat.apply([],f)},guid:1,proxy:function(a,b,d){if(arguments.length===2)if(typeof b==="string"){d=a;a=d[b];b=w}else if(b&&
!c.isFunction(b)){d=b;b=w}if(!b&&a)b=function(){return a.apply(d||this,arguments)};if(a)b.guid=a.guid=a.guid||b.guid||c.guid++;return b},uaMatch:function(a){a=a.toLowerCase();a=/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version)?[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||!/compatible/.test(a)&&/(mozilla)(?:.*? rv:([\w.]+))?/.exec(a)||[];return{browser:a[1]||"",version:a[2]||"0"}},browser:{}});P=c.uaMatch(P);if(P.browser){c.browser[P.browser]=true;c.browser.version=P.version}if(c.browser.webkit)c.browser.safari=
true;if(ya)c.inArray=function(a,b){return ya.call(b,a)};T=c(s);if(s.addEventListener)L=function(){s.removeEventListener("DOMContentLoaded",L,false);c.ready()};else if(s.attachEvent)L=function(){if(s.readyState==="complete"){s.detachEvent("onreadystatechange",L);c.ready()}};(function(){c.support={};var a=s.documentElement,b=s.createElement("script"),d=s.createElement("div"),f="script"+J();d.style.display="none";d.innerHTML=" <link/><table></table><a href='/a' style='color:red;float:left;opacity:.55;'>a</a><input type='checkbox'/>";
var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected,
parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent=
false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="<input type='radio' name='radiotest' checked='checked'/>";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n=
s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true,
applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando];
else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this,
a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b===
w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i,
cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1)if(e.className){for(var j=" "+e.className+" ",
i=e.className,o=0,k=b.length;o<k;o++)if(j.indexOf(" "+b[o]+" ")<0)i+=" "+b[o];e.className=c.trim(i)}else e.className=a}return this},removeClass:function(a){if(c.isFunction(a))return this.each(function(k){var n=c(this);n.removeClass(a.call(this,k,n.attr("class")))});if(a&&typeof a==="string"||a===w)for(var b=(a||"").split(ca),d=0,f=this.length;d<f;d++){var e=this[d];if(e.nodeType===1&&e.className)if(a){for(var j=(" "+e.className+" ").replace(Aa," "),i=0,o=b.length;i<o;i++)j=j.replace(" "+b[i]+" ",
" ");e.className=c.trim(j)}else e.className=""}return this},toggleClass:function(a,b){var d=typeof a,f=typeof b==="boolean";if(c.isFunction(a))return this.each(function(e){var j=c(this);j.toggleClass(a.call(this,e,j.attr("class"),b),b)});return this.each(function(){if(d==="string")for(var e,j=0,i=c(this),o=b,k=a.split(ca);e=k[j++];){o=f?o:!i.hasClass(e);i[o?"addClass":"removeClass"](e)}else if(d==="undefined"||d==="boolean"){this.className&&c.data(this,"__className__",this.className);this.className=
this.className||a===false?"":c.data(this,"__className__")||""}})},hasClass:function(a){a=" "+a+" ";for(var b=0,d=this.length;b<d;b++)if((" "+this[b].className+" ").replace(Aa," ").indexOf(a)>-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j<d;j++){var i=
e[j];if(i.selected){a=c(i).val();if(b)return a;f.push(a)}}return f}if(Ba.test(b.type)&&!c.support.checkOn)return b.getAttribute("value")===null?"on":b.value;return(b.value||"").replace(Za,"")}return w}var o=c.isFunction(a);return this.each(function(k){var n=c(this),r=a;if(this.nodeType===1){if(o)r=a.call(this,k,n.val());if(typeof r==="number")r+="";if(c.isArray(r)&&Ba.test(this.type))this.checked=c.inArray(n.val(),r)>=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected=
c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");
a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g,
function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split(".");
k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a),
C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B<r.length;B++){u=r[B];if(d.guid===u.guid){if(i||k.test(u.namespace)){f==null&&r.splice(B--,1);n.remove&&n.remove.call(a,u)}if(f!=
null)break}}if(r.length===0||f!=null&&r.length===1){if(!n.teardown||n.teardown.call(a,o)===false)Ca(a,e,z.handle);delete C[e]}}else for(var B=0;B<r.length;B++){u=r[B];if(i||k.test(u.namespace)){c.event.remove(a,n,u.handler,B);r.splice(B--,1)}}}if(c.isEmptyObject(C)){if(b=z.handle)b.elem=null;delete z.events;delete z.handle;c.isEmptyObject(z)&&c.removeData(a)}}}}},trigger:function(a,b,d,f){var e=a.type||a;if(!f){a=typeof a==="object"?a[G]?a:c.extend(c.Event(e),a):c.Event(e);if(e.indexOf("!")>=0){a.type=
e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&&
f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;
if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e<j;e++){var i=d[e];if(b||f.test(i.namespace)){a.handler=i.handler;a.data=i.data;a.handleObj=i;i=i.handler.apply(this,arguments);if(i!==w){a.result=i;if(i===false){a.preventDefault();a.stopPropagation()}}if(a.isImmediatePropagationStopped())break}}}return a.result},props:"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "),
fix:function(a){if(a[G])return a;var b=a;a=c.Event(b);for(var d=this.props.length,f;d;){f=this.props[--d];a[f]=b[f]}if(!a.target)a.target=a.srcElement||s;if(a.target.nodeType===3)a.target=a.target.parentNode;if(!a.relatedTarget&&a.fromElement)a.relatedTarget=a.fromElement===a.target?a.toElement:a.fromElement;if(a.pageX==null&&a.clientX!=null){b=s.documentElement;d=s.body;a.pageX=a.clientX+(b&&b.scrollLeft||d&&d.scrollLeft||0)-(b&&b.clientLeft||d&&d.clientLeft||0);a.pageY=a.clientY+(b&&b.scrollTop||
d&&d.scrollTop||0)-(b&&b.clientTop||d&&d.clientTop||0)}if(!a.which&&(a.charCode||a.charCode===0?a.charCode:a.keyCode))a.which=a.charCode||a.keyCode;if(!a.metaKey&&a.ctrlKey)a.metaKey=a.ctrlKey;if(!a.which&&a.button!==w)a.which=a.button&1?1:a.button&2?3:a.button&4?2:0;return a},guid:1E8,proxy:c.proxy,special:{ready:{setup:c.bindReady,teardown:c.noop},live:{add:function(a){c.event.add(this,a.origType,c.extend({},a,{handler:oa}))},remove:function(a){var b=true,d=a.origType.replace(O,"");c.each(c.data(this,
"events").live||[],function(){if(d===this.origType.replace(O,""))return b=false});b&&c.event.remove(this,a.origType,oa)}},beforeunload:{setup:function(a,b,d){if(this.setInterval)this.onbeforeunload=d;return false},teardown:function(a,b){if(this.onbeforeunload===b)this.onbeforeunload=null}}}};var Ca=s.removeEventListener?function(a,b,d){a.removeEventListener(b,d,false)}:function(a,b,d){a.detachEvent("on"+b,d)};c.Event=function(a){if(!this.preventDefault)return new c.Event(a);if(a&&a.type){this.originalEvent=
a;this.type=a.type}else this.type=a;this.timeStamp=J();this[G]=true};c.Event.prototype={preventDefault:function(){this.isDefaultPrevented=Z;var a=this.originalEvent;if(a){a.preventDefault&&a.preventDefault();a.returnValue=false}},stopPropagation:function(){this.isPropagationStopped=Z;var a=this.originalEvent;if(a){a.stopPropagation&&a.stopPropagation();a.cancelBubble=true}},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=Z;this.stopPropagation()},isDefaultPrevented:Y,isPropagationStopped:Y,
isImmediatePropagationStopped:Y};var Da=function(a){var b=a.relatedTarget;try{for(;b&&b!==this;)b=b.parentNode;if(b!==this){a.type=a.data;c.event.handle.apply(this,arguments)}}catch(d){}},Ea=function(a){a.type=a.data;c.event.handle.apply(this,arguments)};c.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(a,b){c.event.special[a]={setup:function(d){c.event.add(this,b,d&&d.selector?Ea:Da,a)},teardown:function(d){c.event.remove(this,b,d&&d.selector?Ea:Da)}}});if(!c.support.submitBubbles)c.event.special.submit=
{setup:function(){if(this.nodeName.toLowerCase()!=="form"){c.event.add(this,"click.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="submit"||d==="image")&&c(b).closest("form").length)return na("submit",this,arguments)});c.event.add(this,"keypress.specialSubmit",function(a){var b=a.target,d=b.type;if((d==="text"||d==="password")&&c(b).closest("form").length&&a.keyCode===13)return na("submit",this,arguments)})}else return false},teardown:function(){c.event.remove(this,".specialSubmit")}};
if(!c.support.changeBubbles){var da=/textarea|input|select/i,ea,Fa=function(a){var b=a.type,d=a.value;if(b==="radio"||b==="checkbox")d=a.checked;else if(b==="select-multiple")d=a.selectedIndex>-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",
e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,
"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a,
d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j<o;j++)c.event.add(this[j],d,i,f)}return this}});c.fn.extend({unbind:function(a,b){if(typeof a==="object"&&
!a.preventDefault)for(var d in a)this.unbind(d,a[d]);else{d=0;for(var f=this.length;d<f;d++)c.event.remove(this[d],a,b)}return this},delegate:function(a,b,d,f){return this.live(b,d,f,a)},undelegate:function(a,b,d){return arguments.length===0?this.unbind("live"):this.die(b,null,d,a)},trigger:function(a,b){return this.each(function(){c.event.trigger(a,b,this)})},triggerHandler:function(a,b){if(this[0]){a=c.Event(a);a.preventDefault();a.stopPropagation();c.event.trigger(a,b,this[0]);return a.result}},
toggle:function(a){for(var b=arguments,d=1;d<b.length;)c.proxy(a,b[d++]);return this.click(c.proxy(a,function(f){var e=(c.data(this,"lastToggle"+a.guid)||0)%d;c.data(this,"lastToggle"+a.guid,e+1);f.preventDefault();return b[e].apply(this,arguments)||false}))},hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}});var Ga={focus:"focusin",blur:"focusout",mouseenter:"mouseover",mouseleave:"mouseout"};c.each(["live","die"],function(a,b){c.fn[b]=function(d,f,e,j){var i,o=0,k,n,r=j||this.selector,
u=j?this:c(this.context);if(c.isFunction(f)){e=f;f=w}for(d=(d||"").split(" ");(i=d[o++])!=null;){j=O.exec(i);k="";if(j){k=j[0];i=i.replace(O,"")}if(i==="hover")d.push("mouseenter"+k,"mouseleave"+k);else{n=i;if(i==="focus"||i==="blur"){d.push(Ga[i]+k);i+=k}else i=(Ga[i]||i)+k;b==="live"?u.each(function(){c.event.add(this,pa(i,r),{data:f,selector:r,handler:e,origType:i,origHandler:e,preType:n})}):u.unbind(pa(i,r),e)}}return this}});c.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error".split(" "),
function(a,b){c.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});A.attachEvent&&!A.addEventListener&&A.attachEvent("onunload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}});(function(){function a(g){for(var h="",l,m=0;g[m];m++){l=g[m];if(l.nodeType===3||l.nodeType===4)h+=l.nodeValue;else if(l.nodeType!==8)h+=a(l.childNodes)}return h}function b(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];
if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1&&!p){t.sizcache=l;t.sizset=q}if(t.nodeName.toLowerCase()===h){y=t;break}t=t[g]}m[q]=y}}}function d(g,h,l,m,q,p){q=0;for(var v=m.length;q<v;q++){var t=m[q];if(t){t=t[g];for(var y=false;t;){if(t.sizcache===l){y=m[t.sizset];break}if(t.nodeType===1){if(!p){t.sizcache=l;t.sizset=q}if(typeof h!=="string"){if(t===h){y=true;break}}else if(k.filter(h,[t]).length>0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,
e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift();
t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D||
g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h<g.length;h++)g[h]===g[h-1]&&g.splice(h--,1)}return g};k.matches=function(g,h){return k(g,null,null,h)};k.find=function(g,h,l){var m,q;if(!g)return[];
for(var p=0,v=n.order.length;p<v;p++){var t=n.order[p];if(q=n.leftMatch[t].exec(g)){var y=q[1];q.splice(1,1);if(y.substr(y.length-1)!=="\\"){q[1]=(q[1]||"").replace(/\\/g,"");m=n.find[t](q,h,l);if(m!=null){g=g.replace(n.match[t],"");break}}}}m||(m=h.getElementsByTagName("*"));return{set:m,expr:g}};k.filter=function(g,h,l,m){for(var q=g,p=[],v=h,t,y,S=h&&h[0]&&x(h[0]);g&&h.length;){for(var H in n.filter)if((t=n.leftMatch[H].exec(g))!=null&&t[2]){var M=n.filter[H],I,D;D=t[1];y=false;t.splice(1,1);if(D.substr(D.length-
1)!=="\\"){if(v===p)p=[];if(n.preFilter[H])if(t=n.preFilter[H](t,v,l,p,m,S)){if(t===true)continue}else y=I=true;if(t)for(var U=0;(D=v[U])!=null;U++)if(D){I=M(D,t,U,v);var Ha=m^!!I;if(l&&I!=null)if(Ha)y=true;else v[U]=false;else if(Ha){p.push(D);y=true}}if(I!==w){l||(v=p);g=g.replace(n.match[H],"");if(!y)return[];break}}}if(g===q)if(y==null)k.error(g);else break;q=g}return v};k.error=function(g){throw"Syntax error, unrecognized expression: "+g;};var n=k.selectors={order:["ID","NAME","TAG"],match:{ID:/#((?:[\w\u00c0-\uFFFF-]|\\.)+)/,
CLASS:/\.((?:[\w\u00c0-\uFFFF-]|\\.)+)/,NAME:/\[name=['"]*((?:[\w\u00c0-\uFFFF-]|\\.)+)['"]*\]/,ATTR:/\[\s*((?:[\w\u00c0-\uFFFF-]|\\.)+)\s*(?:(\S?=)\s*(['"]*)(.*?)\3|)\s*\]/,TAG:/^((?:[\w\u00c0-\uFFFF\*-]|\\.)+)/,CHILD:/:(only|nth|last|first)-child(?:\((even|odd|[\dn+-]*)\))?/,POS:/:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^-]|$)/,PSEUDO:/:((?:[\w\u00c0-\uFFFF-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/},leftMatch:{},attrMap:{"class":"className","for":"htmlFor"},attrHandle:{href:function(g){return g.getAttribute("href")}},
relative:{"+":function(g,h){var l=typeof h==="string",m=l&&!/\W/.test(h);l=l&&!m;if(m)h=h.toLowerCase();m=0;for(var q=g.length,p;m<q;m++)if(p=g[m]){for(;(p=p.previousSibling)&&p.nodeType!==1;);g[m]=l||p&&p.nodeName.toLowerCase()===h?p||false:p===h}l&&k.filter(h,g,true)},">":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m<q;m++){var p=g[m];if(p){l=p.parentNode;g[m]=l.nodeName.toLowerCase()===h?l:false}}}else{m=0;for(q=g.length;m<q;m++)if(p=g[m])g[m]=
l?p.parentNode:p.parentNode===h;l&&k.filter(h,g,true)}},"":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("parentNode",h,m,g,p,l)},"~":function(g,h,l){var m=e++,q=d;if(typeof h==="string"&&!/\W/.test(h)){var p=h=h.toLowerCase();q=b}q("previousSibling",h,m,g,p,l)}},find:{ID:function(g,h,l){if(typeof h.getElementById!=="undefined"&&!l)return(g=h.getElementById(g[1]))?[g]:[]},NAME:function(g,h){if(typeof h.getElementsByName!=="undefined"){var l=[];
h=h.getElementsByName(g[1]);for(var m=0,q=h.length;m<q;m++)h[m].getAttribute("name")===g[1]&&l.push(h[m]);return l.length===0?null:l}},TAG:function(g,h){return h.getElementsByTagName(g[1])}},preFilter:{CLASS:function(g,h,l,m,q,p){g=" "+g[1].replace(/\\/g,"")+" ";if(p)return g;p=0;for(var v;(v=h[p])!=null;p++)if(v)if(q^(v.className&&(" "+v.className+" ").replace(/[\t\n]/g," ").indexOf(g)>=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},
CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m,
g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},
text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},
setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return h<l[3]-0},gt:function(g,h,l){return h>l[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h=
h[3];l=0;for(m=h.length;l<m;l++)if(h[l]===g)return false;return true}else k.error("Syntax error, unrecognized expression: "+q)},CHILD:function(g,h){var l=h[1],m=g;switch(l){case "only":case "first":for(;m=m.previousSibling;)if(m.nodeType===1)return false;if(l==="first")return true;m=g;case "last":for(;m=m.nextSibling;)if(m.nodeType===1)return false;return true;case "nth":l=h[2];var q=h[3];if(l===1&&q===0)return true;h=h[0];var p=g.parentNode;if(p&&(p.sizcache!==h||!g.nodeIndex)){var v=0;for(m=p.firstChild;m;m=
m.nextSibling)if(m.nodeType===1)m.nodeIndex=++v;p.sizcache=h}g=g.nodeIndex-q;return l===0?g===0:g%l===0&&g/l>=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m===
"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g,
h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l<m;l++)h.push(g[l]);else for(l=0;g[l];l++)h.push(g[l]);return h}}var B;if(s.documentElement.compareDocumentPosition)B=function(g,h){if(!g.compareDocumentPosition||
!h.compareDocumentPosition){if(g==h)i=true;return g.compareDocumentPosition?-1:1}g=g.compareDocumentPosition(h)&4?-1:g===h?0:1;if(g===0)i=true;return g};else if("sourceIndex"in s.documentElement)B=function(g,h){if(!g.sourceIndex||!h.sourceIndex){if(g==h)i=true;return g.sourceIndex?-1:1}g=g.sourceIndex-h.sourceIndex;if(g===0)i=true;return g};else if(s.createRange)B=function(g,h){if(!g.ownerDocument||!h.ownerDocument){if(g==h)i=true;return g.ownerDocument?-1:1}var l=g.ownerDocument.createRange(),m=
h.ownerDocument.createRange();l.setStart(g,0);l.setEnd(g,0);m.setStart(h,0);m.setEnd(h,0);g=l.compareBoundaryPoints(Range.START_TO_END,m);if(g===0)i=true;return g};(function(){var g=s.createElement("div"),h="script"+(new Date).getTime();g.innerHTML="<a name='"+h+"'/>";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&&
q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML="<a href='#'></a>";
if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="<p class='TEST'></p>";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}();
(function(){var g=s.createElement("div");g.innerHTML="<div class='test e'></div><div class='test'></div>";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}:
function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q<p;q++)k(g,h[q],l);return k.filter(m,l)};c.find=k;c.expr=k.selectors;c.expr[":"]=c.expr.filters;c.unique=k.uniqueSort;c.text=a;c.isXMLDoc=x;c.contains=E})();var eb=/Until$/,fb=/^(?:parents|prevUntil|prevAll)/,
gb=/,/;R=Array.prototype.slice;var Ia=function(a,b,d){if(c.isFunction(b))return c.grep(a,function(e,j){return!!b.call(e,j,e)===d});else if(b.nodeType)return c.grep(a,function(e){return e===b===d});else if(typeof b==="string"){var f=c.grep(a,function(e){return e.nodeType===1});if(Ua.test(b))return c.filter(b,f,!d);else b=c.filter(b,f)}return c.grep(a,function(e){return c.inArray(e,b)>=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f<e;f++){d=b.length;
c.find(a,this[f],b);if(f>0)for(var j=d;j<b.length;j++)for(var i=0;i<d;i++)if(b[i]===b[j]){b.splice(j--,1);break}}return b},has:function(a){var b=c(a);return this.filter(function(){for(var d=0,f=b.length;d<f;d++)if(c.contains(this,b[d]))return true})},not:function(a){return this.pushStack(Ia(this,a,false),"not",a)},filter:function(a){return this.pushStack(Ia(this,a,true),"filter",a)},is:function(a){return!!a&&c.filter(a,this).length>0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j=
{},i;if(f&&a.length){e=0;for(var o=a.length;e<o;e++){i=a[e];j[i]||(j[i]=c.expr.match.POS.test(i)?c(i,b||this.context):i)}for(;f&&f.ownerDocument&&f!==b;){for(i in j){e=j[i];if(e.jquery?e.index(f)>-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a===
"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",
d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?
a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType===
1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/<tbody/i,jb=/<|&#?\w+;/,ta=/<script|<object|<embed|<option|<style/i,ua=/checked\s*(?:[^=]|=\s*.checked.)/i,Ma=function(a,b,d){return hb.test(d)?
a:b+"></"+d+">"},F={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div<div>","</div>"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d=
c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this},
wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})},
prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,
this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild);
return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja,
""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b<d;b++)if(this[b].nodeType===1){c.cleanData(this[b].getElementsByTagName("*"));this[b].innerHTML=a}}catch(f){this.empty().append(a)}}else c.isFunction(a)?this.each(function(e){var j=c(this),i=j.html();j.empty().append(function(){return a.call(this,e,i)})}):this.empty().append(a);return this},replaceWith:function(a){if(this[0]&&
this[0].parentNode){if(c.isFunction(a))return this.each(function(b){var d=c(this),f=d.html();d.replaceWith(a.call(this,b,f))});if(typeof a!=="string")a=c(a).detach();return this.each(function(){var b=this.nextSibling,d=this.parentNode;c(this).remove();b?c(b).before(a):c(d).append(a)})}else return this.pushStack(c(c.isFunction(a)?a():a),"replaceWith",a)},detach:function(a){return this.remove(a,true)},domManip:function(a,b,d){function f(u){return c.nodeName(u,"table")?u.getElementsByTagName("tbody")[0]||
u.appendChild(u.ownerDocument.createElement("tbody")):u}var e,j,i=a[0],o=[],k;if(!c.support.checkClone&&arguments.length===3&&typeof i==="string"&&ua.test(i))return this.each(function(){c(this).domManip(a,b,d,true)});if(c.isFunction(i))return this.each(function(u){var z=c(this);a[0]=i.call(this,u,b?z.html():w);z.domManip(a,b,d)});if(this[0]){e=i&&i.parentNode;e=c.support.parentNode&&e&&e.nodeType===11&&e.childNodes.length===this.length?{fragment:e}:sa(a,this,o);k=e.fragment;if(j=k.childNodes.length===
1?(k=k.firstChild):k.firstChild){b=b&&c.nodeName(j,"tr");for(var n=0,r=this.length;n<r;n++)d.call(b?f(this[n],j):this[n],n>0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]);
return this}else{e=0;for(var j=d.length;e<j;e++){var i=(e>0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["",
""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]==="<table>"&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e=
c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]?
c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja=
function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter=
Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a,
"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f=
a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=
a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=/<script(.|\s)*?\/script>/gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!==
"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("<div />").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this},
serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),
function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,
global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&&
e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)?
"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache===
false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B=
false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since",
c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E||
d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x);
g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===
1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b===
"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional;
if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");
this[a].style.display=d||"";if(c.css(this[a],"display")==="none"){d=this[a].nodeName;var f;if(la[d])f=la[d];else{var e=c("<"+d+" />").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a<b;a++)this[a].style.display=c.data(this[a],"olddisplay")||"";return this}},hide:function(a,b){if(a||a===0)return this.animate(K("hide",3),a,b);else{a=0;for(b=this.length;a<b;a++){var d=c.data(this[a],"olddisplay");!d&&d!=="none"&&c.data(this[a],
"olddisplay",c.css(this[a],"display"))}a=0;for(b=this.length;a<b;a++)this[a].style.display="none";return this}},_toggle:c.fn.toggle,toggle:function(a,b){var d=typeof a==="boolean";if(c.isFunction(a)&&c.isFunction(b))this._toggle.apply(this,arguments);else a==null||d?this.each(function(){var f=d?a:c(this).is(":hidden");c(this)[f?"show":"hide"]()}):this.animate(K("toggle",3),a,b);return this},fadeTo:function(a,b,d){return this.filter(":hidden").css("opacity",0).show().end().animate({opacity:b},a,d)},
animate:function(a,b,d,f){var e=c.speed(b,d,f);if(c.isEmptyObject(a))return this.each(e.complete);return this[e.queue===false?"each":"queue"](function(){var j=c.extend({},e),i,o=this.nodeType===1&&c(this).is(":hidden"),k=this;for(i in a){var n=i.replace(ia,ja);if(i!==n){a[n]=a[i];delete a[i];i=n}if(a[i]==="hide"&&o||a[i]==="show"&&!o)return j.complete.call(this);if((i==="height"||i==="width")&&this.style){j.display=c.css(this,"display");j.overflow=this.style.overflow}if(c.isArray(a[i])){(j.specialEasing=
j.specialEasing||{})[i]=a[i][1];a[i]=a[i][0]}}if(j.overflow!=null)this.style.overflow="hidden";j.curAnim=c.extend({},a);c.each(a,function(r,u){var z=new c.fx(k,j,r);if(Ab.test(u))z[u==="toggle"?o?"show":"hide":u](a);else{var C=Bb.exec(u),B=z.cur(true)||0;if(C){u=parseFloat(C[2]);var E=C[3]||"px";if(E!=="px"){k.style[r]=(u||1)+E;B=(u||1)/z.cur(true)*B;k.style[r]=B+E}if(C[1])u=(C[1]==="-="?-1:1)*u+B;z.custom(B,u,E)}else z.custom(B,u,"")}});return true})},stop:function(a,b){var d=c.timers;a&&this.queue([]);
this.each(function(){for(var f=d.length-1;f>=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration===
"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||
c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;
this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=
this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem,
e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b<a.length;b++)a[b]()||a.splice(b--,1);a.length||
c.fx.stop()},stop:function(){clearInterval(W);W=null},speeds:{slow:600,fast:200,_default:400},step:{opacity:function(a){c.style(a.elem,"opacity",a.now)},_default:function(a){if(a.elem.style&&a.elem.style[a.prop]!=null)a.elem.style[a.prop]=(a.prop==="width"||a.prop==="height"?Math.max(0,a.now):a.now)+a.unit;else a.elem[a.prop]=a.now}}});if(c.expr&&c.expr.filters)c.expr.filters.animated=function(a){return c.grep(c.timers,function(b){return a===b.elem}).length};c.fn.offset="getBoundingClientRect"in s.documentElement?
function(a){var b=this[0];if(a)return this.each(function(e){c.offset.setOffset(this,a,e)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);var d=b.getBoundingClientRect(),f=b.ownerDocument;b=f.body;f=f.documentElement;return{top:d.top+(self.pageYOffset||c.support.boxModel&&f.scrollTop||b.scrollTop)-(f.clientTop||b.clientTop||0),left:d.left+(self.pageXOffset||c.support.boxModel&&f.scrollLeft||b.scrollLeft)-(f.clientLeft||b.clientLeft||0)}}:function(a){var b=
this[0];if(a)return this.each(function(r){c.offset.setOffset(this,a,r)});if(!b||!b.ownerDocument)return null;if(b===b.ownerDocument.body)return c.offset.bodyOffset(b);c.offset.initialize();var d=b.offsetParent,f=b,e=b.ownerDocument,j,i=e.documentElement,o=e.body;f=(e=e.defaultView)?e.getComputedStyle(b,null):b.currentStyle;for(var k=b.offsetTop,n=b.offsetLeft;(b=b.parentNode)&&b!==o&&b!==i;){if(c.offset.supportsFixedPosition&&f.position==="fixed")break;j=e?e.getComputedStyle(b,null):b.currentStyle;
k-=b.scrollTop;n-=b.scrollLeft;if(b===d){k+=b.offsetTop;n+=b.offsetLeft;if(c.offset.doesNotAddBorder&&!(c.offset.doesAddBorderForTableAndCells&&/^t(able|d|h)$/i.test(b.nodeName))){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=d;d=b.offsetParent}if(c.offset.subtractsBorderForOverflowNotVisible&&j.overflow!=="visible"){k+=parseFloat(j.borderTopWidth)||0;n+=parseFloat(j.borderLeftWidth)||0}f=j}if(f.position==="relative"||f.position==="static"){k+=o.offsetTop;n+=o.offsetLeft}if(c.offset.supportsFixedPosition&&
f.position==="fixed"){k+=Math.max(i.scrollTop,o.scrollTop);n+=Math.max(i.scrollLeft,o.scrollLeft)}return{top:k,left:n}};c.offset={initialize:function(){var a=s.body,b=s.createElement("div"),d,f,e,j=parseFloat(c.curCSS(a,"marginTop",true))||0;c.extend(b.style,{position:"absolute",top:0,left:0,margin:0,border:0,width:"1px",height:"1px",visibility:"hidden"});b.innerHTML="<div style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;'><div></div></div><table style='position:absolute;top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;' cellpadding='0' cellspacing='0'><tr><td></td></tr></table>";
a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b);
c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a,
d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top-
f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset":
"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in
e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window);

View File

@ -1 +0,0 @@
ldapjs.org

View File

@ -1,31 +1,21 @@
---
title: Client API | ldapjs
markdown2extras: tables
---
# ldapjs Client API
<div class="intro">
This document covers the ldapjs client API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
# Create a client
The code to create a new client looks like:
```js
const ldap = require('ldapjs');
const client = ldap.createClient({
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
});
client.on('connectError', (err) => {
// handle connection error
})
```
var ldap = require('ldapjs');
var client = ldap.createClient({
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
});
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
that this will not use the LDAP TLS extended operation, but literally an SSL
@ -41,6 +31,7 @@ client is:
|connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)|
|tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)|
|idleTimeout |Milliseconds after last activity before client emits idle event|
|strictDN |Force strict DN parsing for client methods (Default is true)|
|reconnect |Try to reconnect when the connection gets lost (Default is false)|
### url
@ -62,13 +53,6 @@ Known compatible loggers are:
+ [Bunyan](https://www.npmjs.com/package/bunyan)
+ [Pino](https://www.npmjs.com/package/pino)
### Note On Error Handling
The client is an `EventEmitter`. If you don't register an error handler and
e.g. a connection error occurs, Node.js will print a stack trace and exit the
process ([reference](https://nodejs.org/api/events.html#error-events)).
## Connection management
As LDAP is a stateful protocol (as opposed to HTTP), having connections torn
@ -83,25 +67,6 @@ more sophisticated control, you can provide an Object with the properties
`failAfter` (default: `Infinity`).
After the reconnect you maybe need to [bind](#bind) again.
## Client events
The client is an `EventEmitter` and can emit the following events:
|Event |Description |
|---------------|----------------------------------------------------------|
|error |General error |
|connectRefused |Server refused connection. Most likely bad authentication |
|connectTimeout |Server timeout |
|connectError |Socket connection error |
|setupError |Setup error after successful connection |
|socketTimeout |Socket timeout |
|resultError |Search result error |
|timeout |Search result timeout |
|destroy |After client is disconnected |
|end |Socket end event |
|close |Socket closed |
|connect |Client connected |
|idle |Idle timeout reached |
## Common patterns
@ -113,6 +78,9 @@ Almost every operation has the callback form of `function(err, res)` where err
will be an instance of an `LDAPError` (you can use `instanceof` to switch).
You probably won't need to check the `res` parameter, but it's there if you do.
# bind
`bind(dn, password, controls, callback)`
@ -124,11 +92,9 @@ of `Control` objects. You probably don't need them though...
Example:
```js
client.bind('cn=root', 'secret', (err) => {
assert.ifError(err);
});
```
client.bind('cn=root', 'secret', function(err) {
assert.ifError(err);
});
# add
`add(dn, entry, controls, callback)`
@ -140,17 +106,15 @@ controls are optional.
Example:
```js
const entry = {
cn: 'foo',
sn: 'bar',
email: ['foo@bar.com', 'foo1@bar.com'],
objectclass: 'fooPerson'
};
client.add('cn=foo, o=example', entry, (err) => {
assert.ifError(err);
});
```
var entry = {
cn: 'foo',
sn: 'bar',
email: ['foo@bar.com', 'foo1@bar.com'],
objectclass: 'fooPerson'
};
client.add('cn=foo, o=example', entry, function(err) {
assert.ifError(err);
});
# compare
`compare(dn, attribute, value, controls, callback)`
@ -160,13 +124,11 @@ the entry referenced by dn.
Example:
```js
client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => {
assert.ifError(err);
client.compare('cn=foo, o=example', 'sn', 'bar', function(err, matched) {
assert.ifError(err);
console.log('matched: ' + matched);
});
```
console.log('matched: ' + matched);
});
# del
`del(dn, controls, callback)`
@ -176,11 +138,9 @@ Deletes an entry from the LDAP server.
Example:
```js
client.del('cn=foo, o=example', (err) => {
assert.ifError(err);
});
```
client.del('cn=foo, o=example', function(err) {
assert.ifError(err);
});
# exop
`exop(name, value, controls, callback)`
@ -192,13 +152,11 @@ should be.
Example (performs an LDAP 'whois' extended op):
```js
client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
assert.ifError(err);
client.exop('1.3.6.1.4.1.4203.1.11.3', function(err, value, res) {
assert.ifError(err);
console.log('whois: ' + value);
});
```
console.log('whois: ' + value);
});
# modify
`modify(name, changes, controls, callback)`
@ -209,19 +167,16 @@ pass in a single `Change` or an array of `Change` objects.
Example:
```js
const change = new ldap.Change({
operation: 'add',
modification: {
type: 'pets',
values: ['cat', 'dog']
}
});
var change = new ldap.Change({
operation: 'add',
modification: {
pets: ['cat', 'dog']
}
});
client.modify('cn=foo, o=example', change, (err) => {
assert.ifError(err);
});
```
client.modify('cn=foo, o=example', change, function(err) {
assert.ifError(err);
});
## Change
@ -235,13 +190,7 @@ must be one of:
| add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. |
| delete | Deletes the attribute (and all values) referenced in `modification`. |
`modification` is just a plain old JS object with the required type and values you want.
| Operation | Description |
|-----------|-------------|
| type | String that defines the attribute type for the modification. |
| values | Defines the values for modification. |
`modification` is just a plain old JS object with the values you want.
# modifyDN
`modifyDN(dn, newDN, controls, callback)`
@ -257,11 +206,9 @@ as opposed to just renaming the leaf).
Example:
```js
client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => {
assert.ifError(err);
});
```
client.modifyDN('cn=foo, o=example', 'cn=bar', function(err) {
assert.ifError(err);
});
# search
`search(base, options, controls, callback)`
@ -289,45 +236,40 @@ containing the following fields:
|timeLimit |the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this.|
|paged |enable and/or configure automatic result paging|
Responses inside callback of the `search` method are an `EventEmitter` where you will get a notification for
each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest`
, `searchReference`, `error` and `end` event.
`searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations
like `client.abandon` with `searchRequest.messageId` to abandon this search request. Note that the `error` event will
only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code
(likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as
time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code
matching.
Responses from the `search` method are an `EventEmitter` where you will get a
notification for each `searchEntry` that comes back from the server. You will
additionally be able to listen for a `searchReference`, `error` and `end` event.
Note that the `error` event will only be for client/TCP errors, not LDAP error
codes like the other APIs. You'll want to check the LDAP status code
(likely for `0`) on the `end` event to assert success. LDAP search results
can give you a lot of status codes, such as time or size exceeded, busy,
inappropriate matching, etc., which is why this method doesn't try to wrap up
the code matching.
Example:
```js
const opts = {
filter: '(&(l=Seattle)(email=*@foo.com))',
scope: 'sub',
attributes: ['dn', 'sn', 'cn']
};
var opts = {
filter: '(&(l=Seattle)(email=*@foo.com))',
scope: 'sub',
attributes: ['dn', 'sn', 'cn']
};
client.search('o=example', opts, (err, res) => {
assert.ifError(err);
client.search('o=example', opts, function(err, res) {
assert.ifError(err);
res.on('searchRequest', (searchRequest) => {
console.log('searchRequest: ', searchRequest.messageId);
});
res.on('searchEntry', (entry) => {
console.log('entry: ' + JSON.stringify(entry.pojo));
});
res.on('searchReference', (referral) => {
console.log('referral: ' + referral.uris.join());
});
res.on('error', (err) => {
console.error('error: ' + err.message);
});
res.on('end', (result) => {
console.log('status: ' + result.status);
});
});
```
res.on('searchEntry', function(entry) {
console.log('entry: ' + JSON.stringify(entry.object));
});
res.on('searchReference', function(referral) {
console.log('referral: ' + referral.uris.join());
});
res.on('error', function(err) {
console.error('error: ' + err.message);
});
res.on('end', function(result) {
console.log('status: ' + result.status);
});
});
## Filter Strings
@ -342,31 +284,25 @@ in prefix notation. For example, let's start simple, and build up a complicated
filter. The most basic filter is equality, so let's assume you want to search
for an attribute `email` with a value of `foo@bar.com`. The syntax would be:
```
(email=foo@bar.com)
```
(email=foo@bar.com)
ldapjs requires all filters to be surrounded by '()' blocks. Ok, that was easy.
Let's now assume that you want to find all records where the email is actually
just anything in the "@bar.com" domain and the location attribute is set to
Seattle:
```
(&(email=*@bar.com)(l=Seattle))
```
(&(email=*@bar.com)(l=Seattle))
Now our filter is actually three LDAP filters. We have an `and` filter (single
amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter.
Substrings are wildcard filters. They use `*` as the wildcard. You can put more
than one wildcard for a given string. For example you could do `(email=*@*bar.com)`
to match any email of @bar.com or its subdomains like `"example@foo.bar.com"`.
to match any email of @bar.com or its subdomains like "example@foo.bar.com".
Now, let's say we also want to set our filter to include a
specification that either the employeeType *not* be a manager nor a secretary:
```
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
```
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
The `not` character is represented as a `!`, the `or` as a single pipe `|`.
It gets a little bit complicated, but it's actually quite powerful, and lets you
@ -381,29 +317,27 @@ While callers could choose to do this manually via the `controls` parameter to
most simple way to use the paging automation is to set the `paged` option to
true when performing a search:
```js
const opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: true,
sizeLimit: 200
};
client.search('o=largedir', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', (entry) => {
// do per-entry processing
});
res.on('page', (result) => {
console.log('page end');
});
res.on('error', (resErr) => {
assert.ifError(resErr);
});
res.on('end', (result) => {
console.log('done ');
});
});
```
var opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: true,
sizeLimit: 200
};
client.search('o=largedir', opts, function(err, res) {
assert.ifError(err);
res.on('searchEntry', function(entry) {
// do per-entry processing
});
res.on('page', function(result) {
console.log('page end');
});
res.on('error', function(resErr) {
assert.ifError(resErr);
});
res.on('end', function(result) {
console.log('done ');
});
});
This will enable paging with a default page size of 199 (`sizeLimit` - 1) and
will output all of the resulting objects via the `searchEntry` event. At the
@ -421,34 +355,32 @@ client will wait to request the next page until that callback is executed.
Here is an example where both of those parameters are used:
```js
const queue = new MyWorkQueue(someSlowWorkFunction);
const opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: {
pageSize: 250,
pagePause: true
},
};
client.search('o=largerdir', opts, (err, res) => {
assert.ifError(err);
res.on('searchEntry', (entry) => {
// Submit incoming objects to queue
queue.push(entry);
});
res.on('page', (result, cb) => {
// Allow the queue to flush before fetching next page
queue.cbWhenFlushed(cb);
});
res.on('error', (resErr) => {
assert.ifError(resErr);
});
res.on('end', (result) => {
console.log('done');
});
});
```
var queue = new MyWorkQueue(someSlowWorkFunction);
var opts = {
filter: '(objectclass=commonobject)',
scope: 'sub',
paged: {
pageSize: 250,
pagePause: true
},
};
client.search('o=largerdir', opts, function(err, res) {
assert.ifError(err);
res.on('searchEntry', function(entry) {
// Submit incoming objects to queue
queue.push(entry);
});
res.on('page', function(result, cb) {
// Allow the queue to flush before fetching next page
queue.cbWhenFlushed(cb);
});
res.on('error', function(resErr) {
assert.ifError(resErr);
});
res.on('end', function(result) {
console.log('done');
});
});
# starttls
`starttls(options, controls, callback)`
@ -457,17 +389,15 @@ Attempt to secure existing LDAP connection via STARTTLS.
Example:
```js
const opts = {
ca: [fs.readFileSync('mycacert.pem')]
};
var opts = {
ca: [fs.readFileSync('mycacert.pem')]
};
client.starttls(opts, (err, res) => {
assert.ifError(err);
client.starttls(opts, function(err, res) {
assert.ifError(err);
// Client communication now TLS protected
});
```
// Client communication now TLS protected
});
# unbind
@ -484,8 +414,6 @@ not have a response.
Example:
```js
client.unbind((err) => {
assert.ifError(err);
});
```
client.unbind(function(err) {
assert.ifError(err);
});

View File

@ -4,13 +4,9 @@ title: DN API | ldapjs
# ldapjs DN API
<div class="intro">
This document covers the ldapjs DN API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
DNs are LDAP distinguished names, and are composed of a set of RDNs (relative
distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the
complete specification, but basically an RDN is an attribute value assertion
@ -26,12 +22,10 @@ The `parseDN` API converts a string representation of a DN into an ldapjs DN
object; in most cases this will be handled for you under the covers of the
ldapjs framework, but if you need it, it's there.
```js
const parseDN = require('ldapjs').parseDN;
var parseDN = require('ldapjs').parseDN;
const dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
console.log(dn.toString());
```
var dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
console.log(dn.toString());
# DN
@ -43,46 +37,40 @@ APIs are setup to give you a DN object.
Returns a boolean indicating whether 'this' is a child of the passed in dn. The
`dn` argument can be either a string or a DN.
```js
server.add('o=example', (req, res, next) => {
if (req.dn.childOf('ou=people, o=example')) {
...
} else {
...
}
});
```
server.add('o=example', function(req, res, next) {
if (req.dn.childOf('ou=people, o=example')) {
...
} else {
...
}
});
## parentOf(dn)
The inverse of `childOf`; returns a boolean on whether or not `this` is a parent
of the passed in dn. Like `childOf`, can take either a string or a DN.
```js
server.add('o=example', (req, res, next) => {
const dn = parseDN('ou=people, o=example');
if (dn.parentOf(req.dn)) {
...
} else {
...
}
});
```
server.add('o=example', function(req, res, next) {
var dn = parseDN('ou=people, o=example');
if (dn.parentOf(req.dn)) {
...
} else {
...
}
});
## equals(dn)
Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
argument. `dn` can be a string or a DN.
```js
server.add('o=example', (req, res, next) => {
if (req.dn.equals('cn=foo, ou=people, o=example')) {
...
} else {
...
}
});
```
server.add('o=example', function(req, res, next) {
if (req.dn.equals('cn=foo, ou=people, o=example')) {
...
} else {
...
}
});
## parent()
@ -120,8 +108,6 @@ It accepts the same parameters as `format`.
Returns the string representation of `this`.
```js
server.add('o=example', (req, res, next) => {
console.log(req.dn.toString());
});
```
server.add('o=example', function(req, res, next) {
console.log(req.dn.toString());
});

View File

@ -4,36 +4,30 @@ title: Errors API | ldapjs
# ldapjs Errors API
<div class="intro">
This document covers the ldapjs errors API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
All errors in the ldapjs framework extend from an abstract error type called
`LDAPError`. In addition to the properties listed below, all errors will have
a `stack` property correctly set.
In general, you'll be using the errors in ldapjs like:
```js
const ldap = require('ldapjs');
var ldap = require('ldapjs');
const db = {};
var db = {};
server.add('o=example', (req, res, next) => {
const parent = req.dn.parent();
if (parent) {
if (!db[parent.toString()])
return next(new ldap.NoSuchObjectError(parent.toString()));
}
if (db[req.dn.toString()])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
server.add('o=example', function(req, res, next) {
var parent = req.dn.parent();
if (parent) {
if (!db[parent.toString()])
return next(new ldap.NoSuchObjectError(parent.toString()));
}
if (db[req.dn.toString()])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
...
});
```
...
});
I.e., if you just pass them into the `next()` handler, ldapjs will automatically
return the appropriate LDAP error message, and stop the handler chain.

File diff suppressed because it is too large Load Diff

View File

@ -4,13 +4,9 @@ title: Filters API | ldapjs
# ldapjs Filters API
<div class="intro">
This document covers the ldapjs filters API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
LDAP search filters are really the backbone of LDAP search operations, and
ldapjs tries to get you in "easy" with them if your dataset is small, and also
lets you introspect them if you want to write a "query planner". For reference,
@ -34,17 +30,13 @@ Parses an [RFC2254](http://www.ietf.org/rfc/rfc2254.txt) filter string into an
ldapjs object(s). If the filter is "complex", it will be a "tree" of objects.
For example:
```js
const parseFilter = require('ldapjs').parseFilter;
var parseFilter = require('ldapjs').parseFilter;
const f = parseFilter('(objectclass=*)');
```
var f = parseFilter('(objectclass=*)');
Is a "simple" filter, and would just return a `PresenceFilter` object. However,
```js
const f = parseFilter('(&(employeeType=manager)(l=Seattle))');
```
var f = parseFilter('(&(employeeType=manager)(l=Seattle))');
Would return an `AndFilter`, which would have a `filters` array of two
`EqualityFilter` objects.
@ -56,22 +48,20 @@ syntactically invalid string).
The equality filter is used to check exact matching of attribute/value
assertions. This object will have an `attribute` and `value` property, and the
`name` property will be `equal`.
`name` proerty will be `equal`.
The string syntax for an equality filter is `(attr=value)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and a value matching `value`.
```js
const f = new EqualityFilter({
attribute: 'cn',
value: 'foo'
});
var f = new EqualityFilter({
attribute: 'cn',
value: 'foo'
});
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
```
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
Equality matching uses "strict" type JavaScript comparison, and by default
everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison
@ -89,14 +79,12 @@ The string syntax for a presence filter is `(attr=*)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute`.
```js
const f = new PresenceFilter({
attribute: 'cn'
});
var f = new PresenceFilter({
attribute: 'cn'
});
f.matches({cn: 'foo'}); => true
f.matches({sn: 'foo'}); => false
```
f.matches({cn: 'foo'}); => true
f.matches({sn: 'foo'}); => false
# SubstringFilter
@ -110,28 +98,24 @@ optional. The `name` property will be `substring`.
The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would
map to:
```js
{
initial: 'foo',
any: ['bar', 'cat'],
final: 'dog'
}
```
{
initial: 'foo',
any: ['bar', 'cat'],
final: 'dog'
}
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the "regex" matches the value
```js
const f = new SubstringFilter({
attribute: 'cn',
initial: 'foo',
any: ['bar'],
final: 'baz'
});
var f = new SubstringFilter({
attribute: 'cn',
initial: 'foo',
any: ['bar'],
final: 'baz'
});
f.matches({cn: 'foobigbardogbaz'}); => true
f.matches({sn: 'fobigbardogbaz'}); => false
```
f.matches({cn: 'foobigbardogbaz'}); => true
f.matches({sn: 'fobigbardogbaz'}); => false
# GreaterThanEqualsFilter
@ -147,22 +131,18 @@ property and the `name` property will be `ge`.
The string syntax for a ge filter is:
```
(cn>=foo)
```
(cn>=foo)
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the value is `>=` this filter's `value`.
```js
const f = new GreaterThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
var f = new GreaterThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
f.matches({cn: 'foobar'}); => true
f.matches({cn: 'abc'}); => false
```
f.matches({cn: 'foobar'}); => true
f.matches({cn: 'abc'}); => false
# LessThanEqualsFilter
@ -175,9 +155,7 @@ Note that the ldapjs schema middleware will do this.
The string syntax for a le filter is:
```
(cn<=foo)
```
(cn<=foo)
The LessThanEqualsFilter will have an `attribute` property, a `value`
property and the `name` property will be `le`.
@ -185,15 +163,13 @@ property and the `name` property will be `le`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and the value is `<=` this filter's `value`.
```js
const f = new LessThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
var f = new LessThanEqualsFilter({
attribute: 'cn',
value: 'foo',
});
f.matches({cn: 'abc'}); => true
f.matches({cn: 'foobar'}); => false
```
f.matches({cn: 'abc'}); => true
f.matches({cn: 'foobar'}); => false
# AndFilter
@ -204,30 +180,26 @@ object will have a `filters` property which is an array of `Filter` objects. The
The string syntax for an and filter is (assuming below we're and'ing two
equality filters):
```
(&(cn=foo)(sn=bar))
```
(&(cn=foo)(sn=bar))
The `matches()` method will return true IFF the passed in object matches all
the filters in the `filters` array.
```js
const f = new AndFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
value: 'foo'
}),
new EqualityFilter({
attribute: 'sn',
value: 'bar'
})
]
});
var f = new AndFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
value: 'foo'
}),
new EqualityFilter({
attribute: 'sn',
value: 'bar'
})
]
});
f.matches({cn: 'foo', sn: 'bar'}); => true
f.matches({cn: 'foo', sn: 'baz'}); => false
```
f.matches({cn: 'foo', sn: 'bar'}); => true
f.matches({cn: 'foo', sn: 'baz'}); => false
# OrFilter
@ -238,30 +210,26 @@ object will have a `filters` property which is an array of `Filter` objects. The
The string syntax for an or filter is (assuming below we're or'ing two
equality filters):
```
(|(cn=foo)(sn=bar))
```
(|(cn=foo)(sn=bar))
The `matches()` method will return true IFF the passed in object matches *any*
of the filters in the `filters` array.
```js
const f = new OrFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
value: 'foo'
}),
new EqualityFilter({
attribute: 'sn',
value: 'bar'
})
]
});
var f = new OrFilter({
filters: [
new EqualityFilter({
attribute: 'cn',
value: 'foo'
}),
new EqualityFilter({
attribute: 'sn',
value: 'bar'
})
]
});
f.matches({cn: 'foo', sn: 'baz'}); => true
f.matches({cn: 'bar', sn: 'baz'}); => false
```
f.matches({cn: 'foo', sn: 'baz'}); => true
f.matches({cn: 'bar', sn: 'baz'}); => false
# NotFilter
@ -272,30 +240,26 @@ The `name` property will be `not`.
The string syntax for a not filter is (assuming below we're not'ing an
equality filter):
```
(!(cn=foo))
```
(!(cn=foo))
The `matches()` method will return true IFF the passed in object does not match
the filter in the `filter` property.
```js
const f = new NotFilter({
filter: new EqualityFilter({
attribute: 'cn',
value: 'foo'
})
});
var f = new NotFilter({
filter: new EqualityFilter({
attribute: 'cn',
value: 'foo'
})
});
f.matches({cn: 'bar'}); => true
f.matches({cn: 'foo'}); => false
```
f.matches({cn: 'bar'}); => true
f.matches({cn: 'foo'}); => false
# ApproximateFilter
The approximate filter is used to check "approximate" matching of
attribute/value assertions. This object will have an `attribute` and
`value` property, and the `name` property will be `approx`.
`value` property, and the `name` proerty will be `approx`.
As a side point, this is a useless filter. It's really only here if you have
some whacky client that's sending this. It just does an exact match (which
@ -306,12 +270,11 @@ The string syntax for an equality filter is `(attr~=value)`.
The `matches()` method will return true IFF the passed in object has a
key matching `attribute` and a value exactly matching `value`.
```js
const f = new ApproximateFilter({
attribute: 'cn',
value: 'foo'
});
var f = new ApproximateFilter({
attribute: 'cn',
value: 'foo'
});
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
f.matches({cn: 'foo'}); => true
f.matches({cn: 'bar'}); => false
```

View File

@ -1,11 +1,10 @@
---
title: LDAP Guide | ldapjs
markdown2extras: tables
---
# LDAP Guide
<div class="intro">
This guide was written assuming that you (1) don't know anything about ldapjs,
and perhaps more importantly (2) know little, if anything about LDAP. If you're
already an LDAP whiz, please don't read this and feel it's condescending. Most
@ -15,8 +14,6 @@ password."
By the end of this guide, we'll have a simple LDAP server that accomplishes a
"real" task.
</div>
# What exactly is LDAP?
If you haven't already read the
@ -31,26 +28,23 @@ Access Protocol". A directory service basically breaks down as follows:
It might be helpful to visualize:
```
o=example
/ \
ou=users ou=groups
/ | | \
cn=john cn=jane cn=dudes cn=dudettes
/
keyid=foo
```
o=example
/ \
ou=users ou=groups
/ | | \
cn=john cn=jane cn=dudes cn=dudettes
/
keyid=foo
Let's say we wanted to look at the record cn=john:
```shell
dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: john@example.com
email: john.smith@example.com
objectClass: person
```
dn: cn=john, ou=users, o=example
cn: john
sn: smith
email: john@example.com
email: john.smith@example.com
objectClass: person
A few things to note:
@ -94,7 +88,7 @@ Want to run schema-less in ldapjs, or wire it up with some mongoose models? No
problem. Want to back it to redis? Should be able to get some basics up in a
day or two.
Basically, the ldapjs philosophy is to deal with the "muck" of LDAP, and then
Basically, the ldapjs philospohy is to deal with the "muck" of LDAP, and then
get out of the way so you can just use the "good parts."
# Ok, cool. Learn me some LDAP!
@ -114,9 +108,7 @@ If you don't already have node.js and npm, clearly you need those, so follow
the steps at [nodejs.org](http://nodejs.org) and [npmjs.org](http://npmjs.org),
respectively. After that, run:
```shell
$ npm install ldapjs
```
$ npm install ldapjs
Rather than overload you with client-side programming for now, we'll use
the OpenLDAP CLI to interact with our server. It's almost certainly already
@ -126,22 +118,18 @@ package manager here.
To get started, open some file, and let's get the library loaded and a server
created:
```js
const ldap = require('ldapjs');
var ldap = require('ldapjs');
const server = ldap.createServer();
var server = ldap.createServer();
server.listen(1389, () => {
console.log('/etc/passwd LDAP server up at: %s', server.url);
});
```
server.listen(1389, function() {
console.log('/etc/passwd LDAP server up at: %s', server.url);
});
And run that. Doing anything will give you errors (LDAP "No Such Object")
since we haven't added any support in yet, but go ahead and try it anyway:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -b "o=myhost" objectclass=*
```
$ ldapsearch -H ldap://localhost:1389 -x -b "o=myhost" objectclass=*
Before we go any further, note that the complete code for the server we are
about to build up is on the [examples](examples.html) page.
@ -162,15 +150,13 @@ has no correspondence to our Unix root user, it's just something we're making up
and going to use for allowing an (LDAP) admin to do anything. To do so, add
this code into your file:
```js
server.bind('cn=root', (req, res, next) => {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
return next(new ldap.InvalidCredentialsError());
server.bind('cn=root', function(req, res, next) {
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
return next(new ldap.InvalidCredentialsError());
res.end();
return next();
});
```
res.end();
return next();
});
Not very secure, but this is a demo. What we did there was "mount" a tree in
the ldapjs server, and add a handler for the _bind_ method. If you've ever used
@ -179,9 +165,7 @@ handlers in, as we'll see later.
On to the meat of the method. What's up with this?
```js
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
```
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
The first part `req.dn.toString() !== 'cn=root'`: you're probably thinking
"WTF?!? Does ldapjs allow something other than cn=root into this handler?" Sort
@ -205,22 +189,18 @@ add another handler in later you won't get bit by it not being invoked.
Blah blah, let's try running the ldap client again, first with a bad password:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
ldap_bind: Invalid credentials (49)
matched DN: cn=root
additional info: Invalid Credentials
```
ldap_bind: Invalid credentials (49)
matched DN: cn=root
additional info: Invalid Credentials
And again with the correct one:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
No such object (32)
Additional information: No tree found for: o=myhost
```
No such object (32)
Additional information: No tree found for: o=myhost
Don't worry about all the flags we're passing into OpenLDAP, that's just to make
their CLI less annonyingly noisy. This time, we got another `No such object`
@ -234,14 +214,12 @@ what if the remote end doesn't authenticate at all? Right, nothing says they
*have to* bind, that's just what the common clients do. Let's add a quick
authorization handler that we'll use in all our subsequent routes:
```js
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
return next();
}
```
return next();
}
Should be pretty self-explanatory, but as a reminder, LDAP is connection
oriented, so we check that the connection remote user was indeed our `cn=root`
@ -252,9 +230,7 @@ oriented, so we check that the connection remote user was indeed our `cn=root`
We said we wanted to allow LDAP operations over /etc/passwd, so let's detour
for a moment to explain an /etc/passwd record.
```shell
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
```
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
The sample record above maps to:
@ -269,86 +245,77 @@ The sample record above maps to:
|/bin/sh |Shell |
Let's write some handlers to parse that and transform it into an LDAP search
record (note, you'll need to add `const fs = require('fs');` at the top of the
record (note, you'll need to add `var fs = require('fs');` at the top of the
source file).
First, make a handler that just loads the "user database" in a "pre" handler:
```js
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
if (err)
return next(new ldap.OperationsError(err.message));
function loadPasswdFile(req, res, next) {
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
if (err)
return next(new ldap.OperationsError(err.message));
req.users = {};
req.users = {};
const lines = data.split('\n');
for (const line of lines) {
if (!line || /^#/.test(line))
continue;
var lines = data.split('\n');
for (var i = 0; i < lines.length; i++) {
if (!lines[i] || /^#/.test(lines[i]))
continue;
const record = line.split(':');
if (!record || !record.length)
continue;
var record = lines[i].split(':');
if (!record || !record.length)
continue;
req.users[record[0]] = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
req.users[record[0]] = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
}
};
}
return next();
});
}
```
return next();
});
}
Ok, all that did is tack the /etc/passwd records onto req.users so that any
subsequent handler doesn't have to reload the file. Next, let's write a search
handler to process that:
```js
const pre = [authorize, loadPasswdFile];
var pre = [authorize, loadPasswdFile];
server.search('o=myhost', pre, (req, res, next) => {
const keys = Object.keys(req.users);
for (const k of keys) {
if (req.filter.matches(req.users[k].attributes))
res.send(req.users[k]);
}
server.search('o=myhost', pre, function(req, res, next) {
Object.keys(req.users).forEach(function(k) {
if (req.filter.matches(req.users[k].attributes))
res.send(req.users[k]);
});
res.end();
return next();
});
```
res.end();
return next();
});
And try running:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
dn: cn=root, ou=users, o=myhost
cn: root
uid: 0
gid: 0
description: System Administrator
homedirectory: /var/root
shell: /bin/sh
objectclass: unixUser
```
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
dn: cn=root, ou=users, o=myhost
cn: root
uid: 0
gid: 0
description: System Administrator
homedirectory: /var/root
shell: /bin/sh
objectclass: unixUser
Sweet! Try this out too:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
...
```
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
...
You should have seen an entry for every record in /etc/passwd with the second.
What all did we do here? A lot. Let's break this down...
@ -357,9 +324,7 @@ What all did we do here? A lot. Let's break this down...
Let's start with looking at what you even asked for:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
```
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
We can throw away `ldapsearch -H -x -D -w -LLL`, as those just specify the URL
to connect to, the bind credentials and the `-LLL` just quiets down OpenLDAP.
@ -392,20 +357,18 @@ and made the cheesiest transform ever, which is making up a "search entry." A
search entry _must_ have a DN so the client knows what record it is, and a set
of attributes. So that's why we did this:
```js
const entry = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
```
var entry = {
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
attributes: {
cn: record[0],
uid: record[2],
gid: record[3],
description: record[4],
homedirectory: record[5],
shell: record[6] || '',
objectclass: 'unixUser'
}
};
Next, we let ldapjs do all the hard work of figuring out LDAP search filters
for us by calling `req.filter.matches`. If it matched, we return the whole
@ -420,133 +383,120 @@ shell set to `/bin/false` and whose name starts with `p` (I'm doing this
on Ubuntu). Then, let's say we only care about their login name and primary
group id. We'd do this:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" "(&(shell=/bin/false)(cn=p*))" cn gid
dn: cn=proxy, ou=users, o=myhost
cn: proxy
gid: 13
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" "(&(shell=/bin/false)(cn=p*))" cn gid
dn: cn=proxy, ou=users, o=myhost
cn: proxy
gid: 13
dn: cn=pulse, ou=users, o=myhost
cn: pulse
gid: 114
```
dn: cn=pulse, ou=users, o=myhost
cn: pulse
gid: 114
## Add
This is going to be a little bit ghetto, since what we're going to do is just
use node's child process module to spawn calls to `adduser`. Go ahead and add
the following code in as another handler (you'll need a
`const { spawn } = require('child_process');` at the top of your file):
`var spawn = require('child_process').spawn;` at the top of your file):
```js
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn)
return next(new ldap.ConstraintViolationError('cn required'));
server.add('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
if (req.users[req.dn.rdns[0].cn])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
const entry = req.toObject().attributes;
var entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
const opts = ['-m'];
if (entry.description) {
opts.push('-c');
opts.push(entry.description[0]);
}
if (entry.homedirectory) {
opts.push('-d');
opts.push(entry.homedirectory[0]);
}
if (entry.gid) {
opts.push('-g');
opts.push(entry.gid[0]);
}
if (entry.shell) {
opts.push('-s');
opts.push(entry.shell[0]);
}
if (entry.uid) {
opts.push('-u');
opts.push(entry.uid[0]);
}
opts.push(entry.cn[0]);
const useradd = spawn('useradd', opts);
var opts = ['-m'];
if (entry.description) {
opts.push('-c');
opts.push(entry.description[0]);
}
if (entry.homedirectory) {
opts.push('-d');
opts.push(entry.homedirectory[0]);
}
if (entry.gid) {
opts.push('-g');
opts.push(entry.gid[0]);
}
if (entry.shell) {
opts.push('-s');
opts.push(entry.shell[0]);
}
if (entry.uid) {
opts.push('-u');
opts.push(entry.uid[0]);
}
opts.push(entry.cn[0]);
var useradd = spawn('useradd', opts);
const messages = [];
var messages = [];
useradd.stdout.on('data', (data) => {
messages.push(data.toString());
});
useradd.stderr.on('data', (data) => {
messages.push(data.toString());
});
useradd.stdout.on('data', function(data) {
messages.push(data.toString());
});
useradd.stderr.on('data', function(data) {
messages.push(data.toString());
});
useradd.on('exit', (code) => {
if (code !== 0) {
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
useradd.on('exit', function(code) {
if (code !== 0) {
var msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
});
});
```
res.end();
return next();
});
});
Then, you'll need to be root to have this running, so start your server with
`sudo` (or be root, whatever). Now, go ahead and create a file called
`user.ldif` with the following contents:
```shell
dn: cn=ldapjs, ou=users, o=myhost
objectClass: unixUser
cn: ldapjs
shell: /bin/bash
description: Created via ldapadd
```
dn: cn=ldapjs, ou=users, o=myhost
objectClass: unixUser
cn: ldapjs
shell: /bin/bash
description: Created via ldapadd
Now go ahead and invoke with:
```shell
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
adding new entry "cn=ldapjs, ou=users, o=myhost"
```
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
adding new entry "cn=ldapjs, ou=users, o=myhost"
Let's confirm he got added with an ldapsearch:
```shell
$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs
dn: cn=ldapjs, ou=users, o=myhost
cn: ldapjs
uid: 1001
gid: 1001
description: Created via ldapadd
homedirectory: /home/ldapjs
shell: /bin/bash
objectclass: unixUser
```
$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs
dn: cn=ldapjs, ou=users, o=myhost
cn: ldapjs
uid: 1001
gid: 1001
description: Created via ldapadd
homedirectory: /home/ldapjs
shell: /bin/bash
objectclass: unixUser
As before, here's a breakdown of the code:
```js
server.add('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn)
return next(new ldap.ConstraintViolationError('cn required'));
server.add('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn)
return next(new ldap.ConstraintViolationError('cn required'));
if (req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
if (req.users[req.dn.rdns[0].cn])
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
const entry = req.toObject().attributes;
var entry = req.toObject().attributes;
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
});
```
if (entry.objectclass.indexOf('unixUser') === -1)
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
A few new things:
@ -581,43 +531,42 @@ Unlike HTTP, "partial" document updates are fully specified as part of the
RFC, so appending, removing, or replacing a single attribute is pretty natural.
Go ahead and add the following code into your source file:
```js
server.modify('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
if (!req.changes.length)
return next(new ldap.ProtocolError('changes required'));
if (!req.changes.length)
return next(new ldap.ProtocolError('changes required'));
const user = req.users[req.dn.rdns[0].attrs.cn.value].attributes;
let mod;
var user = req.users[req.dn.rdns[0].cn].attributes;
var mod;
for (const i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
case 'replace':
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
return next(new ldap.UnwillingToPerformError('only password updates ' +
'allowed'));
break;
case 'add':
case 'delete':
return next(new ldap.UnwillingToPerformError('only replace allowed'));
}
}
for (var i = 0; i < req.changes.length; i++) {
mod = req.changes[i].modification;
switch (req.changes[i].operation) {
case 'replace':
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
return next(new ldap.UnwillingToPerformError('only password updates ' +
'allowed'));
break;
case 'add':
case 'delete':
return next(new ldap.UnwillingToPerformError('only replace allowed'));
}
}
const passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
var passwd = spawn('chpasswd', ['-c', 'MD5']);
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
passwd.on('exit', (code) => {
if (code !== 0)
return next(new ldap.OperationsError(code));
passwd.on('exit', function(code) {
if (code !== 0)
return next(new ldap.OperationsError(code));
res.end();
return next();
});
});
res.end();
return next();
});
});
```
Basically, we made sure the remote client was targeting an entry that exists,
ensuring that they were asking to "replace" the `userPassword` attribute (which
@ -626,19 +575,15 @@ is the 'standard' LDAP attribute for passwords; if you think it's easier to use
command (which lets you change a user's password over stdin). Next, go ahead
and create a `passwd.ldif` file:
```shell
dn: cn=ldapjs, ou=users, o=myhost
changetype: modify
replace: userPassword
userPassword: secret
-
```
dn: cn=ldapjs, ou=users, o=myhost
changetype: modify
replace: userPassword
userPassword: secret
-
And then run the OpenLDAP CLI:
```shell
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
```
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
You should now be able to login to your box as the ldapjs user. Let's get
the last "mainline" piece of work out of the way, and delete the user.
@ -648,40 +593,37 @@ the last "mainline" piece of work out of the way, and delete the user.
Delete is pretty straightforward. The client gives you a dn to delete, and you
delete it :). Add the following code into your server:
```js
server.del('ou=users, o=myhost', pre, (req, res, next) => {
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
server.del('ou=users, o=myhost', pre, function(req, res, next) {
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
return next(new ldap.NoSuchObjectError(req.dn.toString()));
const userdel = spawn('userdel', ['-f', req.dn.rdns[0].attrs.cn.value]);
var userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
const messages = [];
userdel.stdout.on('data', (data) => {
messages.push(data.toString());
});
userdel.stderr.on('data', (data) => {
messages.push(data.toString());
});
var messages = [];
userdel.stdout.on('data', function(data) {
messages.push(data.toString());
});
userdel.stderr.on('data', function(data) {
messages.push(data.toString());
});
userdel.on('exit', (code) => {
if (code !== 0) {
let msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
userdel.on('exit', function(code) {
if (code !== 0) {
var msg = '' + code;
if (messages.length)
msg += ': ' + messages.join();
return next(new ldap.OperationsError(msg));
}
res.end();
return next();
});
});
```
res.end();
return next();
});
});
And then run the following command:
```shell
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
```
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
# Where to go from here

View File

@ -1,5 +1,6 @@
---
title: ldapjs
markdown2extras: tables
---
<div id="indextagline">
@ -8,45 +9,37 @@ Reimagining <a href="http://tools.ietf.org/html/rfc4510" id="indextaglink">LDAP<
# Overview
<div class="intro">
ldapjs is a pure JavaScript, from-scratch framework for implementing
[LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in
[Node.js](http://nodejs.org). It is intended for developers used to interacting
with HTTP services in node and [restify](http://restify.com).
</div>
var ldap = require('ldapjs');
```js
const ldap = require('ldapjs');
var server = ldap.createServer();
const server = ldap.createServer();
server.search('o=example', function(req, res, next) {
var obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
};
server.search('o=example', (req, res, next) => {
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
};
if (req.filter.matches(obj.attributes))
res.send(obj);
if (req.filter.matches(obj.attributes))
res.send(obj);
res.end();
});
res.end();
});
server.listen(1389, () => {
console.log('LDAP server listening at %s', server.url);
});
```
server.listen(1389, function() {
console.log('LDAP server listening at %s', server.url);
});
Try hitting that with:
```shell
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
```
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
# Features
@ -59,9 +52,7 @@ that you can build LDAP over anything you want, not just traditional databases.
# Getting started
```shell
$ npm install ldapjs
```
$ npm install ldapjs
If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
API documentation is:
@ -79,7 +70,9 @@ API documentation is:
# More information
- License:[MIT](http://opensource.org/licenses/mit-license.php)
- Code: [ldapjs/node-ldapjs](https://github.com/ldapjs/node-ldapjs)
- Code: [mcavage/node-ldapjs](https://github.com/mcavage/node-ldapjs)
- node.js version: >=0.8
- Twitter: [@pfmooney](http://twitter.com/pfmooney)
# What's not in the box?
@ -93,3 +86,5 @@ Specifically:
* Extensible matching
There are a few others, but those are the "big" ones.

View File

@ -1,23 +1,18 @@
---
title: Server API | ldapjs
markdown2extras: wiki-tables
---
# ldapjs Server API
<div class="intro">
This document covers the ldapjs server API and assumes that you are familiar
with LDAP. If you're not, read the [guide](guide.html) first.
</div>
# Create a server
The code to create a new server looks like:
```js
const server = ldap.createServer();
```
var server = ldap.createServer();
The full list of options is:
@ -70,25 +65,17 @@ available.
Example:
```js
server.listen(389, '127.0.0.1', function() {
console.log('LDAP server listening at: ' + server.url);
});
```
server.listen(389, '127.0.0.1', function() {
console.log('LDAP server listening at: ' + server.url);
});
### Port and Host
`listen(port, [host], [callback])`
Begin accepting connections on the specified port and host. If the host is
omitted, the server will accept connections directed to the IPv4 address
`127.0.0.1`. To listen on any other address, supply said address as the `host`
parameter. For example, to listen on all available IPv6 addresses supply
`::` as the `host` (note, this _may_ also result in listening on all
available IPv4 addresses, depending on operating system behavior).
We highly recommend being as explicit as possible with the `host` parameter.
Listening on all available addresses (through `::` or `0.0.0.0`) can lead
to potential security issues.
omitted, the server will accept connections directed to any IPv4 address
(INADDR\_ANY).
This function is asynchronous. The last parameter callback will be called when
the server has been bound.
@ -125,16 +112,14 @@ paradigm of programming. Essentially every method is of the form
handlers together by calling `next()` and ordering your functions in the
definition of the route. For example:
```js
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
function authorize(req, res, next) {
if (!req.connection.ldap.bindDN.equals('cn=root'))
return next(new ldap.InsufficientAccessRightsError());
return next();
}
return next();
}
server.search('o=example', authorize, function(req, res, next) { ... });
```
server.search('o=example', authorize, function(req, res, next) { ... });
Note that ldapjs is also slightly different, since it's often going to be backed
to a DB-like entity, in that it also has an API where you can pass in a
@ -146,25 +131,23 @@ complete implementation of the LDAP protocol over
[Riak](https://github.com/basho/riak). Getting an LDAP server up with riak
looks like:
```js
const ldap = require('ldapjs');
const ldapRiak = require('ldapjs-riak');
var ldap = require('ldapjs');
var ldapRiak = require('ldapjs-riak');
const server = ldap.createServer();
const backend = ldapRiak.createBackend({
"host": "localhost",
"port": 8098,
"bucket": "example",
"indexes": ["l", "cn"],
"uniqueIndexes": ["uid"],
"numConnections": 5
});
var server = ldap.createServer();
var backend = ldapRiak.createBackend({
"host": "localhost",
"port": 8098,
"bucket": "example",
"indexes": ["l", "cn"],
"uniqueIndexes": ["uid"],
"numConnections": 5
});
server.add("o=example",
backend,
backend.add());
...
```
server.add("o=example",
backend,
backend.add());
...
The first parameter to an ldapjs route is always the point in the
tree to mount the handler chain at. The second argument is _optionally_ a
@ -177,12 +160,10 @@ operation requires specific methods/fields on the request/response
objects. However, there is a `.use()` method availabe, similar to
that on express/connect, allowing you to chain up "middleware":
```js
server.use(function(req, res, next) {
console.log('hello world');
return next();
});
```
server.use(function(req, res, next) {
console.log('hello world');
return next();
});
## Common Request Elements
@ -204,7 +185,7 @@ authenticated as on this connection. If the client didn't bind, then a DN object
will be there defaulted to `cn=anonymous`.
Additionally, request will have a `logId` parameter you can use to uniquely
identify the request/connection pair in logs (includes the LDAP messageId).
identify the request/connection pair in logs (includes the LDAP messageID).
## Common Response Elements
@ -226,13 +207,11 @@ the paradigm is something defined like CONSTRAINT\_VIOLATION in the RFC would be
`ConstraintViolationError` in ldapjs. Upon calling `next(new LDAPError())`,
ldapjs will _stop_ calling your handler chain. For example:
```js
server.search('o=example',
(req, res, next) => { return next(); },
(req, res, next) => { return next(new ldap.OperationsError()); },
(req, res, next) => { res.end(); }
);
```
server.search('o=example',
function(req, res, next) { return next(); },
function(req, res, next) { return next(new ldap.OperationsError()); },
function(req, res, next) { res.end(); }
);
In the code snipped above, the third handler would never get invoked.
@ -240,13 +219,11 @@ In the code snipped above, the third handler would never get invoked.
Adds a mount in the tree to perform LDAP binds with. Example:
```js
server.bind('ou=people, o=example', (req, res, next) => {
console.log('bind DN: ' + req.dn.toString());
console.log('bind PW: ' + req.credentials);
res.end();
});
```
server.bind('ou=people, o=example', function(req, res, next) {
console.log('bind DN: ' + req.dn.toString());
console.log('bind PW: ' + req.credentials);
res.end();
});
## BindRequest
@ -279,13 +256,11 @@ No extra methods above an `LDAPResult` API call.
Adds a mount in the tree to perform LDAP adds with.
```js
server.add('ou=people, o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('Entry attributes: ' + req.toObject().attributes);
res.end();
});
```
server.add('ou=people, o=example', function(req, res, next) {
console.log('DN: ' + req.dn.toString());
console.log('Entry attributes: ' + req.toObject().attributes);
res.end();
});
## AddRequest
@ -309,16 +284,14 @@ a standard JavaScript object.
This operation will return a plain JavaScript object from the request that looks
like:
```js
{
dn: 'cn=foo, o=example', // string, not DN object
attributes: {
cn: ['foo'],
sn: ['bar'],
objectclass: ['person', 'top']
}
}
```
{
dn: 'cn=foo, o=example', // string, not DN object
attributes: {
cn: ['foo'],
sn: ['bar'],
objectclass: ['person', 'top']
}
}
## AddResponse
@ -328,14 +301,12 @@ No extra methods above an `LDAPResult` API call.
Adds a handler for the LDAP search operation.
```js
server.search('o=example', (req, res, next) => {
console.log('base object: ' + req.dn.toString());
console.log('scope: ' + req.scope);
console.log('filter: ' + req.filter.toString());
res.end();
});
```
server.search('o=example', function(req, res, next) {
console.log('base object: ' + req.dn.toString());
console.log('scope: ' + req.scope);
console.log('filter: ' + req.filter.toString());
res.end();
});
## SearchRequest
@ -393,38 +364,34 @@ explicitly pass in a `SearchEntry` object, and can instead just send a plain
JavaScript object that matches the format used from `AddRequest.toObject()`.
```js
server.search('o=example', (req, res, next) => {
const obj = {
dn: 'o=example',
attributes: {
objectclass: ['top', 'organization'],
o: ['example']
}
};
server.search('o=example', function(req, res, next) {
var obj = {
dn: 'o=example',
attributes: {
objectclass: ['top', 'organization'],
o: ['example']
}
};
if (req.filter.matches(obj))
res.send(obj)
if (req.filter.matches(obj))
res.send(obj)
res.end();
});
```
res.end();
});
# modify
Allows you to handle an LDAP modify operation.
```js
server.modify('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('changes:');
for (const c of req.changes) {
console.log(' operation: ' + c.operation);
console.log(' modification: ' + c.modification.toString());
}
res.end();
});
```
server.modify('o=example', function(req, res, next) {
console.log('DN: ' + req.dn.toString());
console.log('changes:');
req.changes.forEach(function(c) {
console.log(' operation: ' + c.operation);
console.log(' modification: ' + c.modification.toString());
});
res.end();
});
## ModifyRequest
@ -461,12 +428,10 @@ No extra methods above an `LDAPResult` API call.
Allows you to handle an LDAP delete operation.
```js
server.del('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
res.end();
});
```
server.del('o=example', function(req, res, next) {
console.log('DN: ' + req.dn.toString());
res.end();
});
## DeleteRequest
@ -483,14 +448,12 @@ No extra methods above an `LDAPResult` API call.
Allows you to handle an LDAP compare operation.
```js
server.compare('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('attribute name: ' + req.attribute);
console.log('attribute value: ' + req.value);
res.end(req.value === 'foo');
});
```
server.compare('o=example', function(req, res, next) {
console.log('DN: ' + req.dn.toString());
console.log('attribute name: ' + req.attribute);
console.log('attribute value: ' + req.value);
res.end(req.value === 'foo');
});
## CompareRequest
@ -517,17 +480,15 @@ that, there are no extra methods above an `LDAPResult` API call.
Allows you to handle an LDAP modifyDN operation.
```js
server.modifyDN('o=example', (req, res, next) => {
console.log('DN: ' + req.dn.toString());
console.log('new RDN: ' + req.newRdn.toString());
console.log('deleteOldRDN: ' + req.deleteOldRdn);
console.log('new superior: ' +
(req.newSuperior ? req.newSuperior.toString() : ''));
server.modifyDN('o=example', function(req, res, next) {
console.log('DN: ' + req.dn.toString());
console.log('new RDN: ' + req.newRdn.toString());
console.log('deleteOldRDN: ' + req.deleteOldRdn);
console.log('new superior: ' +
(req.newSuperior ? req.newSuperior.toString() : ''));
res.end();
});
```
res.end();
});
## ModifyDNRequest
@ -561,16 +522,14 @@ OID, but ldapjs makes no such restrictions; it just needs to be a string.
Unlike the other operations, extended operations don't map to any location in
the tree, so routing here will be exact match, as opposed to subtree.
```js
// LDAP whoami
server.exop('1.3.6.1.4.1.4203.1.11.3', (req, res, next) => {
console.log('name: ' + req.name);
console.log('value: ' + req.value);
res.value = 'u:xxyyz@EXAMPLE.NET';
res.end();
return next();
});
```
// LDAP whoami
server.exop('1.3.6.1.4.1.4203.1.11.3', function(req, res, next) {
console.log('name: ' + req.name);
console.log('value: ' + req.value);
res.value = 'u:xxyyz@EXAMPLE.NET';
res.end();
return next();
});
## ExtendedRequest
@ -601,11 +560,9 @@ and cleans up any internals (in ldapjs core). You can override this handler
if you need to clean up any items in your backend, or perform any other cleanup
tasks you need to.
```js
server.unbind((req, res, next) => {
res.end();
});
```
server.unbind(function(req, res, next) {
res.end();
});
Note that the LDAP unbind operation actually doesn't send any response (by
definition in the RFC), so the UnbindResponse is really just a stub that

BIN
dt.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

View File

@ -1,65 +0,0 @@
const cluster = require('cluster')
const ldap = require('ldapjs')
const net = require('net')
const os = require('os')
const threads = []
threads.getNext = function () {
return (Math.floor(Math.random() * this.length))
}
const serverOptions = {
port: 1389
}
if (cluster.isMaster) {
const server = net.createServer(serverOptions, (socket) => {
socket.pause()
console.log('ldapjs client requesting connection')
const routeTo = threads.getNext()
threads[routeTo].send({ type: 'connection' }, socket)
})
for (let i = 0; i < os.cpus().length; i++) {
const thread = cluster.fork({
id: i
})
thread.id = i
thread.on('message', function () {
})
threads.push(thread)
}
server.listen(serverOptions.port, function () {
console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port)
})
} else {
const server = ldap.createServer(serverOptions)
const threadId = process.env.id
process.on('message', (msg, socket) => {
switch (msg.type) {
case 'connection':
server.newConnection(socket)
socket.resume()
console.log('ldapjs client connection accepted on ' + threadId.toString())
}
})
server.search('dc=example', function (req, res) {
console.log('ldapjs search initiated on ' + threadId.toString())
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
}
if (req.filter.matches(obj.attributes)) { res.send(obj) }
res.end()
})
}

View File

@ -1,65 +0,0 @@
const cluster = require('cluster')
const ldap = require('ldapjs')
const os = require('os')
const threads = []
threads.getNext = function () {
return (Math.floor(Math.random() * this.length))
}
const serverOptions = {
connectionRouter: (socket) => {
socket.pause()
console.log('ldapjs client requesting connection')
const routeTo = threads.getNext()
threads[routeTo].send({ type: 'connection' }, socket)
}
}
const server = ldap.createServer(serverOptions)
if (cluster.isMaster) {
for (let i = 0; i < os.cpus().length; i++) {
const thread = cluster.fork({
id: i
})
thread.id = i
thread.on('message', function () {
})
threads.push(thread)
}
server.listen(1389, function () {
console.log('ldapjs listening at ' + server.url)
})
} else {
const threadId = process.env.id
serverOptions.connectionRouter = () => {
console.log('should not be hit')
}
process.on('message', (msg, socket) => {
switch (msg.type) {
case 'connection':
server.newConnection(socket)
socket.resume()
console.log('ldapjs client connection accepted on ' + threadId.toString())
}
})
server.search('dc=example', function (req, res) {
console.log('ldapjs search initiated on ' + threadId.toString())
const obj = {
dn: req.dn.toString(),
attributes: {
objectclass: ['organization', 'top'],
o: 'example'
}
}
if (req.filter.matches(obj.attributes)) { res.send(obj) }
res.end()
})
}

View File

@ -130,7 +130,7 @@ server.search(SUFFIX, authorize, function (req, res, next) {
case 'base':
if (req.filter.matches(db[dn])) {
res.send({
dn,
dn: dn,
attributes: db[dn]
})
}

24
examples/snoopldap.d Executable file
View File

@ -0,0 +1,24 @@
#!/usr/sbin/dtrace -s
#pragma D option quiet
BEGIN
{
printf("%-8s %-8s %-16s %-15s %-15s %s\n",
"LATENCY", "OPTYPE", "REMOTE IP", "BIND DN", "REQ DN",
"STATUS");
}
ldapjs*:::server-*-start
{
starts[arg0] = timestamp;
}
ldapjs*:::server-*-done
/starts[arg0]/
{
printf("%6dms %-8s %-16s %-15s %-15s %d\n",
(timestamp - starts[arg0]) / 1000000, strtok(probename + 7, "-"),
copyinstr(arg1), copyinstr(arg2), copyinstr(arg3), arg4);
starts[arg0] = 0;
}

54
lib/assert.js Normal file
View File

@ -0,0 +1,54 @@
// Copyright 2015 Joyent, Inc.
const assert = require('assert')
const util = require('util')
const isDN = require('./dn').DN.isDN
const isAttribute = require('./attribute').isAttribute
/// --- Helpers
// Copied from mcavage/node-assert-plus
function _assert (arg, type, name) {
name = name || type
throw new assert.AssertionError({
message: util.format('%s (%s) required', name, type),
actual: typeof (arg),
expected: type,
operator: '===',
stackStartFunction: _assert.caller
})
}
/// --- API
function stringDN (input, name) {
if (isDN(input) || typeof (input) === 'string') { return }
_assert(input, 'DN or string', name)
}
function optionalStringDN (input, name) {
if (input === undefined || isDN(input) || typeof (input) === 'string') { return }
_assert(input, 'DN or string', name)
}
function optionalDN (input, name) {
if (input !== undefined && !isDN(input)) { _assert(input, 'DN', name) }
}
function optionalArrayOfAttribute (input, name) {
if (input === undefined) { return }
if (!Array.isArray(input) ||
input.some(function (v) { return !isAttribute(v) })) {
_assert(input, 'array of Attribute', name)
}
}
/// --- Exports
module.exports = {
stringDN: stringDN,
optionalStringDN: optionalStringDN,
optionalDN: optionalDN,
optionalArrayOfAttribute: optionalArrayOfAttribute
}

160
lib/attribute.js Normal file
View File

@ -0,0 +1,160 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const asn1 = require('asn1')
const Protocol = require('./protocol')
/// --- API
function Attribute (options) {
if (options) {
if (typeof (options) !== 'object') { throw new TypeError('options must be an object') }
if (options.type && typeof (options.type) !== 'string') { throw new TypeError('options.type must be a string') }
} else {
options = {}
}
this.type = options.type || ''
this._vals = []
if (options.vals !== undefined && options.vals !== null) { this.vals = options.vals }
}
module.exports = Attribute
Object.defineProperties(Attribute.prototype, {
buffers: {
get: function getBuffers () {
return this._vals
},
configurable: false
},
json: {
get: function getJson () {
return {
type: this.type,
vals: this.vals
}
},
configurable: false
},
vals: {
get: function getVals () {
const eType = _bufferEncoding(this.type)
return this._vals.map(function (v) {
return v.toString(eType)
})
},
set: function setVals (vals) {
const self = this
this._vals = []
if (Array.isArray(vals)) {
vals.forEach(function (v) {
self.addValue(v)
})
} else {
self.addValue(vals)
}
},
configurable: false
}
})
Attribute.prototype.addValue = function addValue (val) {
if (Buffer.isBuffer(val)) {
this._vals.push(val)
} else {
this._vals.push(Buffer.from(val + '', _bufferEncoding(this.type)))
}
}
/* BEGIN JSSTYLED */
Attribute.compare = function compare (a, b) {
if (!(Attribute.isAttribute(a)) || !(Attribute.isAttribute(b))) {
throw new TypeError('can only compare Attributes')
}
if (a.type < b.type) return -1
if (a.type > b.type) return 1
if (a.vals.length < b.vals.length) return -1
if (a.vals.length > b.vals.length) return 1
for (let i = 0; i < a.vals.length; i++) {
if (a.vals[i] < b.vals[i]) return -1
if (a.vals[i] > b.vals[i]) return 1
}
return 0
}
/* END JSSTYLED */
Attribute.prototype.parse = function parse (ber) {
assert.ok(ber)
ber.readSequence()
this.type = ber.readString()
if (ber.peek() === Protocol.LBER_SET) {
if (ber.readSequence(Protocol.LBER_SET)) {
const end = ber.offset + ber.length
while (ber.offset < end) { this._vals.push(ber.readString(asn1.Ber.OctetString, true)) }
}
}
return true
}
Attribute.prototype.toBer = function toBer (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeString(this.type)
ber.startSequence(Protocol.LBER_SET)
if (this._vals.length) {
this._vals.forEach(function (b) {
ber.writeByte(asn1.Ber.OctetString)
ber.writeLength(b.length)
for (let i = 0; i < b.length; i++) { ber.writeByte(b[i]) }
})
} else {
ber.writeStringArray([])
}
ber.endSequence()
ber.endSequence()
return ber
}
Attribute.prototype.toString = function () {
return JSON.stringify(this.json)
}
Attribute.toBer = function (attr, ber) {
return Attribute.prototype.toBer.call(attr, ber)
}
Attribute.isAttribute = function isAttribute (attr) {
if (!attr || typeof (attr) !== 'object') {
return false
}
if (attr instanceof Attribute) {
return true
}
if ((typeof (attr.toBer) === 'function') &&
(typeof (attr.type) === 'string') &&
(Array.isArray(attr.vals)) &&
(attr.vals.filter(function (item) {
return (typeof (item) === 'string' ||
Buffer.isBuffer(item))
}).length === attr.vals.length)) {
return true
}
return false
}
function _bufferEncoding (type) {
/* JSSTYLED */
return /;binary$/.test(type) ? 'base64' : 'utf8'
}

213
lib/change.js Normal file
View File

@ -0,0 +1,213 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const Attribute = require('./attribute')
// var Protocol = require('./protocol')
/// --- API
function Change (options) {
if (options) {
assert.object(options)
assert.optionalString(options.operation)
} else {
options = {}
}
this._modification = false
this.operation = options.operation || options.type || 'add'
this.modification = options.modification || {}
}
Object.defineProperties(Change.prototype, {
operation: {
get: function getOperation () {
switch (this._operation) {
case 0x00: return 'add'
case 0x01: return 'delete'
case 0x02: return 'replace'
default:
throw new Error('0x' + this._operation.toString(16) + ' is invalid')
}
},
set: function setOperation (val) {
assert.string(val)
switch (val.toLowerCase()) {
case 'add':
this._operation = 0x00
break
case 'delete':
this._operation = 0x01
break
case 'replace':
this._operation = 0x02
break
default:
throw new Error('Invalid operation type: 0x' + val.toString(16))
}
},
configurable: false
},
modification: {
get: function getModification () {
return this._modification
},
set: function setModification (val) {
if (Attribute.isAttribute(val)) {
this._modification = val
return
}
// Does it have an attribute-like structure
if (Object.keys(val).length === 2 &&
typeof (val.type) === 'string' &&
Array.isArray(val.vals)) {
this._modification = new Attribute({
type: val.type,
vals: val.vals
})
return
}
const keys = Object.keys(val)
if (keys.length > 1) {
throw new Error('Only one attribute per Change allowed')
} else if (keys.length === 0) {
return
}
const k = keys[0]
const _attr = new Attribute({ type: k })
if (Array.isArray(val[k])) {
val[k].forEach(function (v) {
_attr.addValue(v.toString())
})
} else if (Buffer.isBuffer(val[k])) {
_attr.addValue(val[k])
} else if (val[k] !== undefined && val[k] !== null) {
_attr.addValue(val[k].toString())
}
this._modification = _attr
},
configurable: false
},
json: {
get: function getJSON () {
return {
operation: this.operation,
modification: this._modification ? this._modification.json : {}
}
},
configurable: false
}
})
Change.isChange = function isChange (change) {
if (!change || typeof (change) !== 'object') {
return false
}
if ((change instanceof Change) ||
((typeof (change.toBer) === 'function') &&
(change.modification !== undefined) &&
(change.operation !== undefined))) {
return true
}
return false
}
Change.compare = function (a, b) {
if (!Change.isChange(a) || !Change.isChange(b)) { throw new TypeError('can only compare Changes') }
if (a.operation < b.operation) { return -1 }
if (a.operation > b.operation) { return 1 }
return Attribute.compare(a.modification, b.modification)
}
/**
* Apply a Change to properties of an object.
*
* @param {Object} change the change to apply.
* @param {Object} obj the object to apply it to.
* @param {Boolean} scalar convert single-item arrays to scalars. Default: false
*/
Change.apply = function apply (change, obj, scalar) {
assert.string(change.operation)
assert.string(change.modification.type)
assert.ok(Array.isArray(change.modification.vals))
assert.object(obj)
const type = change.modification.type
const vals = change.modification.vals
let data = obj[type]
if (data !== undefined) {
if (!Array.isArray(data)) {
data = [data]
}
} else {
data = []
}
switch (change.operation) {
case 'replace':
if (vals.length === 0) {
// replace empty is a delete
delete obj[type]
return obj
} else {
data = vals
}
break
case 'add': {
// add only new unique entries
const newValues = vals.filter(function (entry) {
return (data.indexOf(entry) === -1)
})
data = data.concat(newValues)
break
}
case 'delete':
data = data.filter(function (entry) {
return (vals.indexOf(entry) === -1)
})
if (data.length === 0) {
// Erase the attribute if empty
delete obj[type]
return obj
}
break
default:
break
}
if (scalar && data.length === 1) {
// store single-value outputs as scalars, if requested
obj[type] = data[0]
} else {
obj[type] = data
}
return obj
}
Change.prototype.parse = function (ber) {
assert.ok(ber)
ber.readSequence()
this._operation = ber.readEnumeration()
this._modification = new Attribute()
this._modification.parse(ber)
return true
}
Change.prototype.toBer = function (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeEnumeration(this._operation)
ber = this._modification.toBer(ber)
ber.endSequence()
return ber
}
/// --- Exports
module.exports = Change

View File

@ -15,37 +15,37 @@ const vasync = require('vasync')
const assert = require('assert-plus')
const VError = require('verror').VError
const Attribute = require('@ldapjs/attribute')
const Change = require('@ldapjs/change')
const Attribute = require('../attribute')
const Change = require('../change')
const Control = require('../controls/index').Control
const { Control: LdapControl } = require('@ldapjs/controls')
const SearchPager = require('./search_pager')
const Protocol = require('@ldapjs/protocol')
const { DN } = require('@ldapjs/dn')
const Protocol = require('../protocol')
const dn = require('../dn')
const errors = require('../errors')
const filters = require('@ldapjs/filter')
const Parser = require('../messages/parser')
const filters = require('../filters')
const messages = require('../messages')
const url = require('../url')
const CorkedEmitter = require('../corked_emitter')
/// --- Globals
const messages = require('@ldapjs/messages')
const {
AbandonRequest,
AddRequest,
BindRequest,
CompareRequest,
DeleteRequest,
ExtensionRequest: ExtendedRequest,
ModifyRequest,
ModifyDnRequest: ModifyDNRequest,
SearchRequest,
UnbindRequest,
LdapResult: LDAPResult,
SearchResultEntry: SearchEntry,
SearchResultReference: SearchReference
} = messages
const AbandonRequest = messages.AbandonRequest
const AddRequest = messages.AddRequest
const BindRequest = messages.BindRequest
const CompareRequest = messages.CompareRequest
const DeleteRequest = messages.DeleteRequest
const ExtendedRequest = messages.ExtendedRequest
const ModifyRequest = messages.ModifyRequest
const ModifyDNRequest = messages.ModifyDNRequest
const SearchRequest = messages.SearchRequest
const UnbindRequest = messages.UnbindRequest
const UnbindResponse = messages.UnbindResponse
const LDAPResult = messages.LDAPResult
const SearchEntry = messages.SearchEntry
const SearchReference = messages.SearchReference
// var SearchResponse = messages.SearchResponse
const Parser = messages.Parser
const PresenceFilter = filters.PresenceFilter
@ -67,9 +67,9 @@ function nextClientId () {
function validateControls (controls) {
if (Array.isArray(controls)) {
controls.forEach(function (c) {
if (!(c instanceof Control) && !(c instanceof LdapControl)) { throw new TypeError('controls must be [Control]') }
if (!(c instanceof Control)) { throw new TypeError('controls must be [Control]') }
})
} else if (controls instanceof Control || controls instanceof LdapControl) {
} else if (controls instanceof Control) {
controls = [controls]
} else {
throw new TypeError('controls must be [Control]')
@ -78,11 +78,13 @@ function validateControls (controls) {
return controls
}
function ensureDN (input) {
if (DN.isDn(input)) {
return input
function ensureDN (input, strict) {
if (dn.DN.isDN(input)) {
return dn
} else if (strict) {
return dn.parse(input)
} else if (typeof (input) === 'string') {
return DN.fromString(input)
return input
} else {
throw new Error('invalid DN')
}
@ -134,6 +136,7 @@ function Client (options) {
failAfter: parseInt(rOpts.failAfter, 10) || Infinity
}
}
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
this.queue = requestQueueFactory({
size: parseInt((options.queueSize || 0), 10),
@ -174,13 +177,13 @@ module.exports = Client
* The callback will be invoked as soon as the data is flushed out to the
* network, as there is never a response from abandon.
*
* @param {Number} messageId the messageId to abandon.
* @param {Number} messageID the messageID to abandon.
* @param {Control} controls (optional) either a Control or [Control].
* @param {Function} callback of the form f(err).
* @throws {TypeError} on invalid input.
*/
Client.prototype.abandon = function abandon (messageId, controls, callback) {
assert.number(messageId, 'messageId')
Client.prototype.abandon = function abandon (messageID, controls, callback) {
assert.number(messageID, 'messageID')
if (typeof (controls) === 'function') {
callback = controls
controls = []
@ -190,8 +193,8 @@ Client.prototype.abandon = function abandon (messageId, controls, callback) {
assert.func(callback, 'callback')
const req = new AbandonRequest({
abandonId: messageId,
controls
abandonID: messageID,
controls: controls
})
return this._send(req, 'abandon', null, callback)
@ -235,8 +238,6 @@ Client.prototype.add = function add (name, entry, controls, callback) {
save[k].forEach(function (v) {
attr.addValue(v.toString())
})
} else if (Buffer.isBuffer(save[k])) {
attr.addValue(save[k])
} else {
attr.addValue(save[k].toString())
}
@ -245,9 +246,9 @@ Client.prototype.add = function add (name, entry, controls, callback) {
}
const req = new AddRequest({
entry: ensureDN(name),
entry: ensureDN(name, this.strictDN),
attributes: entry,
controls
controls: controls
})
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
@ -267,12 +268,7 @@ Client.prototype.bind = function bind (name,
controls,
callback,
_bypass) {
if (
typeof (name) !== 'string' &&
Object.prototype.toString.call(name) !== '[object LdapDn]'
) {
throw new TypeError('name (string) required')
}
if (typeof (name) !== 'string' && !(name instanceof dn.DN)) { throw new TypeError('name (string) required') }
assert.optionalString(credentials, 'credentials')
if (typeof (controls) === 'function') {
callback = controls
@ -286,7 +282,7 @@ Client.prototype.bind = function bind (name,
name: name || '',
authentication: 'Simple',
credentials: credentials || '',
controls
controls: controls
})
// Connection errors will be reported to the bind callback too (useful when the LDAP server is not available)
@ -327,10 +323,10 @@ Client.prototype.compare = function compare (name,
assert.func(callback, 'callback')
const req = new CompareRequest({
entry: ensureDN(name),
entry: ensureDN(name, this.strictDN),
attribute: attr,
value,
controls
value: value,
controls: controls
})
return this._send(req, CMP_EXPECT, null, function (err, res) {
@ -359,8 +355,8 @@ Client.prototype.del = function del (name, controls, callback) {
assert.func(callback, 'callback')
const req = new DeleteRequest({
entry: ensureDN(name),
controls
entry: ensureDN(name, this.strictDN),
controls: controls
})
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
@ -397,7 +393,7 @@ Client.prototype.exop = function exop (name, value, controls, callback) {
const req = new ExtendedRequest({
requestName: name,
requestValue: value,
controls
controls: controls
})
return this._send(req, [errors.LDAP_SUCCESS], null, function (err, res) {
@ -422,25 +418,25 @@ Client.prototype.modify = function modify (name, change, controls, callback) {
const changes = []
function changeFromObject (obj) {
if (!obj.operation && !obj.type) { throw new Error('change.operation required') }
if (typeof (obj.modification) !== 'object') { throw new Error('change.modification (object) required') }
function changeFromObject (change) {
if (!change.operation && !change.type) { throw new Error('change.operation required') }
if (typeof (change.modification) !== 'object') { throw new Error('change.modification (object) required') }
if (Object.keys(obj.modification).length === 2 &&
typeof (obj.modification.type) === 'string' &&
Array.isArray(obj.modification.vals)) {
if (Object.keys(change.modification).length === 2 &&
typeof (change.modification.type) === 'string' &&
Array.isArray(change.modification.vals)) {
// Use modification directly if it's already normalized:
changes.push(new Change({
operation: obj.operation || obj.type,
modification: obj.modification
operation: change.operation || change.type,
modification: change.modification
}))
} else {
// Normalize the modification object
Object.keys(obj.modification).forEach(function (k) {
Object.keys(change.modification).forEach(function (k) {
const mod = {}
mod[k] = obj.modification[k]
mod[k] = change.modification[k]
changes.push(new Change({
operation: obj.operation || obj.type,
operation: change.operation || change.type,
modification: mod
}))
})
@ -470,9 +466,9 @@ Client.prototype.modify = function modify (name, change, controls, callback) {
assert.func(callback, 'callback')
const req = new ModifyRequest({
object: ensureDN(name),
changes,
controls
object: ensureDN(name, this.strictDN),
changes: changes,
controls: controls
})
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
@ -506,16 +502,18 @@ Client.prototype.modifyDN = function modifyDN (name,
}
assert.func(callback)
const newDN = DN.fromString(newName)
const DN = ensureDN(name)
// TODO: is non-strict handling desired here?
const newDN = dn.parse(newName)
const req = new ModifyDNRequest({
entry: DN.fromString(name),
entry: DN,
deleteOldRdn: true,
controls
controls: controls
})
if (newDN.length !== 1) {
req.newRdn = DN.fromString(newDN.shift().toString())
req.newRdn = dn.parse(newDN.rdns.shift().toString())
req.newSuperior = newDN
} else {
req.newRdn = newDN
@ -571,7 +569,7 @@ Client.prototype.search = function search (base,
options.filter = filters.parseString(options.filter)
} else if (!options.filter) {
options.filter = new PresenceFilter({ attribute: 'objectclass' })
} else if (Object.prototype.toString.call(options.filter) !== '[object FilterString]') {
} else if (!filters.isFilter(options.filter)) {
throw new TypeError('options.filter (Filter) required')
}
if (typeof (controls) === 'function') {
@ -593,14 +591,14 @@ Client.prototype.search = function search (base,
}
const self = this
const baseDN = ensureDN(base)
const baseDN = ensureDN(base, this.strictDN)
function sendRequest (ctrls, emitter, cb) {
const req = new SearchRequest({
baseObject: baseDN,
scope: options.scope || 'base',
filter: options.filter,
derefAliases: options.derefAliases || Protocol.search.NEVER_DEREF_ALIASES,
derefAliases: options.derefAliases || Protocol.NEVER_DEREF_ALIASES,
sizeLimit: options.sizeLimit || 0,
timeLimit: options.timeLimit || 10,
typesOnly: options.typesOnly || false,
@ -629,12 +627,12 @@ Client.prototype.search = function search (base,
}
const pager = new SearchPager({
callback,
controls,
callback: callback,
controls: controls,
pageSize: size,
pagePause: pageOpts.pagePause,
sendRequest
pagePause: pageOpts.pagePause
})
pager.on('search', sendRequest)
pager.begin()
} else {
sendRequest(controls, new CorkedEmitter(), callback)
@ -681,9 +679,9 @@ Client.prototype.starttls = function starttls (options,
return callback(new Error('STARTTLS already in progress or active'))
}
function onSend (sendErr, emitter) {
if (sendErr) {
callback(sendErr)
function onSend (err, emitter) {
if (err) {
callback(err)
return
}
/*
@ -699,7 +697,7 @@ Client.prototype.starttls = function starttls (options,
self._starttls = null
callback(err)
})
emitter.on('end', function (_res) {
emitter.on('end', function (res) {
const sock = self._socket
/*
* Unplumb socket data during SSL negotiation.
@ -723,7 +721,7 @@ Client.prototype.starttls = function starttls (options,
self._tracker.parser.write(data)
})
secure.on('error', function (err) {
self.log.trace({ err }, 'error event: %s', new Error().stack)
self.log.trace({ err: err }, 'error event: %s', new Error().stack)
self.emit('error', err)
sock.destroy()
@ -744,7 +742,7 @@ Client.prototype.starttls = function starttls (options,
const req = new ExtendedRequest({
requestName: '1.3.6.1.4.1.1466.20037',
requestValue: null,
controls
controls: controls
})
return this._send(req,
@ -776,11 +774,9 @@ Client.prototype.destroy = function destroy (err) {
})
if (this.connected) {
this.unbind()
}
if (this._socket) {
} else if (this._socket) {
this._socket.destroy()
}
this.emit('destroy', err)
}
@ -854,10 +850,10 @@ Client.prototype.connect = function connect () {
}
// Initialize socket events and LDAP parser.
function initSocket (server) {
function initSocket (url) {
tracker = messageTrackerFactory({
id: server ? server.href : self.socketPath,
parser: new Parser({ log })
id: url ? url.href : self.socketPath,
parser: new Parser({ log: log })
})
// This won't be set on TLS. So. Very. Annoying.
@ -876,52 +872,15 @@ Client.prototype.connect = function connect () {
})
// The "router"
//
// This is invoked after the incoming BER has been parsed into a JavaScript
// object.
tracker.parser.on('message', function onMessage (message) {
message.connection = self._socket
const trackedObject = tracker.fetch(message.messageId)
if (!trackedObject) {
log.error({ message: message.pojo }, 'unmatched server message received')
return false
}
const { message: trackedMessage, callback } = trackedObject
const callback = tracker.fetch(message.messageID)
if (!callback) {
log.error({ message: message.pojo }, 'unsolicited message')
log.error({ message: message.json }, 'unsolicited message')
return false
}
// Some message types have narrower implementations and require extra
// parsing to be complete. In particular, ExtensionRequest messages will
// return responses that do not identify the request that generated them.
// Therefore, we have to match the response to the request and handle
// the extra processing accordingly.
switch (trackedMessage.type) {
case 'ExtensionRequest': {
const extensionType = ExtendedRequest.recognizedOIDs().lookupName(trackedMessage.requestName)
switch (extensionType) {
case 'PASSWORD_MODIFY': {
message = messages.PasswordModifyResponse.fromResponse(message)
break
}
case 'WHO_AM_I': {
message = messages.WhoAmIResponse.fromResponse(message)
break
}
default:
}
break
}
default:
}
return callback(message)
})
@ -979,7 +938,7 @@ Client.prototype.connect = function connect () {
f(basicClient, callback)
},
inputs: self.listeners('setup')
}, function (err, _res) {
}, function (err, res) {
if (err) {
self.emit('setupError', err)
}
@ -1002,7 +961,7 @@ Client.prototype.connect = function connect () {
socket.end()
})
socket.on('error', function onSocketError (err) {
log.trace({ err }, 'error event: %s', new Error().stack)
log.trace({ err: err }, 'error event: %s', new Error().stack)
self.emit('error', err)
socket.destroy()
@ -1042,7 +1001,7 @@ Client.prototype.connect = function connect () {
}
retry.failAfter(failAfter)
retry.on('ready', function (num, _delay) {
retry.on('ready', function (num, delay) {
if (self.destroyed) {
// Cease connection attempts if destroyed
return
@ -1070,9 +1029,9 @@ Client.prototype.connect = function connect () {
self.log.debug('failed to connect after %d attempts', failAfter)
// Communicate the last-encountered error
if (err instanceof ConnectionError) {
self.emitError('connectTimeout', err)
self.emit('connectTimeout', err)
} else if (err.code === 'ECONNREFUSED') {
self.emitError('connectRefused', err)
self.emit('connectRefused', err)
} else {
self.emit('error', err)
}
@ -1120,16 +1079,8 @@ Client.prototype._onClose = function _onClose (closeError) {
return cb(new ConnectionError(tracker.id + ' closed'))
} else {
// Unbinds will be communicated as a success since we're closed
// TODO: we are faking this "UnbindResponse" object in order to make
// tests pass. There is no such thing as an "unbind response" in the LDAP
// protocol. When the client is revamped, this logic should be removed.
// ~ jsumners 2023-02-16
const Unbind = class extends LDAPResult {
messageID = msgid
messageId = msgid
status = 'unbind'
}
const unbind = new Unbind()
const unbind = new UnbindResponse({ messageID: msgid })
unbind.status = 'unbind'
return cb(unbind)
}
})
@ -1247,23 +1198,21 @@ Client.prototype._sendSocket = function _sendSocket (message,
function messageCallback (msg) {
if (timer) { clearTimeout(timer) }
log.trace({ msg: msg ? msg.pojo : null }, 'response received')
log.trace({ msg: msg ? msg.json : null }, 'response received')
if (expect === 'abandon') { return sendResult('end', null) }
if (msg instanceof SearchEntry || msg instanceof SearchReference) {
let event = msg.constructor.name
// Generate the event name for the event emitter, i.e. "searchEntry"
// and "searchReference".
event = (event[0].toLowerCase() + event.slice(1)).replaceAll('Result', '')
event = event[0].toLowerCase() + event.slice(1)
return sendResult(event, msg)
} else {
tracker.remove(message.messageId)
tracker.remove(message.messageID)
// Potentially mark client as idle
self._updateIdle()
if (msg instanceof LDAPResult) {
if (msg.status !== 0 && expect.indexOf(msg.status) === -1) {
if (expect.indexOf(msg.status) === -1) {
return sendResult('error', errors.getError(msg))
}
return sendResult('end', msg)
@ -1277,7 +1226,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
function onRequestTimeout () {
self.emit('timeout', message)
const { callback: cb } = tracker.fetch(message.messageId)
const cb = tracker.fetch(message.messageID)
if (cb) {
// FIXME: the timed-out request should be abandoned
cb(new errors.TimeoutError('request timeout (client interrupt)'))
@ -1286,8 +1235,8 @@ Client.prototype._sendSocket = function _sendSocket (message,
function writeCallback () {
if (expect === 'abandon') {
// Mark the messageId specified as abandoned
tracker.abandon(message.abandonId)
// Mark the messageID specified as abandoned
tracker.abandon(message.abandonID)
// No need to track the abandon request itself
tracker.remove(message.id)
return callback(null)
@ -1302,9 +1251,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
conn.end()
} else if (emitter) {
sentEmitter = true
callback(null, emitter)
emitter.emit('searchRequest', message)
return
return callback(null, emitter)
}
return false
}
@ -1319,11 +1266,10 @@ Client.prototype._sendSocket = function _sendSocket (message,
timer = setTimeout(onRequestTimeout, self.timeout)
}
log.trace('sending request %j', message.pojo)
log.trace('sending request %j', message.json)
try {
const messageBer = message.toBer()
return conn.write(messageBer.buffer, writeCallback)
return conn.write(message.toBer(), writeCallback)
} catch (e) {
if (timer) { clearTimeout(timer) }
@ -1331,15 +1277,3 @@ Client.prototype._sendSocket = function _sendSocket (message,
return callback(e)
}
}
Client.prototype.emitError = function emitError (event, err) {
if (event !== 'error' && err && this.listenerCount(event) === 0) {
if (typeof err === 'string') {
err = event + ': ' + err
} else if (err.message) {
err.message = event + ': ' + err.message
}
this.emit('error', err)
}
this.emit(event, err)
}

View File

@ -4,7 +4,7 @@ const logger = require('../logger')
const Client = require('./client')
module.exports = {
Client,
Client: Client,
createClient: function createClient (options) {
if (isObject(options) === false) throw TypeError('options (object) required')
if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')

View File

@ -16,8 +16,8 @@ const { MAX_MSGID } = require('../constants')
module.exports = function idGeneratorFactory (start = 0) {
let currentID = start
return function nextID () {
const id = currentID + 1
currentID = (id >= MAX_MSGID) ? 1 : id
const nextID = currentID + 1
currentID = (nextID >= MAX_MSGID) ? 1 : nextID
return currentID
}
}

View File

@ -62,23 +62,13 @@ module.exports = function messageTrackerFactory (options) {
*/
tracker.abandon = function abandonMessage (msgID) {
if (messages.has(msgID) === false) return false
const toAbandon = messages.get(msgID)
abandoned.set(msgID, {
age: currentID,
message: toAbandon.message,
cb: toAbandon.callback
cb: messages.get(msgID)
})
return messages.delete(msgID)
}
/**
* @typedef {object} Tracked
* @property {object} message The tracked message. Usually the outgoing
* request object.
* @property {Function} callback The handler to use when receiving a
* response to the tracked message.
*/
/**
* Retrieves the message handler for a message. Removes abandoned messages
* that have been given time to be resolved.
@ -89,10 +79,10 @@ module.exports = function messageTrackerFactory (options) {
* @method fetch
*/
tracker.fetch = function fetchMessage (msgID) {
const tracked = messages.get(msgID)
if (tracked) {
const messageCB = messages.get(msgID)
if (messageCB) {
purgeAbandoned(msgID, abandoned)
return tracked
return messageCB
}
// We sent an abandon request but the server either wasn't able to process
@ -101,7 +91,7 @@ module.exports = function messageTrackerFactory (options) {
// to be processed normally.
const abandonedMsg = abandoned.get(msgID)
if (abandonedMsg) {
return { message: abandonedMsg, callback: abandonedMsg.cb }
return abandonedMsg.cb
}
return null
@ -120,7 +110,7 @@ module.exports = function messageTrackerFactory (options) {
messages.forEach((val, key) => {
purgeAbandoned(key, abandoned)
tracker.remove(key)
cb(key, val.callback)
cb(key, val)
})
}
@ -142,7 +132,7 @@ module.exports = function messageTrackerFactory (options) {
* Add a message handler to be tracked.
*
* @param {object} message The message object to be tracked. This object will
* have a new property added to it: `messageId`.
* have a new property added to it: `messageID`.
* @param {function} callback The handler for the message.
*
* @memberof MessageTracker
@ -153,8 +143,8 @@ module.exports = function messageTrackerFactory (options) {
// This side effect is not ideal but the client doesn't attach the tracker
// to itself until after the `.connect` method has fired. If this can be
// refactored later, then we can possibly get rid of this side effect.
message.messageId = currentID
messages.set(currentID, { callback, message })
message.messageID = currentID
messages.set(currentID, callback)
}
return tracker

View File

@ -14,7 +14,7 @@
* is not accepting any requests.
*/
module.exports = function enqueue (message, expect, emitter, cb) {
if (this._queue.size >= this.size || this._frozen) {
if (this._queue.length >= this.size || this._frozen) {
return false
}

View File

@ -2,9 +2,13 @@
const EventEmitter = require('events').EventEmitter
const util = require('util')
const assert = require('assert-plus')
const { PagedResultsControl } = require('@ldapjs/controls')
const CorkedEmitter = require('../corked_emitter.js')
// var dn = require('../dn')
// var messages = require('../messages/index')
// var Protocol = require('../protocol')
const PagedControl = require('../controls/paged_results_control.js')
/// --- API
@ -25,23 +29,22 @@ const CorkedEmitter = require('../corked_emitter.js')
* will be emitted (and 'end' will not be). By listening to
* 'pageError', a successful search that lacks paging will be
* able to emit 'end'.
* 3. search - Emitted as an internal event to trigger another client search.
*/
function SearchPager (opts) {
assert.object(opts)
assert.func(opts.callback)
assert.number(opts.pageSize)
assert.func(opts.sendRequest)
CorkedEmitter.call(this, {})
EventEmitter.call(this, {})
this.callback = opts.callback
this.controls = opts.controls
this.pageSize = opts.pageSize
this.pagePause = opts.pagePause
this.sendRequest = opts.sendRequest
this.controls.forEach(function (control) {
if (control.type === PagedResultsControl.OID) {
if (control.type === PagedControl.OID) {
// The point of using SearchPager is not having to do this.
// Toss an error if the pagedResultsControl is present
throw new Error('redundant pagedResultControl')
@ -52,13 +55,12 @@ function SearchPager (opts) {
this.started = false
const emitter = new EventEmitter()
emitter.on('searchRequest', this.emit.bind(this, 'searchRequest'))
emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
emitter.on('end', this._onEnd.bind(this))
emitter.on('error', this._onError.bind(this))
this.childEmitter = emitter
}
util.inherits(SearchPager, CorkedEmitter)
util.inherits(SearchPager, EventEmitter)
module.exports = SearchPager
/**
@ -73,7 +75,7 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
const self = this
let cookie = null
res.controls.forEach(function (control) {
if (control.type === PagedResultsControl.OID) {
if (control.type === PagedControl.OID) {
cookie = control.value.cookie
}
})
@ -89,13 +91,13 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
if (this.listeners('pageError').length > 0) {
this.emit('pageError', err)
// If the consumer as subscribed to pageError, SearchPager is absolved
// from delivering the fault via the 'error' event. Emitting an 'end'
// from deliverying the fault via the 'error' event. Emitting an 'end'
// event after 'error' breaks the contract that the standard client
// provides, so it's only a possibility if 'pageError' is used instead.
this.emit('end', res)
} else {
this.emit('error', err)
// No end event possible per explanation above.
// No end event possible per explaination above.
}
return
}
@ -134,20 +136,21 @@ SearchPager.prototype._onError = function _onError (err) {
*/
SearchPager.prototype._nextPage = function _nextPage (cookie) {
const controls = this.controls.slice(0)
controls.push(new PagedResultsControl({
controls.push(new PagedControl({
value: {
size: this.pageSize,
cookie
cookie: cookie
}
}))
this.sendRequest(controls, this.childEmitter, this._sendCallback.bind(this))
this.emit('search', controls, this.childEmitter,
this._sendCallback.bind(this))
}
/**
* Callback provided to the client API for successful transmission.
*/
SearchPager.prototype._sendCallback = function _sendCallback (err) {
SearchPager.prototype._sendCallback = function _sendCallback (err, res) {
if (err) {
this.finished = true
if (!this.started) {

61
lib/controls/control.js Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
// var asn1 = require('asn1')
// var Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
/// --- API
function Control (options) {
assert.optionalObject(options)
options = options || {}
assert.optionalString(options.type)
assert.optionalBool(options.criticality)
if (options.value) {
assert.buffer(options.value)
}
this.type = options.type || ''
this.criticality = options.critical || options.criticality || false
this.value = options.value || null
}
Object.defineProperties(Control.prototype, {
json: {
get: function getJson () {
const obj = {
controlType: this.type,
criticality: this.criticality,
controlValue: this.value
}
return (typeof (this._json) === 'function' ? this._json(obj) : obj)
}
}
})
Control.prototype.toBer = function toBer (ber) {
assert.ok(ber)
ber.startSequence()
ber.writeString(this.type || '')
ber.writeBoolean(this.criticality)
if (typeof (this._toBer) === 'function') {
this._toBer(ber)
} else {
if (this.value) { ber.writeString(this.value) }
}
ber.endSequence()
}
Control.prototype.toString = function toString () {
return this.json
}
/// --- Exports
module.exports = Control

View File

@ -0,0 +1,83 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function EntryChangeNotificationControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = EntryChangeNotificationControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(EntryChangeNotificationControl, Control)
Object.defineProperties(EntryChangeNotificationControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
EntryChangeNotificationControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {
changeType: ber.readInt()
}
// if the operation was moddn, then parse the optional previousDN attr
if (this._value.changeType === 8) { this._value.previousDN = ber.readString() }
this._value.changeNumber = ber.readInt()
return true
}
return false
}
EntryChangeNotificationControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.changeType)
if (this.value.previousDN) { writer.writeString(this.value.previousDN) }
writer.writeInt(parseInt(this.value.changeNumber, 10))
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
EntryChangeNotificationControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
EntryChangeNotificationControl.OID = '2.16.840.1.113730.3.4.7'
/// --- Exports
module.exports = EntryChangeNotificationControl

View File

@ -1,4 +1,86 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const controls = require('@ldapjs/controls')
module.exports = controls
const assert = require('assert')
const Ber = require('asn1').Ber
const Control = require('./control')
const EntryChangeNotificationControl =
require('./entry_change_notification_control')
const PersistentSearchControl = require('./persistent_search_control')
const PagedResultsControl = require('./paged_results_control')
const ServerSideSortingRequestControl =
require('./server_side_sorting_request_control.js')
const ServerSideSortingResponseControl =
require('./server_side_sorting_response_control.js')
const VirtualListViewRequestControl =
require('./virtual_list_view_request_control.js')
const VirtualListViewResponseControl =
require('./virtual_list_view_response_control.js')
/// --- API
module.exports = {
getControl: function getControl (ber) {
assert.ok(ber)
if (ber.readSequence() === null) { return null }
let type
const opts = {
criticality: false,
value: null
}
if (ber.length) {
const end = ber.offset + ber.length
type = ber.readString()
if (ber.offset < end) {
if (ber.peek() === Ber.Boolean) { opts.criticality = ber.readBoolean() }
}
if (ber.offset < end) { opts.value = ber.readString(Ber.OctetString, true) }
}
let control
switch (type) {
case PersistentSearchControl.OID:
control = new PersistentSearchControl(opts)
break
case EntryChangeNotificationControl.OID:
control = new EntryChangeNotificationControl(opts)
break
case PagedResultsControl.OID:
control = new PagedResultsControl(opts)
break
case ServerSideSortingRequestControl.OID:
control = new ServerSideSortingRequestControl(opts)
break
case ServerSideSortingResponseControl.OID:
control = new ServerSideSortingResponseControl(opts)
break
case VirtualListViewRequestControl.OID:
control = new VirtualListViewRequestControl(opts)
break
case VirtualListViewResponseControl.OID:
control = new VirtualListViewResponseControl(opts)
break
default:
opts.type = type
control = new Control(opts)
break
}
return control
},
Control: Control,
EntryChangeNotificationControl: EntryChangeNotificationControl,
PagedResultsControl: PagedResultsControl,
PersistentSearchControl: PersistentSearchControl,
ServerSideSortingRequestControl: ServerSideSortingRequestControl,
ServerSideSortingResponseControl: ServerSideSortingResponseControl,
VirtualListViewRequestControl: VirtualListViewRequestControl,
VirtualListViewResponseControl: VirtualListViewResponseControl
}

View File

@ -0,0 +1,82 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function PagedResultsControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = PagedResultsControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(PagedResultsControl, Control)
Object.defineProperties(PagedResultsControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
PagedResultsControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
this._value.size = ber.readInt()
this._value.cookie = ber.readString(asn1.Ber.OctetString, true)
// readString returns '' instead of a zero-length buffer
if (!this._value.cookie) { this._value.cookie = Buffer.alloc(0) }
return true
}
return false
}
PagedResultsControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.size)
if (this.value.cookie && this.value.cookie.length > 0) {
writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString)
} else {
writer.writeString('') // writeBuffer rejects zero-length buffers
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
PagedResultsControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
PagedResultsControl.OID = '1.2.840.113556.1.4.319'
/// --- Exports
module.exports = PagedResultsControl

View File

@ -0,0 +1,82 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function PersistentSearchControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = PersistentSearchControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(PersistentSearchControl, Control)
Object.defineProperties(PersistentSearchControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
PersistentSearchControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {
changeTypes: ber.readInt(),
changesOnly: ber.readBoolean(),
returnECs: ber.readBoolean()
}
return true
}
return false
}
PersistentSearchControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value) { return }
const writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.value.changeTypes)
writer.writeBoolean(this.value.changesOnly)
writer.writeBoolean(this.value.returnECs)
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
PersistentSearchControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
PersistentSearchControl.OID = '2.16.840.1.113730.3.4.3'
/// --- Exports
module.exports = PersistentSearchControl

View File

@ -0,0 +1,108 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function ServerSideSortingRequestControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = ServerSideSortingRequestControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (Array.isArray(options.value)) {
assert.arrayOfObject(options.value, 'options.value must be Objects')
for (let i = 0; i < options.value.length; i++) {
if (Object.prototype.hasOwnProperty.call(options.value[i], 'attributeType') === false) {
throw new Error('Missing required key: attributeType')
}
}
this._value = options.value
} else if (typeof (options.value) === 'object') {
if (Object.prototype.hasOwnProperty.call(options.value, 'attributeType') === false) {
throw new Error('Missing required key: attributeType')
}
this._value = [options.value]
} else {
throw new TypeError('options.value must be a Buffer, Array or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(ServerSideSortingRequestControl, Control)
Object.defineProperties(ServerSideSortingRequestControl.prototype, {
value: {
get: function () { return this._value || [] },
configurable: false
}
})
ServerSideSortingRequestControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
let item
if (ber.readSequence(0x30)) {
this._value = []
while (ber.readSequence(0x30)) {
item = {}
item.attributeType = ber.readString(asn1.Ber.OctetString)
if (ber.peek() === 0x80) {
item.orderingRule = ber.readString(0x80)
}
if (ber.peek() === 0x81) {
item.reverseOrder = (ber._readTag(0x81) !== 0)
}
this._value.push(item)
}
return true
}
return false
}
ServerSideSortingRequestControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) { return }
const writer = new BerWriter()
writer.startSequence(0x30)
for (let i = 0; i < this.value.length; i++) {
const item = this.value[i]
writer.startSequence(0x30)
if (item.attributeType) {
writer.writeString(item.attributeType, asn1.Ber.OctetString)
}
if (item.orderingRule) {
writer.writeString(item.orderingRule, 0x80)
}
if (item.reverseOrder) {
writer.writeBoolean(item.reverseOrder, 0x81)
}
writer.endSequence()
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
ServerSideSortingRequestControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
ServerSideSortingRequestControl.OID = '1.2.840.113556.1.4.473'
/// ---Exports
module.exports = ServerSideSortingRequestControl

View File

@ -0,0 +1,100 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
const CODES = require('../errors/codes')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const VALID_CODES = [
CODES.LDAP_SUCCESS,
CODES.LDAP_OPERATIONS_ERROR,
CODES.LDAP_TIME_LIMIT_EXCEEDED,
CODES.LDAP_STRONG_AUTH_REQUIRED,
CODES.LDAP_ADMIN_LIMIT_EXCEEDED,
CODES.LDAP_NO_SUCH_ATTRIBUTE,
CODES.LDAP_INAPPROPRIATE_MATCHING,
CODES.LDAP_INSUFFICIENT_ACCESS_RIGHTS,
CODES.LDAP_BUSY,
CODES.LDAP_UNWILLING_TO_PERFORM,
CODES.LDAP_OTHER
]
function ServerSideSortingResponseControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = ServerSideSortingResponseControl.OID
options.criticality = false
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (VALID_CODES.indexOf(options.value.result) === -1) {
throw new Error('Invalid result code')
}
if (options.value.failedAttribute &&
typeof (options.value.failedAttribute) !== 'string') {
throw new Error('failedAttribute must be String')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(ServerSideSortingResponseControl, Control)
Object.defineProperties(ServerSideSortingResponseControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
ServerSideSortingResponseControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence(0x30)) {
this._value = {}
this._value.result = ber.readEnumeration()
if (ber.peek() === 0x80) {
this._value.failedAttribute = ber.readString(0x80)
}
return true
}
return false
}
ServerSideSortingResponseControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) { return }
const writer = new BerWriter()
writer.startSequence(0x30)
writer.writeEnumeration(this.value.result)
if (this.value.result !== CODES.LDAP_SUCCESS && this.value.failedAttribute) {
writer.writeString(this.value.failedAttribute, 0x80)
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
ServerSideSortingResponseControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
ServerSideSortingResponseControl.OID = '1.2.840.113556.1.4.474'
/// --- Exports
module.exports = ServerSideSortingResponseControl

View File

@ -0,0 +1,94 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
/// --- API
function VirtualListViewControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = VirtualListViewControl.OID
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (Object.prototype.hasOwnProperty.call(options.value, 'beforeCount') === false) {
throw new Error('Missing required key: beforeCount')
}
if (Object.prototype.hasOwnProperty.call(options.value, 'afterCount') === false) {
throw new Error('Missing required key: afterCount')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(VirtualListViewControl, Control)
Object.defineProperties(VirtualListViewControl.prototype, {
value: {
get: function () { return this._value || [] },
configurable: false
}
})
VirtualListViewControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
this._value.beforeCount = ber.readInt()
this._value.afterCount = ber.readInt()
if (ber.peek() === 0xa0) {
if (ber.readSequence(0xa0)) {
this._value.targetOffset = ber.readInt()
this._value.contentCount = ber.readInt()
}
}
if (ber.peek() === 0x81) {
this._value.greaterThanOrEqual = ber.readString(0x81)
}
return true
}
return false
}
VirtualListViewControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) {
return
}
const writer = new BerWriter()
writer.startSequence(0x30)
writer.writeInt(this.value.beforeCount)
writer.writeInt(this.value.afterCount)
if (this.value.targetOffset !== undefined) {
writer.startSequence(0xa0)
writer.writeInt(this.value.targetOffset)
writer.writeInt(this.value.contentCount)
writer.endSequence()
} else if (this.value.greaterThanOrEqual !== undefined) {
writer.writeString(this.value.greaterThanOrEqual, 0x81)
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
VirtualListViewControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
VirtualListViewControl.OID = '2.16.840.1.113730.3.4.9'
/// ---Exports
module.exports = VirtualListViewControl

View File

@ -0,0 +1,112 @@
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const Control = require('./control')
const CODES = require('../errors/codes')
/// --- Globals
const BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const VALID_CODES = [
CODES.LDAP_SUCCESS,
CODES.LDAP_OPERATIONS_ERROR,
CODES.LDAP_UNWILLING_TO_PERFORM,
CODES.LDAP_INSUFFICIENT_ACCESS_RIGHTS,
CODES.LDAP_BUSY,
CODES.LDAP_TIME_LIMIT_EXCEEDED,
CODES.LDAP_ADMIN_LIMIT_EXCEEDED,
CODES.LDAP_SORT_CONTROL_MISSING,
CODES.LDAP_INDEX_RANGE_ERROR,
CODES.LDAP_CONTROL_ERROR,
CODES.LDAP_OTHER
]
function VirtualListViewResponseControl (options) {
assert.optionalObject(options)
options = options || {}
options.type = VirtualListViewResponseControl.OID
options.criticality = false
if (options.value) {
if (Buffer.isBuffer(options.value)) {
this.parse(options.value)
} else if (typeof (options.value) === 'object') {
if (VALID_CODES.indexOf(options.value.result) === -1) {
throw new Error('Invalid result code')
}
this._value = options.value
} else {
throw new TypeError('options.value must be a Buffer or Object')
}
options.value = null
}
Control.call(this, options)
}
util.inherits(VirtualListViewResponseControl, Control)
Object.defineProperties(VirtualListViewResponseControl.prototype, {
value: {
get: function () { return this._value || {} },
configurable: false
}
})
VirtualListViewResponseControl.prototype.parse = function parse (buffer) {
assert.ok(buffer)
const ber = new BerReader(buffer)
if (ber.readSequence()) {
this._value = {}
if (ber.peek(0x02)) {
this._value.targetPosition = ber.readInt()
}
if (ber.peek(0x02)) {
this._value.contentCount = ber.readInt()
}
this._value.result = ber.readEnumeration()
this._value.cookie = ber.readString(asn1.Ber.OctetString, true)
// readString returns '' instead of a zero-length buffer
if (!this._value.cookie) {
this._value.cookie = Buffer.alloc(0)
}
return true
}
return false
}
VirtualListViewResponseControl.prototype._toBer = function (ber) {
assert.ok(ber)
if (!this._value || this.value.length === 0) {
return
}
const writer = new BerWriter()
writer.startSequence()
if (this.value.targetPosition !== undefined) {
writer.writeInt(this.value.targetPosition)
}
if (this.value.contentCount !== undefined) {
writer.writeInt(this.value.contentCount)
}
writer.writeEnumeration(this.value.result)
if (this.value.cookie && this.value.cookie.length > 0) {
writer.writeBuffer(this.value.cookie, asn1.Ber.OctetString)
} else {
writer.writeString('') // writeBuffer rejects zero-length buffers
}
writer.endSequence()
ber.writeBuffer(writer.buffer, 0x04)
}
VirtualListViewResponseControl.prototype._json = function (obj) {
obj.controlValue = this.value
return obj
}
VirtualListViewResponseControl.OID = '2.16.840.1.113730.3.4.10'
/// --- Exports
module.exports = VirtualListViewResponseControl

473
lib/dn.js Normal file
View File

@ -0,0 +1,473 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
/// --- Helpers
function invalidDN (name) {
const e = new Error()
e.name = 'InvalidDistinguishedNameError'
e.message = name
return e
}
function isAlphaNumeric (c) {
const re = /[A-Za-z0-9]/
return re.test(c)
}
function isWhitespace (c) {
const re = /\s/
return re.test(c)
}
function repeatChar (c, n) {
let out = ''
const max = n || 0
for (let i = 0; i < max; i++) { out += c }
return out
}
/// --- API
function RDN (obj) {
const self = this
this.attrs = {}
if (obj) {
Object.keys(obj).forEach(function (k) {
self.set(k, obj[k])
})
}
}
RDN.prototype.set = function rdnSet (name, value, opts) {
assert.string(name, 'name (string) required')
assert.string(value, 'value (string) required')
const self = this
const lname = name.toLowerCase()
this.attrs[lname] = {
value: value,
name: name
}
if (opts && typeof (opts) === 'object') {
Object.keys(opts).forEach(function (k) {
if (k !== 'value') { self.attrs[lname][k] = opts[k] }
})
}
}
RDN.prototype.equals = function rdnEquals (rdn) {
if (typeof (rdn) !== 'object') { return false }
const ourKeys = Object.keys(this.attrs)
const theirKeys = Object.keys(rdn.attrs)
if (ourKeys.length !== theirKeys.length) { return false }
ourKeys.sort()
theirKeys.sort()
for (let i = 0; i < ourKeys.length; i++) {
if (ourKeys[i] !== theirKeys[i]) { return false }
if (this.attrs[ourKeys[i]].value !== rdn.attrs[ourKeys[i]].value) { return false }
}
return true
}
/**
* Convert RDN to string according to specified formatting options.
* (see: DN.format for option details)
*/
RDN.prototype.format = function rdnFormat (options) {
assert.optionalObject(options, 'options must be an object')
options = options || {}
const self = this
let str = ''
function escapeValue (val, forceQuote) {
let out = ''
let cur = 0
const len = val.length
let quoted = false
/* BEGIN JSSTYLED */
// TODO: figure out what this regex is actually trying to test for and
// fix it to appease the linter.
/* eslint-disable-next-line no-useless-escape */
const escaped = /[\\\"]/
const special = /[,=+<>#;]/
/* END JSSTYLED */
if (len > 0) {
// Wrap strings with trailing or leading spaces in quotes
quoted = forceQuote || (val[0] === ' ' || val[len - 1] === ' ')
}
while (cur < len) {
if (escaped.test(val[cur]) || (!quoted && special.test(val[cur]))) {
out += '\\'
}
out += val[cur++]
}
if (quoted) { out = '"' + out + '"' }
return out
}
function sortParsed (a, b) {
return self.attrs[a].order - self.attrs[b].order
}
function sortStandard (a, b) {
const nameCompare = a.localeCompare(b)
if (nameCompare === 0) {
// TODO: Handle binary values
return self.attrs[a].value.localeCompare(self.attrs[b].value)
} else {
return nameCompare
}
}
const keys = Object.keys(this.attrs)
if (options.keepOrder) {
keys.sort(sortParsed)
} else {
keys.sort(sortStandard)
}
keys.forEach(function (key) {
const attr = self.attrs[key]
if (str.length) { str += '+' }
if (options.keepCase) {
str += attr.name
} else {
if (options.upperName) { str += key.toUpperCase() } else { str += key }
}
str += '=' + escapeValue(attr.value, (options.keepQuote && attr.quoted))
})
return str
}
RDN.prototype.toString = function rdnToString () {
return this.format()
}
// Thank you OpenJDK!
function parse (name) {
if (typeof (name) !== 'string') { throw new TypeError('name (string) required') }
let cur = 0
const len = name.length
function parseRdn () {
const rdn = new RDN()
let order = 0
rdn.spLead = trim()
while (cur < len) {
const opts = {
order: order
}
const attr = parseAttrType()
trim()
if (cur >= len || name[cur++] !== '=') { throw invalidDN(name) }
trim()
// Parameters about RDN value are set in 'opts' by parseAttrValue
const value = parseAttrValue(opts)
rdn.set(attr, value, opts)
rdn.spTrail = trim()
if (cur >= len || name[cur] !== '+') { break }
++cur
++order
}
return rdn
}
function trim () {
let count = 0
while ((cur < len) && isWhitespace(name[cur])) {
++cur
count++
}
return count
}
function parseAttrType () {
const beg = cur
while (cur < len) {
const c = name[cur]
if (isAlphaNumeric(c) ||
c === '.' ||
c === '-' ||
c === ' ') {
++cur
} else {
break
}
}
// Back out any trailing spaces.
while ((cur > beg) && (name[cur - 1] === ' ')) { --cur }
if (beg === cur) { throw invalidDN(name) }
return name.slice(beg, cur)
}
function parseAttrValue (opts) {
if (cur < len && name[cur] === '#') {
opts.binary = true
return parseBinaryAttrValue()
} else if (cur < len && name[cur] === '"') {
opts.quoted = true
return parseQuotedAttrValue()
} else {
return parseStringAttrValue()
}
}
function parseBinaryAttrValue () {
const beg = cur++
while (cur < len && isAlphaNumeric(name[cur])) { ++cur }
return name.slice(beg, cur)
}
function parseQuotedAttrValue () {
let str = ''
++cur // Consume the first quote
while ((cur < len) && name[cur] !== '"') {
if (name[cur] === '\\') { cur++ }
str += name[cur++]
}
if (cur++ >= len) {
// no closing quote
throw invalidDN(name)
}
return str
}
function parseStringAttrValue () {
const beg = cur
let str = ''
let esc = -1
while ((cur < len) && !atTerminator()) {
if (name[cur] === '\\') {
// Consume the backslash and mark its place just in case it's escaping
// whitespace which needs to be preserved.
esc = cur++
}
if (cur === len) {
// backslash followed by nothing
throw invalidDN(name)
}
str += name[cur++]
}
// Trim off (unescaped) trailing whitespace and rewind cursor to the end of
// the AttrValue to record whitespace length.
for (; cur > beg; cur--) {
if (!isWhitespace(name[cur - 1]) || (esc === (cur - 1))) { break }
}
return str.slice(0, cur - beg)
}
function atTerminator () {
return (cur < len &&
(name[cur] === ',' ||
name[cur] === ';' ||
name[cur] === '+'))
}
const rdns = []
// Short-circuit for empty DNs
if (len === 0) { return new DN(rdns) }
rdns.push(parseRdn())
while (cur < len) {
if (name[cur] === ',' || name[cur] === ';') {
++cur
rdns.push(parseRdn())
} else {
throw invalidDN(name)
}
}
return new DN(rdns)
}
function DN (rdns) {
assert.optionalArrayOfObject(rdns, '[object] required')
this.rdns = rdns ? rdns.slice() : []
this._format = {}
}
Object.defineProperties(DN.prototype, {
length: {
get: function getLength () { return this.rdns.length },
configurable: false
}
})
/**
* Convert DN to string according to specified formatting options.
*
* Parameters:
* - options: formatting parameters (optional, details below)
*
* Options are divided into two types:
* - Preservation options: Using data recorded during parsing, details of the
* original DN are preserved when converting back into a string.
* - Modification options: Alter string formatting defaults.
*
* Preservation options _always_ take precedence over modification options.
*
* Preservation Options:
* - keepOrder: Order of multi-value RDNs.
* - keepQuote: RDN values which were quoted will remain so.
* - keepSpace: Leading/trailing spaces will be output.
* - keepCase: Parsed attr name will be output instead of lowercased version.
*
* Modification Options:
* - upperName: RDN names will be uppercased instead of lowercased.
* - skipSpace: Disable trailing space after RDN separators
*/
DN.prototype.format = function dnFormat (options) {
assert.optionalObject(options, 'options must be an object')
options = options || this._format
let str = ''
this.rdns.forEach(function (rdn) {
const rdnString = rdn.format(options)
if (str.length !== 0) {
str += ','
}
if (options.keepSpace) {
str += (repeatChar(' ', rdn.spLead) +
rdnString + repeatChar(' ', rdn.spTrail))
} else if (options.skipSpace === true || str.length === 0) {
str += rdnString
} else {
str += ' ' + rdnString
}
})
return str
}
/**
* Set default string formatting options.
*/
DN.prototype.setFormat = function setFormat (options) {
assert.object(options, 'options must be an object')
this._format = options
}
DN.prototype.toString = function dnToString () {
return this.format()
}
DN.prototype.parentOf = function parentOf (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
if (this.rdns.length >= dn.rdns.length) { return false }
const diff = dn.rdns.length - this.rdns.length
for (let i = this.rdns.length - 1; i >= 0; i--) {
const myRDN = this.rdns[i]
const theirRDN = dn.rdns[i + diff]
if (!myRDN.equals(theirRDN)) { return false }
}
return true
}
DN.prototype.childOf = function childOf (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
return dn.parentOf(this)
}
DN.prototype.isEmpty = function isEmpty () {
return (this.rdns.length === 0)
}
DN.prototype.equals = function dnEquals (dn) {
if (typeof (dn) !== 'object') { dn = parse(dn) }
if (this.rdns.length !== dn.rdns.length) { return false }
for (let i = 0; i < this.rdns.length; i++) {
if (!this.rdns[i].equals(dn.rdns[i])) { return false }
}
return true
}
DN.prototype.parent = function dnParent () {
if (this.rdns.length !== 0) {
const save = this.rdns.shift()
const dn = new DN(this.rdns)
this.rdns.unshift(save)
return dn
}
return null
}
DN.prototype.clone = function dnClone () {
const dn = new DN(this.rdns)
dn._format = this._format
return dn
}
DN.prototype.reverse = function dnReverse () {
this.rdns.reverse()
return this
}
DN.prototype.pop = function dnPop () {
return this.rdns.pop()
}
DN.prototype.push = function dnPush (rdn) {
assert.object(rdn, 'rdn (RDN) required')
return this.rdns.push(rdn)
}
DN.prototype.shift = function dnShift () {
return this.rdns.shift()
}
DN.prototype.unshift = function dnUnshift (rdn) {
assert.object(rdn, 'rdn (RDN) required')
return this.rdns.unshift(rdn)
}
DN.isDN = function isDN (dn) {
if (!dn || typeof (dn) !== 'object') {
return false
}
if (dn instanceof DN) {
return true
}
if (Array.isArray(dn.rdns)) {
// Really simple duck-typing for now
return true
}
return false
}
/// --- Exports
module.exports = {
parse: parse,
DN: DN,
RDN: RDN
}

120
lib/dtrace.js Normal file
View File

@ -0,0 +1,120 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.s
/// --- Globals
let SERVER_PROVIDER
let DTRACE_ID = 0
const MAX_INT = 4294967295
/*
* Args:
* server-*-start:
* 0 -> id
* 1 -> remoteIP
* 2 -> bindDN
* 3 -> req.dn
* 4,5 -> op specific
*
* server-*-done:
* 0 -> id
* 1 -> remoteIp
* 2 -> bindDN
* 3 -> requsetDN
* 4 -> status
* 5 -> errorMessage
*
*/
const SERVER_PROBES = {
// 4: attributes.length
'server-add-start': ['int', 'char *', 'char *', 'char *', 'int'],
'server-add-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-bind-start': ['int', 'char *', 'char *', 'char *'],
'server-bind-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: attribute, 5: value
'server-compare-start': ['int', 'char *', 'char *', 'char *',
'char *', 'char *'],
'server-compare-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-delete-start': ['int', 'char *', 'char *', 'char *'],
'server-delete-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: requestName, 5: requestValue
'server-exop-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-exop-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: changes.length
'server-modify-start': ['int', 'char *', 'char *', 'char *', 'int'],
'server-modify-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// 4: newRdn, 5: newSuperior
'server-modifydn-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-modifydn-done': ['int', 'char *', 'char *', 'char *', 'int',
'char *'],
// 4: scope, 5: filter
'server-search-start': ['int', 'char *', 'char *', 'char *', 'char *',
'char *'],
'server-search-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// Last two are searchEntry.DN and seachEntry.attributes.length
'server-search-entry': ['int', 'char *', 'char *', 'char *', 'char *', 'int'],
'server-unbind-start': ['int', 'char *', 'char *', 'char *'],
'server-unbind-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
'server-abandon-start': ['int', 'char *', 'char *', 'char *'],
'server-abandon-done': ['int', 'char *', 'char *', 'char *', 'int', 'char *'],
// remote IP
'server-connection': ['char *']
}
/// --- API
module.exports = (function () {
if (!SERVER_PROVIDER) {
try {
const dtrace = require('dtrace-provider')
SERVER_PROVIDER = dtrace.createDTraceProvider('ldapjs')
Object.keys(SERVER_PROBES).forEach(function (p) {
const args = SERVER_PROBES[p].splice(0)
args.unshift(p)
dtrace.DTraceProvider.prototype.addProbe.apply(SERVER_PROVIDER, args)
})
} catch (e) {
SERVER_PROVIDER = {
fire: function () {
},
enable: function () {
},
addProbe: function () {
const p = {
fire: function () {
}
}
return (p)
},
removeProbe: function () {
},
disable: function () {
}
}
}
SERVER_PROVIDER.enable()
SERVER_PROVIDER._nextId = function () {
if (DTRACE_ID === MAX_INT) { DTRACE_ID = 0 }
return ++DTRACE_ID
}
}
return SERVER_PROVIDER
}())

View File

@ -32,9 +32,6 @@ Object.defineProperties(LDAPError.prototype, {
get: function getMessage () {
return this.lde_message || this.name
},
set: function setMessage (message) {
this.lde_message = message
},
configurable: false
},
dn: {
@ -86,7 +83,7 @@ Object.keys(CODES).forEach(function (code) {
})
ERRORS[CODES[code]] = {
err,
err: err,
message: msg
}
})

27
lib/filters/and_filter.js Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function AndFilter (options) {
parents.AndFilter.call(this, options)
}
util.inherits(AndFilter, parents.AndFilter)
Filter.mixin(AndFilter)
module.exports = AndFilter
AndFilter.prototype._toBer = function (ber) {
assert.ok(ber)
this.filters.forEach(function (f) {
ber = f.toBer(ber)
})
return ber
}

View File

@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function ApproximateFilter (options) {
parents.ApproximateFilter.call(this, options)
}
util.inherits(ApproximateFilter, parents.ApproximateFilter)
Filter.mixin(ApproximateFilter)
module.exports = ApproximateFilter
ApproximateFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
ApproximateFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}

View File

@ -0,0 +1,60 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const ASN1 = require('asn1').Ber
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function EqualityFilter (options) {
parents.EqualityFilter.call(this, options)
}
util.inherits(EqualityFilter, parents.EqualityFilter)
Filter.mixin(EqualityFilter)
module.exports = EqualityFilter
EqualityFilter.prototype.matches = function (target, strictAttrCase) {
assert.object(target, 'target')
const tv = parents.getAttrValue(target, this.attribute, strictAttrCase)
let value = this.value
if (this.attribute.toLowerCase() === 'objectclass') {
/*
* Perform case-insensitive match for objectClass since nearly every LDAP
* implementation behaves in this manner.
*/
value = value.toLowerCase()
return parents.testValues(function (v) {
return value === v.toLowerCase()
}, tv)
} else {
return parents.testValues(function (v) {
return value === v
}, tv)
}
}
EqualityFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString(ASN1.OctetString, true)
if (this.attribute === 'objectclass') { this.value = this.value.toLowerCase() }
return true
}
EqualityFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeBuffer(this.raw, ASN1.OctetString)
return ber
}

44
lib/filters/escape.js Normal file
View File

@ -0,0 +1,44 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
/**
* RFC 2254 Escaping of filter strings
*
* Raw Escaped
* (o=Parens (R Us)) (o=Parens \28R Us\29)
* (cn=star*) (cn=star\2A)
* (filename=C:\MyFile) (filename=C:\5cMyFile)
*
* Use substr_filter to avoid having * ecsaped.
*
* @author [Austin King](https://github.com/ozten)
*/
exports.escape = function (inp) {
if (typeof (inp) === 'string') {
let esc = ''
for (let i = 0; i < inp.length; i++) {
switch (inp[i]) {
case '*':
esc += '\\2a'
break
case '(':
esc += '\\28'
break
case ')':
esc += '\\29'
break
case '\\':
esc += '\\5c'
break
case '\0':
esc += '\\00'
break
default:
esc += inp[i]
break
}
}
return esc
} else {
return inp
}
}

59
lib/filters/ext_filter.js Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
// THIS IS A STUB!
//
// ldapjs does not support server side extensible matching.
// This class exists only for the client to send them.
/// --- API
function ExtensibleFilter (options) {
parents.ExtensibleFilter.call(this, options)
}
util.inherits(ExtensibleFilter, parents.ExtensibleFilter)
Filter.mixin(ExtensibleFilter)
module.exports = ExtensibleFilter
ExtensibleFilter.prototype.parse = function (ber) {
const end = ber.offset + ber.length
while (ber.offset < end) {
const tag = ber.peek()
switch (tag) {
case 0x81:
this.rule = ber.readString(tag)
break
case 0x82:
this.matchType = ber.readString(tag)
break
case 0x83:
this.value = ber.readString(tag)
break
case 0x84:
this.dnAttributes = ber.readBoolean(tag)
break
default:
throw new Error('Invalid ext_match filter type: 0x' + tag.toString(16))
}
}
return true
}
ExtensibleFilter.prototype._toBer = function (ber) {
assert.ok(ber)
if (this.rule) { ber.writeString(this.rule, 0x81) }
if (this.matchType) { ber.writeString(this.matchType, 0x82) }
ber.writeString(this.value, 0x83)
if (this.dnAttributes) { ber.writeBoolean(this.dnAttributes, 0x84) }
return ber
}

60
lib/filters/filter.js Normal file
View File

@ -0,0 +1,60 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
// var assert = require('assert')
const Protocol = require('../protocol')
/// --- Globals
const TYPES = {
and: Protocol.FILTER_AND,
or: Protocol.FILTER_OR,
not: Protocol.FILTER_NOT,
equal: Protocol.FILTER_EQUALITY,
substring: Protocol.FILTER_SUBSTRINGS,
ge: Protocol.FILTER_GE,
le: Protocol.FILTER_LE,
present: Protocol.FILTER_PRESENT,
approx: Protocol.FILTER_APPROX,
ext: Protocol.FILTER_EXT
}
/// --- API
function isFilter (filter) {
if (!filter || typeof (filter) !== 'object') {
return false
}
// Do our best to duck-type it
if (typeof (filter.toBer) === 'function' &&
typeof (filter.matches) === 'function' &&
TYPES[filter.type] !== undefined) {
return true
}
return false
}
function isBerWriter (ber) {
return Boolean(
ber &&
typeof (ber) === 'object' &&
typeof (ber.startSequence) === 'function' &&
typeof (ber.endSequence) === 'function'
)
}
function mixin (target) {
target.prototype.toBer = function toBer (ber) {
if (isBerWriter(ber) === false) { throw new TypeError('ber (BerWriter) required') }
ber.startSequence(TYPES[this.type])
ber = this._toBer(ber)
ber.endSequence()
return ber
}
}
module.exports = {
isFilter: isFilter,
mixin: mixin
}

35
lib/filters/ge_filter.js Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function GreaterThanEqualsFilter (options) {
parents.GreaterThanEqualsFilter.call(this, options)
}
util.inherits(GreaterThanEqualsFilter, parents.GreaterThanEqualsFilter)
Filter.mixin(GreaterThanEqualsFilter)
module.exports = GreaterThanEqualsFilter
GreaterThanEqualsFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
GreaterThanEqualsFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}

208
lib/filters/index.js Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const asn1 = require('asn1')
const parents = require('ldap-filter')
const Protocol = require('../protocol')
const Filter = require('./filter')
const AndFilter = require('./and_filter')
const ApproximateFilter = require('./approx_filter')
const EqualityFilter = require('./equality_filter')
const ExtensibleFilter = require('./ext_filter')
const GreaterThanEqualsFilter = require('./ge_filter')
const LessThanEqualsFilter = require('./le_filter')
const NotFilter = require('./not_filter')
const OrFilter = require('./or_filter')
const PresenceFilter = require('./presence_filter')
const SubstringFilter = require('./substr_filter')
/// --- Globals
const BerReader = asn1.BerReader
/// --- Internal Parsers
/*
* A filter looks like this coming in:
* Filter ::= CHOICE {
* and [0] SET OF Filter,
* or [1] SET OF Filter,
* not [2] Filter,
* equalityMatch [3] AttributeValueAssertion,
* substrings [4] SubstringFilter,
* greaterOrEqual [5] AttributeValueAssertion,
* lessOrEqual [6] AttributeValueAssertion,
* present [7] AttributeType,
* approxMatch [8] AttributeValueAssertion,
* extensibleMatch [9] MatchingRuleAssertion --v3 only
* }
*
* SubstringFilter ::= SEQUENCE {
* type AttributeType,
* SEQUENCE OF CHOICE {
* initial [0] IA5String,
* any [1] IA5String,
* final [2] IA5String
* }
* }
*
* The extensibleMatch was added in LDAPv3:
*
* MatchingRuleAssertion ::= SEQUENCE {
* matchingRule [1] MatchingRuleID OPTIONAL,
* type [2] AttributeDescription OPTIONAL,
* matchValue [3] AssertionValue,
* dnAttributes [4] BOOLEAN DEFAULT FALSE
* }
*/
function _parse (ber) {
assert.ok(ber)
function parseSet (f) {
const end = ber.offset + ber.length
while (ber.offset < end) { f.addFilter(_parse(ber)) }
}
let f
const type = ber.readSequence()
switch (type) {
case Protocol.FILTER_AND:
f = new AndFilter()
parseSet(f)
break
case Protocol.FILTER_APPROX:
f = new ApproximateFilter()
f.parse(ber)
break
case Protocol.FILTER_EQUALITY:
f = new EqualityFilter()
f.parse(ber)
return f
case Protocol.FILTER_EXT:
f = new ExtensibleFilter()
f.parse(ber)
return f
case Protocol.FILTER_GE:
f = new GreaterThanEqualsFilter()
f.parse(ber)
return f
case Protocol.FILTER_LE:
f = new LessThanEqualsFilter()
f.parse(ber)
return f
case Protocol.FILTER_NOT:
f = new NotFilter({
filter: _parse(ber)
})
break
case Protocol.FILTER_OR:
f = new OrFilter()
parseSet(f)
break
case Protocol.FILTER_PRESENT:
f = new PresenceFilter()
f.parse(ber)
break
case Protocol.FILTER_SUBSTRINGS:
f = new SubstringFilter()
f.parse(ber)
break
default:
throw new Error('Invalid search filter type: 0x' + type.toString(16))
}
assert.ok(f)
return f
}
function cloneFilter (input) {
let child
if (input.type === 'and' || input.type === 'or') {
child = input.filters.map(cloneFilter)
} else if (input.type === 'not') {
child = cloneFilter(input.filter)
}
switch (input.type) {
case 'and':
return new AndFilter({ filters: child })
case 'or':
return new OrFilter({ filters: child })
case 'not':
return new NotFilter({ filter: child })
case 'equal':
return new EqualityFilter(input)
case 'substring':
return new SubstringFilter(input)
case 'ge':
return new GreaterThanEqualsFilter(input)
case 'le':
return new LessThanEqualsFilter(input)
case 'present':
return new PresenceFilter(input)
case 'approx':
return new ApproximateFilter(input)
case 'ext':
return new ExtensibleFilter(input)
default:
throw new Error('invalid filter type:' + input.type)
}
}
function escapedToHex (str) {
return str.replace(/\\([0-9a-f](?![0-9a-f])|[^0-9a-f]|$)/gi, function (match, p1) {
if (!p1) {
return '\\5c'
}
const hexCode = p1.charCodeAt(0).toString(16)
return '\\' + hexCode
})
}
function parseString (str) {
const hexStr = escapedToHex(str)
const generic = parents.parse(hexStr)
// The filter object(s) return from ldap-filter.parse lack the toBer/parse
// decoration that native ldapjs filter possess. cloneFilter adds that back.
return cloneFilter(generic)
}
/// --- API
module.exports = {
parse: function (ber) {
if (!ber || !(ber instanceof BerReader)) { throw new TypeError('ber (BerReader) required') }
return _parse(ber)
},
parseString: parseString,
isFilter: Filter.isFilter,
AndFilter: AndFilter,
ApproximateFilter: ApproximateFilter,
EqualityFilter: EqualityFilter,
ExtensibleFilter: ExtensibleFilter,
GreaterThanEqualsFilter: GreaterThanEqualsFilter,
LessThanEqualsFilter: LessThanEqualsFilter,
NotFilter: NotFilter,
OrFilter: OrFilter,
PresenceFilter: PresenceFilter,
SubstringFilter: SubstringFilter
}

35
lib/filters/le_filter.js Normal file
View File

@ -0,0 +1,35 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function LessThanEqualsFilter (options) {
parents.LessThanEqualsFilter.call(this, options)
}
util.inherits(LessThanEqualsFilter, parents.LessThanEqualsFilter)
Filter.mixin(LessThanEqualsFilter)
module.exports = LessThanEqualsFilter
LessThanEqualsFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
LessThanEqualsFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.writeString(this.value)
return ber
}

23
lib/filters/not_filter.js Normal file
View File

@ -0,0 +1,23 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function NotFilter (options) {
parents.NotFilter.call(this, options)
}
util.inherits(NotFilter, parents.NotFilter)
Filter.mixin(NotFilter)
module.exports = NotFilter
NotFilter.prototype._toBer = function (ber) {
assert.ok(ber)
return this.filter.toBer(ber)
}

27
lib/filters/or_filter.js Normal file
View File

@ -0,0 +1,27 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function OrFilter (options) {
parents.OrFilter.call(this, options)
}
util.inherits(OrFilter, parents.OrFilter)
Filter.mixin(OrFilter)
module.exports = OrFilter
OrFilter.prototype._toBer = function (ber) {
assert.ok(ber)
this.filters.forEach(function (f) {
ber = f.toBer(ber)
})
return ber
}

View File

@ -0,0 +1,36 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function PresenceFilter (options) {
parents.PresenceFilter.call(this, options)
}
util.inherits(PresenceFilter, parents.PresenceFilter)
Filter.mixin(PresenceFilter)
module.exports = PresenceFilter
PresenceFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute =
ber.buffer.slice(0, ber.length).toString('utf8').toLowerCase()
ber._offset += ber.length
return true
}
PresenceFilter.prototype._toBer = function (ber) {
assert.ok(ber)
for (let i = 0; i < this.attribute.length; i++) { ber.writeByte(this.attribute.charCodeAt(i)) }
return ber
}

View File

@ -0,0 +1,70 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert')
const util = require('util')
const parents = require('ldap-filter')
const Filter = require('./filter')
/// --- API
function SubstringFilter (options) {
parents.SubstringFilter.call(this, options)
}
util.inherits(SubstringFilter, parents.SubstringFilter)
Filter.mixin(SubstringFilter)
module.exports = SubstringFilter
SubstringFilter.prototype.parse = function (ber) {
assert.ok(ber)
this.attribute = ber.readString().toLowerCase()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const tag = ber.peek()
switch (tag) {
case 0x80: // Initial
this.initial = ber.readString(tag)
if (this.attribute === 'objectclass') { this.initial = this.initial.toLowerCase() }
break
case 0x81: { // Any
let anyVal = ber.readString(tag)
if (this.attribute === 'objectclass') { anyVal = anyVal.toLowerCase() }
this.any.push(anyVal)
break
}
case 0x82: // Final
this.final = ber.readString(tag)
if (this.attribute === 'objectclass') { this.final = this.final.toLowerCase() }
break
default:
throw new Error('Invalid substrings filter type: 0x' + tag.toString(16))
}
}
return true
}
SubstringFilter.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.attribute)
ber.startSequence()
if (this.initial) { ber.writeString(this.initial, 0x80) }
if (this.any && this.any.length) {
this.any.forEach(function (s) {
ber.writeString(s, 0x81)
})
}
if (this.final) { ber.writeString(this.final, 0x82) }
ber.endSequence()
return ber
}

View File

@ -3,16 +3,16 @@
const logger = require('./logger')
const client = require('./client')
const Attribute = require('@ldapjs/attribute')
const Change = require('@ldapjs/change')
const Protocol = require('@ldapjs/protocol')
const Attribute = require('./attribute')
const Change = require('./change')
const Protocol = require('./protocol')
const Server = require('./server')
const controls = require('./controls')
const persistentSearch = require('./persistent_search')
const dn = require('@ldapjs/dn')
const dn = require('./dn')
const errors = require('./errors')
const filters = require('@ldapjs/filter')
const filters = require('./filters')
const messages = require('./messages')
const url = require('./url')
@ -24,7 +24,7 @@ module.exports = {
Client: client.Client,
createClient: client.createClient,
Server,
Server: Server,
createServer: function (options) {
if (options === undefined) { options = {} }
@ -37,21 +37,21 @@ module.exports = {
return new Server(options)
},
Attribute,
Change,
Attribute: Attribute,
Change: Change,
dn,
dn: dn,
DN: dn.DN,
RDN: dn.RDN,
parseDN: dn.DN.fromString,
parseDN: dn.parse,
persistentSearch,
persistentSearch: persistentSearch,
PersistentSearchCache: persistentSearch.PersistentSearchCache,
filters,
filters: filters,
parseFilter: filters.parseString,
url,
url: url,
parseURL: url.parse
}

View File

@ -0,0 +1,87 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- API
function AbandonRequest (options) {
options = options || {}
assert.object(options)
assert.optionalNumber(options.abandonID)
options.protocolOp = Protocol.LDAP_REQ_ABANDON
LDAPMessage.call(this, options)
this.abandonID = options.abandonID || 0
}
util.inherits(AbandonRequest, LDAPMessage)
Object.defineProperties(AbandonRequest.prototype, {
type: {
get: function getType () { return 'AbandonRequest' },
configurable: false
}
})
AbandonRequest.prototype._parse = function (ber, length) {
assert.ok(ber)
assert.ok(length)
// What a PITA - have to replicate ASN.1 integer logic to work around the
// way abandon is encoded and the way ldapjs framework handles "normal"
// messages
const buf = ber.buffer
let offset = 0
let value = 0
const fb = buf[offset++]
value = fb & 0x7F
for (let i = 1; i < length; i++) {
value <<= 8
value |= (buf[offset++] & 0xff)
}
if ((fb & 0x80) === 0x80) { value = -value }
ber._offset += length
this.abandonID = value
return true
}
AbandonRequest.prototype._toBer = function (ber) {
assert.ok(ber)
let i = this.abandonID
let sz = 4
while ((((i & 0xff800000) === 0) || ((i & 0xff800000) === 0xff800000)) &&
(sz > 1)) {
sz--
i <<= 8
}
assert.ok(sz <= 4)
while (sz-- > 0) {
ber.writeByte((i & 0xff000000) >> 24)
i <<= 8
}
return ber
}
AbandonRequest.prototype._json = function (j) {
assert.ok(j)
j.abandonID = this.abandonID
return j
}
/// --- Exports
module.exports = AbandonRequest

View File

@ -0,0 +1,34 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./result')
// var Protocol = require('../protocol')
/// --- API
function AbandonResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = 0
LDAPMessage.call(this, options)
}
util.inherits(AbandonResponse, LDAPMessage)
Object.defineProperties(AbandonResponse.prototype, {
type: {
get: function getType () { return 'AbandonResponse' },
configurable: false
}
})
AbandonResponse.prototype.end = function (status) {}
AbandonResponse.prototype._json = function (j) {
return j
}
/// --- Exports
module.exports = AbandonResponse

159
lib/messages/add_request.js Normal file
View File

@ -0,0 +1,159 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Attribute = require('../attribute')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function AddRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.entry)
lassert.optionalArrayOfAttribute(options.attributes)
options.protocolOp = Protocol.LDAP_REQ_ADD
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.attributes = options.attributes ? options.attributes.slice(0) : []
}
util.inherits(AddRequest, LDAPMessage)
Object.defineProperties(AddRequest.prototype, {
type: {
get: function getType () { return 'AddRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
AddRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const a = new Attribute()
a.parse(ber)
a.type = a.type.toLowerCase()
if (a.type === 'objectclass') {
for (let i = 0; i < a.vals.length; i++) { a.vals[i] = a.vals[i].toLowerCase() }
}
this.attributes.push(a)
}
this.attributes.sort(Attribute.compare)
return true
}
AddRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.entry.toString())
ber.startSequence()
this.attributes.forEach(function (a) {
a.toBer(ber)
})
ber.endSequence()
return ber
}
AddRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.attributes = []
this.attributes.forEach(function (a) {
j.attributes.push(a.json)
})
return j
}
AddRequest.prototype.indexOf = function (attr) {
if (!attr || typeof (attr) !== 'string') { throw new TypeError('attr (string) required') }
for (let i = 0; i < this.attributes.length; i++) {
if (this.attributes[i].type === attr) { return i }
}
return -1
}
AddRequest.prototype.attributeNames = function () {
const attrs = []
for (let i = 0; i < this.attributes.length; i++) { attrs.push(this.attributes[i].type.toLowerCase()) }
return attrs
}
AddRequest.prototype.getAttribute = function (name) {
if (!name || typeof (name) !== 'string') { throw new TypeError('attribute name (string) required') }
name = name.toLowerCase()
for (let i = 0; i < this.attributes.length; i++) {
if (this.attributes[i].type === name) { return this.attributes[i] }
}
return null
}
AddRequest.prototype.addAttribute = function (attr) {
if (!(attr instanceof Attribute)) { throw new TypeError('attribute (Attribute) required') }
return this.attributes.push(attr)
}
/**
* Returns a "pure" JS representation of this object.
*
* An example object would look like:
*
* {
* "dn": "cn=unit, dc=test",
* "attributes": {
* "cn": ["unit", "foo"],
* "objectclass": ["top", "person"]
* }
* }
*
* @return {Object} that looks like the above.
*/
AddRequest.prototype.toObject = function () {
const self = this
const obj = {
dn: self.entry ? self.entry.toString() : '',
attributes: {}
}
if (!this.attributes || !this.attributes.length) { return obj }
this.attributes.forEach(function (a) {
if (!obj.attributes[a.type]) { obj.attributes[a.type] = [] }
a.vals.forEach(function (v) {
if (obj.attributes[a.type].indexOf(v) === -1) { obj.attributes[a.type].push(v) }
})
})
return obj
}
/// --- Exports
module.exports = AddRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function AddResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_ADD
LDAPResult.call(this, options)
}
util.inherits(AddResponse, LDAPResult)
/// --- Exports
module.exports = AddResponse

View File

@ -0,0 +1,84 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- Globals
const Ber = asn1.Ber
const LDAP_BIND_SIMPLE = 'simple'
// var LDAP_BIND_SASL = 'sasl'
/// --- API
function BindRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_BIND
LDAPMessage.call(this, options)
this.version = options.version || 0x03
this.name = options.name || null
this.authentication = options.authentication || LDAP_BIND_SIMPLE
this.credentials = options.credentials || ''
}
util.inherits(BindRequest, LDAPMessage)
Object.defineProperties(BindRequest.prototype, {
type: {
get: function getType () { return 'BindRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.name },
configurable: false
}
})
BindRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.version = ber.readInt()
this.name = ber.readString()
const t = ber.peek()
// TODO add support for SASL et al
if (t !== Ber.Context) { throw new Error('authentication 0x' + t.toString(16) + ' not supported') }
this.authentication = LDAP_BIND_SIMPLE
this.credentials = ber.readString(Ber.Context)
return true
}
BindRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeInt(this.version)
ber.writeString((this.name || '').toString())
// TODO add support for SASL et al
ber.writeString((this.credentials || ''), Ber.Context)
return ber
}
BindRequest.prototype._json = function (j) {
assert.ok(j)
j.version = this.version
j.name = this.name
j.authenticationType = this.authentication
j.credentials = this.credentials
return j
}
/// --- Exports
module.exports = BindRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function BindResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_BIND
LDAPResult.call(this, options)
}
util.inherits(BindResponse, LDAPResult)
/// --- Exports
module.exports = BindResponse

View File

@ -0,0 +1,74 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function CompareRequest (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.attribute)
assert.optionalString(options.value)
lassert.optionalStringDN(options.entry)
options.protocolOp = Protocol.LDAP_REQ_COMPARE
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.attribute = options.attribute || ''
this.value = options.value || ''
}
util.inherits(CompareRequest, LDAPMessage)
Object.defineProperties(CompareRequest.prototype, {
type: {
get: function getType () { return 'CompareRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
CompareRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
ber.readSequence()
this.attribute = ber.readString().toLowerCase()
this.value = ber.readString()
return true
}
CompareRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.entry.toString())
ber.startSequence()
ber.writeString(this.attribute)
ber.writeString(this.value)
ber.endSequence()
return ber
}
CompareRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.attribute = this.attribute
j.value = this.value
return j
}
/// --- Exports
module.exports = CompareRequest

View File

@ -0,0 +1,33 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function CompareResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_COMPARE
LDAPResult.call(this, options)
}
util.inherits(CompareResponse, LDAPResult)
CompareResponse.prototype.end = function (matches) {
let status = 0x06
if (typeof (matches) === 'boolean') {
if (!matches) { status = 0x05 } // Compare false
} else {
status = matches
}
return LDAPResult.prototype.end.call(this, status)
}
/// --- Exports
module.exports = CompareResponse

View File

@ -0,0 +1,62 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function DeleteRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.entry)
options.protocolOp = Protocol.LDAP_REQ_DELETE
LDAPMessage.call(this, options)
this.entry = options.entry || null
}
util.inherits(DeleteRequest, LDAPMessage)
Object.defineProperties(DeleteRequest.prototype, {
type: {
get: function getType () { return 'DeleteRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
DeleteRequest.prototype._parse = function (ber, length) {
assert.ok(ber)
this.entry = ber.buffer.slice(0, length).toString('utf8')
ber._offset += ber.length
return true
}
DeleteRequest.prototype._toBer = function (ber) {
assert.ok(ber)
const buf = Buffer.from(this.entry.toString())
for (let i = 0; i < buf.length; i++) { ber.writeByte(buf[i]) }
return ber
}
DeleteRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry
return j
}
/// --- Exports
module.exports = DeleteRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function DeleteResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_DELETE
LDAPResult.call(this, options)
}
util.inherits(DeleteResponse, LDAPResult)
/// --- Exports
module.exports = DeleteResponse

117
lib/messages/ext_request.js Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- API
function ExtendedRequest (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.requestName)
if (options.requestValue &&
!(Buffer.isBuffer(options.requestValue) ||
typeof (options.requestValue) === 'string')) {
throw new TypeError('options.requestValue must be a buffer or a string')
}
options.protocolOp = Protocol.LDAP_REQ_EXTENSION
LDAPMessage.call(this, options)
this.requestName = options.requestName || ''
this.requestValue = options.requestValue
if (Buffer.isBuffer(this.requestValue)) {
this.requestValueBuffer = this.requestValue
} else {
this.requestValueBuffer = Buffer.from(this.requestValue || '', 'utf8')
}
}
util.inherits(ExtendedRequest, LDAPMessage)
Object.defineProperties(ExtendedRequest.prototype, {
type: {
get: function getType () { return 'ExtendedRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.requestName },
configurable: false
},
name: {
get: function getName () { return this.requestName },
set: function setName (val) {
assert.string(val)
this.requestName = val
},
configurable: false
},
value: {
get: function getValue () { return this.requestValue },
set: function setValue (val) {
if (!(Buffer.isBuffer(val) || typeof (val) === 'string')) { throw new TypeError('value must be a buffer or a string') }
if (Buffer.isBuffer(val)) {
this.requestValueBuffer = val
} else {
this.requestValueBuffer = Buffer.from(val, 'utf8')
}
this.requestValue = val
},
configurable: false
},
valueBuffer: {
get: function getValueBuffer () {
return this.requestValueBuffer
},
set: function setValueBuffer (val) {
if (!Buffer.isBuffer(val)) { throw new TypeError('valueBuffer must be a buffer') }
this.value = val
},
configurable: false
}
})
ExtendedRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.requestName = ber.readString(0x80)
if (ber.peek() === 0x81) {
this.requestValueBuffer = ber.readString(0x81, true)
this.requestValue = this.requestValueBuffer.toString('utf8')
}
return true
}
ExtendedRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.requestName, 0x80)
if (Buffer.isBuffer(this.requestValue)) {
ber.writeBuffer(this.requestValue, 0x81)
} else if (typeof (this.requestValue) === 'string') {
ber.writeString(this.requestValue, 0x81)
}
return ber
}
ExtendedRequest.prototype._json = function (j) {
assert.ok(j)
j.requestName = this.requestName
j.requestValue = (Buffer.isBuffer(this.requestValue))
? this.requestValue.toString('hex')
: this.requestValue
return j
}
/// --- Exports
module.exports = ExtendedRequest

View File

@ -0,0 +1,86 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ExtendedResponse (options) {
options = options || {}
assert.object(options)
assert.optionalString(options.responseName)
assert.optionalString(options.responsevalue)
this.responseName = options.responseName || undefined
this.responseValue = options.responseValue || undefined
options.protocolOp = Protocol.LDAP_REP_EXTENSION
LDAPResult.call(this, options)
}
util.inherits(ExtendedResponse, LDAPResult)
Object.defineProperties(ExtendedResponse.prototype, {
type: {
get: function getType () { return 'ExtendedResponse' },
configurable: false
},
_dn: {
get: function getDN () { return this.responseName },
configurable: false
},
name: {
get: function getName () { return this.responseName },
set: function setName (val) {
assert.string(val)
this.responseName = val
},
configurable: false
},
value: {
get: function getValue () { return this.responseValue },
set: function (val) {
assert.string(val)
this.responseValue = val
},
configurable: false
}
})
ExtendedResponse.prototype._parse = function (ber) {
assert.ok(ber)
if (!LDAPResult.prototype._parse.call(this, ber)) { return false }
if (ber.peek() === 0x8a) { this.responseName = ber.readString(0x8a) }
if (ber.peek() === 0x8b) { this.responseValue = ber.readString(0x8b) }
return true
}
ExtendedResponse.prototype._toBer = function (ber) {
assert.ok(ber)
if (!LDAPResult.prototype._toBer.call(this, ber)) { return false }
if (this.responseName) { ber.writeString(this.responseName, 0x8a) }
if (this.responseValue) { ber.writeString(this.responseValue, 0x8b) }
return ber
}
ExtendedResponse.prototype._json = function (j) {
assert.ok(j)
j = LDAPResult.prototype._json.call(this, j)
j.responseName = this.responseName
j.responseValue = this.responseValue
return j
}
/// --- Exports
module.exports = ExtendedResponse

View File

@ -1,39 +1,61 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const messages = require('@ldapjs/messages')
const LDAPMessage = require('./message')
const LDAPResult = require('./result')
const Parser = require('./parser')
const AbandonRequest = require('./abandon_request')
const AbandonResponse = require('./abandon_response')
const AddRequest = require('./add_request')
const AddResponse = require('./add_response')
const BindRequest = require('./bind_request')
const BindResponse = require('./bind_response')
const CompareRequest = require('./compare_request')
const CompareResponse = require('./compare_response')
const DeleteRequest = require('./del_request')
const DeleteResponse = require('./del_response')
const ExtendedRequest = require('./ext_request')
const ExtendedResponse = require('./ext_response')
const ModifyRequest = require('./modify_request')
const ModifyResponse = require('./modify_response')
const ModifyDNRequest = require('./moddn_request')
const ModifyDNResponse = require('./moddn_response')
const SearchRequest = require('./search_request')
const SearchEntry = require('./search_entry')
const SearchReference = require('./search_reference')
const SearchResponse = require('./search_response')
const UnbindRequest = require('./unbind_request')
const UnbindResponse = require('./unbind_response')
/// --- API
module.exports = {
LDAPMessage: messages.LdapMessage,
LDAPResult: messages.LdapResult,
Parser,
LDAPMessage: LDAPMessage,
LDAPResult: LDAPResult,
Parser: Parser,
AbandonRequest: messages.AbandonRequest,
AbandonResponse: messages.AbandonResponse,
AddRequest: messages.AddRequest,
AddResponse: messages.AddResponse,
BindRequest: messages.BindRequest,
BindResponse: messages.BindResponse,
CompareRequest: messages.CompareRequest,
CompareResponse: messages.CompareResponse,
DeleteRequest: messages.DeleteRequest,
DeleteResponse: messages.DeleteResponse,
ExtendedRequest: messages.ExtensionRequest,
ExtendedResponse: messages.ExtensionResponse,
ModifyRequest: messages.ModifyRequest,
ModifyResponse: messages.ModifyResponse,
ModifyDNRequest: messages.ModifyDnRequest,
ModifyDNResponse: messages.ModifyDnResponse,
SearchRequest: messages.SearchRequest,
SearchEntry: messages.SearchResultEntry,
SearchReference: messages.SearchResultReference,
SearchResponse,
UnbindRequest: messages.UnbindRequest
AbandonRequest: AbandonRequest,
AbandonResponse: AbandonResponse,
AddRequest: AddRequest,
AddResponse: AddResponse,
BindRequest: BindRequest,
BindResponse: BindResponse,
CompareRequest: CompareRequest,
CompareResponse: CompareResponse,
DeleteRequest: DeleteRequest,
DeleteResponse: DeleteResponse,
ExtendedRequest: ExtendedRequest,
ExtendedResponse: ExtendedResponse,
ModifyRequest: ModifyRequest,
ModifyResponse: ModifyResponse,
ModifyDNRequest: ModifyDNRequest,
ModifyDNResponse: ModifyDNResponse,
SearchRequest: SearchRequest,
SearchEntry: SearchEntry,
SearchReference: SearchReference,
SearchResponse: SearchResponse,
UnbindRequest: UnbindRequest,
UnbindResponse: UnbindResponse
}

110
lib/messages/message.js Normal file
View File

@ -0,0 +1,110 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const logger = require('../logger')
// var Control = require('../controls').Control
// var Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
// var BerReader = asn1.BerReader
const BerWriter = asn1.BerWriter
const getControl = require('../controls').getControl
/// --- API
/**
* LDAPMessage structure.
*
* @param {Object} options stuff.
*/
function LDAPMessage (options) {
assert.object(options)
this.messageID = options.messageID || 0
this.protocolOp = options.protocolOp || undefined
this.controls = options.controls ? options.controls.slice(0) : []
this.log = options.log || logger
}
Object.defineProperties(LDAPMessage.prototype, {
id: {
get: function getId () { return this.messageID },
configurable: false
},
dn: {
get: function getDN () { return this._dn || '' },
configurable: false
},
type: {
get: function getType () { return 'LDAPMessage' },
configurable: false
},
json: {
get: function () {
const out = this._json({
messageID: this.messageID,
protocolOp: this.type
})
out.controls = this.controls
return out
},
configurable: false
}
})
LDAPMessage.prototype.toString = function () {
return JSON.stringify(this.json)
}
LDAPMessage.prototype.parse = function (ber) {
assert.ok(ber)
this.log.trace('parse: data=%s', util.inspect(ber.buffer))
// Delegate off to the specific type to parse
this._parse(ber, ber.length)
// Look for controls
if (ber.peek() === 0xa0) {
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = getControl(ber)
if (c) { this.controls.push(c) }
}
}
this.log.trace('Parsing done: %j', this.json)
return true
}
LDAPMessage.prototype.toBer = function () {
let writer = new BerWriter()
writer.startSequence()
writer.writeInt(this.messageID)
writer.startSequence(this.protocolOp)
if (this._toBer) { writer = this._toBer(writer) }
writer.endSequence()
if (this.controls && this.controls.length) {
writer.startSequence(0xa0)
this.controls.forEach(function (c) {
c.toBer(writer)
})
writer.endSequence()
}
writer.endSequence()
return writer.buffer
}
/// --- Exports
module.exports = LDAPMessage

View File

@ -0,0 +1,85 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const dn = require('../dn')
const lassert = require('../assert')
/// --- API
function ModifyDNRequest (options) {
options = options || {}
assert.object(options)
assert.optionalBool(options.deleteOldRdn)
lassert.optionalStringDN(options.entry)
lassert.optionalDN(options.newRdn)
lassert.optionalDN(options.newSuperior)
options.protocolOp = Protocol.LDAP_REQ_MODRDN
LDAPMessage.call(this, options)
this.entry = options.entry || null
this.newRdn = options.newRdn || null
this.deleteOldRdn = options.deleteOldRdn || true
this.newSuperior = options.newSuperior || null
}
util.inherits(ModifyDNRequest, LDAPMessage)
Object.defineProperties(ModifyDNRequest.prototype, {
type: {
get: function getType () { return 'ModifyDNRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.entry },
configurable: false
}
})
ModifyDNRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.entry = ber.readString()
this.newRdn = dn.parse(ber.readString())
this.deleteOldRdn = ber.readBoolean()
if (ber.peek() === 0x80) { this.newSuperior = dn.parse(ber.readString(0x80)) }
return true
}
ModifyDNRequest.prototype._toBer = function (ber) {
// assert.ok(ber);
ber.writeString(this.entry.toString())
ber.writeString(this.newRdn.toString())
ber.writeBoolean(this.deleteOldRdn)
if (this.newSuperior) {
const s = this.newSuperior.toString()
const len = Buffer.byteLength(s)
ber.writeByte(0x80) // MODIFY_DN_REQUEST_NEW_SUPERIOR_TAG
ber.writeByte(len)
ber._ensure(len)
ber._buf.write(s, ber._offset)
ber._offset += len
}
return ber
}
ModifyDNRequest.prototype._json = function (j) {
assert.ok(j)
j.entry = this.entry.toString()
j.newRdn = this.newRdn.toString()
j.deleteOldRdn = this.deleteOldRdn
j.newSuperior = this.newSuperior ? this.newSuperior.toString() : ''
return j
}
/// --- Exports
module.exports = ModifyDNRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ModifyDNResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_MODRDN
LDAPResult.call(this, options)
}
util.inherits(ModifyDNResponse, LDAPResult)
/// --- Exports
module.exports = ModifyDNResponse

View File

@ -0,0 +1,83 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const Change = require('../change')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- API
function ModifyRequest (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.object)
lassert.optionalArrayOfAttribute(options.attributes)
options.protocolOp = Protocol.LDAP_REQ_MODIFY
LDAPMessage.call(this, options)
this.object = options.object || null
this.changes = options.changes ? options.changes.slice(0) : []
}
util.inherits(ModifyRequest, LDAPMessage)
Object.defineProperties(ModifyRequest.prototype, {
type: {
get: function getType () { return 'ModifyRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.object },
configurable: false
}
})
ModifyRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.object = ber.readString()
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) {
const c = new Change()
c.parse(ber)
c.modification.type = c.modification.type.toLowerCase()
this.changes.push(c)
}
this.changes.sort(Change.compare)
return true
}
ModifyRequest.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeString(this.object.toString())
ber.startSequence()
this.changes.forEach(function (c) {
c.toBer(ber)
})
ber.endSequence()
return ber
}
ModifyRequest.prototype._json = function (j) {
assert.ok(j)
j.object = this.object
j.changes = []
this.changes.forEach(function (c) {
j.changes.push(c.json)
})
return j
}
/// --- Exports
module.exports = ModifyRequest

View File

@ -0,0 +1,22 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPResult = require('./result')
const Protocol = require('../protocol')
/// --- API
function ModifyResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_MODIFY
LDAPResult.call(this, options)
}
util.inherits(ModifyResponse, LDAPResult)
/// --- Exports
module.exports = ModifyResponse

View File

@ -4,36 +4,40 @@ const EventEmitter = require('events').EventEmitter
const util = require('util')
const assert = require('assert-plus')
const asn1 = require('@ldapjs/asn1')
const asn1 = require('asn1')
// var VError = require('verror').VError
const logger = require('../logger')
const messages = require('@ldapjs/messages')
const AbandonRequest = messages.AbandonRequest
const AddRequest = messages.AddRequest
const AddResponse = messages.AddResponse
const BindRequest = messages.BindRequest
const BindResponse = messages.BindResponse
const CompareRequest = messages.CompareRequest
const CompareResponse = messages.CompareResponse
const DeleteRequest = messages.DeleteRequest
const DeleteResponse = messages.DeleteResponse
const ExtendedRequest = messages.ExtensionRequest
const ExtendedResponse = messages.ExtensionResponse
const ModifyRequest = messages.ModifyRequest
const ModifyResponse = messages.ModifyResponse
const ModifyDNRequest = messages.ModifyDnRequest
const ModifyDNResponse = messages.ModifyDnResponse
const SearchRequest = messages.SearchRequest
const SearchEntry = messages.SearchResultEntry
const SearchReference = messages.SearchResultReference
const AbandonRequest = require('./abandon_request')
const AddRequest = require('./add_request')
const AddResponse = require('./add_response')
const BindRequest = require('./bind_request')
const BindResponse = require('./bind_response')
const CompareRequest = require('./compare_request')
const CompareResponse = require('./compare_response')
const DeleteRequest = require('./del_request')
const DeleteResponse = require('./del_response')
const ExtendedRequest = require('./ext_request')
const ExtendedResponse = require('./ext_response')
const ModifyRequest = require('./modify_request')
const ModifyResponse = require('./modify_response')
const ModifyDNRequest = require('./moddn_request')
const ModifyDNResponse = require('./moddn_response')
const SearchRequest = require('./search_request')
const SearchEntry = require('./search_entry')
const SearchReference = require('./search_reference')
const SearchResponse = require('./search_response')
const UnbindRequest = messages.UnbindRequest
const LDAPResult = messages.LdapResult
const UnbindRequest = require('./unbind_request')
// var UnbindResponse = require('./unbind_response')
const Protocol = require('@ldapjs/protocol')
const LDAPResult = require('./result')
// var Message = require('./message')
const Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
const BerReader = asn1.BerReader
/// --- API
@ -48,13 +52,6 @@ function Parser (options = {}) {
}
util.inherits(Parser, EventEmitter)
/**
* The LDAP server/client implementations will receive data from a stream and feed
* it into this method. This method will collect that data into an internal
* growing buffer. As that buffer fills with enough data to constitute a valid
* LDAP message, the data will be parsed, emitted as a message object, and
* reset the buffer to account for any next message in the stream.
*/
Parser.prototype.write = function (data) {
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
@ -67,9 +64,9 @@ Parser.prototype.write = function (data) {
return true
}
self.buffer = self.buffer ? Buffer.concat([self.buffer, data]) : data
self.buffer = (self.buffer ? Buffer.concat([self.buffer, data]) : data)
let ber = new BerReader(self.buffer)
const ber = new BerReader(self.buffer)
let foundSeq = false
try {
@ -83,22 +80,9 @@ Parser.prototype.write = function (data) {
return false
} else if (ber.remain > ber.length) {
// ETOOMUCH
// This is an odd branch. Basically, it is setting `nextMessage` to
// a buffer that represents data part of a message subsequent to the one
// being processed. It then re-creates `ber` as a representation of
// the message being processed and advances its offset to the value
// position of the TLV.
// Set `nextMessage` to the bytes subsequent to the current message's
// value bytes. That is, slice from the byte immediately following the
// current message's value bytes until the end of the buffer.
// This is sort of ugly, but allows us to make miminal copies
nextMessage = self.buffer.slice(ber.offset + ber.length)
const currOffset = ber.offset
ber = new BerReader(ber.buffer.subarray(0, currOffset + ber.length))
ber.readSequence()
ber._size = ber.offset + ber.length
assert.equal(ber.remain, ber.length)
}
@ -108,25 +92,13 @@ Parser.prototype.write = function (data) {
let message
try {
if (Object.prototype.toString.call(ber) === '[object BerReader]') {
// Parse the BER into a JavaScript object representation. The message
// objects require the full sequence in order to construct the object.
// At this point, we have already read the sequence tag and length, so
// we need to rewind the buffer a bit. The `.sequenceToReader` method
// does this for us.
message = messages.LdapMessage.parse(ber.sequenceToReader())
} else {
// Bail here if peer isn't speaking protocol at all
message = this.getMessage(ber)
}
// Bail here if peer isn't speaking protocol at all
message = this.getMessage(ber)
if (!message) {
return end()
}
// TODO: find a better way to handle logging now that messages and the
// server are decoupled. ~ jsumners 2023-02-17
message.log = this.log
message.parse(ber)
} catch (e) {
this.emit('error', e, message)
return false
@ -141,88 +113,88 @@ Parser.prototype.getMessage = function (ber) {
const self = this
const messageId = ber.readInt()
const messageID = ber.readInt()
const type = ber.readSequence()
let Message
switch (type) {
case Protocol.operations.LDAP_REQ_ABANDON:
case Protocol.LDAP_REQ_ABANDON:
Message = AbandonRequest
break
case Protocol.operations.LDAP_REQ_ADD:
case Protocol.LDAP_REQ_ADD:
Message = AddRequest
break
case Protocol.operations.LDAP_RES_ADD:
case Protocol.LDAP_REP_ADD:
Message = AddResponse
break
case Protocol.operations.LDAP_REQ_BIND:
case Protocol.LDAP_REQ_BIND:
Message = BindRequest
break
case Protocol.operations.LDAP_RES_BIND:
case Protocol.LDAP_REP_BIND:
Message = BindResponse
break
case Protocol.operations.LDAP_REQ_COMPARE:
case Protocol.LDAP_REQ_COMPARE:
Message = CompareRequest
break
case Protocol.operations.LDAP_RES_COMPARE:
case Protocol.LDAP_REP_COMPARE:
Message = CompareResponse
break
case Protocol.operations.LDAP_REQ_DELETE:
case Protocol.LDAP_REQ_DELETE:
Message = DeleteRequest
break
case Protocol.operations.LDAP_RES_DELETE:
case Protocol.LDAP_REP_DELETE:
Message = DeleteResponse
break
case Protocol.operations.LDAP_REQ_EXTENSION:
case Protocol.LDAP_REQ_EXTENSION:
Message = ExtendedRequest
break
case Protocol.operations.LDAP_RES_EXTENSION:
case Protocol.LDAP_REP_EXTENSION:
Message = ExtendedResponse
break
case Protocol.operations.LDAP_REQ_MODIFY:
case Protocol.LDAP_REQ_MODIFY:
Message = ModifyRequest
break
case Protocol.operations.LDAP_RES_MODIFY:
case Protocol.LDAP_REP_MODIFY:
Message = ModifyResponse
break
case Protocol.operations.LDAP_REQ_MODRDN:
case Protocol.LDAP_REQ_MODRDN:
Message = ModifyDNRequest
break
case Protocol.operations.LDAP_RES_MODRDN:
case Protocol.LDAP_REP_MODRDN:
Message = ModifyDNResponse
break
case Protocol.operations.LDAP_REQ_SEARCH:
case Protocol.LDAP_REQ_SEARCH:
Message = SearchRequest
break
case Protocol.operations.LDAP_RES_SEARCH_ENTRY:
case Protocol.LDAP_REP_SEARCH_ENTRY:
Message = SearchEntry
break
case Protocol.operations.LDAP_RES_SEARCH_REF:
case Protocol.LDAP_REP_SEARCH_REF:
Message = SearchReference
break
case Protocol.operations.LDAP_RES_SEARCH:
case Protocol.LDAP_REP_SEARCH:
Message = SearchResponse
break
case Protocol.operations.LDAP_REQ_UNBIND:
case Protocol.LDAP_REQ_UNBIND:
Message = UnbindRequest
break
@ -231,15 +203,15 @@ Parser.prototype.getMessage = function (ber) {
new Error('Op 0x' + (type ? type.toString(16) : '??') +
' not supported'),
new LDAPResult({
messageId,
protocolOp: type || Protocol.operations.LDAP_RES_EXTENSION
messageID: messageID,
protocolOp: type || Protocol.LDAP_REP_EXTENSION
}))
return false
}
return new Message({
messageId,
messageID: messageID,
log: self.log
})
}

121
lib/messages/result.js Normal file
View File

@ -0,0 +1,121 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
// var asn1 = require('asn1')
const dtrace = require('../dtrace')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
/// --- Globals
// var Ber = asn1.Ber
// var BerWriter = asn1.BerWriter
/// --- API
function LDAPResult (options) {
options = options || {}
assert.object(options)
assert.optionalNumber(options.status)
assert.optionalString(options.matchedDN)
assert.optionalString(options.errorMessage)
assert.optionalArrayOfString(options.referrals)
LDAPMessage.call(this, options)
this.status = options.status || 0 // LDAP SUCCESS
this.matchedDN = options.matchedDN || ''
this.errorMessage = options.errorMessage || ''
this.referrals = options.referrals || []
this.connection = options.connection || null
}
util.inherits(LDAPResult, LDAPMessage)
Object.defineProperties(LDAPResult.prototype, {
type: {
get: function getType () { return 'LDAPResult' },
configurable: false
}
})
LDAPResult.prototype.end = function (status) {
assert.ok(this.connection)
if (typeof (status) === 'number') { this.status = status }
const ber = this.toBer()
this.log.debug('%s: sending: %j', this.connection.ldap.id, this.json)
try {
const self = this
this.connection.write(ber)
if (self._dtraceOp && self._dtraceId) {
dtrace.fire('server-' + self._dtraceOp + '-done', function () {
const c = self.connection || { ldap: {} }
return [
self._dtraceId || 0,
(c.remoteAddress || ''),
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
(self.requestDN ? self.requestDN.toString() : ''),
status || self.status,
self.errorMessage
]
})
}
} catch (e) {
this.log.warn(e, '%s failure to write message %j',
this.connection.ldap.id, this.json)
}
}
LDAPResult.prototype._parse = function (ber) {
assert.ok(ber)
this.status = ber.readEnumeration()
this.matchedDN = ber.readString()
this.errorMessage = ber.readString()
const t = ber.peek()
if (t === Protocol.LDAP_REP_REFERRAL) {
const end = ber.offset + ber.length
while (ber.offset < end) { this.referrals.push(ber.readString()) }
}
return true
}
LDAPResult.prototype._toBer = function (ber) {
assert.ok(ber)
ber.writeEnumeration(this.status)
ber.writeString(this.matchedDN || '')
ber.writeString(this.errorMessage || '')
if (this.referrals.length) {
ber.startSequence(Protocol.LDAP_REP_REFERRAL)
ber.writeStringArray(this.referrals)
ber.endSequence()
}
return ber
}
LDAPResult.prototype._json = function (j) {
assert.ok(j)
j.status = this.status
j.matchedDN = this.matchedDN
j.errorMessage = this.errorMessage
j.referrals = this.referrals
return j
}
/// --- Exports
module.exports = LDAPResult

View File

@ -0,0 +1,188 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
// var asn1 = require('asn1')
const LDAPMessage = require('./message')
const Attribute = require('../attribute')
const Protocol = require('../protocol')
const lassert = require('../assert')
/// --- Globals
// var BerWriter = asn1.BerWriter
/// --- API
function SearchEntry (options) {
options = options || {}
assert.object(options)
lassert.optionalStringDN(options.objectName)
options.protocolOp = Protocol.LDAP_REP_SEARCH_ENTRY
LDAPMessage.call(this, options)
this.objectName = options.objectName || null
this.setAttributes(options.attributes || [])
}
util.inherits(SearchEntry, LDAPMessage)
Object.defineProperties(SearchEntry.prototype, {
type: {
get: function getType () { return 'SearchEntry' },
configurable: false
},
_dn: {
get: function getDN () { return this.objectName },
configurable: false
},
object: {
get: function getObject () {
const obj = {
dn: this.dn.toString(),
controls: []
}
this.attributes.forEach(function (a) {
if (a.vals && a.vals.length) {
if (a.vals.length > 1) {
obj[a.type] = a.vals.slice()
} else {
obj[a.type] = a.vals[0]
}
} else {
obj[a.type] = []
}
})
this.controls.forEach(function (element, index, array) {
obj.controls.push(element.json)
})
return obj
},
configurable: false
},
raw: {
get: function getRaw () {
const obj = {
dn: this.dn.toString(),
controls: []
}
this.attributes.forEach(function (a) {
if (a.buffers && a.buffers.length) {
if (a.buffers.length > 1) {
obj[a.type] = a.buffers.slice()
} else {
obj[a.type] = a.buffers[0]
}
} else {
obj[a.type] = []
}
})
this.controls.forEach(function (element, index, array) {
obj.controls.push(element.json)
})
return obj
},
configurable: false
}
})
SearchEntry.prototype.addAttribute = function (attr) {
if (!attr || typeof (attr) !== 'object') { throw new TypeError('attr (attribute) required') }
this.attributes.push(attr)
}
SearchEntry.prototype.toObject = function () {
return this.object
}
SearchEntry.prototype.fromObject = function (obj) {
if (typeof (obj) !== 'object') { throw new TypeError('object required') }
const self = this
if (obj.controls) { this.controls = obj.controls }
if (obj.attributes) { obj = obj.attributes }
this.attributes = []
Object.keys(obj).forEach(function (k) {
self.attributes.push(new Attribute({ type: k, vals: obj[k] }))
})
return true
}
SearchEntry.prototype.setAttributes = function (obj) {
if (typeof (obj) !== 'object') { throw new TypeError('object required') }
if (Array.isArray(obj)) {
obj.forEach(function (a) {
if (!Attribute.isAttribute(a)) { throw new TypeError('entry must be an Array of Attributes') }
})
this.attributes = obj
} else {
const self = this
self.attributes = []
Object.keys(obj).forEach(function (k) {
const attr = new Attribute({ type: k })
if (Array.isArray(obj[k])) {
obj[k].forEach(function (v) {
attr.addValue(v.toString())
})
} else {
attr.addValue(obj[k].toString())
}
self.attributes.push(attr)
})
}
}
SearchEntry.prototype._json = function (j) {
assert.ok(j)
j.objectName = this.objectName.toString()
j.attributes = []
this.attributes.forEach(function (a) {
j.attributes.push(a.json || a)
})
return j
}
SearchEntry.prototype._parse = function (ber) {
assert.ok(ber)
this.objectName = ber.readString()
assert.ok(ber.readSequence())
const end = ber.offset + ber.length
while (ber.offset < end) {
const a = new Attribute()
a.parse(ber)
this.attributes.push(a)
}
return true
}
SearchEntry.prototype._toBer = function (ber) {
assert.ok(ber)
const formattedObjectName = this.objectName.format({ skipSpace: true })
ber.writeString(formattedObjectName)
ber.startSequence()
this.attributes.forEach(function (a) {
// This may or may not be an attribute
ber = Attribute.toBer(a, ber)
})
ber.endSequence()
return ber
}
/// --- Exports
module.exports = SearchEntry

View File

@ -0,0 +1,101 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
// var asn1 = require('asn1')
const LDAPMessage = require('./message')
const Protocol = require('../protocol')
const dn = require('../dn')
const url = require('../url')
/// --- Globals
// var BerWriter = asn1.BerWriter
const parseURL = url.parse
/// --- API
function SearchReference (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REP_SEARCH_REF
LDAPMessage.call(this, options)
this.uris = options.uris || []
}
util.inherits(SearchReference, LDAPMessage)
Object.defineProperties(SearchReference.prototype, {
type: {
get: function getType () { return 'SearchReference' },
configurable: false
},
_dn: {
get: function getDN () { return new dn.DN('') },
configurable: false
},
object: {
get: function getObject () {
return {
dn: this.dn.toString(),
uris: this.uris.slice()
}
},
configurable: false
},
urls: {
get: function getUrls () { return this.uris },
set: function setUrls (val) {
assert.ok(val)
assert.ok(Array.isArray(val))
this.uris = val.slice()
},
configurable: false
}
})
SearchReference.prototype.toObject = function () {
return this.object
}
SearchReference.prototype.fromObject = function (obj) {
if (typeof (obj) !== 'object') { throw new TypeError('object required') }
this.uris = obj.uris ? obj.uris.slice() : []
return true
}
SearchReference.prototype._json = function (j) {
assert.ok(j)
j.uris = this.uris.slice()
return j
}
SearchReference.prototype._parse = function (ber, length) {
assert.ok(ber)
while (ber.offset < length) {
const _url = ber.readString()
parseURL(_url)
this.uris.push(_url)
}
return true
}
SearchReference.prototype._toBer = function (ber) {
assert.ok(ber)
this.uris.forEach(function (u) {
ber.writeString(u.href || u)
})
return ber
}
/// --- Exports
module.exports = SearchReference

View File

@ -0,0 +1,152 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const asn1 = require('asn1')
const LDAPMessage = require('./message')
// var LDAPResult = require('./result')
const dn = require('../dn')
const filters = require('../filters')
const Protocol = require('../protocol')
/// --- Globals
const Ber = asn1.Ber
/// --- API
function SearchRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_SEARCH
LDAPMessage.call(this, options)
if (options.baseObject !== undefined) {
this.baseObject = options.baseObject
} else {
this.baseObject = dn.parse('')
}
this.scope = options.scope || 'base'
this.derefAliases = options.derefAliases || Protocol.NEVER_DEREF_ALIASES
this.sizeLimit = options.sizeLimit || 0
this.timeLimit = options.timeLimit || 0
this.typesOnly = options.typesOnly || false
this.filter = options.filter || null
this.attributes = options.attributes ? options.attributes.slice(0) : []
}
util.inherits(SearchRequest, LDAPMessage)
Object.defineProperties(SearchRequest.prototype, {
type: {
get: function getType () { return 'SearchRequest' },
configurable: false
},
_dn: {
get: function getDN () { return this.baseObject },
configurable: false
},
scope: {
get: function getScope () {
switch (this._scope) {
case Protocol.SCOPE_BASE_OBJECT: return 'base'
case Protocol.SCOPE_ONE_LEVEL: return 'one'
case Protocol.SCOPE_SUBTREE: return 'sub'
default:
throw new Error(this._scope + ' is an invalid search scope')
}
},
set: function setScope (val) {
if (typeof (val) === 'string') {
switch (val) {
case 'base':
this._scope = Protocol.SCOPE_BASE_OBJECT
break
case 'one':
this._scope = Protocol.SCOPE_ONE_LEVEL
break
case 'sub':
this._scope = Protocol.SCOPE_SUBTREE
break
default:
throw new Error(val + ' is an invalid search scope')
}
} else {
this._scope = val
}
},
configurable: false
}
})
SearchRequest.prototype._parse = function (ber) {
assert.ok(ber)
this.baseObject = ber.readString()
this.scope = ber.readEnumeration()
this.derefAliases = ber.readEnumeration()
this.sizeLimit = ber.readInt()
this.timeLimit = ber.readInt()
this.typesOnly = ber.readBoolean()
this.filter = filters.parse(ber)
// look for attributes
if (ber.peek() === 0x30) {
ber.readSequence()
const end = ber.offset + ber.length
while (ber.offset < end) { this.attributes.push(ber.readString().toLowerCase()) }
}
return true
}
SearchRequest.prototype._toBer = function (ber) {
assert.ok(ber)
// Format only with commas, since that is what RFC 4514 mandates.
// There's a gotcha here: even though it's called baseObject,
// it can be a string or a DN object.
const formattedDN = dn.DN.isDN(this.baseObject)
? this.baseObject.format({ skipSpace: true })
: this.baseObject.toString()
ber.writeString(formattedDN)
ber.writeEnumeration(this._scope)
ber.writeEnumeration(this.derefAliases)
ber.writeInt(this.sizeLimit)
ber.writeInt(this.timeLimit)
ber.writeBoolean(this.typesOnly)
const f = this.filter || new filters.PresenceFilter({ attribute: 'objectclass' })
ber = f.toBer(ber)
ber.startSequence(Ber.Sequence | Ber.Constructor)
if (this.attributes && this.attributes.length) {
this.attributes.forEach(function (a) {
ber.writeString(a)
})
}
ber.endSequence()
return ber
}
SearchRequest.prototype._json = function (j) {
assert.ok(j)
j.baseObject = this.baseObject
j.scope = this.scope
j.derefAliases = this.derefAliases
j.sizeLimit = this.sizeLimit
j.timeLimit = this.timeLimit
j.typesOnly = this.typesOnly
j.filter = this.filter.toString()
j.attributes = this.attributes
return j
}
/// --- Exports
module.exports = SearchRequest

View File

@ -1,31 +1,31 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const Attribute = require('@ldapjs/attribute')
const {
SearchResultEntry: SearchEntry,
SearchResultReference: SearchReference,
SearchResultDone
} = require('@ldapjs/messages')
const LDAPResult = require('./result')
const SearchEntry = require('./search_entry')
const SearchReference = require('./search_reference')
const parseDN = require('@ldapjs/dn').DN.fromString
const dtrace = require('../dtrace')
const parseDN = require('../dn').parse
const parseURL = require('../url').parse
const Protocol = require('../protocol')
/// --- API
class SearchResponse extends SearchResultDone {
attributes
notAttributes
sentEntries
function SearchResponse (options) {
options = options || {}
assert.object(options)
constructor (options = {}) {
super(options)
options.protocolOp = Protocol.LDAP_REP_SEARCH
LDAPResult.call(this, options)
this.attributes = options.attributes ? options.attributes.slice() : []
this.notAttributes = []
this.sentEntries = 0
}
this.attributes = options.attributes ? options.attributes.slice() : []
this.notAttributes = []
this.sentEntries = 0
}
util.inherits(SearchResponse, LDAPResult)
/**
* Allows you to send a SearchEntry back to the client.
@ -42,18 +42,14 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
const self = this
const savedAttrs = {}
let save = null
const save = null
if (entry instanceof SearchEntry || entry instanceof SearchReference) {
if (!entry.messageId) { entry.messageId = this.messageId }
if (entry.messageId !== this.messageId) {
throw new Error('SearchEntry messageId mismatch')
}
if (!entry.messageID) { entry.messageID = this.messageID }
if (entry.messageID !== this.messageID) { throw new Error('SearchEntry messageID mismatch') }
} else {
if (!entry.attributes) { throw new Error('entry.attributes required') }
const all = (self.attributes.indexOf('*') !== -1)
// Filter attributes in a plain object according to the magic `_` prefix
// and presence in `notAttributes`.
Object.keys(entry.attributes).forEach(function (a) {
const _a = a.toLowerCase()
if (!nofiltering && _a.length && _a[0] === '_') {
@ -70,27 +66,42 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
}
})
save = entry
const save = entry
entry = new SearchEntry({
objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn,
messageId: self.messageId,
attributes: Attribute.fromObject(entry.attributes)
messageID: self.messageID,
log: self.log
})
entry.fromObject(save)
}
try {
this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.pojo)
this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.json)
this.connection.write(entry.toBer().buffer)
this.connection.write(entry.toBer())
this.sentEntries++
if (self._dtraceOp && self._dtraceId) {
dtrace.fire('server-search-entry', function () {
const c = self.connection || { ldap: {} }
return [
self._dtraceId || 0,
(c.remoteAddress || ''),
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
(self.requestDN ? self.requestDN.toString() : ''),
entry.objectName.toString(),
entry.attributes.length
]
})
}
// Restore attributes
Object.keys(savedAttrs).forEach(function (k) {
save.attributes[k] = savedAttrs[k]
})
} catch (e) {
this.log.warn(e, '%s failure to write message %j',
this.connection.ldap.id, this.pojo)
this.connection.ldap.id, this.json)
}
}
@ -98,10 +109,11 @@ SearchResponse.prototype.createSearchEntry = function (object) {
assert.object(object)
const entry = new SearchEntry({
messageId: this.messageId,
objectName: object.objectName || object.dn,
attributes: object.attributes ?? []
messageID: this.messageID,
log: this.log,
objectName: object.objectName || object.dn
})
entry.fromObject((object.attributes || object))
return entry
}
@ -110,10 +122,15 @@ SearchResponse.prototype.createSearchReference = function (uris) {
if (!Array.isArray(uris)) { uris = [uris] }
for (let i = 0; i < uris.length; i++) {
if (typeof (uris[i]) === 'string') { uris[i] = parseURL(uris[i]) }
}
const self = this
return new SearchReference({
messageId: self.messageId,
uri: uris
messageID: self.messageID,
log: self.log,
uris: uris
})
}

View File

@ -0,0 +1,62 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const LDAPMessage = require('./message')
const dn = require('../dn')
const Protocol = require('../protocol')
/// --- Globals
const DN = dn.DN
const RDN = dn.RDN
/// --- API
function UnbindRequest (options) {
options = options || {}
assert.object(options)
options.protocolOp = Protocol.LDAP_REQ_UNBIND
LDAPMessage.call(this, options)
}
util.inherits(UnbindRequest, LDAPMessage)
Object.defineProperties(UnbindRequest.prototype, {
type: {
get: function getType () { return 'UnbindRequest' },
configurable: false
},
_dn: {
get: function getDN () {
if (this.connection) {
return this.connection.ldap.bindDN
} else {
return new DN([new RDN({ cn: 'anonymous' })])
}
},
configurable: false
}
})
UnbindRequest.prototype._parse = function (ber) {
assert.ok(ber)
return true
}
UnbindRequest.prototype._toBer = function (ber) {
assert.ok(ber)
return ber
}
UnbindRequest.prototype._json = function (j) {
assert.ok(j)
return j
}
/// --- Exports
module.exports = UnbindRequest

View File

@ -0,0 +1,65 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
const assert = require('assert-plus')
const util = require('util')
const dtrace = require('../dtrace')
const LDAPMessage = require('./result')
// var Protocol = require('../protocol')
/// --- API
// Ok, so there's really no such thing as an unbind 'response', but to make
// the framework not suck, I just made this up, and have it stubbed so it's
// not such a one-off.
function UnbindResponse (options) {
options = options || {}
assert.object(options)
options.protocolOp = 0
LDAPMessage.call(this, options)
}
util.inherits(UnbindResponse, LDAPMessage)
Object.defineProperties(UnbindResponse.prototype, {
type: {
get: function getType () { return 'UnbindResponse' },
configurable: false
}
})
/**
* Special override that just ends the connection, if present.
*
* @param {Number} status completely ignored.
*/
UnbindResponse.prototype.end = function (status) {
assert.ok(this.connection)
this.log.trace('%s: unbinding!', this.connection.ldap.id)
this.connection.end()
const self = this
if (self._dtraceOp && self._dtraceId) {
dtrace.fire('server-' + self._dtraceOp + '-done', function () {
const c = self.connection || { ldap: {} }
return [
self._dtraceId || 0,
(c.remoteAddress || ''),
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
(self.requestDN ? self.requestDN.toString() : ''),
0,
''
]
})
}
}
UnbindResponse.prototype._json = function (j) {
return j
}
/// --- Exports
module.exports = UnbindResponse

View File

@ -77,7 +77,7 @@ function getOperationType (requestType) {
}
}
function getEntryChangeNotificationControl (req, obj) {
function getEntryChangeNotificationControl (req, obj, callback) {
// if we want to return a ECNC
if (req.persistentSearch.value.returnECs) {
const attrs = obj.attributes
@ -89,7 +89,7 @@ function getEntryChangeNotificationControl (req, obj) {
}
value.changeNumber = attrs.changenumber
return new EntryChangeNotificationControl({ value })
return new EntryChangeNotificationControl({ value: value })
} else {
return false
}
@ -104,6 +104,6 @@ function checkChangeType (req, requestType) {
module.exports = {
PersistentSearchCache: PersistentSearch,
checkChangeType,
getEntryChangeNotificationControl
checkChangeType: checkChangeType,
getEntryChangeNotificationControl: getEntryChangeNotificationControl
}

53
lib/protocol.js Normal file
View File

@ -0,0 +1,53 @@
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
module.exports = {
// Misc
LDAP_VERSION_3: 0x03,
LBER_SET: 0x31,
LDAP_CONTROLS: 0xa0,
// Search
SCOPE_BASE_OBJECT: 0,
SCOPE_ONE_LEVEL: 1,
SCOPE_SUBTREE: 2,
NEVER_DEREF_ALIASES: 0,
DEREF_IN_SEARCHING: 1,
DEREF_BASE_OBJECT: 2,
DEREF_ALWAYS: 3,
FILTER_AND: 0xa0,
FILTER_OR: 0xa1,
FILTER_NOT: 0xa2,
FILTER_EQUALITY: 0xa3,
FILTER_SUBSTRINGS: 0xa4,
FILTER_GE: 0xa5,
FILTER_LE: 0xa6,
FILTER_PRESENT: 0x87,
FILTER_APPROX: 0xa8,
FILTER_EXT: 0xa9,
// Protocol Operations
LDAP_REQ_BIND: 0x60,
LDAP_REQ_UNBIND: 0x42,
LDAP_REQ_SEARCH: 0x63,
LDAP_REQ_MODIFY: 0x66,
LDAP_REQ_ADD: 0x68,
LDAP_REQ_DELETE: 0x4a,
LDAP_REQ_MODRDN: 0x6c,
LDAP_REQ_COMPARE: 0x6e,
LDAP_REQ_ABANDON: 0x50,
LDAP_REQ_EXTENSION: 0x77,
LDAP_REP_BIND: 0x61,
LDAP_REP_SEARCH_ENTRY: 0x64,
LDAP_REP_SEARCH_REF: 0x73,
LDAP_REP_SEARCH: 0x65,
LDAP_REP_MODIFY: 0x67,
LDAP_REP_ADD: 0x69,
LDAP_REP_DELETE: 0x6b,
LDAP_REP_MODRDN: 0x6d,
LDAP_REP_COMPARE: 0x6f,
LDAP_REP_EXTENSION: 0x78
}

View File

@ -6,33 +6,33 @@ const net = require('net')
const tls = require('tls')
const util = require('util')
// var asn1 = require('@ldapjs/asn1')
// var asn1 = require('asn1')
const VError = require('verror').VError
const { DN, RDN } = require('@ldapjs/dn')
const dn = require('./dn')
const dtrace = require('./dtrace')
const errors = require('./errors')
const Protocol = require('@ldapjs/protocol')
const messages = require('@ldapjs/messages')
const Protocol = require('./protocol')
const Parser = require('./messages').Parser
const LdapResult = messages.LdapResult
const AbandonResponse = messages.AbandonResponse
const AddResponse = messages.AddResponse
const BindResponse = messages.BindResponse
const CompareResponse = messages.CompareResponse
const DeleteResponse = messages.DeleteResponse
const ExtendedResponse = messages.ExtensionResponse
const ModifyResponse = messages.ModifyResponse
const ModifyDnResponse = messages.ModifyDnResponse
const SearchRequest = messages.SearchRequest
const AbandonResponse = require('./messages/abandon_response')
const AddResponse = require('./messages/add_response')
const BindResponse = require('./messages/bind_response')
const CompareResponse = require('./messages/compare_response')
const DeleteResponse = require('./messages/del_response')
const ExtendedResponse = require('./messages/ext_response')
// var LDAPResult = require('./messages/result')
const ModifyResponse = require('./messages/modify_response')
const ModifyDNResponse = require('./messages/moddn_response')
const SearchRequest = require('./messages/search_request')
const SearchResponse = require('./messages/search_response')
const UnbindResponse = require('./messages/unbind_response')
/// --- Globals
// var Ber = asn1.Ber
// var BerReader = asn1.BerReader
// const DN = dn.DN
const DN = dn.DN
// var sprintf = util.format
@ -47,15 +47,15 @@ function mergeFunctionArgs (argv, start, end) {
const handlers = []
for (let i = start; i < end; i++) {
if (Array.isArray(argv[i])) {
if (argv[i] instanceof Array) {
const arr = argv[i]
for (let j = 0; j < arr.length; j++) {
if (typeof arr[j] !== 'function') {
if (!(arr[j] instanceof Function)) {
throw new TypeError('Invalid argument type: ' + typeof (arr[j]))
}
handlers.push(arr[j])
}
} else if (typeof argv[i] === 'function') {
} else if (argv[i] instanceof Function) {
handlers.push(argv[i])
} else {
throw new TypeError('Invalid argument type: ' + typeof (argv[i]))
@ -71,42 +71,35 @@ function getResponse (req) {
let Response
switch (req.protocolOp) {
case Protocol.operations.LDAP_REQ_BIND:
case Protocol.LDAP_REQ_BIND:
Response = BindResponse
break
case Protocol.operations.LDAP_REQ_ABANDON:
case Protocol.LDAP_REQ_ABANDON:
Response = AbandonResponse
break
case Protocol.operations.LDAP_REQ_ADD:
case Protocol.LDAP_REQ_ADD:
Response = AddResponse
break
case Protocol.operations.LDAP_REQ_COMPARE:
case Protocol.LDAP_REQ_COMPARE:
Response = CompareResponse
break
case Protocol.operations.LDAP_REQ_DELETE:
case Protocol.LDAP_REQ_DELETE:
Response = DeleteResponse
break
case Protocol.operations.LDAP_REQ_EXTENSION:
case Protocol.LDAP_REQ_EXTENSION:
Response = ExtendedResponse
break
case Protocol.operations.LDAP_REQ_MODIFY:
case Protocol.LDAP_REQ_MODIFY:
Response = ModifyResponse
break
case Protocol.operations.LDAP_REQ_MODRDN:
Response = ModifyDnResponse
case Protocol.LDAP_REQ_MODRDN:
Response = ModifyDNResponse
break
case Protocol.operations.LDAP_REQ_SEARCH:
case Protocol.LDAP_REQ_SEARCH:
Response = SearchResponse
break
case Protocol.operations.LDAP_REQ_UNBIND:
// TODO: when the server receives an unbind request this made up response object was returned.
// Instead, we need to just terminate the connection. ~ jsumners
Response = class extends LdapResult {
status = 0
end () {
req.connection.end()
}
}
case Protocol.LDAP_REQ_UNBIND:
Response = UnbindResponse
break
default:
return null
@ -114,83 +107,16 @@ function getResponse (req) {
assert.ok(Response)
const res = new Response({
messageId: req.messageId,
messageID: req.messageID,
log: req.log,
attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
})
res.log = req.log
res.connection = req.connection
res.logId = req.logId
if (typeof res.end !== 'function') {
// This is a hack to re-add the original tight coupling of the message
// objects and the server connection.
// TODO: remove this during server refactoring ~ jsumners 2023-02-16
switch (res.protocolOp) {
case 0: {
res.end = abandonResponseEnd
break
}
case Protocol.operations.LDAP_RES_COMPARE: {
res.end = compareResponseEnd
break
}
default: {
res.end = defaultResponseEnd
break
}
}
}
return res
}
/**
* Response connection end handler for most responses.
*
* @param {number} status
*/
function defaultResponseEnd (status) {
if (typeof status === 'number') { this.status = status }
const ber = this.toBer()
this.log.debug('%s: sending: %j', this.connection.ldap.id, this.pojo)
try {
this.connection.write(ber.buffer)
} catch (error) {
this.log.warn(
error,
'%s failure to write message %j',
this.connection.ldap.id,
this.pojo
)
}
}
/**
* Response connection end handler for ABANDON responses.
*/
function abandonResponseEnd () {}
/**
* Response connection end handler for COMPARE responses.
*
* @param {number | boolean} status
*/
function compareResponseEnd (status) {
let result = 0x06
if (typeof status === 'boolean') {
if (status === false) {
result = 0x05
}
} else {
result = status
}
return defaultResponseEnd.call(this, result)
}
function defaultHandler (req, res, next) {
assert.ok(req)
assert.ok(res)
@ -231,6 +157,69 @@ function noExOpHandler (req, res, next) {
return next()
}
function fireDTraceProbe (req, res) {
assert.ok(req)
req._dtraceId = res._dtraceId = dtrace._nextId()
const probeArgs = [
req._dtraceId,
req.connection.remoteAddress || 'localhost',
req.connection.ldap.bindDN.toString(),
req.dn.toString()
]
let op
switch (req.protocolOp) {
case Protocol.LDAP_REQ_ABANDON:
op = 'abandon'
break
case Protocol.LDAP_REQ_ADD:
op = 'add'
probeArgs.push(req.attributes.length)
break
case Protocol.LDAP_REQ_BIND:
op = 'bind'
break
case Protocol.LDAP_REQ_COMPARE:
op = 'compare'
probeArgs.push(req.attribute)
probeArgs.push(req.value)
break
case Protocol.LDAP_REQ_DELETE:
op = 'delete'
break
case Protocol.LDAP_REQ_EXTENSION:
op = 'exop'
probeArgs.push(req.name)
probeArgs.push(req.value)
break
case Protocol.LDAP_REQ_MODIFY:
op = 'modify'
probeArgs.push(req.changes.length)
break
case Protocol.LDAP_REQ_MODRDN:
op = 'modifydn'
probeArgs.push(req.newRdn.toString())
probeArgs.push((req.newSuperior ? req.newSuperior.toString() : ''))
break
case Protocol.LDAP_REQ_SEARCH:
op = 'search'
probeArgs.push(req.scope)
probeArgs.push(req.filter.toString())
break
case Protocol.LDAP_REQ_UNBIND:
op = 'unbind'
break
default:
break
}
res._dtraceOp = op
dtrace.fire('server-' + op + '-start', function () {
return probeArgs
})
}
/// --- API
/**
@ -272,6 +261,8 @@ function Server (options) {
this._chain = []
this.log = options.log
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
const log = this.log
function setupConnection (c) {
@ -286,12 +277,12 @@ function Server (options) {
c.remotePort = c.socket.remotePort
}
const rdn = new RDN({ cn: 'anonymous' })
const rdn = new dn.RDN({ cn: 'anonymous' })
c.ldap = {
id: c.remoteAddress + ':' + c.remotePort,
config: options,
_bindDN: new DN({ rdns: [rdn] })
_bindDN: new DN([rdn])
}
c.addListener('timeout', function () {
log.trace('%s timed out', c.ldap.id)
@ -314,9 +305,7 @@ function Server (options) {
return c.ldap._bindDN
})
c.ldap.__defineSetter__('bindDN', function (val) {
if (Object.prototype.toString.call(val) !== '[object LdapDn]') {
throw new TypeError('DN required')
}
if (!(val instanceof DN)) { throw new TypeError('DN required') }
c.ldap._bindDN = val
return val
@ -324,78 +313,62 @@ function Server (options) {
return c
}
self.newConnection = function (conn) {
// TODO: make `newConnection` available on the `Server` prototype
// https://github.com/ldapjs/node-ldapjs/pull/727/files#r636572294
setupConnection(conn)
log.trace('new connection from %s', conn.ldap.id)
function newConnection (c) {
setupConnection(c)
log.trace('new connection from %s', c.ldap.id)
conn.parser = new Parser({
dtrace.fire('server-connection', function () {
return [c.remoteAddress]
})
c.parser = new Parser({
log: options.log
})
conn.parser.on('message', function (req) {
// TODO: this is mutating the `@ldapjs/message` objects.
// We should avoid doing that. ~ jsumners 2023-02-16
req.connection = conn
req.logId = conn.ldap.id + '::' + req.messageId
c.parser.on('message', function (req) {
req.connection = c
req.logId = c.ldap.id + '::' + req.messageID
req.startTime = new Date().getTime()
log.debug('%s: message received: req=%j', conn.ldap.id, req.pojo)
log.debug('%s: message received: req=%j', c.ldap.id, req.json)
const res = getResponse(req)
if (!res) {
log.warn('Unimplemented server method: %s', req.type)
conn.destroy()
c.destroy()
return false
}
// parse string DNs for routing/etc
try {
switch (req.protocolOp) {
case Protocol.operations.LDAP_REQ_BIND: {
req.name = DN.fromString(req.name)
case Protocol.LDAP_REQ_BIND:
req.name = dn.parse(req.name)
break
}
case Protocol.operations.LDAP_REQ_ADD:
case Protocol.operations.LDAP_REQ_COMPARE:
case Protocol.operations.LDAP_REQ_DELETE: {
if (typeof req.entry === 'string') {
req.entry = DN.fromString(req.entry)
} else if (Object.prototype.toString.call(req.entry) !== '[object LdapDn]') {
throw Error('invalid entry object for operation')
}
case Protocol.LDAP_REQ_ADD:
case Protocol.LDAP_REQ_COMPARE:
case Protocol.LDAP_REQ_DELETE:
req.entry = dn.parse(req.entry)
break
}
case Protocol.operations.LDAP_REQ_MODIFY: {
req.object = DN.fromString(req.object)
case Protocol.LDAP_REQ_MODIFY:
req.object = dn.parse(req.object)
break
}
case Protocol.operations.LDAP_REQ_MODRDN: {
if (typeof req.entry === 'string') {
req.entry = DN.fromString(req.entry)
} else if (Object.prototype.toString.call(req.entry) !== '[object LdapDn]') {
throw Error('invalid entry object for operation')
}
case Protocol.LDAP_REQ_MODRDN:
req.entry = dn.parse(req.entry)
// TODO: handle newRdn/Superior
break
}
case Protocol.operations.LDAP_REQ_SEARCH: {
case Protocol.LDAP_REQ_SEARCH:
req.baseObject = dn.parse(req.baseObject)
break
}
default: {
default:
break
}
}
} catch (e) {
return res.end(errors.LDAP_INVALID_DN_SYNTAX)
if (self.strictDN) {
return res.end(errors.LDAP_INVALID_DN_SYNTAX)
}
}
res.connection = conn
res.connection = c
res.logId = req.logId
res.requestDN = req.dn
@ -403,10 +376,10 @@ function Server (options) {
let i = 0
return (function messageIIFE (err) {
function sendError (sendErr) {
res.status = sendErr.code || errors.LDAP_OPERATIONS_ERROR
function sendError (err) {
res.status = err.code || errors.LDAP_OPERATIONS_ERROR
res.matchedDN = req.suffix ? req.suffix.toString() : ''
res.errorMessage = sendErr.message || ''
res.errorMessage = err.message || ''
return res.end()
}
@ -415,8 +388,8 @@ function Server (options) {
function next () {} // stub out next for the post chain
self._postChain.forEach(function (cb) {
cb.call(self, req, res, next)
self._postChain.forEach(function (c) {
c.call(self, req, res, next)
})
}
@ -431,20 +404,7 @@ function Server (options) {
const next = messageIIFE
if (chain.handlers[i]) { return chain.handlers[i++].call(chain.backend, req, res, next) }
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND && res.status === 0) {
// 0 length == anonymous bind
if (req.dn.length === 0 && req.credentials === '') {
conn.ldap.bindDN = new DN({ rdns: [new RDN({ cn: 'anonymous' })] })
} else {
conn.ldap.bindDN = DN.fromString(req.dn)
}
}
// unbind clear bindDN for safety
// conn should terminate on unbind (RFC4511 4.3)
if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND && res.status === 0) {
conn.ldap.bindDN = new DN({ rdns: [new RDN({ cn: 'anonymous' })] })
}
if (req.protocolOp === Protocol.LDAP_REQ_BIND && res.status === 0) { c.ldap.bindDN = req.dn }
return after()
} catch (e) {
@ -455,32 +415,32 @@ function Server (options) {
}())
})
conn.parser.on('error', function (err, message) {
self.emit('error', new VError(err, 'Parser error for %s', conn.ldap.id))
c.parser.on('error', function (err, message) {
self.emit('error', new VError(err, 'Parser error for %s', c.ldap.id))
if (!message) { return conn.destroy() }
if (!message) { return c.destroy() }
const res = getResponse(message)
if (!res) { return conn.destroy() }
if (!res) { return c.destroy() }
res.status = 0x02 // protocol error
res.errorMessage = err.toString()
return conn.end(res.toBer())
return c.end(res.toBer())
})
conn.on('data', function (data) {
log.trace('data on %s: %s', conn.ldap.id, util.inspect(data))
c.on('data', function (data) {
log.trace('data on %s: %s', c.ldap.id, util.inspect(data))
conn.parser.write(data)
c.parser.write(data)
})
} // end newConnection
this.routes = {}
if ((options.cert || options.certificate) && options.key) {
options.cert = options.cert || options.certificate
this.server = tls.createServer(options, options.connectionRouter ? options.connectionRouter : self.newConnection)
this.server = tls.createServer(options, newConnection)
} else {
this.server = net.createServer(options.connectionRouter ? options.connectionRouter : self.newConnection)
this.server = net.createServer(newConnection)
}
this.server.log = options.log
this.server.ldap = {
@ -533,15 +493,7 @@ Object.defineProperties(Server.prototype, {
} else {
str = 'ldap://'
}
let host = this.host
// Node 18 switched family from returning a string to returning a number
// https://nodejs.org/api/net.html#serveraddress
if (addr.family === 'IPv6' || addr.family === 6) {
host = '[' + this.host + ']'
}
str += host + ':' + this.port
str += this.host + ':' + this.port
return str
},
configurable: false
@ -561,7 +513,7 @@ module.exports = Server
*/
Server.prototype.add = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_ADD, name, args)
return this._mount(Protocol.LDAP_REQ_ADD, name, args)
}
/**
@ -576,7 +528,7 @@ Server.prototype.add = function (name) {
*/
Server.prototype.bind = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_BIND, name, args)
return this._mount(Protocol.LDAP_REQ_BIND, name, args)
}
/**
@ -591,7 +543,7 @@ Server.prototype.bind = function (name) {
*/
Server.prototype.compare = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_COMPARE, name, args)
return this._mount(Protocol.LDAP_REQ_COMPARE, name, args)
}
/**
@ -606,7 +558,7 @@ Server.prototype.compare = function (name) {
*/
Server.prototype.del = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_DELETE, name, args)
return this._mount(Protocol.LDAP_REQ_DELETE, name, args)
}
/**
@ -621,7 +573,7 @@ Server.prototype.del = function (name) {
*/
Server.prototype.exop = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_EXTENSION, name, args, true)
return this._mount(Protocol.LDAP_REQ_EXTENSION, name, args, true)
}
/**
@ -636,7 +588,7 @@ Server.prototype.exop = function (name) {
*/
Server.prototype.modify = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_MODIFY, name, args)
return this._mount(Protocol.LDAP_REQ_MODIFY, name, args)
}
/**
@ -651,7 +603,7 @@ Server.prototype.modify = function (name) {
*/
Server.prototype.modifyDN = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_MODRDN, name, args)
return this._mount(Protocol.LDAP_REQ_MODRDN, name, args)
}
/**
@ -666,7 +618,7 @@ Server.prototype.modifyDN = function (name) {
*/
Server.prototype.search = function (name) {
const args = Array.prototype.slice.call(arguments, 1)
return this._mount(Protocol.operations.LDAP_REQ_SEARCH, name, args)
return this._mount(Protocol.LDAP_REQ_SEARCH, name, args)
}
/**
@ -680,7 +632,7 @@ Server.prototype.search = function (name) {
*/
Server.prototype.unbind = function () {
const args = Array.prototype.slice.call(arguments, 0)
return this._mount(Protocol.operations.LDAP_REQ_UNBIND, 'unbind', args, true)
return this._mount(Protocol.LDAP_REQ_UNBIND, 'unbind', args, true)
}
Server.prototype.use = function use () {
@ -701,13 +653,13 @@ Server.prototype.after = function () {
})
}
// All these just re-expose the requisite net.Server APIs
// All these just reexpose the requisite net.Server APIs
Server.prototype.listen = function (port, host, callback) {
if (typeof (port) !== 'number' && typeof (port) !== 'string') { throw new TypeError('port (number or path) required') }
if (typeof (host) === 'function') {
callback = host
host = '127.0.0.1'
host = '0.0.0.0'
}
if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
// Disambiguate between string ports and file paths
@ -750,10 +702,12 @@ Server.prototype.getConnections = function (callback) {
}
Server.prototype._getRoute = function (_dn, backend) {
assert.ok(dn)
if (!backend) { backend = this }
let name
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') {
if (_dn instanceof dn.DN) {
name = _dn.toString()
} else {
name = _dn
@ -780,10 +734,10 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
Object.keys(this.routes).forEach(function (key) {
const _dn = self.routes[key].dn
// Ignore non-DN routes such as exop or unbind
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') {
if (_dn instanceof dn.DN) {
const reversed = _dn.clone()
reversed.reverse()
reversedRDNsToKeys[reversed.toString()] = key
reversed.rdns.reverse()
reversedRDNsToKeys[reversed.format()] = key
}
})
const output = []
@ -800,15 +754,17 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
return this._routeKeyCache
}
Server.prototype._getHandlerChain = function _getHandlerChain (req) {
Server.prototype._getHandlerChain = function _getHandlerChain (req, res) {
assert.ok(req)
fireDTraceProbe(req, res)
const self = this
const routes = this.routes
let route
// check anonymous bind
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND &&
if (req.protocolOp === Protocol.LDAP_REQ_BIND &&
req.dn.toString() === '' &&
req.credentials === '') {
return {
@ -820,7 +776,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
const op = '0x' + req.protocolOp.toString(16)
// Special cases are exops, unbinds and abandons. Handle those first.
if (req.protocolOp === Protocol.operations.LDAP_REQ_EXTENSION) {
if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) {
route = routes[req.requestName]
if (route) {
return {
@ -833,7 +789,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
handlers: [noExOpHandler]
}
}
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND) {
} else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
route = routes.unbind
if (route) {
return {
@ -846,7 +802,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
handlers: [defaultNoOpHandler]
}
}
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_ABANDON) {
} else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) {
return {
backend: self,
handlers: [defaultNoOpHandler]
@ -854,11 +810,11 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
}
// Otherwise, match via DN rules
assert.ok(req.dn)
const keys = this._sortedRouteKeys()
let fallbackHandler = [noSuffixHandler]
// invalid DNs in non-strict mode are routed to the default handler
const testDN = (typeof (req.dn) === 'string') ? DN.fromString(req.dn) : req.dn
assert.ok(testDN)
const testDN = (typeof (req.dn) === 'string') ? '' : req.dn
for (let i = 0; i < keys.length; i++) {
const suffix = keys[i]
@ -905,7 +861,7 @@ Server.prototype._mount = function (op, name, argv, notDN) {
backend = argv[0]
index = 1
}
const route = this._getRoute(notDN ? name : DN.fromString(name), backend)
const route = this._getRoute(notDN ? name : dn.parse(name), backend)
const chain = this._chain.slice()
argv.slice(index).forEach(function (a) {

View File

@ -2,8 +2,8 @@
const querystring = require('querystring')
const url = require('url')
const { DN } = require('@ldapjs/dn')
const filter = require('@ldapjs/filter')
const dn = require('./dn')
const filter = require('./filters/')
module.exports = {
@ -38,7 +38,7 @@ module.exports = {
if (u.pathname) {
u.pathname = querystring.unescape(u.pathname.substr(1))
u.DN = parseDN ? DN.fromString(u.pathname) : u.pathname
u.DN = parseDN ? dn.parse(u.pathname) : u.pathname
}
if (u.search) {

View File

@ -3,57 +3,50 @@
"name": "ldapjs",
"homepage": "http://ldapjs.org",
"description": "LDAP client and server APIs",
"version": "3.0.7",
"version": "2.2.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "git://github.com/ldapjs/node-ldapjs.git"
},
"main": "lib/index.js",
"directories": {
"lib": "./lib"
},
"engines": {
"node": ">=10.13.0"
},
"dependencies": {
"@ldapjs/asn1": "^2.0.0",
"@ldapjs/attribute": "^1.0.0",
"@ldapjs/change": "^1.0.0",
"@ldapjs/controls": "^2.1.0",
"@ldapjs/dn": "^1.1.0",
"@ldapjs/filter": "^2.1.1",
"@ldapjs/messages": "^1.3.0",
"@ldapjs/protocol": "^1.2.1",
"abstract-logging": "^2.0.1",
"abstract-logging": "^2.0.0",
"asn1": "^0.2.4",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"ldap-filter": "^0.3.3",
"once": "^1.4.0",
"vasync": "^2.2.1",
"verror": "^1.10.1"
"vasync": "^2.2.0",
"verror": "^1.8.1"
},
"devDependencies": {
"@fastify/pre-commit": "^2.0.2",
"eslint": "^8.44.0",
"eslint-config-standard": "^17.0.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "6.1.1",
"front-matter": "^4.0.2",
"get-port": "^5.1.1",
"highlight.js": "^11.7.0",
"marked": "^4.2.12",
"tap": "^16.3.7"
"husky": "^4.2.5",
"snazzy": "^9.0.0",
"standard": "^16.0.0",
"tap": "14.10.8"
},
"scripts": {
"test": "tap --no-cov -R terse",
"test:ci": "tap --coverage-report=lcovonly -R terse",
"test:cov": "tap -R terse",
"test:cov:html": "tap --coverage-report=html -R terse",
"test:watch": "tap -n -w --no-coverage-report -R terse",
"test:integration": "tap --no-cov -R terse 'test-integration/**/*.test.js'",
"test:integration:local": "docker-compose up -d --wait && npm run test:integration ; docker-compose down",
"lint": "eslint . --fix",
"lint:ci": "eslint .",
"docs": "node scripts/build-docs.js"
"test": "tap --no-cov",
"test:ci": "tap --coverage-report=lcovonly",
"test:cov": "tap",
"test:cov:html": "tap --coverage-report=html",
"test:watch": "tap -n -w --no-coverage-report",
"test:integration": "tap --no-cov 'test-integration/**/*.test.js'",
"test:integration:local": "docker-compose up -d && npm run test:integration && docker-compose down",
"lint": "standard | snazzy",
"lint:ci": "standard"
},
"pre-commit": [
"lint:ci",
"test"
]
"husky": {
"hooks": {
"pre-commit": "npm run lint && npm run test"
}
}
}

View File

@ -1,131 +0,0 @@
const fs = require('fs/promises')
const path = require('path')
const { marked } = require('marked')
const fm = require('front-matter')
const { highlight } = require('highlight.js')
marked.use({
highlight: (code, lang) => {
if (lang) {
return highlight(code, { language: lang }).value
}
return code
}
})
function tocHTML (toc) {
let html = '<ul>\n'
for (const li of toc) {
html += '<li>\n'
html += `<div>\n<a href="#${li.slug}">${li.text}</a>\n</div>\n`
if (li.children && li.children.length > 0) {
html += tocHTML(li.children)
}
html += '</li>\n'
}
html += '</ul>\n'
return html
}
function markdownTOC (markdown) {
const tokens = marked.lexer(markdown)
const slugger = new marked.Slugger()
const toc = []
let currentHeading
let ignoreFirst = true
for (const token of tokens) {
if (token.type === 'heading') {
if (token.depth === 1) {
if (ignoreFirst) {
ignoreFirst = false
continue
}
currentHeading = {
text: token.text,
slug: slugger.slug(token.text),
children: []
}
toc.push(currentHeading)
} else if (token.depth === 2) {
if (!currentHeading) {
continue
}
currentHeading.children.push({
text: token.text,
slug: slugger.slug(token.text)
})
}
}
}
return {
toc: tocHTML(toc),
html: marked.parser(tokens)
}
}
function createHTML (template, text) {
const { attributes, body } = fm(text)
const { toc, html } = markdownTOC(body)
attributes.toc_html = toc
attributes.content = html
for (const prop in attributes) {
template = template.replace(new RegExp(`%\\(${prop}\\)s`, 'ig'), attributes[prop])
}
return template
}
async function copyRecursive (src, dest) {
const stats = await fs.stat(src)
const isDirectory = stats.isDirectory()
if (isDirectory) {
await fs.mkdir(dest)
const files = await fs.readdir(src)
for (const file of files) {
await copyRecursive(path.join(src, file), path.join(dest, file))
}
} else {
await fs.copyFile(src, dest)
}
}
async function createDocs () {
const docs = path.resolve(__dirname, '..', 'docs')
const dist = path.resolve(__dirname, '..', 'public')
const branding = path.join(docs, 'branding')
const src = path.join(branding, 'public')
try {
await fs.rm(dist, { recursive: true })
} catch (ex) {
if (ex.code !== 'ENOENT') {
throw ex
}
}
await copyRecursive(src, dist)
const highlightjsStyles = path.resolve(__dirname, '..', 'node_modules', 'highlight.js', 'styles')
await fs.copyFile(path.join(highlightjsStyles, 'default.css'), path.join(dist, 'media', 'css', 'highlight.css'))
const template = await fs.readFile(path.join(branding, 'template.html'), { encoding: 'utf8' })
const files = await fs.readdir(docs)
for (const file of files) {
if (!file.endsWith('.md')) {
continue
}
const text = await fs.readFile(path.join(docs, file), { encoding: 'utf8' })
const html = createHTML(template, text)
await fs.writeFile(path.join(dist, file.replace(/md$/, 'html')), html)
}
}
createDocs().catch(ex => {
console.error(ex)
process.exitCode = 1
})

View File

@ -1,5 +0,0 @@
module.exports = {
rules: {
'no-shadow': 'off'
}
}

Some files were not shown because too many files have changed in this diff Show More