Compare commits
No commits in common. "master" and "v2.2.1" have entirely different histories.
|
@ -1,4 +0,0 @@
|
||||||
node_modules/
|
|
||||||
coverage/
|
|
||||||
.nyc_output/
|
|
||||||
docs/
|
|
20
.eslintrc.js
20
.eslintrc.js
|
@ -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: '^_'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'
|
|
|
@ -4,34 +4,35 @@ name: 'Integration Tests'
|
||||||
# https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538
|
# https://github.community/t5/GitHub-Actions/Github-Actions-services-not-reachable/m-p/30739/highlight/true#M538
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- next
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- next
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
baseline:
|
baseline:
|
||||||
name: Baseline Tests
|
name: Baseline Tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
services:
|
# services:
|
||||||
openldap:
|
# openldap:
|
||||||
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30
|
# image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:1.0
|
||||||
ports:
|
# ports:
|
||||||
- 389:389
|
# - 389:389
|
||||||
- 636:636
|
# - 636:636
|
||||||
options: >
|
|
||||||
--health-cmd "ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v1
|
||||||
with:
|
|
||||||
node-version: 'lts/*'
|
# 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
|
- name: Install Packages
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
|
@ -4,21 +4,17 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- next
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
- next
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: Lint Check
|
name: Lint Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v1
|
||||||
with:
|
|
||||||
node-version: 'lts/*'
|
|
||||||
- name: Install Packages
|
- name: Install Packages
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Lint Code
|
- name: Lint Code
|
||||||
|
@ -32,16 +28,27 @@ jobs:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
node:
|
node:
|
||||||
- 16
|
- 10.13.0
|
||||||
- 18
|
- 10.x
|
||||||
- 20
|
- 12.x
|
||||||
|
- 14.x
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
- name: Install Packages
|
- name: Install Packages
|
||||||
run: npm install
|
run: npm install
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: npm run test:ci
|
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
|
||||||
|
|
|
@ -75,6 +75,3 @@ xcuserdata
|
||||||
*.pem
|
*.pem
|
||||||
*.env.json
|
*.env.json
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
# built docs
|
|
||||||
/public
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
.gitmodules
|
||||||
|
Makefile
|
||||||
|
deps
|
||||||
|
docs
|
||||||
|
test
|
||||||
|
tools
|
||||||
|
coverage
|
|
@ -0,0 +1,6 @@
|
||||||
|
esm: false
|
||||||
|
jsx: false
|
||||||
|
ts: false
|
||||||
|
|
||||||
|
files:
|
||||||
|
- 'test/**/*.test.js'
|
10
.taprc.yml
10
.taprc.yml
|
@ -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'
|
|
74
README.md
74
README.md
|
@ -1,34 +1,54 @@
|
||||||
# Project Decomissioned
|
# LDAPjs
|
||||||
|
|
||||||
This project has been decomissioned. I, James Sumners, took it on when it was
|
[](https://github.com/ldapjs/node-ldapjs/actions)
|
||||||
languishing without any maintenance as it filled a need in the ecosystem and
|
[](https://coveralls.io/github/ldapjs/node-ldapjs/)
|
||||||
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.
|
|
||||||
|
|
||||||
So, why am I just now deciding to decomission this project? Because today,
|
LDAPjs makes the LDAP protocol a first class citizen in Node.js.
|
||||||
2024-05-14, I received the following email:
|
|
||||||
|
|
||||||

|
## Usage
|
||||||
|
|
||||||
I will not tolerate abuse, and I especially will not tolerate tacit death
|
For full docs, head on over to <http://ldapjs.org>.
|
||||||
threats, over a hobby. You can thank the author of that email for the
|
|
||||||
decomissioning on this project.
|
|
||||||
|
|
||||||
My recommendation to you in regard to LDAP operations: write a gateway in a
|
```javascript
|
||||||
language that is more suited to these types of operations. I'd suggest
|
var ldap = require('ldapjs');
|
||||||
[Go](https://go.dev).
|
|
||||||
|
|
||||||
👋
|
var server = ldap.createServer();
|
||||||
|
|
||||||
P.S.: if I ever do need this project again, I might revive it. But I'd fight
|
server.search('dc=example', function(req, res, next) {
|
||||||
hard for my suggestion above. Also, I will consider turning it over to an
|
var obj = {
|
||||||
interested party, but I will require at least one recommendation from a
|
dn: req.dn.toString(),
|
||||||
Node.js core contributor that I can vet with the people that I know on that
|
attributes: {
|
||||||
team.
|
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>.
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
openldap:
|
openldap:
|
||||||
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30
|
image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
|
||||||
ports:
|
ports:
|
||||||
- 389:389
|
- 389:389
|
||||||
- 636:636
|
- 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
|
|
||||||
|
|
|
@ -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> </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>
|
|
@ -3,8 +3,8 @@
|
||||||
<head>
|
<head>
|
||||||
<title>%(title)s</title>
|
<title>%(title)s</title>
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
<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="%(mediaroot)s/css/restdown.css">
|
||||||
<link rel="stylesheet" type="text/css" href="media/css/highlight.css">
|
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="header">
|
<div id="header">
|
||||||
|
@ -30,10 +30,3 @@
|
||||||
</span>
|
</span>
|
||||||
%(toc_html)s
|
%(toc_html)s
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="content">
|
|
||||||
%(content)s
|
|
||||||
</div><!-- end #content -->
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -7,7 +7,7 @@ body {
|
||||||
color: #4a3f2d;
|
color: #4a3f2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus:not(:focus-visible) {
|
:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,6 +83,22 @@ h4 {
|
||||||
display: inline;
|
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 and sidebar */
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
|
@ -134,6 +150,15 @@ h4 {
|
||||||
z-index:0;
|
z-index:0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sidebar .vertical_divider {
|
||||||
|
background-color:#FFFFFF;
|
||||||
|
bottom:0px;
|
||||||
|
position:absolute;
|
||||||
|
top:0px;
|
||||||
|
right:0px;
|
||||||
|
width:1px;
|
||||||
|
}
|
||||||
|
|
||||||
#sidebar h1 {
|
#sidebar h1 {
|
||||||
font-size:1.2em;
|
font-size:1.2em;
|
||||||
padding:0px;
|
padding:0px;
|
||||||
|
@ -162,6 +187,25 @@ h4 {
|
||||||
padding:1px 0px 1px 2px;
|
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 */
|
/* ---- intro */
|
||||||
|
|
||||||
|
@ -175,6 +219,13 @@ h4 {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-bottom:40px;
|
margin-bottom:40px;
|
||||||
}
|
}
|
||||||
|
.intro pre.base {
|
||||||
|
background:#444;
|
||||||
|
color:#29231A;
|
||||||
|
color:#fff;
|
||||||
|
border-color:#fff;
|
||||||
|
font-size:1.5em;
|
||||||
|
}
|
||||||
.intro h1 {
|
.intro h1 {
|
||||||
color: #1C313C;
|
color: #1C313C;
|
||||||
}
|
}
|
||||||
|
@ -208,6 +259,14 @@ h1 + h2 {
|
||||||
margin-top: 0px;
|
margin-top: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2.fixed {
|
||||||
|
position:fixed;
|
||||||
|
margin-top: 0;
|
||||||
|
border-top:none;
|
||||||
|
right:45px;
|
||||||
|
top:66px;
|
||||||
|
}
|
||||||
|
|
||||||
h2 span {
|
h2 span {
|
||||||
background: #979592;
|
background: #979592;
|
||||||
float:right;
|
float:right;
|
|
@ -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;
|
||||||
|
}
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
|
@ -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);
|
|
@ -1 +0,0 @@
|
||||||
ldapjs.org
|
|
342
docs/client.md
342
docs/client.md
|
@ -1,31 +1,21 @@
|
||||||
---
|
---
|
||||||
title: Client API | ldapjs
|
title: Client API | ldapjs
|
||||||
|
markdown2extras: tables
|
||||||
---
|
---
|
||||||
|
|
||||||
# ldapjs Client API
|
# ldapjs Client API
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
This document covers the ldapjs client API and assumes that you are familiar
|
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.
|
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# Create a client
|
# Create a client
|
||||||
|
|
||||||
The code to create a new client looks like:
|
The code to create a new client looks like:
|
||||||
|
|
||||||
```js
|
var ldap = require('ldapjs');
|
||||||
const ldap = require('ldapjs');
|
var client = ldap.createClient({
|
||||||
|
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
|
||||||
const client = ldap.createClient({
|
});
|
||||||
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on('connectError', (err) => {
|
|
||||||
// handle connection error
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
|
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
|
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)|
|
|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)|
|
|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|
|
|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)|
|
|reconnect |Try to reconnect when the connection gets lost (Default is false)|
|
||||||
|
|
||||||
### url
|
### url
|
||||||
|
@ -62,13 +53,6 @@ Known compatible loggers are:
|
||||||
+ [Bunyan](https://www.npmjs.com/package/bunyan)
|
+ [Bunyan](https://www.npmjs.com/package/bunyan)
|
||||||
+ [Pino](https://www.npmjs.com/package/pino)
|
+ [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
|
## Connection management
|
||||||
|
|
||||||
As LDAP is a stateful protocol (as opposed to HTTP), having connections torn
|
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`).
|
`failAfter` (default: `Infinity`).
|
||||||
After the reconnect you maybe need to [bind](#bind) again.
|
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
|
## 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).
|
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.
|
You probably won't need to check the `res` parameter, but it's there if you do.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# bind
|
# bind
|
||||||
`bind(dn, password, controls, callback)`
|
`bind(dn, password, controls, callback)`
|
||||||
|
|
||||||
|
@ -124,11 +92,9 @@ of `Control` objects. You probably don't need them though...
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
client.bind('cn=root', 'secret', function(err) {
|
||||||
client.bind('cn=root', 'secret', (err) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
# add
|
# add
|
||||||
`add(dn, entry, controls, callback)`
|
`add(dn, entry, controls, callback)`
|
||||||
|
@ -140,17 +106,15 @@ controls are optional.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
var entry = {
|
||||||
const entry = {
|
cn: 'foo',
|
||||||
cn: 'foo',
|
sn: 'bar',
|
||||||
sn: 'bar',
|
email: ['foo@bar.com', 'foo1@bar.com'],
|
||||||
email: ['foo@bar.com', 'foo1@bar.com'],
|
objectclass: 'fooPerson'
|
||||||
objectclass: 'fooPerson'
|
};
|
||||||
};
|
client.add('cn=foo, o=example', entry, function(err) {
|
||||||
client.add('cn=foo, o=example', entry, (err) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
# compare
|
# compare
|
||||||
`compare(dn, attribute, value, controls, callback)`
|
`compare(dn, attribute, value, controls, callback)`
|
||||||
|
@ -160,13 +124,11 @@ the entry referenced by dn.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
client.compare('cn=foo, o=example', 'sn', 'bar', function(err, matched) {
|
||||||
client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
|
||||||
|
|
||||||
console.log('matched: ' + matched);
|
console.log('matched: ' + matched);
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
# del
|
# del
|
||||||
`del(dn, controls, callback)`
|
`del(dn, controls, callback)`
|
||||||
|
@ -176,11 +138,9 @@ Deletes an entry from the LDAP server.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
client.del('cn=foo, o=example', function(err) {
|
||||||
client.del('cn=foo, o=example', (err) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
# exop
|
# exop
|
||||||
`exop(name, value, controls, callback)`
|
`exop(name, value, controls, callback)`
|
||||||
|
@ -192,13 +152,11 @@ should be.
|
||||||
|
|
||||||
Example (performs an LDAP 'whois' extended op):
|
Example (performs an LDAP 'whois' extended op):
|
||||||
|
|
||||||
```js
|
client.exop('1.3.6.1.4.1.4203.1.11.3', function(err, value, res) {
|
||||||
client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
|
||||||
|
|
||||||
console.log('whois: ' + value);
|
console.log('whois: ' + value);
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
# modify
|
# modify
|
||||||
`modify(name, changes, controls, callback)`
|
`modify(name, changes, controls, callback)`
|
||||||
|
@ -209,19 +167,16 @@ pass in a single `Change` or an array of `Change` objects.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
var change = new ldap.Change({
|
||||||
const change = new ldap.Change({
|
operation: 'add',
|
||||||
operation: 'add',
|
modification: {
|
||||||
modification: {
|
pets: ['cat', 'dog']
|
||||||
type: 'pets',
|
}
|
||||||
values: ['cat', 'dog']
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.modify('cn=foo, o=example', change, (err) => {
|
client.modify('cn=foo, o=example', change, function(err) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
## Change
|
## 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. |
|
| 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`. |
|
| 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.
|
`modification` is just a plain old JS object with the values you want.
|
||||||
|
|
||||||
| Operation | Description |
|
|
||||||
|-----------|-------------|
|
|
||||||
| type | String that defines the attribute type for the modification. |
|
|
||||||
| values | Defines the values for modification. |
|
|
||||||
|
|
||||||
|
|
||||||
# modifyDN
|
# modifyDN
|
||||||
`modifyDN(dn, newDN, controls, callback)`
|
`modifyDN(dn, newDN, controls, callback)`
|
||||||
|
@ -257,11 +206,9 @@ as opposed to just renaming the leaf).
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
client.modifyDN('cn=foo, o=example', 'cn=bar', function(err) {
|
||||||
client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
# search
|
# search
|
||||||
`search(base, options, controls, callback)`
|
`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.|
|
|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|
|
|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
|
Responses from the `search` method are an `EventEmitter` where you will get a
|
||||||
each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest`
|
notification for each `searchEntry` that comes back from the server. You will
|
||||||
, `searchReference`, `error` and `end` event.
|
additionally be able to listen for a `searchReference`, `error` and `end` event.
|
||||||
`searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations
|
Note that the `error` event will only be for client/TCP errors, not LDAP error
|
||||||
like `client.abandon` with `searchRequest.messageId` to abandon this search request. Note that the `error` event will
|
codes like the other APIs. You'll want to check the LDAP status code
|
||||||
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
|
||||||
(likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as
|
can give you a lot of status codes, such as time or size exceeded, busy,
|
||||||
time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code
|
inappropriate matching, etc., which is why this method doesn't try to wrap up
|
||||||
matching.
|
the code matching.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
var opts = {
|
||||||
const opts = {
|
filter: '(&(l=Seattle)(email=*@foo.com))',
|
||||||
filter: '(&(l=Seattle)(email=*@foo.com))',
|
scope: 'sub',
|
||||||
scope: 'sub',
|
attributes: ['dn', 'sn', 'cn']
|
||||||
attributes: ['dn', 'sn', 'cn']
|
};
|
||||||
};
|
|
||||||
|
|
||||||
client.search('o=example', opts, (err, res) => {
|
client.search('o=example', opts, function(err, res) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
|
|
||||||
res.on('searchRequest', (searchRequest) => {
|
res.on('searchEntry', function(entry) {
|
||||||
console.log('searchRequest: ', searchRequest.messageId);
|
console.log('entry: ' + JSON.stringify(entry.object));
|
||||||
});
|
});
|
||||||
res.on('searchEntry', (entry) => {
|
res.on('searchReference', function(referral) {
|
||||||
console.log('entry: ' + JSON.stringify(entry.pojo));
|
console.log('referral: ' + referral.uris.join());
|
||||||
});
|
});
|
||||||
res.on('searchReference', (referral) => {
|
res.on('error', function(err) {
|
||||||
console.log('referral: ' + referral.uris.join());
|
console.error('error: ' + err.message);
|
||||||
});
|
});
|
||||||
res.on('error', (err) => {
|
res.on('end', function(result) {
|
||||||
console.error('error: ' + err.message);
|
console.log('status: ' + result.status);
|
||||||
});
|
});
|
||||||
res.on('end', (result) => {
|
});
|
||||||
console.log('status: ' + result.status);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Filter Strings
|
## 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
|
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:
|
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.
|
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
|
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
|
just anything in the "@bar.com" domain and the location attribute is set to
|
||||||
Seattle:
|
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
|
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.
|
amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter.
|
||||||
Substrings are wildcard filters. They use `*` as the wildcard. You can put more
|
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)`
|
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
|
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:
|
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 `|`.
|
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
|
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
|
most simple way to use the paging automation is to set the `paged` option to
|
||||||
true when performing a search:
|
true when performing a search:
|
||||||
|
|
||||||
```js
|
var opts = {
|
||||||
const opts = {
|
filter: '(objectclass=commonobject)',
|
||||||
filter: '(objectclass=commonobject)',
|
scope: 'sub',
|
||||||
scope: 'sub',
|
paged: true,
|
||||||
paged: true,
|
sizeLimit: 200
|
||||||
sizeLimit: 200
|
};
|
||||||
};
|
client.search('o=largedir', opts, function(err, res) {
|
||||||
client.search('o=largedir', opts, (err, res) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
res.on('searchEntry', function(entry) {
|
||||||
res.on('searchEntry', (entry) => {
|
// do per-entry processing
|
||||||
// do per-entry processing
|
});
|
||||||
});
|
res.on('page', function(result) {
|
||||||
res.on('page', (result) => {
|
console.log('page end');
|
||||||
console.log('page end');
|
});
|
||||||
});
|
res.on('error', function(resErr) {
|
||||||
res.on('error', (resErr) => {
|
assert.ifError(resErr);
|
||||||
assert.ifError(resErr);
|
});
|
||||||
});
|
res.on('end', function(result) {
|
||||||
res.on('end', (result) => {
|
console.log('done ');
|
||||||
console.log('done ');
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
This will enable paging with a default page size of 199 (`sizeLimit` - 1) and
|
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
|
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:
|
Here is an example where both of those parameters are used:
|
||||||
|
|
||||||
```js
|
var queue = new MyWorkQueue(someSlowWorkFunction);
|
||||||
const queue = new MyWorkQueue(someSlowWorkFunction);
|
var opts = {
|
||||||
const opts = {
|
filter: '(objectclass=commonobject)',
|
||||||
filter: '(objectclass=commonobject)',
|
scope: 'sub',
|
||||||
scope: 'sub',
|
paged: {
|
||||||
paged: {
|
pageSize: 250,
|
||||||
pageSize: 250,
|
pagePause: true
|
||||||
pagePause: true
|
},
|
||||||
},
|
};
|
||||||
};
|
client.search('o=largerdir', opts, function(err, res) {
|
||||||
client.search('o=largerdir', opts, (err, res) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
res.on('searchEntry', function(entry) {
|
||||||
res.on('searchEntry', (entry) => {
|
// Submit incoming objects to queue
|
||||||
// Submit incoming objects to queue
|
queue.push(entry);
|
||||||
queue.push(entry);
|
});
|
||||||
});
|
res.on('page', function(result, cb) {
|
||||||
res.on('page', (result, cb) => {
|
// Allow the queue to flush before fetching next page
|
||||||
// Allow the queue to flush before fetching next page
|
queue.cbWhenFlushed(cb);
|
||||||
queue.cbWhenFlushed(cb);
|
});
|
||||||
});
|
res.on('error', function(resErr) {
|
||||||
res.on('error', (resErr) => {
|
assert.ifError(resErr);
|
||||||
assert.ifError(resErr);
|
});
|
||||||
});
|
res.on('end', function(result) {
|
||||||
res.on('end', (result) => {
|
console.log('done');
|
||||||
console.log('done');
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
# starttls
|
# starttls
|
||||||
`starttls(options, controls, callback)`
|
`starttls(options, controls, callback)`
|
||||||
|
@ -457,17 +389,15 @@ Attempt to secure existing LDAP connection via STARTTLS.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
var opts = {
|
||||||
const opts = {
|
ca: [fs.readFileSync('mycacert.pem')]
|
||||||
ca: [fs.readFileSync('mycacert.pem')]
|
};
|
||||||
};
|
|
||||||
|
|
||||||
client.starttls(opts, (err, res) => {
|
client.starttls(opts, function(err, res) {
|
||||||
assert.ifError(err);
|
assert.ifError(err);
|
||||||
|
|
||||||
// Client communication now TLS protected
|
// Client communication now TLS protected
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
# unbind
|
# unbind
|
||||||
|
@ -484,8 +414,6 @@ not have a response.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
client.unbind(function(err) {
|
||||||
client.unbind((err) => {
|
assert.ifError(err);
|
||||||
assert.ifError(err);
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
70
docs/dn.md
70
docs/dn.md
|
@ -4,13 +4,9 @@ title: DN API | ldapjs
|
||||||
|
|
||||||
# ldapjs DN API
|
# ldapjs DN API
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
This document covers the ldapjs DN API and assumes that you are familiar
|
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.
|
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
|
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
|
distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the
|
||||||
complete specification, but basically an RDN is an attribute value assertion
|
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
|
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.
|
ldapjs framework, but if you need it, it's there.
|
||||||
|
|
||||||
```js
|
var parseDN = require('ldapjs').parseDN;
|
||||||
const parseDN = require('ldapjs').parseDN;
|
|
||||||
|
|
||||||
const dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
|
var dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
|
||||||
console.log(dn.toString());
|
console.log(dn.toString());
|
||||||
```
|
|
||||||
|
|
||||||
# DN
|
# 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
|
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.
|
`dn` argument can be either a string or a DN.
|
||||||
|
|
||||||
```js
|
server.add('o=example', function(req, res, next) {
|
||||||
server.add('o=example', (req, res, next) => {
|
if (req.dn.childOf('ou=people, o=example')) {
|
||||||
if (req.dn.childOf('ou=people, o=example')) {
|
...
|
||||||
...
|
} else {
|
||||||
} else {
|
...
|
||||||
...
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## parentOf(dn)
|
## parentOf(dn)
|
||||||
|
|
||||||
The inverse of `childOf`; returns a boolean on whether or not `this` is a parent
|
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.
|
of the passed in dn. Like `childOf`, can take either a string or a DN.
|
||||||
|
|
||||||
```js
|
server.add('o=example', function(req, res, next) {
|
||||||
server.add('o=example', (req, res, next) => {
|
var dn = parseDN('ou=people, o=example');
|
||||||
const dn = parseDN('ou=people, o=example');
|
if (dn.parentOf(req.dn)) {
|
||||||
if (dn.parentOf(req.dn)) {
|
...
|
||||||
...
|
} else {
|
||||||
} else {
|
...
|
||||||
...
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## equals(dn)
|
## equals(dn)
|
||||||
|
|
||||||
Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
|
Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
|
||||||
argument. `dn` can be a string or a DN.
|
argument. `dn` can be a string or a DN.
|
||||||
|
|
||||||
```js
|
server.add('o=example', function(req, res, next) {
|
||||||
server.add('o=example', (req, res, next) => {
|
if (req.dn.equals('cn=foo, ou=people, o=example')) {
|
||||||
if (req.dn.equals('cn=foo, ou=people, o=example')) {
|
...
|
||||||
...
|
} else {
|
||||||
} else {
|
...
|
||||||
...
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## parent()
|
## parent()
|
||||||
|
|
||||||
|
@ -120,8 +108,6 @@ It accepts the same parameters as `format`.
|
||||||
|
|
||||||
Returns the string representation of `this`.
|
Returns the string representation of `this`.
|
||||||
|
|
||||||
```js
|
server.add('o=example', function(req, res, next) {
|
||||||
server.add('o=example', (req, res, next) => {
|
console.log(req.dn.toString());
|
||||||
console.log(req.dn.toString());
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
|
@ -4,36 +4,30 @@ title: Errors API | ldapjs
|
||||||
|
|
||||||
# ldapjs Errors API
|
# ldapjs Errors API
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
This document covers the ldapjs errors API and assumes that you are familiar
|
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.
|
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
|
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
|
`LDAPError`. In addition to the properties listed below, all errors will have
|
||||||
a `stack` property correctly set.
|
a `stack` property correctly set.
|
||||||
|
|
||||||
In general, you'll be using the errors in ldapjs like:
|
In general, you'll be using the errors in ldapjs like:
|
||||||
|
|
||||||
```js
|
var ldap = require('ldapjs');
|
||||||
const ldap = require('ldapjs');
|
|
||||||
|
|
||||||
const db = {};
|
var db = {};
|
||||||
|
|
||||||
server.add('o=example', (req, res, next) => {
|
server.add('o=example', function(req, res, next) {
|
||||||
const parent = req.dn.parent();
|
var parent = req.dn.parent();
|
||||||
if (parent) {
|
if (parent) {
|
||||||
if (!db[parent.toString()])
|
if (!db[parent.toString()])
|
||||||
return next(new ldap.NoSuchObjectError(parent.toString()));
|
return next(new ldap.NoSuchObjectError(parent.toString()));
|
||||||
}
|
}
|
||||||
if (db[req.dn.toString()])
|
if (db[req.dn.toString()])
|
||||||
return next(new ldap.EntryAlreadyExistsError(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
|
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.
|
return the appropriate LDAP error message, and stop the handler chain.
|
||||||
|
|
942
docs/examples.md
942
docs/examples.md
File diff suppressed because it is too large
Load Diff
215
docs/filters.md
215
docs/filters.md
|
@ -4,13 +4,9 @@ title: Filters API | ldapjs
|
||||||
|
|
||||||
# ldapjs Filters API
|
# ldapjs Filters API
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
This document covers the ldapjs filters API and assumes that you are familiar
|
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.
|
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
|
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
|
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,
|
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.
|
ldapjs object(s). If the filter is "complex", it will be a "tree" of objects.
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```js
|
var parseFilter = require('ldapjs').parseFilter;
|
||||||
const parseFilter = require('ldapjs').parseFilter;
|
|
||||||
|
|
||||||
const f = parseFilter('(objectclass=*)');
|
var f = parseFilter('(objectclass=*)');
|
||||||
```
|
|
||||||
|
|
||||||
Is a "simple" filter, and would just return a `PresenceFilter` object. However,
|
Is a "simple" filter, and would just return a `PresenceFilter` object. However,
|
||||||
|
|
||||||
```js
|
var f = parseFilter('(&(employeeType=manager)(l=Seattle))');
|
||||||
const f = parseFilter('(&(employeeType=manager)(l=Seattle))');
|
|
||||||
```
|
|
||||||
|
|
||||||
Would return an `AndFilter`, which would have a `filters` array of two
|
Would return an `AndFilter`, which would have a `filters` array of two
|
||||||
`EqualityFilter` objects.
|
`EqualityFilter` objects.
|
||||||
|
@ -56,22 +48,20 @@ syntactically invalid string).
|
||||||
|
|
||||||
The equality filter is used to check exact matching of attribute/value
|
The equality filter is used to check exact matching of attribute/value
|
||||||
assertions. This object will have an `attribute` and `value` property, and the
|
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 string syntax for an equality filter is `(attr=value)`.
|
||||||
|
|
||||||
The `matches()` method will return true IFF the passed in object has a
|
The `matches()` method will return true IFF the passed in object has a
|
||||||
key matching `attribute` and a value matching `value`.
|
key matching `attribute` and a value matching `value`.
|
||||||
|
|
||||||
```js
|
var f = new EqualityFilter({
|
||||||
const f = new EqualityFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo'
|
||||||
value: 'foo'
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'foo'}); => true
|
f.matches({cn: 'foo'}); => true
|
||||||
f.matches({cn: 'bar'}); => false
|
f.matches({cn: 'bar'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
Equality matching uses "strict" type JavaScript comparison, and by default
|
Equality matching uses "strict" type JavaScript comparison, and by default
|
||||||
everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison
|
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
|
The `matches()` method will return true IFF the passed in object has a
|
||||||
key matching `attribute`.
|
key matching `attribute`.
|
||||||
|
|
||||||
```js
|
var f = new PresenceFilter({
|
||||||
const f = new PresenceFilter({
|
attribute: 'cn'
|
||||||
attribute: 'cn'
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'foo'}); => true
|
f.matches({cn: 'foo'}); => true
|
||||||
f.matches({sn: 'foo'}); => false
|
f.matches({sn: 'foo'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# SubstringFilter
|
# 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
|
The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would
|
||||||
map to:
|
map to:
|
||||||
|
|
||||||
```js
|
{
|
||||||
{
|
initial: 'foo',
|
||||||
initial: 'foo',
|
any: ['bar', 'cat'],
|
||||||
any: ['bar', 'cat'],
|
final: 'dog'
|
||||||
final: 'dog'
|
}
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The `matches()` method will return true IFF the passed in object has a
|
The `matches()` method will return true IFF the passed in object has a
|
||||||
key matching `attribute` and the "regex" matches the value
|
key matching `attribute` and the "regex" matches the value
|
||||||
|
|
||||||
```js
|
var f = new SubstringFilter({
|
||||||
const f = new SubstringFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
initial: 'foo',
|
||||||
initial: 'foo',
|
any: ['bar'],
|
||||||
any: ['bar'],
|
final: 'baz'
|
||||||
final: 'baz'
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'foobigbardogbaz'}); => true
|
f.matches({cn: 'foobigbardogbaz'}); => true
|
||||||
f.matches({sn: 'fobigbardogbaz'}); => false
|
f.matches({sn: 'fobigbardogbaz'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# GreaterThanEqualsFilter
|
# GreaterThanEqualsFilter
|
||||||
|
|
||||||
|
@ -147,22 +131,18 @@ property and the `name` property will be `ge`.
|
||||||
|
|
||||||
The string syntax for a ge filter is:
|
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
|
The `matches()` method will return true IFF the passed in object has a
|
||||||
key matching `attribute` and the value is `>=` this filter's `value`.
|
key matching `attribute` and the value is `>=` this filter's `value`.
|
||||||
|
|
||||||
```js
|
var f = new GreaterThanEqualsFilter({
|
||||||
const f = new GreaterThanEqualsFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo',
|
||||||
value: 'foo',
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'foobar'}); => true
|
f.matches({cn: 'foobar'}); => true
|
||||||
f.matches({cn: 'abc'}); => false
|
f.matches({cn: 'abc'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# LessThanEqualsFilter
|
# LessThanEqualsFilter
|
||||||
|
|
||||||
|
@ -175,9 +155,7 @@ Note that the ldapjs schema middleware will do this.
|
||||||
|
|
||||||
The string syntax for a le filter is:
|
The string syntax for a le filter is:
|
||||||
|
|
||||||
```
|
(cn<=foo)
|
||||||
(cn<=foo)
|
|
||||||
```
|
|
||||||
|
|
||||||
The LessThanEqualsFilter will have an `attribute` property, a `value`
|
The LessThanEqualsFilter will have an `attribute` property, a `value`
|
||||||
property and the `name` property will be `le`.
|
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
|
The `matches()` method will return true IFF the passed in object has a
|
||||||
key matching `attribute` and the value is `<=` this filter's `value`.
|
key matching `attribute` and the value is `<=` this filter's `value`.
|
||||||
|
|
||||||
```js
|
var f = new LessThanEqualsFilter({
|
||||||
const f = new LessThanEqualsFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo',
|
||||||
value: 'foo',
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'abc'}); => true
|
f.matches({cn: 'abc'}); => true
|
||||||
f.matches({cn: 'foobar'}); => false
|
f.matches({cn: 'foobar'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# AndFilter
|
# 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
|
The string syntax for an and filter is (assuming below we're and'ing two
|
||||||
equality filters):
|
equality filters):
|
||||||
|
|
||||||
```
|
(&(cn=foo)(sn=bar))
|
||||||
(&(cn=foo)(sn=bar))
|
|
||||||
```
|
|
||||||
|
|
||||||
The `matches()` method will return true IFF the passed in object matches all
|
The `matches()` method will return true IFF the passed in object matches all
|
||||||
the filters in the `filters` array.
|
the filters in the `filters` array.
|
||||||
|
|
||||||
```js
|
var f = new AndFilter({
|
||||||
const f = new AndFilter({
|
filters: [
|
||||||
filters: [
|
new EqualityFilter({
|
||||||
new EqualityFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo'
|
||||||
value: 'foo'
|
}),
|
||||||
}),
|
new EqualityFilter({
|
||||||
new EqualityFilter({
|
attribute: 'sn',
|
||||||
attribute: 'sn',
|
value: 'bar'
|
||||||
value: 'bar'
|
})
|
||||||
})
|
]
|
||||||
]
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'foo', sn: 'bar'}); => true
|
f.matches({cn: 'foo', sn: 'bar'}); => true
|
||||||
f.matches({cn: 'foo', sn: 'baz'}); => false
|
f.matches({cn: 'foo', sn: 'baz'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# OrFilter
|
# 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
|
The string syntax for an or filter is (assuming below we're or'ing two
|
||||||
equality filters):
|
equality filters):
|
||||||
|
|
||||||
```
|
(|(cn=foo)(sn=bar))
|
||||||
(|(cn=foo)(sn=bar))
|
|
||||||
```
|
|
||||||
|
|
||||||
The `matches()` method will return true IFF the passed in object matches *any*
|
The `matches()` method will return true IFF the passed in object matches *any*
|
||||||
of the filters in the `filters` array.
|
of the filters in the `filters` array.
|
||||||
|
|
||||||
```js
|
var f = new OrFilter({
|
||||||
const f = new OrFilter({
|
filters: [
|
||||||
filters: [
|
new EqualityFilter({
|
||||||
new EqualityFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo'
|
||||||
value: 'foo'
|
}),
|
||||||
}),
|
new EqualityFilter({
|
||||||
new EqualityFilter({
|
attribute: 'sn',
|
||||||
attribute: 'sn',
|
value: 'bar'
|
||||||
value: 'bar'
|
})
|
||||||
})
|
]
|
||||||
]
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'foo', sn: 'baz'}); => true
|
f.matches({cn: 'foo', sn: 'baz'}); => true
|
||||||
f.matches({cn: 'bar', sn: 'baz'}); => false
|
f.matches({cn: 'bar', sn: 'baz'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# NotFilter
|
# 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
|
The string syntax for a not filter is (assuming below we're not'ing an
|
||||||
equality filter):
|
equality filter):
|
||||||
|
|
||||||
```
|
(!(cn=foo))
|
||||||
(!(cn=foo))
|
|
||||||
```
|
|
||||||
|
|
||||||
The `matches()` method will return true IFF the passed in object does not match
|
The `matches()` method will return true IFF the passed in object does not match
|
||||||
the filter in the `filter` property.
|
the filter in the `filter` property.
|
||||||
|
|
||||||
```js
|
var f = new NotFilter({
|
||||||
const f = new NotFilter({
|
filter: new EqualityFilter({
|
||||||
filter: new EqualityFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo'
|
||||||
value: 'foo'
|
})
|
||||||
})
|
});
|
||||||
});
|
|
||||||
|
|
||||||
f.matches({cn: 'bar'}); => true
|
f.matches({cn: 'bar'}); => true
|
||||||
f.matches({cn: 'foo'}); => false
|
f.matches({cn: 'foo'}); => false
|
||||||
```
|
|
||||||
|
|
||||||
# ApproximateFilter
|
# ApproximateFilter
|
||||||
|
|
||||||
The approximate filter is used to check "approximate" matching of
|
The approximate filter is used to check "approximate" matching of
|
||||||
attribute/value assertions. This object will have an `attribute` and
|
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
|
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
|
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
|
The `matches()` method will return true IFF the passed in object has a
|
||||||
key matching `attribute` and a value exactly matching `value`.
|
key matching `attribute` and a value exactly matching `value`.
|
||||||
|
|
||||||
```js
|
var f = new ApproximateFilter({
|
||||||
const f = new ApproximateFilter({
|
attribute: 'cn',
|
||||||
attribute: 'cn',
|
value: 'foo'
|
||||||
value: 'foo'
|
});
|
||||||
});
|
|
||||||
|
f.matches({cn: 'foo'}); => true
|
||||||
|
f.matches({cn: 'bar'}); => false
|
||||||
|
|
||||||
f.matches({cn: 'foo'}); => true
|
|
||||||
f.matches({cn: 'bar'}); => false
|
|
||||||
```
|
|
||||||
|
|
548
docs/guide.md
548
docs/guide.md
|
@ -1,11 +1,10 @@
|
||||||
---
|
---
|
||||||
title: LDAP Guide | ldapjs
|
title: LDAP Guide | ldapjs
|
||||||
|
markdown2extras: tables
|
||||||
---
|
---
|
||||||
|
|
||||||
# LDAP Guide
|
# LDAP Guide
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
This guide was written assuming that you (1) don't know anything about ldapjs,
|
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
|
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
|
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
|
By the end of this guide, we'll have a simple LDAP server that accomplishes a
|
||||||
"real" task.
|
"real" task.
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# What exactly is LDAP?
|
# What exactly is LDAP?
|
||||||
|
|
||||||
If you haven't already read the
|
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:
|
It might be helpful to visualize:
|
||||||
|
|
||||||
```
|
o=example
|
||||||
o=example
|
/ \
|
||||||
/ \
|
ou=users ou=groups
|
||||||
ou=users ou=groups
|
/ | | \
|
||||||
/ | | \
|
cn=john cn=jane cn=dudes cn=dudettes
|
||||||
cn=john cn=jane cn=dudes cn=dudettes
|
/
|
||||||
/
|
keyid=foo
|
||||||
keyid=foo
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's say we wanted to look at the record cn=john:
|
Let's say we wanted to look at the record cn=john:
|
||||||
|
|
||||||
```shell
|
dn: cn=john, ou=users, o=example
|
||||||
dn: cn=john, ou=users, o=example
|
cn: john
|
||||||
cn: john
|
sn: smith
|
||||||
sn: smith
|
email: john@example.com
|
||||||
email: john@example.com
|
email: john.smith@example.com
|
||||||
email: john.smith@example.com
|
objectClass: person
|
||||||
objectClass: person
|
|
||||||
```
|
|
||||||
|
|
||||||
A few things to note:
|
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
|
problem. Want to back it to redis? Should be able to get some basics up in a
|
||||||
day or two.
|
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."
|
get out of the way so you can just use the "good parts."
|
||||||
|
|
||||||
# Ok, cool. Learn me some LDAP!
|
# 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),
|
the steps at [nodejs.org](http://nodejs.org) and [npmjs.org](http://npmjs.org),
|
||||||
respectively. After that, run:
|
respectively. After that, run:
|
||||||
|
|
||||||
```shell
|
$ npm install ldapjs
|
||||||
$ npm install ldapjs
|
|
||||||
```
|
|
||||||
|
|
||||||
Rather than overload you with client-side programming for now, we'll use
|
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
|
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
|
To get started, open some file, and let's get the library loaded and a server
|
||||||
created:
|
created:
|
||||||
|
|
||||||
```js
|
var ldap = require('ldapjs');
|
||||||
const ldap = require('ldapjs');
|
|
||||||
|
|
||||||
const server = ldap.createServer();
|
var server = ldap.createServer();
|
||||||
|
|
||||||
server.listen(1389, () => {
|
server.listen(1389, function() {
|
||||||
console.log('/etc/passwd LDAP server up at: %s', server.url);
|
console.log('/etc/passwd LDAP server up at: %s', server.url);
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
And run that. Doing anything will give you errors (LDAP "No Such Object")
|
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:
|
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
|
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.
|
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
|
and going to use for allowing an (LDAP) admin to do anything. To do so, add
|
||||||
this code into your file:
|
this code into your file:
|
||||||
|
|
||||||
```js
|
server.bind('cn=root', function(req, res, next) {
|
||||||
server.bind('cn=root', (req, res, next) => {
|
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
|
||||||
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
|
return next(new ldap.InvalidCredentialsError());
|
||||||
return next(new ldap.InvalidCredentialsError());
|
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
Not very secure, but this is a demo. What we did there was "mount" a tree in
|
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
|
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?
|
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
|
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
|
"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:
|
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)
|
ldap_bind: Invalid credentials (49)
|
||||||
matched DN: cn=root
|
matched DN: cn=root
|
||||||
additional info: Invalid Credentials
|
additional info: Invalid Credentials
|
||||||
```
|
|
||||||
|
|
||||||
And again with the correct one:
|
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)
|
No such object (32)
|
||||||
Additional information: No tree found for: o=myhost
|
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
|
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`
|
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
|
*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:
|
authorization handler that we'll use in all our subsequent routes:
|
||||||
|
|
||||||
```js
|
function authorize(req, res, next) {
|
||||||
function authorize(req, res, next) {
|
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
return next(new ldap.InsufficientAccessRightsError());
|
||||||
return next(new ldap.InsufficientAccessRightsError());
|
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
Should be pretty self-explanatory, but as a reminder, LDAP is connection
|
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`
|
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
|
We said we wanted to allow LDAP operations over /etc/passwd, so let's detour
|
||||||
for a moment to explain an /etc/passwd record.
|
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:
|
The sample record above maps to:
|
||||||
|
|
||||||
|
@ -269,86 +245,77 @@ The sample record above maps to:
|
||||||
|/bin/sh |Shell |
|
|/bin/sh |Shell |
|
||||||
|
|
||||||
Let's write some handlers to parse that and transform it into an LDAP search
|
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).
|
source file).
|
||||||
|
|
||||||
First, make a handler that just loads the "user database" in a "pre" handler:
|
First, make a handler that just loads the "user database" in a "pre" handler:
|
||||||
|
|
||||||
```js
|
function loadPasswdFile(req, res, next) {
|
||||||
function loadPasswdFile(req, res, next) {
|
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
|
||||||
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
|
if (err)
|
||||||
if (err)
|
return next(new ldap.OperationsError(err.message));
|
||||||
return next(new ldap.OperationsError(err.message));
|
|
||||||
|
|
||||||
req.users = {};
|
req.users = {};
|
||||||
|
|
||||||
const lines = data.split('\n');
|
var lines = data.split('\n');
|
||||||
for (const line of lines) {
|
for (var i = 0; i < lines.length; i++) {
|
||||||
if (!line || /^#/.test(line))
|
if (!lines[i] || /^#/.test(lines[i]))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const record = line.split(':');
|
var record = lines[i].split(':');
|
||||||
if (!record || !record.length)
|
if (!record || !record.length)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
req.users[record[0]] = {
|
req.users[record[0]] = {
|
||||||
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
||||||
attributes: {
|
attributes: {
|
||||||
cn: record[0],
|
cn: record[0],
|
||||||
uid: record[2],
|
uid: record[2],
|
||||||
gid: record[3],
|
gid: record[3],
|
||||||
description: record[4],
|
description: record[4],
|
||||||
homedirectory: record[5],
|
homedirectory: record[5],
|
||||||
shell: record[6] || '',
|
shell: record[6] || '',
|
||||||
objectclass: 'unixUser'
|
objectclass: 'unixUser'
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
```
|
|
||||||
|
|
||||||
Ok, all that did is tack the /etc/passwd records onto req.users so that any
|
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
|
subsequent handler doesn't have to reload the file. Next, let's write a search
|
||||||
handler to process that:
|
handler to process that:
|
||||||
|
|
||||||
```js
|
var pre = [authorize, loadPasswdFile];
|
||||||
const pre = [authorize, loadPasswdFile];
|
|
||||||
|
|
||||||
server.search('o=myhost', pre, (req, res, next) => {
|
server.search('o=myhost', pre, function(req, res, next) {
|
||||||
const keys = Object.keys(req.users);
|
Object.keys(req.users).forEach(function(k) {
|
||||||
for (const k of keys) {
|
if (req.filter.matches(req.users[k].attributes))
|
||||||
if (req.filter.matches(req.users[k].attributes))
|
res.send(req.users[k]);
|
||||||
res.send(req.users[k]);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
And try running:
|
And try running:
|
||||||
|
|
||||||
```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
|
dn: cn=root, ou=users, o=myhost
|
||||||
dn: cn=root, ou=users, o=myhost
|
cn: root
|
||||||
cn: root
|
uid: 0
|
||||||
uid: 0
|
gid: 0
|
||||||
gid: 0
|
description: System Administrator
|
||||||
description: System Administrator
|
homedirectory: /var/root
|
||||||
homedirectory: /var/root
|
shell: /bin/sh
|
||||||
shell: /bin/sh
|
objectclass: unixUser
|
||||||
objectclass: unixUser
|
|
||||||
```
|
|
||||||
|
|
||||||
Sweet! Try this out too:
|
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.
|
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...
|
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:
|
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
|
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.
|
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
|
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:
|
of attributes. So that's why we did this:
|
||||||
|
|
||||||
```js
|
var entry = {
|
||||||
const entry = {
|
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
||||||
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
attributes: {
|
||||||
attributes: {
|
cn: record[0],
|
||||||
cn: record[0],
|
uid: record[2],
|
||||||
uid: record[2],
|
gid: record[3],
|
||||||
gid: record[3],
|
description: record[4],
|
||||||
description: record[4],
|
homedirectory: record[5],
|
||||||
homedirectory: record[5],
|
shell: record[6] || '',
|
||||||
shell: record[6] || '',
|
objectclass: 'unixUser'
|
||||||
objectclass: 'unixUser'
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, we let ldapjs do all the hard work of figuring out LDAP search filters
|
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
|
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
|
on Ubuntu). Then, let's say we only care about their login name and primary
|
||||||
group id. We'd do this:
|
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
|
||||||
$ 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
|
||||||
dn: cn=proxy, ou=users, o=myhost
|
cn: proxy
|
||||||
cn: proxy
|
gid: 13
|
||||||
gid: 13
|
|
||||||
|
|
||||||
dn: cn=pulse, ou=users, o=myhost
|
dn: cn=pulse, ou=users, o=myhost
|
||||||
cn: pulse
|
cn: pulse
|
||||||
gid: 114
|
gid: 114
|
||||||
```
|
|
||||||
|
|
||||||
## Add
|
## Add
|
||||||
|
|
||||||
This is going to be a little bit ghetto, since what we're going to do is just
|
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
|
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
|
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, function(req, res, next) {
|
||||||
server.add('ou=users, o=myhost', pre, (req, res, next) => {
|
if (!req.dn.rdns[0].cn)
|
||||||
if (!req.dn.rdns[0].attrs.cn)
|
return next(new ldap.ConstraintViolationError('cn required'));
|
||||||
return next(new ldap.ConstraintViolationError('cn required'));
|
|
||||||
|
|
||||||
if (req.users[req.dn.rdns[0].attrs.cn.value])
|
if (req.users[req.dn.rdns[0].cn])
|
||||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||||
|
|
||||||
const entry = req.toObject().attributes;
|
var entry = req.toObject().attributes;
|
||||||
|
|
||||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||||
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
||||||
|
|
||||||
const opts = ['-m'];
|
var opts = ['-m'];
|
||||||
if (entry.description) {
|
if (entry.description) {
|
||||||
opts.push('-c');
|
opts.push('-c');
|
||||||
opts.push(entry.description[0]);
|
opts.push(entry.description[0]);
|
||||||
}
|
}
|
||||||
if (entry.homedirectory) {
|
if (entry.homedirectory) {
|
||||||
opts.push('-d');
|
opts.push('-d');
|
||||||
opts.push(entry.homedirectory[0]);
|
opts.push(entry.homedirectory[0]);
|
||||||
}
|
}
|
||||||
if (entry.gid) {
|
if (entry.gid) {
|
||||||
opts.push('-g');
|
opts.push('-g');
|
||||||
opts.push(entry.gid[0]);
|
opts.push(entry.gid[0]);
|
||||||
}
|
}
|
||||||
if (entry.shell) {
|
if (entry.shell) {
|
||||||
opts.push('-s');
|
opts.push('-s');
|
||||||
opts.push(entry.shell[0]);
|
opts.push(entry.shell[0]);
|
||||||
}
|
}
|
||||||
if (entry.uid) {
|
if (entry.uid) {
|
||||||
opts.push('-u');
|
opts.push('-u');
|
||||||
opts.push(entry.uid[0]);
|
opts.push(entry.uid[0]);
|
||||||
}
|
}
|
||||||
opts.push(entry.cn[0]);
|
opts.push(entry.cn[0]);
|
||||||
const useradd = spawn('useradd', opts);
|
var useradd = spawn('useradd', opts);
|
||||||
|
|
||||||
const messages = [];
|
var messages = [];
|
||||||
|
|
||||||
useradd.stdout.on('data', (data) => {
|
useradd.stdout.on('data', function(data) {
|
||||||
messages.push(data.toString());
|
messages.push(data.toString());
|
||||||
});
|
});
|
||||||
useradd.stderr.on('data', (data) => {
|
useradd.stderr.on('data', function(data) {
|
||||||
messages.push(data.toString());
|
messages.push(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
useradd.on('exit', (code) => {
|
useradd.on('exit', function(code) {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
let msg = '' + code;
|
var msg = '' + code;
|
||||||
if (messages.length)
|
if (messages.length)
|
||||||
msg += ': ' + messages.join();
|
msg += ': ' + messages.join();
|
||||||
return next(new ldap.OperationsError(msg));
|
return next(new ldap.OperationsError(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
Then, you'll need to be root to have this running, so start your server with
|
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
|
`sudo` (or be root, whatever). Now, go ahead and create a file called
|
||||||
`user.ldif` with the following contents:
|
`user.ldif` with the following contents:
|
||||||
|
|
||||||
```shell
|
dn: cn=ldapjs, ou=users, o=myhost
|
||||||
dn: cn=ldapjs, ou=users, o=myhost
|
objectClass: unixUser
|
||||||
objectClass: unixUser
|
cn: ldapjs
|
||||||
cn: ldapjs
|
shell: /bin/bash
|
||||||
shell: /bin/bash
|
description: Created via ldapadd
|
||||||
description: Created via ldapadd
|
|
||||||
```
|
|
||||||
|
|
||||||
Now go ahead and invoke with:
|
Now go ahead and invoke with:
|
||||||
|
|
||||||
```shell
|
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
|
||||||
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
|
adding new entry "cn=ldapjs, ou=users, o=myhost"
|
||||||
adding new entry "cn=ldapjs, ou=users, o=myhost"
|
|
||||||
```
|
|
||||||
|
|
||||||
Let's confirm he got added with an ldapsearch:
|
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
|
||||||
$ 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
|
||||||
dn: cn=ldapjs, ou=users, o=myhost
|
cn: ldapjs
|
||||||
cn: ldapjs
|
uid: 1001
|
||||||
uid: 1001
|
gid: 1001
|
||||||
gid: 1001
|
description: Created via ldapadd
|
||||||
description: Created via ldapadd
|
homedirectory: /home/ldapjs
|
||||||
homedirectory: /home/ldapjs
|
shell: /bin/bash
|
||||||
shell: /bin/bash
|
objectclass: unixUser
|
||||||
objectclass: unixUser
|
|
||||||
```
|
|
||||||
|
|
||||||
As before, here's a breakdown of the code:
|
As before, here's a breakdown of the code:
|
||||||
|
|
||||||
```js
|
server.add('ou=users, o=myhost', pre, function(req, res, next) {
|
||||||
server.add('ou=users, o=myhost', pre, (req, res, next) => {
|
if (!req.dn.rdns[0].cn)
|
||||||
if (!req.dn.rdns[0].attrs.cn)
|
return next(new ldap.ConstraintViolationError('cn required'));
|
||||||
return next(new ldap.ConstraintViolationError('cn required'));
|
|
||||||
|
|
||||||
if (req.users[req.dn.rdns[0].attrs.cn.value])
|
if (req.users[req.dn.rdns[0].cn])
|
||||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||||
|
|
||||||
const entry = req.toObject().attributes;
|
var entry = req.toObject().attributes;
|
||||||
|
|
||||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||||
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
A few new things:
|
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.
|
RFC, so appending, removing, or replacing a single attribute is pretty natural.
|
||||||
Go ahead and add the following code into your source file:
|
Go ahead and add the following code into your source file:
|
||||||
|
|
||||||
```js
|
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
|
||||||
server.modify('ou=users, o=myhost', pre, (req, res, next) => {
|
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
||||||
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
|
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
|
||||||
|
|
||||||
if (!req.changes.length)
|
if (!req.changes.length)
|
||||||
return next(new ldap.ProtocolError('changes required'));
|
return next(new ldap.ProtocolError('changes required'));
|
||||||
|
|
||||||
const user = req.users[req.dn.rdns[0].attrs.cn.value].attributes;
|
var user = req.users[req.dn.rdns[0].cn].attributes;
|
||||||
let mod;
|
var mod;
|
||||||
|
|
||||||
for (const i = 0; i < req.changes.length; i++) {
|
for (var i = 0; i < req.changes.length; i++) {
|
||||||
mod = req.changes[i].modification;
|
mod = req.changes[i].modification;
|
||||||
switch (req.changes[i].operation) {
|
switch (req.changes[i].operation) {
|
||||||
case 'replace':
|
case 'replace':
|
||||||
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
|
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
|
||||||
return next(new ldap.UnwillingToPerformError('only password updates ' +
|
return next(new ldap.UnwillingToPerformError('only password updates ' +
|
||||||
'allowed'));
|
'allowed'));
|
||||||
break;
|
break;
|
||||||
case 'add':
|
case 'add':
|
||||||
case 'delete':
|
case 'delete':
|
||||||
return next(new ldap.UnwillingToPerformError('only replace allowed'));
|
return next(new ldap.UnwillingToPerformError('only replace allowed'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwd = spawn('chpasswd', ['-c', 'MD5']);
|
var passwd = spawn('chpasswd', ['-c', 'MD5']);
|
||||||
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
|
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
|
||||||
|
|
||||||
passwd.on('exit', (code) => {
|
passwd.on('exit', function(code) {
|
||||||
if (code !== 0)
|
if (code !== 0)
|
||||||
return next(new ldap.OperationsError(code));
|
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,
|
Basically, we made sure the remote client was targeting an entry that exists,
|
||||||
ensuring that they were asking to "replace" the `userPassword` attribute (which
|
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
|
command (which lets you change a user's password over stdin). Next, go ahead
|
||||||
and create a `passwd.ldif` file:
|
and create a `passwd.ldif` file:
|
||||||
|
|
||||||
```shell
|
dn: cn=ldapjs, ou=users, o=myhost
|
||||||
dn: cn=ldapjs, ou=users, o=myhost
|
changetype: modify
|
||||||
changetype: modify
|
replace: userPassword
|
||||||
replace: userPassword
|
userPassword: secret
|
||||||
userPassword: secret
|
-
|
||||||
-
|
|
||||||
```
|
|
||||||
|
|
||||||
And then run the OpenLDAP CLI:
|
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
|
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.
|
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 is pretty straightforward. The client gives you a dn to delete, and you
|
||||||
delete it :). Add the following code into your server:
|
delete it :). Add the following code into your server:
|
||||||
|
|
||||||
```js
|
server.del('ou=users, o=myhost', pre, function(req, res, next) {
|
||||||
server.del('ou=users, o=myhost', pre, (req, res, next) => {
|
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
||||||
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
|
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||||
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 = [];
|
var messages = [];
|
||||||
userdel.stdout.on('data', (data) => {
|
userdel.stdout.on('data', function(data) {
|
||||||
messages.push(data.toString());
|
messages.push(data.toString());
|
||||||
});
|
});
|
||||||
userdel.stderr.on('data', (data) => {
|
userdel.stderr.on('data', function(data) {
|
||||||
messages.push(data.toString());
|
messages.push(data.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
userdel.on('exit', (code) => {
|
userdel.on('exit', function(code) {
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
let msg = '' + code;
|
var msg = '' + code;
|
||||||
if (messages.length)
|
if (messages.length)
|
||||||
msg += ': ' + messages.join();
|
msg += ': ' + messages.join();
|
||||||
return next(new ldap.OperationsError(msg));
|
return next(new ldap.OperationsError(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
return next();
|
return next();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
And then run the following command:
|
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
|
# Where to go from here
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
---
|
---
|
||||||
title: ldapjs
|
title: ldapjs
|
||||||
|
markdown2extras: tables
|
||||||
---
|
---
|
||||||
|
|
||||||
<div id="indextagline">
|
<div id="indextagline">
|
||||||
|
@ -8,45 +9,37 @@ Reimagining <a href="http://tools.ietf.org/html/rfc4510" id="indextaglink">LDAP<
|
||||||
|
|
||||||
# Overview
|
# Overview
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
ldapjs is a pure JavaScript, from-scratch framework for implementing
|
ldapjs is a pure JavaScript, from-scratch framework for implementing
|
||||||
[LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in
|
[LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in
|
||||||
[Node.js](http://nodejs.org). It is intended for developers used to interacting
|
[Node.js](http://nodejs.org). It is intended for developers used to interacting
|
||||||
with HTTP services in node and [restify](http://restify.com).
|
with HTTP services in node and [restify](http://restify.com).
|
||||||
|
|
||||||
</div>
|
var ldap = require('ldapjs');
|
||||||
|
|
||||||
```js
|
var server = ldap.createServer();
|
||||||
const ldap = require('ldapjs');
|
|
||||||
|
|
||||||
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) => {
|
if (req.filter.matches(obj.attributes))
|
||||||
const obj = {
|
res.send(obj);
|
||||||
dn: req.dn.toString(),
|
|
||||||
attributes: {
|
|
||||||
objectclass: ['organization', 'top'],
|
|
||||||
o: 'example'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (req.filter.matches(obj.attributes))
|
res.end();
|
||||||
res.send(obj);
|
});
|
||||||
|
|
||||||
res.end();
|
server.listen(1389, function() {
|
||||||
});
|
console.log('LDAP server listening at %s', server.url);
|
||||||
|
});
|
||||||
server.listen(1389, () => {
|
|
||||||
console.log('LDAP server listening at %s', server.url);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Try hitting that with:
|
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
|
# Features
|
||||||
|
|
||||||
|
@ -59,9 +52,7 @@ that you can build LDAP over anything you want, not just traditional databases.
|
||||||
|
|
||||||
# Getting started
|
# Getting started
|
||||||
|
|
||||||
```shell
|
$ npm install ldapjs
|
||||||
$ npm install ldapjs
|
|
||||||
```
|
|
||||||
|
|
||||||
If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
|
If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
|
||||||
API documentation is:
|
API documentation is:
|
||||||
|
@ -79,7 +70,9 @@ API documentation is:
|
||||||
# More information
|
# More information
|
||||||
|
|
||||||
- License:[MIT](http://opensource.org/licenses/mit-license.php)
|
- 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?
|
# What's not in the box?
|
||||||
|
|
||||||
|
@ -93,3 +86,5 @@ Specifically:
|
||||||
* Extensible matching
|
* Extensible matching
|
||||||
|
|
||||||
There are a few others, but those are the "big" ones.
|
There are a few others, but those are the "big" ones.
|
||||||
|
|
||||||
|
|
||||||
|
|
269
docs/server.md
269
docs/server.md
|
@ -1,23 +1,18 @@
|
||||||
---
|
---
|
||||||
title: Server API | ldapjs
|
title: Server API | ldapjs
|
||||||
|
markdown2extras: wiki-tables
|
||||||
---
|
---
|
||||||
|
|
||||||
# ldapjs Server API
|
# ldapjs Server API
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
|
|
||||||
This document covers the ldapjs server API and assumes that you are familiar
|
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.
|
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
# Create a server
|
# Create a server
|
||||||
|
|
||||||
The code to create a new server looks like:
|
The code to create a new server looks like:
|
||||||
|
|
||||||
```js
|
var server = ldap.createServer();
|
||||||
const server = ldap.createServer();
|
|
||||||
```
|
|
||||||
|
|
||||||
The full list of options is:
|
The full list of options is:
|
||||||
|
|
||||||
|
@ -70,25 +65,17 @@ available.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
```js
|
server.listen(389, '127.0.0.1', function() {
|
||||||
server.listen(389, '127.0.0.1', function() {
|
console.log('LDAP server listening at: ' + server.url);
|
||||||
console.log('LDAP server listening at: ' + server.url);
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port and Host
|
### Port and Host
|
||||||
`listen(port, [host], [callback])`
|
`listen(port, [host], [callback])`
|
||||||
|
|
||||||
Begin accepting connections on the specified port and host. If the host is
|
Begin accepting connections on the specified port and host. If the host is
|
||||||
omitted, the server will accept connections directed to the IPv4 address
|
omitted, the server will accept connections directed to any IPv4 address
|
||||||
`127.0.0.1`. To listen on any other address, supply said address as the `host`
|
(INADDR\_ANY).
|
||||||
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.
|
|
||||||
|
|
||||||
This function is asynchronous. The last parameter callback will be called when
|
This function is asynchronous. The last parameter callback will be called when
|
||||||
the server has been bound.
|
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
|
handlers together by calling `next()` and ordering your functions in the
|
||||||
definition of the route. For example:
|
definition of the route. For example:
|
||||||
|
|
||||||
```js
|
function authorize(req, res, next) {
|
||||||
function authorize(req, res, next) {
|
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
return next(new ldap.InsufficientAccessRightsError());
|
||||||
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
|
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
|
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
|
[Riak](https://github.com/basho/riak). Getting an LDAP server up with riak
|
||||||
looks like:
|
looks like:
|
||||||
|
|
||||||
```js
|
var ldap = require('ldapjs');
|
||||||
const ldap = require('ldapjs');
|
var ldapRiak = require('ldapjs-riak');
|
||||||
const ldapRiak = require('ldapjs-riak');
|
|
||||||
|
|
||||||
const server = ldap.createServer();
|
var server = ldap.createServer();
|
||||||
const backend = ldapRiak.createBackend({
|
var backend = ldapRiak.createBackend({
|
||||||
"host": "localhost",
|
"host": "localhost",
|
||||||
"port": 8098,
|
"port": 8098,
|
||||||
"bucket": "example",
|
"bucket": "example",
|
||||||
"indexes": ["l", "cn"],
|
"indexes": ["l", "cn"],
|
||||||
"uniqueIndexes": ["uid"],
|
"uniqueIndexes": ["uid"],
|
||||||
"numConnections": 5
|
"numConnections": 5
|
||||||
});
|
});
|
||||||
|
|
||||||
server.add("o=example",
|
server.add("o=example",
|
||||||
backend,
|
backend,
|
||||||
backend.add());
|
backend.add());
|
||||||
...
|
...
|
||||||
```
|
|
||||||
|
|
||||||
The first parameter to an ldapjs route is always the point in the
|
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
|
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
|
objects. However, there is a `.use()` method availabe, similar to
|
||||||
that on express/connect, allowing you to chain up "middleware":
|
that on express/connect, allowing you to chain up "middleware":
|
||||||
|
|
||||||
```js
|
server.use(function(req, res, next) {
|
||||||
server.use(function(req, res, next) {
|
console.log('hello world');
|
||||||
console.log('hello world');
|
return next();
|
||||||
return next();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Request Elements
|
## 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`.
|
will be there defaulted to `cn=anonymous`.
|
||||||
|
|
||||||
Additionally, request will have a `logId` parameter you can use to uniquely
|
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
|
## 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())`,
|
`ConstraintViolationError` in ldapjs. Upon calling `next(new LDAPError())`,
|
||||||
ldapjs will _stop_ calling your handler chain. For example:
|
ldapjs will _stop_ calling your handler chain. For example:
|
||||||
|
|
||||||
```js
|
server.search('o=example',
|
||||||
server.search('o=example',
|
function(req, res, next) { return next(); },
|
||||||
(req, res, next) => { return next(); },
|
function(req, res, next) { return next(new ldap.OperationsError()); },
|
||||||
(req, res, next) => { return next(new ldap.OperationsError()); },
|
function(req, res, next) { res.end(); }
|
||||||
(req, res, next) => { res.end(); }
|
);
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
In the code snipped above, the third handler would never get invoked.
|
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:
|
Adds a mount in the tree to perform LDAP binds with. Example:
|
||||||
|
|
||||||
```js
|
server.bind('ou=people, o=example', function(req, res, next) {
|
||||||
server.bind('ou=people, o=example', (req, res, next) => {
|
console.log('bind DN: ' + req.dn.toString());
|
||||||
console.log('bind DN: ' + req.dn.toString());
|
console.log('bind PW: ' + req.credentials);
|
||||||
console.log('bind PW: ' + req.credentials);
|
res.end();
|
||||||
res.end();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## BindRequest
|
## BindRequest
|
||||||
|
|
||||||
|
@ -279,13 +256,11 @@ No extra methods above an `LDAPResult` API call.
|
||||||
|
|
||||||
Adds a mount in the tree to perform LDAP adds with.
|
Adds a mount in the tree to perform LDAP adds with.
|
||||||
|
|
||||||
```js
|
server.add('ou=people, o=example', function(req, res, next) {
|
||||||
server.add('ou=people, o=example', (req, res, next) => {
|
console.log('DN: ' + req.dn.toString());
|
||||||
console.log('DN: ' + req.dn.toString());
|
console.log('Entry attributes: ' + req.toObject().attributes);
|
||||||
console.log('Entry attributes: ' + req.toObject().attributes);
|
res.end();
|
||||||
res.end();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## AddRequest
|
## AddRequest
|
||||||
|
|
||||||
|
@ -309,16 +284,14 @@ a standard JavaScript object.
|
||||||
This operation will return a plain JavaScript object from the request that looks
|
This operation will return a plain JavaScript object from the request that looks
|
||||||
like:
|
like:
|
||||||
|
|
||||||
```js
|
{
|
||||||
{
|
dn: 'cn=foo, o=example', // string, not DN object
|
||||||
dn: 'cn=foo, o=example', // string, not DN object
|
attributes: {
|
||||||
attributes: {
|
cn: ['foo'],
|
||||||
cn: ['foo'],
|
sn: ['bar'],
|
||||||
sn: ['bar'],
|
objectclass: ['person', 'top']
|
||||||
objectclass: ['person', 'top']
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## AddResponse
|
## AddResponse
|
||||||
|
|
||||||
|
@ -328,14 +301,12 @@ No extra methods above an `LDAPResult` API call.
|
||||||
|
|
||||||
Adds a handler for the LDAP search operation.
|
Adds a handler for the LDAP search operation.
|
||||||
|
|
||||||
```js
|
server.search('o=example', function(req, res, next) {
|
||||||
server.search('o=example', (req, res, next) => {
|
console.log('base object: ' + req.dn.toString());
|
||||||
console.log('base object: ' + req.dn.toString());
|
console.log('scope: ' + req.scope);
|
||||||
console.log('scope: ' + req.scope);
|
console.log('filter: ' + req.filter.toString());
|
||||||
console.log('filter: ' + req.filter.toString());
|
res.end();
|
||||||
res.end();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## SearchRequest
|
## 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()`.
|
JavaScript object that matches the format used from `AddRequest.toObject()`.
|
||||||
|
|
||||||
|
|
||||||
```js
|
server.search('o=example', function(req, res, next) {
|
||||||
server.search('o=example', (req, res, next) => {
|
var obj = {
|
||||||
const obj = {
|
dn: 'o=example',
|
||||||
dn: 'o=example',
|
attributes: {
|
||||||
attributes: {
|
objectclass: ['top', 'organization'],
|
||||||
objectclass: ['top', 'organization'],
|
o: ['example']
|
||||||
o: ['example']
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
|
|
||||||
if (req.filter.matches(obj))
|
if (req.filter.matches(obj))
|
||||||
res.send(obj)
|
res.send(obj)
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
# modify
|
# modify
|
||||||
|
|
||||||
Allows you to handle an LDAP modify operation.
|
Allows you to handle an LDAP modify operation.
|
||||||
|
|
||||||
```js
|
server.modify('o=example', function(req, res, next) {
|
||||||
server.modify('o=example', (req, res, next) => {
|
console.log('DN: ' + req.dn.toString());
|
||||||
console.log('DN: ' + req.dn.toString());
|
console.log('changes:');
|
||||||
console.log('changes:');
|
req.changes.forEach(function(c) {
|
||||||
for (const c of req.changes) {
|
console.log(' operation: ' + c.operation);
|
||||||
console.log(' operation: ' + c.operation);
|
console.log(' modification: ' + c.modification.toString());
|
||||||
console.log(' modification: ' + c.modification.toString());
|
});
|
||||||
}
|
res.end();
|
||||||
res.end();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## ModifyRequest
|
## ModifyRequest
|
||||||
|
|
||||||
|
@ -461,12 +428,10 @@ No extra methods above an `LDAPResult` API call.
|
||||||
|
|
||||||
Allows you to handle an LDAP delete operation.
|
Allows you to handle an LDAP delete operation.
|
||||||
|
|
||||||
```js
|
server.del('o=example', function(req, res, next) {
|
||||||
server.del('o=example', (req, res, next) => {
|
console.log('DN: ' + req.dn.toString());
|
||||||
console.log('DN: ' + req.dn.toString());
|
res.end();
|
||||||
res.end();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## DeleteRequest
|
## DeleteRequest
|
||||||
|
|
||||||
|
@ -483,14 +448,12 @@ No extra methods above an `LDAPResult` API call.
|
||||||
|
|
||||||
Allows you to handle an LDAP compare operation.
|
Allows you to handle an LDAP compare operation.
|
||||||
|
|
||||||
```js
|
server.compare('o=example', function(req, res, next) {
|
||||||
server.compare('o=example', (req, res, next) => {
|
console.log('DN: ' + req.dn.toString());
|
||||||
console.log('DN: ' + req.dn.toString());
|
console.log('attribute name: ' + req.attribute);
|
||||||
console.log('attribute name: ' + req.attribute);
|
console.log('attribute value: ' + req.value);
|
||||||
console.log('attribute value: ' + req.value);
|
res.end(req.value === 'foo');
|
||||||
res.end(req.value === 'foo');
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## CompareRequest
|
## CompareRequest
|
||||||
|
|
||||||
|
@ -517,17 +480,15 @@ that, there are no extra methods above an `LDAPResult` API call.
|
||||||
|
|
||||||
Allows you to handle an LDAP modifyDN operation.
|
Allows you to handle an LDAP modifyDN operation.
|
||||||
|
|
||||||
```js
|
server.modifyDN('o=example', function(req, res, next) {
|
||||||
server.modifyDN('o=example', (req, res, next) => {
|
console.log('DN: ' + req.dn.toString());
|
||||||
console.log('DN: ' + req.dn.toString());
|
console.log('new RDN: ' + req.newRdn.toString());
|
||||||
console.log('new RDN: ' + req.newRdn.toString());
|
console.log('deleteOldRDN: ' + req.deleteOldRdn);
|
||||||
console.log('deleteOldRDN: ' + req.deleteOldRdn);
|
console.log('new superior: ' +
|
||||||
console.log('new superior: ' +
|
(req.newSuperior ? req.newSuperior.toString() : ''));
|
||||||
(req.newSuperior ? req.newSuperior.toString() : ''));
|
|
||||||
|
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
```
|
|
||||||
|
|
||||||
## ModifyDNRequest
|
## 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
|
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.
|
the tree, so routing here will be exact match, as opposed to subtree.
|
||||||
|
|
||||||
```js
|
// LDAP whoami
|
||||||
// LDAP whoami
|
server.exop('1.3.6.1.4.1.4203.1.11.3', function(req, res, next) {
|
||||||
server.exop('1.3.6.1.4.1.4203.1.11.3', (req, res, next) => {
|
console.log('name: ' + req.name);
|
||||||
console.log('name: ' + req.name);
|
console.log('value: ' + req.value);
|
||||||
console.log('value: ' + req.value);
|
res.value = 'u:xxyyz@EXAMPLE.NET';
|
||||||
res.value = 'u:xxyyz@EXAMPLE.NET';
|
res.end();
|
||||||
res.end();
|
return next();
|
||||||
return next();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## ExtendedRequest
|
## 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
|
if you need to clean up any items in your backend, or perform any other cleanup
|
||||||
tasks you need to.
|
tasks you need to.
|
||||||
|
|
||||||
```js
|
server.unbind(function(req, res, next) {
|
||||||
server.unbind((req, res, next) => {
|
res.end();
|
||||||
res.end();
|
});
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that the LDAP unbind operation actually doesn't send any response (by
|
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
|
definition in the RFC), so the UnbindResponse is really just a stub that
|
||||||
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -130,7 +130,7 @@ server.search(SUFFIX, authorize, function (req, res, next) {
|
||||||
case 'base':
|
case 'base':
|
||||||
if (req.filter.matches(db[dn])) {
|
if (req.filter.matches(db[dn])) {
|
||||||
res.send({
|
res.send({
|
||||||
dn,
|
dn: dn,
|
||||||
attributes: db[dn]
|
attributes: db[dn]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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'
|
||||||
|
}
|
|
@ -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
|
|
@ -15,37 +15,37 @@ const vasync = require('vasync')
|
||||||
const assert = require('assert-plus')
|
const assert = require('assert-plus')
|
||||||
const VError = require('verror').VError
|
const VError = require('verror').VError
|
||||||
|
|
||||||
const Attribute = require('@ldapjs/attribute')
|
const Attribute = require('../attribute')
|
||||||
const Change = require('@ldapjs/change')
|
const Change = require('../change')
|
||||||
const Control = require('../controls/index').Control
|
const Control = require('../controls/index').Control
|
||||||
const { Control: LdapControl } = require('@ldapjs/controls')
|
|
||||||
const SearchPager = require('./search_pager')
|
const SearchPager = require('./search_pager')
|
||||||
const Protocol = require('@ldapjs/protocol')
|
const Protocol = require('../protocol')
|
||||||
const { DN } = require('@ldapjs/dn')
|
const dn = require('../dn')
|
||||||
const errors = require('../errors')
|
const errors = require('../errors')
|
||||||
const filters = require('@ldapjs/filter')
|
const filters = require('../filters')
|
||||||
const Parser = require('../messages/parser')
|
const messages = require('../messages')
|
||||||
const url = require('../url')
|
const url = require('../url')
|
||||||
const CorkedEmitter = require('../corked_emitter')
|
const CorkedEmitter = require('../corked_emitter')
|
||||||
|
|
||||||
/// --- Globals
|
/// --- Globals
|
||||||
|
|
||||||
const messages = require('@ldapjs/messages')
|
const AbandonRequest = messages.AbandonRequest
|
||||||
const {
|
const AddRequest = messages.AddRequest
|
||||||
AbandonRequest,
|
const BindRequest = messages.BindRequest
|
||||||
AddRequest,
|
const CompareRequest = messages.CompareRequest
|
||||||
BindRequest,
|
const DeleteRequest = messages.DeleteRequest
|
||||||
CompareRequest,
|
const ExtendedRequest = messages.ExtendedRequest
|
||||||
DeleteRequest,
|
const ModifyRequest = messages.ModifyRequest
|
||||||
ExtensionRequest: ExtendedRequest,
|
const ModifyDNRequest = messages.ModifyDNRequest
|
||||||
ModifyRequest,
|
const SearchRequest = messages.SearchRequest
|
||||||
ModifyDnRequest: ModifyDNRequest,
|
const UnbindRequest = messages.UnbindRequest
|
||||||
SearchRequest,
|
const UnbindResponse = messages.UnbindResponse
|
||||||
UnbindRequest,
|
|
||||||
LdapResult: LDAPResult,
|
const LDAPResult = messages.LDAPResult
|
||||||
SearchResultEntry: SearchEntry,
|
const SearchEntry = messages.SearchEntry
|
||||||
SearchResultReference: SearchReference
|
const SearchReference = messages.SearchReference
|
||||||
} = messages
|
// var SearchResponse = messages.SearchResponse
|
||||||
|
const Parser = messages.Parser
|
||||||
|
|
||||||
const PresenceFilter = filters.PresenceFilter
|
const PresenceFilter = filters.PresenceFilter
|
||||||
|
|
||||||
|
@ -67,9 +67,9 @@ function nextClientId () {
|
||||||
function validateControls (controls) {
|
function validateControls (controls) {
|
||||||
if (Array.isArray(controls)) {
|
if (Array.isArray(controls)) {
|
||||||
controls.forEach(function (c) {
|
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]
|
controls = [controls]
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError('controls must be [Control]')
|
throw new TypeError('controls must be [Control]')
|
||||||
|
@ -78,11 +78,13 @@ function validateControls (controls) {
|
||||||
return controls
|
return controls
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDN (input) {
|
function ensureDN (input, strict) {
|
||||||
if (DN.isDn(input)) {
|
if (dn.DN.isDN(input)) {
|
||||||
return input
|
return dn
|
||||||
|
} else if (strict) {
|
||||||
|
return dn.parse(input)
|
||||||
} else if (typeof (input) === 'string') {
|
} else if (typeof (input) === 'string') {
|
||||||
return DN.fromString(input)
|
return input
|
||||||
} else {
|
} else {
|
||||||
throw new Error('invalid DN')
|
throw new Error('invalid DN')
|
||||||
}
|
}
|
||||||
|
@ -134,6 +136,7 @@ function Client (options) {
|
||||||
failAfter: parseInt(rOpts.failAfter, 10) || Infinity
|
failAfter: parseInt(rOpts.failAfter, 10) || Infinity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
|
||||||
|
|
||||||
this.queue = requestQueueFactory({
|
this.queue = requestQueueFactory({
|
||||||
size: parseInt((options.queueSize || 0), 10),
|
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
|
* The callback will be invoked as soon as the data is flushed out to the
|
||||||
* network, as there is never a response from abandon.
|
* 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 {Control} controls (optional) either a Control or [Control].
|
||||||
* @param {Function} callback of the form f(err).
|
* @param {Function} callback of the form f(err).
|
||||||
* @throws {TypeError} on invalid input.
|
* @throws {TypeError} on invalid input.
|
||||||
*/
|
*/
|
||||||
Client.prototype.abandon = function abandon (messageId, controls, callback) {
|
Client.prototype.abandon = function abandon (messageID, controls, callback) {
|
||||||
assert.number(messageId, 'messageId')
|
assert.number(messageID, 'messageID')
|
||||||
if (typeof (controls) === 'function') {
|
if (typeof (controls) === 'function') {
|
||||||
callback = controls
|
callback = controls
|
||||||
controls = []
|
controls = []
|
||||||
|
@ -190,8 +193,8 @@ Client.prototype.abandon = function abandon (messageId, controls, callback) {
|
||||||
assert.func(callback, 'callback')
|
assert.func(callback, 'callback')
|
||||||
|
|
||||||
const req = new AbandonRequest({
|
const req = new AbandonRequest({
|
||||||
abandonId: messageId,
|
abandonID: messageID,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req, 'abandon', null, callback)
|
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) {
|
save[k].forEach(function (v) {
|
||||||
attr.addValue(v.toString())
|
attr.addValue(v.toString())
|
||||||
})
|
})
|
||||||
} else if (Buffer.isBuffer(save[k])) {
|
|
||||||
attr.addValue(save[k])
|
|
||||||
} else {
|
} else {
|
||||||
attr.addValue(save[k].toString())
|
attr.addValue(save[k].toString())
|
||||||
}
|
}
|
||||||
|
@ -245,9 +246,9 @@ Client.prototype.add = function add (name, entry, controls, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const req = new AddRequest({
|
const req = new AddRequest({
|
||||||
entry: ensureDN(name),
|
entry: ensureDN(name, this.strictDN),
|
||||||
attributes: entry,
|
attributes: entry,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
||||||
|
@ -267,12 +268,7 @@ Client.prototype.bind = function bind (name,
|
||||||
controls,
|
controls,
|
||||||
callback,
|
callback,
|
||||||
_bypass) {
|
_bypass) {
|
||||||
if (
|
if (typeof (name) !== 'string' && !(name instanceof dn.DN)) { throw new TypeError('name (string) required') }
|
||||||
typeof (name) !== 'string' &&
|
|
||||||
Object.prototype.toString.call(name) !== '[object LdapDn]'
|
|
||||||
) {
|
|
||||||
throw new TypeError('name (string) required')
|
|
||||||
}
|
|
||||||
assert.optionalString(credentials, 'credentials')
|
assert.optionalString(credentials, 'credentials')
|
||||||
if (typeof (controls) === 'function') {
|
if (typeof (controls) === 'function') {
|
||||||
callback = controls
|
callback = controls
|
||||||
|
@ -286,7 +282,7 @@ Client.prototype.bind = function bind (name,
|
||||||
name: name || '',
|
name: name || '',
|
||||||
authentication: 'Simple',
|
authentication: 'Simple',
|
||||||
credentials: credentials || '',
|
credentials: credentials || '',
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
// Connection errors will be reported to the bind callback too (useful when the LDAP server is not available)
|
// 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')
|
assert.func(callback, 'callback')
|
||||||
|
|
||||||
const req = new CompareRequest({
|
const req = new CompareRequest({
|
||||||
entry: ensureDN(name),
|
entry: ensureDN(name, this.strictDN),
|
||||||
attribute: attr,
|
attribute: attr,
|
||||||
value,
|
value: value,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req, CMP_EXPECT, null, function (err, res) {
|
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')
|
assert.func(callback, 'callback')
|
||||||
|
|
||||||
const req = new DeleteRequest({
|
const req = new DeleteRequest({
|
||||||
entry: ensureDN(name),
|
entry: ensureDN(name, this.strictDN),
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
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({
|
const req = new ExtendedRequest({
|
||||||
requestName: name,
|
requestName: name,
|
||||||
requestValue: value,
|
requestValue: value,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req, [errors.LDAP_SUCCESS], null, function (err, res) {
|
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 = []
|
const changes = []
|
||||||
|
|
||||||
function changeFromObject (obj) {
|
function changeFromObject (change) {
|
||||||
if (!obj.operation && !obj.type) { throw new Error('change.operation required') }
|
if (!change.operation && !change.type) { throw new Error('change.operation required') }
|
||||||
if (typeof (obj.modification) !== 'object') { throw new Error('change.modification (object) required') }
|
if (typeof (change.modification) !== 'object') { throw new Error('change.modification (object) required') }
|
||||||
|
|
||||||
if (Object.keys(obj.modification).length === 2 &&
|
if (Object.keys(change.modification).length === 2 &&
|
||||||
typeof (obj.modification.type) === 'string' &&
|
typeof (change.modification.type) === 'string' &&
|
||||||
Array.isArray(obj.modification.vals)) {
|
Array.isArray(change.modification.vals)) {
|
||||||
// Use modification directly if it's already normalized:
|
// Use modification directly if it's already normalized:
|
||||||
changes.push(new Change({
|
changes.push(new Change({
|
||||||
operation: obj.operation || obj.type,
|
operation: change.operation || change.type,
|
||||||
modification: obj.modification
|
modification: change.modification
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
// Normalize the modification object
|
// Normalize the modification object
|
||||||
Object.keys(obj.modification).forEach(function (k) {
|
Object.keys(change.modification).forEach(function (k) {
|
||||||
const mod = {}
|
const mod = {}
|
||||||
mod[k] = obj.modification[k]
|
mod[k] = change.modification[k]
|
||||||
changes.push(new Change({
|
changes.push(new Change({
|
||||||
operation: obj.operation || obj.type,
|
operation: change.operation || change.type,
|
||||||
modification: mod
|
modification: mod
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
@ -470,9 +466,9 @@ Client.prototype.modify = function modify (name, change, controls, callback) {
|
||||||
assert.func(callback, 'callback')
|
assert.func(callback, 'callback')
|
||||||
|
|
||||||
const req = new ModifyRequest({
|
const req = new ModifyRequest({
|
||||||
object: ensureDN(name),
|
object: ensureDN(name, this.strictDN),
|
||||||
changes,
|
changes: changes,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
||||||
|
@ -506,16 +502,18 @@ Client.prototype.modifyDN = function modifyDN (name,
|
||||||
}
|
}
|
||||||
assert.func(callback)
|
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({
|
const req = new ModifyDNRequest({
|
||||||
entry: DN.fromString(name),
|
entry: DN,
|
||||||
deleteOldRdn: true,
|
deleteOldRdn: true,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
if (newDN.length !== 1) {
|
if (newDN.length !== 1) {
|
||||||
req.newRdn = DN.fromString(newDN.shift().toString())
|
req.newRdn = dn.parse(newDN.rdns.shift().toString())
|
||||||
req.newSuperior = newDN
|
req.newSuperior = newDN
|
||||||
} else {
|
} else {
|
||||||
req.newRdn = newDN
|
req.newRdn = newDN
|
||||||
|
@ -571,7 +569,7 @@ Client.prototype.search = function search (base,
|
||||||
options.filter = filters.parseString(options.filter)
|
options.filter = filters.parseString(options.filter)
|
||||||
} else if (!options.filter) {
|
} else if (!options.filter) {
|
||||||
options.filter = new PresenceFilter({ attribute: 'objectclass' })
|
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')
|
throw new TypeError('options.filter (Filter) required')
|
||||||
}
|
}
|
||||||
if (typeof (controls) === 'function') {
|
if (typeof (controls) === 'function') {
|
||||||
|
@ -593,14 +591,14 @@ Client.prototype.search = function search (base,
|
||||||
}
|
}
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
const baseDN = ensureDN(base)
|
const baseDN = ensureDN(base, this.strictDN)
|
||||||
|
|
||||||
function sendRequest (ctrls, emitter, cb) {
|
function sendRequest (ctrls, emitter, cb) {
|
||||||
const req = new SearchRequest({
|
const req = new SearchRequest({
|
||||||
baseObject: baseDN,
|
baseObject: baseDN,
|
||||||
scope: options.scope || 'base',
|
scope: options.scope || 'base',
|
||||||
filter: options.filter,
|
filter: options.filter,
|
||||||
derefAliases: options.derefAliases || Protocol.search.NEVER_DEREF_ALIASES,
|
derefAliases: options.derefAliases || Protocol.NEVER_DEREF_ALIASES,
|
||||||
sizeLimit: options.sizeLimit || 0,
|
sizeLimit: options.sizeLimit || 0,
|
||||||
timeLimit: options.timeLimit || 10,
|
timeLimit: options.timeLimit || 10,
|
||||||
typesOnly: options.typesOnly || false,
|
typesOnly: options.typesOnly || false,
|
||||||
|
@ -629,12 +627,12 @@ Client.prototype.search = function search (base,
|
||||||
}
|
}
|
||||||
|
|
||||||
const pager = new SearchPager({
|
const pager = new SearchPager({
|
||||||
callback,
|
callback: callback,
|
||||||
controls,
|
controls: controls,
|
||||||
pageSize: size,
|
pageSize: size,
|
||||||
pagePause: pageOpts.pagePause,
|
pagePause: pageOpts.pagePause
|
||||||
sendRequest
|
|
||||||
})
|
})
|
||||||
|
pager.on('search', sendRequest)
|
||||||
pager.begin()
|
pager.begin()
|
||||||
} else {
|
} else {
|
||||||
sendRequest(controls, new CorkedEmitter(), callback)
|
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'))
|
return callback(new Error('STARTTLS already in progress or active'))
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSend (sendErr, emitter) {
|
function onSend (err, emitter) {
|
||||||
if (sendErr) {
|
if (err) {
|
||||||
callback(sendErr)
|
callback(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
|
@ -699,7 +697,7 @@ Client.prototype.starttls = function starttls (options,
|
||||||
self._starttls = null
|
self._starttls = null
|
||||||
callback(err)
|
callback(err)
|
||||||
})
|
})
|
||||||
emitter.on('end', function (_res) {
|
emitter.on('end', function (res) {
|
||||||
const sock = self._socket
|
const sock = self._socket
|
||||||
/*
|
/*
|
||||||
* Unplumb socket data during SSL negotiation.
|
* Unplumb socket data during SSL negotiation.
|
||||||
|
@ -723,7 +721,7 @@ Client.prototype.starttls = function starttls (options,
|
||||||
self._tracker.parser.write(data)
|
self._tracker.parser.write(data)
|
||||||
})
|
})
|
||||||
secure.on('error', function (err) {
|
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)
|
self.emit('error', err)
|
||||||
sock.destroy()
|
sock.destroy()
|
||||||
|
@ -744,7 +742,7 @@ Client.prototype.starttls = function starttls (options,
|
||||||
const req = new ExtendedRequest({
|
const req = new ExtendedRequest({
|
||||||
requestName: '1.3.6.1.4.1.1466.20037',
|
requestName: '1.3.6.1.4.1.1466.20037',
|
||||||
requestValue: null,
|
requestValue: null,
|
||||||
controls
|
controls: controls
|
||||||
})
|
})
|
||||||
|
|
||||||
return this._send(req,
|
return this._send(req,
|
||||||
|
@ -776,11 +774,9 @@ Client.prototype.destroy = function destroy (err) {
|
||||||
})
|
})
|
||||||
if (this.connected) {
|
if (this.connected) {
|
||||||
this.unbind()
|
this.unbind()
|
||||||
}
|
} else if (this._socket) {
|
||||||
if (this._socket) {
|
|
||||||
this._socket.destroy()
|
this._socket.destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('destroy', err)
|
this.emit('destroy', err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -854,10 +850,10 @@ Client.prototype.connect = function connect () {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize socket events and LDAP parser.
|
// Initialize socket events and LDAP parser.
|
||||||
function initSocket (server) {
|
function initSocket (url) {
|
||||||
tracker = messageTrackerFactory({
|
tracker = messageTrackerFactory({
|
||||||
id: server ? server.href : self.socketPath,
|
id: url ? url.href : self.socketPath,
|
||||||
parser: new Parser({ log })
|
parser: new Parser({ log: log })
|
||||||
})
|
})
|
||||||
|
|
||||||
// This won't be set on TLS. So. Very. Annoying.
|
// This won't be set on TLS. So. Very. Annoying.
|
||||||
|
@ -876,52 +872,15 @@ Client.prototype.connect = function connect () {
|
||||||
})
|
})
|
||||||
|
|
||||||
// The "router"
|
// The "router"
|
||||||
//
|
|
||||||
// This is invoked after the incoming BER has been parsed into a JavaScript
|
|
||||||
// object.
|
|
||||||
tracker.parser.on('message', function onMessage (message) {
|
tracker.parser.on('message', function onMessage (message) {
|
||||||
message.connection = self._socket
|
message.connection = self._socket
|
||||||
const trackedObject = tracker.fetch(message.messageId)
|
const callback = tracker.fetch(message.messageID)
|
||||||
if (!trackedObject) {
|
|
||||||
log.error({ message: message.pojo }, 'unmatched server message received')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const { message: trackedMessage, callback } = trackedObject
|
|
||||||
|
|
||||||
if (!callback) {
|
if (!callback) {
|
||||||
log.error({ message: message.pojo }, 'unsolicited message')
|
log.error({ message: message.json }, 'unsolicited message')
|
||||||
return false
|
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)
|
return callback(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -979,7 +938,7 @@ Client.prototype.connect = function connect () {
|
||||||
f(basicClient, callback)
|
f(basicClient, callback)
|
||||||
},
|
},
|
||||||
inputs: self.listeners('setup')
|
inputs: self.listeners('setup')
|
||||||
}, function (err, _res) {
|
}, function (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
self.emit('setupError', err)
|
self.emit('setupError', err)
|
||||||
}
|
}
|
||||||
|
@ -1002,7 +961,7 @@ Client.prototype.connect = function connect () {
|
||||||
socket.end()
|
socket.end()
|
||||||
})
|
})
|
||||||
socket.on('error', function onSocketError (err) {
|
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)
|
self.emit('error', err)
|
||||||
socket.destroy()
|
socket.destroy()
|
||||||
|
@ -1042,7 +1001,7 @@ Client.prototype.connect = function connect () {
|
||||||
}
|
}
|
||||||
retry.failAfter(failAfter)
|
retry.failAfter(failAfter)
|
||||||
|
|
||||||
retry.on('ready', function (num, _delay) {
|
retry.on('ready', function (num, delay) {
|
||||||
if (self.destroyed) {
|
if (self.destroyed) {
|
||||||
// Cease connection attempts if destroyed
|
// Cease connection attempts if destroyed
|
||||||
return
|
return
|
||||||
|
@ -1070,9 +1029,9 @@ Client.prototype.connect = function connect () {
|
||||||
self.log.debug('failed to connect after %d attempts', failAfter)
|
self.log.debug('failed to connect after %d attempts', failAfter)
|
||||||
// Communicate the last-encountered error
|
// Communicate the last-encountered error
|
||||||
if (err instanceof ConnectionError) {
|
if (err instanceof ConnectionError) {
|
||||||
self.emitError('connectTimeout', err)
|
self.emit('connectTimeout', err)
|
||||||
} else if (err.code === 'ECONNREFUSED') {
|
} else if (err.code === 'ECONNREFUSED') {
|
||||||
self.emitError('connectRefused', err)
|
self.emit('connectRefused', err)
|
||||||
} else {
|
} else {
|
||||||
self.emit('error', err)
|
self.emit('error', err)
|
||||||
}
|
}
|
||||||
|
@ -1120,16 +1079,8 @@ Client.prototype._onClose = function _onClose (closeError) {
|
||||||
return cb(new ConnectionError(tracker.id + ' closed'))
|
return cb(new ConnectionError(tracker.id + ' closed'))
|
||||||
} else {
|
} else {
|
||||||
// Unbinds will be communicated as a success since we're closed
|
// Unbinds will be communicated as a success since we're closed
|
||||||
// TODO: we are faking this "UnbindResponse" object in order to make
|
const unbind = new UnbindResponse({ messageID: msgid })
|
||||||
// tests pass. There is no such thing as an "unbind response" in the LDAP
|
unbind.status = 'unbind'
|
||||||
// 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()
|
|
||||||
return cb(unbind)
|
return cb(unbind)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1247,23 +1198,21 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
||||||
function messageCallback (msg) {
|
function messageCallback (msg) {
|
||||||
if (timer) { clearTimeout(timer) }
|
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 (expect === 'abandon') { return sendResult('end', null) }
|
||||||
|
|
||||||
if (msg instanceof SearchEntry || msg instanceof SearchReference) {
|
if (msg instanceof SearchEntry || msg instanceof SearchReference) {
|
||||||
let event = msg.constructor.name
|
let event = msg.constructor.name
|
||||||
// Generate the event name for the event emitter, i.e. "searchEntry"
|
event = event[0].toLowerCase() + event.slice(1)
|
||||||
// and "searchReference".
|
|
||||||
event = (event[0].toLowerCase() + event.slice(1)).replaceAll('Result', '')
|
|
||||||
return sendResult(event, msg)
|
return sendResult(event, msg)
|
||||||
} else {
|
} else {
|
||||||
tracker.remove(message.messageId)
|
tracker.remove(message.messageID)
|
||||||
// Potentially mark client as idle
|
// Potentially mark client as idle
|
||||||
self._updateIdle()
|
self._updateIdle()
|
||||||
|
|
||||||
if (msg instanceof LDAPResult) {
|
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('error', errors.getError(msg))
|
||||||
}
|
}
|
||||||
return sendResult('end', msg)
|
return sendResult('end', msg)
|
||||||
|
@ -1277,7 +1226,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
||||||
|
|
||||||
function onRequestTimeout () {
|
function onRequestTimeout () {
|
||||||
self.emit('timeout', message)
|
self.emit('timeout', message)
|
||||||
const { callback: cb } = tracker.fetch(message.messageId)
|
const cb = tracker.fetch(message.messageID)
|
||||||
if (cb) {
|
if (cb) {
|
||||||
// FIXME: the timed-out request should be abandoned
|
// FIXME: the timed-out request should be abandoned
|
||||||
cb(new errors.TimeoutError('request timeout (client interrupt)'))
|
cb(new errors.TimeoutError('request timeout (client interrupt)'))
|
||||||
|
@ -1286,8 +1235,8 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
||||||
|
|
||||||
function writeCallback () {
|
function writeCallback () {
|
||||||
if (expect === 'abandon') {
|
if (expect === 'abandon') {
|
||||||
// Mark the messageId specified as abandoned
|
// Mark the messageID specified as abandoned
|
||||||
tracker.abandon(message.abandonId)
|
tracker.abandon(message.abandonID)
|
||||||
// No need to track the abandon request itself
|
// No need to track the abandon request itself
|
||||||
tracker.remove(message.id)
|
tracker.remove(message.id)
|
||||||
return callback(null)
|
return callback(null)
|
||||||
|
@ -1302,9 +1251,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
||||||
conn.end()
|
conn.end()
|
||||||
} else if (emitter) {
|
} else if (emitter) {
|
||||||
sentEmitter = true
|
sentEmitter = true
|
||||||
callback(null, emitter)
|
return callback(null, emitter)
|
||||||
emitter.emit('searchRequest', message)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -1319,11 +1266,10 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
||||||
timer = setTimeout(onRequestTimeout, self.timeout)
|
timer = setTimeout(onRequestTimeout, self.timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.trace('sending request %j', message.pojo)
|
log.trace('sending request %j', message.json)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const messageBer = message.toBer()
|
return conn.write(message.toBer(), writeCallback)
|
||||||
return conn.write(messageBer.buffer, writeCallback)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (timer) { clearTimeout(timer) }
|
if (timer) { clearTimeout(timer) }
|
||||||
|
|
||||||
|
@ -1331,15 +1277,3 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
||||||
return callback(e)
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ const logger = require('../logger')
|
||||||
const Client = require('./client')
|
const Client = require('./client')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Client,
|
Client: Client,
|
||||||
createClient: function createClient (options) {
|
createClient: function createClient (options) {
|
||||||
if (isObject(options) === false) throw TypeError('options (object) required')
|
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')
|
if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')
|
||||||
|
|
|
@ -16,8 +16,8 @@ const { MAX_MSGID } = require('../constants')
|
||||||
module.exports = function idGeneratorFactory (start = 0) {
|
module.exports = function idGeneratorFactory (start = 0) {
|
||||||
let currentID = start
|
let currentID = start
|
||||||
return function nextID () {
|
return function nextID () {
|
||||||
const id = currentID + 1
|
const nextID = currentID + 1
|
||||||
currentID = (id >= MAX_MSGID) ? 1 : id
|
currentID = (nextID >= MAX_MSGID) ? 1 : nextID
|
||||||
return currentID
|
return currentID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,23 +62,13 @@ module.exports = function messageTrackerFactory (options) {
|
||||||
*/
|
*/
|
||||||
tracker.abandon = function abandonMessage (msgID) {
|
tracker.abandon = function abandonMessage (msgID) {
|
||||||
if (messages.has(msgID) === false) return false
|
if (messages.has(msgID) === false) return false
|
||||||
const toAbandon = messages.get(msgID)
|
|
||||||
abandoned.set(msgID, {
|
abandoned.set(msgID, {
|
||||||
age: currentID,
|
age: currentID,
|
||||||
message: toAbandon.message,
|
cb: messages.get(msgID)
|
||||||
cb: toAbandon.callback
|
|
||||||
})
|
})
|
||||||
return messages.delete(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
|
* Retrieves the message handler for a message. Removes abandoned messages
|
||||||
* that have been given time to be resolved.
|
* that have been given time to be resolved.
|
||||||
|
@ -89,10 +79,10 @@ module.exports = function messageTrackerFactory (options) {
|
||||||
* @method fetch
|
* @method fetch
|
||||||
*/
|
*/
|
||||||
tracker.fetch = function fetchMessage (msgID) {
|
tracker.fetch = function fetchMessage (msgID) {
|
||||||
const tracked = messages.get(msgID)
|
const messageCB = messages.get(msgID)
|
||||||
if (tracked) {
|
if (messageCB) {
|
||||||
purgeAbandoned(msgID, abandoned)
|
purgeAbandoned(msgID, abandoned)
|
||||||
return tracked
|
return messageCB
|
||||||
}
|
}
|
||||||
|
|
||||||
// We sent an abandon request but the server either wasn't able to process
|
// 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.
|
// to be processed normally.
|
||||||
const abandonedMsg = abandoned.get(msgID)
|
const abandonedMsg = abandoned.get(msgID)
|
||||||
if (abandonedMsg) {
|
if (abandonedMsg) {
|
||||||
return { message: abandonedMsg, callback: abandonedMsg.cb }
|
return abandonedMsg.cb
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
|
@ -120,7 +110,7 @@ module.exports = function messageTrackerFactory (options) {
|
||||||
messages.forEach((val, key) => {
|
messages.forEach((val, key) => {
|
||||||
purgeAbandoned(key, abandoned)
|
purgeAbandoned(key, abandoned)
|
||||||
tracker.remove(key)
|
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.
|
* Add a message handler to be tracked.
|
||||||
*
|
*
|
||||||
* @param {object} message The message object to be tracked. This object will
|
* @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.
|
* @param {function} callback The handler for the message.
|
||||||
*
|
*
|
||||||
* @memberof MessageTracker
|
* @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
|
// 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
|
// 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.
|
// refactored later, then we can possibly get rid of this side effect.
|
||||||
message.messageId = currentID
|
message.messageID = currentID
|
||||||
messages.set(currentID, { callback, message })
|
messages.set(currentID, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tracker
|
return tracker
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
* is not accepting any requests.
|
* is not accepting any requests.
|
||||||
*/
|
*/
|
||||||
module.exports = function enqueue (message, expect, emitter, cb) {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,13 @@
|
||||||
|
|
||||||
const EventEmitter = require('events').EventEmitter
|
const EventEmitter = require('events').EventEmitter
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
|
|
||||||
const assert = require('assert-plus')
|
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
|
/// --- API
|
||||||
|
|
||||||
|
@ -25,23 +29,22 @@ const CorkedEmitter = require('../corked_emitter.js')
|
||||||
* will be emitted (and 'end' will not be). By listening to
|
* will be emitted (and 'end' will not be). By listening to
|
||||||
* 'pageError', a successful search that lacks paging will be
|
* 'pageError', a successful search that lacks paging will be
|
||||||
* able to emit 'end'.
|
* able to emit 'end'.
|
||||||
|
* 3. search - Emitted as an internal event to trigger another client search.
|
||||||
*/
|
*/
|
||||||
function SearchPager (opts) {
|
function SearchPager (opts) {
|
||||||
assert.object(opts)
|
assert.object(opts)
|
||||||
assert.func(opts.callback)
|
assert.func(opts.callback)
|
||||||
assert.number(opts.pageSize)
|
assert.number(opts.pageSize)
|
||||||
assert.func(opts.sendRequest)
|
|
||||||
|
|
||||||
CorkedEmitter.call(this, {})
|
EventEmitter.call(this, {})
|
||||||
|
|
||||||
this.callback = opts.callback
|
this.callback = opts.callback
|
||||||
this.controls = opts.controls
|
this.controls = opts.controls
|
||||||
this.pageSize = opts.pageSize
|
this.pageSize = opts.pageSize
|
||||||
this.pagePause = opts.pagePause
|
this.pagePause = opts.pagePause
|
||||||
this.sendRequest = opts.sendRequest
|
|
||||||
|
|
||||||
this.controls.forEach(function (control) {
|
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.
|
// The point of using SearchPager is not having to do this.
|
||||||
// Toss an error if the pagedResultsControl is present
|
// Toss an error if the pagedResultsControl is present
|
||||||
throw new Error('redundant pagedResultControl')
|
throw new Error('redundant pagedResultControl')
|
||||||
|
@ -52,13 +55,12 @@ function SearchPager (opts) {
|
||||||
this.started = false
|
this.started = false
|
||||||
|
|
||||||
const emitter = new EventEmitter()
|
const emitter = new EventEmitter()
|
||||||
emitter.on('searchRequest', this.emit.bind(this, 'searchRequest'))
|
|
||||||
emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
|
emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
|
||||||
emitter.on('end', this._onEnd.bind(this))
|
emitter.on('end', this._onEnd.bind(this))
|
||||||
emitter.on('error', this._onError.bind(this))
|
emitter.on('error', this._onError.bind(this))
|
||||||
this.childEmitter = emitter
|
this.childEmitter = emitter
|
||||||
}
|
}
|
||||||
util.inherits(SearchPager, CorkedEmitter)
|
util.inherits(SearchPager, EventEmitter)
|
||||||
module.exports = SearchPager
|
module.exports = SearchPager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -73,7 +75,7 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
|
||||||
const self = this
|
const self = this
|
||||||
let cookie = null
|
let cookie = null
|
||||||
res.controls.forEach(function (control) {
|
res.controls.forEach(function (control) {
|
||||||
if (control.type === PagedResultsControl.OID) {
|
if (control.type === PagedControl.OID) {
|
||||||
cookie = control.value.cookie
|
cookie = control.value.cookie
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -89,13 +91,13 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
|
||||||
if (this.listeners('pageError').length > 0) {
|
if (this.listeners('pageError').length > 0) {
|
||||||
this.emit('pageError', err)
|
this.emit('pageError', err)
|
||||||
// If the consumer as subscribed to pageError, SearchPager is absolved
|
// 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
|
// event after 'error' breaks the contract that the standard client
|
||||||
// provides, so it's only a possibility if 'pageError' is used instead.
|
// provides, so it's only a possibility if 'pageError' is used instead.
|
||||||
this.emit('end', res)
|
this.emit('end', res)
|
||||||
} else {
|
} else {
|
||||||
this.emit('error', err)
|
this.emit('error', err)
|
||||||
// No end event possible per explanation above.
|
// No end event possible per explaination above.
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -134,20 +136,21 @@ SearchPager.prototype._onError = function _onError (err) {
|
||||||
*/
|
*/
|
||||||
SearchPager.prototype._nextPage = function _nextPage (cookie) {
|
SearchPager.prototype._nextPage = function _nextPage (cookie) {
|
||||||
const controls = this.controls.slice(0)
|
const controls = this.controls.slice(0)
|
||||||
controls.push(new PagedResultsControl({
|
controls.push(new PagedControl({
|
||||||
value: {
|
value: {
|
||||||
size: this.pageSize,
|
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.
|
* Callback provided to the client API for successful transmission.
|
||||||
*/
|
*/
|
||||||
SearchPager.prototype._sendCallback = function _sendCallback (err) {
|
SearchPager.prototype._sendCallback = function _sendCallback (err, res) {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.finished = true
|
this.finished = true
|
||||||
if (!this.started) {
|
if (!this.started) {
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,86 @@
|
||||||
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
|
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
|
||||||
|
|
||||||
const controls = require('@ldapjs/controls')
|
const assert = require('assert')
|
||||||
module.exports = controls
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}())
|
|
@ -32,9 +32,6 @@ Object.defineProperties(LDAPError.prototype, {
|
||||||
get: function getMessage () {
|
get: function getMessage () {
|
||||||
return this.lde_message || this.name
|
return this.lde_message || this.name
|
||||||
},
|
},
|
||||||
set: function setMessage (message) {
|
|
||||||
this.lde_message = message
|
|
||||||
},
|
|
||||||
configurable: false
|
configurable: false
|
||||||
},
|
},
|
||||||
dn: {
|
dn: {
|
||||||
|
@ -86,7 +83,7 @@ Object.keys(CODES).forEach(function (code) {
|
||||||
})
|
})
|
||||||
|
|
||||||
ERRORS[CODES[code]] = {
|
ERRORS[CODES[code]] = {
|
||||||
err,
|
err: err,
|
||||||
message: msg
|
message: msg
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
26
lib/index.js
26
lib/index.js
|
@ -3,16 +3,16 @@
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|
||||||
const client = require('./client')
|
const client = require('./client')
|
||||||
const Attribute = require('@ldapjs/attribute')
|
const Attribute = require('./attribute')
|
||||||
const Change = require('@ldapjs/change')
|
const Change = require('./change')
|
||||||
const Protocol = require('@ldapjs/protocol')
|
const Protocol = require('./protocol')
|
||||||
const Server = require('./server')
|
const Server = require('./server')
|
||||||
|
|
||||||
const controls = require('./controls')
|
const controls = require('./controls')
|
||||||
const persistentSearch = require('./persistent_search')
|
const persistentSearch = require('./persistent_search')
|
||||||
const dn = require('@ldapjs/dn')
|
const dn = require('./dn')
|
||||||
const errors = require('./errors')
|
const errors = require('./errors')
|
||||||
const filters = require('@ldapjs/filter')
|
const filters = require('./filters')
|
||||||
const messages = require('./messages')
|
const messages = require('./messages')
|
||||||
const url = require('./url')
|
const url = require('./url')
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ module.exports = {
|
||||||
Client: client.Client,
|
Client: client.Client,
|
||||||
createClient: client.createClient,
|
createClient: client.createClient,
|
||||||
|
|
||||||
Server,
|
Server: Server,
|
||||||
createServer: function (options) {
|
createServer: function (options) {
|
||||||
if (options === undefined) { options = {} }
|
if (options === undefined) { options = {} }
|
||||||
|
|
||||||
|
@ -37,21 +37,21 @@ module.exports = {
|
||||||
return new Server(options)
|
return new Server(options)
|
||||||
},
|
},
|
||||||
|
|
||||||
Attribute,
|
Attribute: Attribute,
|
||||||
Change,
|
Change: Change,
|
||||||
|
|
||||||
dn,
|
dn: dn,
|
||||||
DN: dn.DN,
|
DN: dn.DN,
|
||||||
RDN: dn.RDN,
|
RDN: dn.RDN,
|
||||||
parseDN: dn.DN.fromString,
|
parseDN: dn.parse,
|
||||||
|
|
||||||
persistentSearch,
|
persistentSearch: persistentSearch,
|
||||||
PersistentSearchCache: persistentSearch.PersistentSearchCache,
|
PersistentSearchCache: persistentSearch.PersistentSearchCache,
|
||||||
|
|
||||||
filters,
|
filters: filters,
|
||||||
parseFilter: filters.parseString,
|
parseFilter: filters.parseString,
|
||||||
|
|
||||||
url,
|
url: url,
|
||||||
parseURL: url.parse
|
parseURL: url.parse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,39 +1,61 @@
|
||||||
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
|
// 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 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 SearchResponse = require('./search_response')
|
||||||
|
const UnbindRequest = require('./unbind_request')
|
||||||
|
const UnbindResponse = require('./unbind_response')
|
||||||
|
|
||||||
/// --- API
|
/// --- API
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
LDAPMessage: messages.LdapMessage,
|
LDAPMessage: LDAPMessage,
|
||||||
LDAPResult: messages.LdapResult,
|
LDAPResult: LDAPResult,
|
||||||
Parser,
|
Parser: Parser,
|
||||||
|
|
||||||
AbandonRequest: messages.AbandonRequest,
|
AbandonRequest: AbandonRequest,
|
||||||
AbandonResponse: messages.AbandonResponse,
|
AbandonResponse: AbandonResponse,
|
||||||
AddRequest: messages.AddRequest,
|
AddRequest: AddRequest,
|
||||||
AddResponse: messages.AddResponse,
|
AddResponse: AddResponse,
|
||||||
BindRequest: messages.BindRequest,
|
BindRequest: BindRequest,
|
||||||
BindResponse: messages.BindResponse,
|
BindResponse: BindResponse,
|
||||||
CompareRequest: messages.CompareRequest,
|
CompareRequest: CompareRequest,
|
||||||
CompareResponse: messages.CompareResponse,
|
CompareResponse: CompareResponse,
|
||||||
DeleteRequest: messages.DeleteRequest,
|
DeleteRequest: DeleteRequest,
|
||||||
DeleteResponse: messages.DeleteResponse,
|
DeleteResponse: DeleteResponse,
|
||||||
ExtendedRequest: messages.ExtensionRequest,
|
ExtendedRequest: ExtendedRequest,
|
||||||
ExtendedResponse: messages.ExtensionResponse,
|
ExtendedResponse: ExtendedResponse,
|
||||||
ModifyRequest: messages.ModifyRequest,
|
ModifyRequest: ModifyRequest,
|
||||||
ModifyResponse: messages.ModifyResponse,
|
ModifyResponse: ModifyResponse,
|
||||||
ModifyDNRequest: messages.ModifyDnRequest,
|
ModifyDNRequest: ModifyDNRequest,
|
||||||
ModifyDNResponse: messages.ModifyDnResponse,
|
ModifyDNResponse: ModifyDNResponse,
|
||||||
SearchRequest: messages.SearchRequest,
|
SearchRequest: SearchRequest,
|
||||||
SearchEntry: messages.SearchResultEntry,
|
SearchEntry: SearchEntry,
|
||||||
SearchReference: messages.SearchResultReference,
|
SearchReference: SearchReference,
|
||||||
SearchResponse,
|
SearchResponse: SearchResponse,
|
||||||
UnbindRequest: messages.UnbindRequest
|
UnbindRequest: UnbindRequest,
|
||||||
|
UnbindResponse: UnbindResponse
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -4,36 +4,40 @@ const EventEmitter = require('events').EventEmitter
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
|
|
||||||
const assert = require('assert-plus')
|
const assert = require('assert-plus')
|
||||||
const asn1 = require('@ldapjs/asn1')
|
const asn1 = require('asn1')
|
||||||
|
// var VError = require('verror').VError
|
||||||
const logger = require('../logger')
|
const logger = require('../logger')
|
||||||
|
|
||||||
const messages = require('@ldapjs/messages')
|
const AbandonRequest = require('./abandon_request')
|
||||||
const AbandonRequest = messages.AbandonRequest
|
const AddRequest = require('./add_request')
|
||||||
const AddRequest = messages.AddRequest
|
const AddResponse = require('./add_response')
|
||||||
const AddResponse = messages.AddResponse
|
const BindRequest = require('./bind_request')
|
||||||
const BindRequest = messages.BindRequest
|
const BindResponse = require('./bind_response')
|
||||||
const BindResponse = messages.BindResponse
|
const CompareRequest = require('./compare_request')
|
||||||
const CompareRequest = messages.CompareRequest
|
const CompareResponse = require('./compare_response')
|
||||||
const CompareResponse = messages.CompareResponse
|
const DeleteRequest = require('./del_request')
|
||||||
const DeleteRequest = messages.DeleteRequest
|
const DeleteResponse = require('./del_response')
|
||||||
const DeleteResponse = messages.DeleteResponse
|
const ExtendedRequest = require('./ext_request')
|
||||||
const ExtendedRequest = messages.ExtensionRequest
|
const ExtendedResponse = require('./ext_response')
|
||||||
const ExtendedResponse = messages.ExtensionResponse
|
const ModifyRequest = require('./modify_request')
|
||||||
const ModifyRequest = messages.ModifyRequest
|
const ModifyResponse = require('./modify_response')
|
||||||
const ModifyResponse = messages.ModifyResponse
|
const ModifyDNRequest = require('./moddn_request')
|
||||||
const ModifyDNRequest = messages.ModifyDnRequest
|
const ModifyDNResponse = require('./moddn_response')
|
||||||
const ModifyDNResponse = messages.ModifyDnResponse
|
const SearchRequest = require('./search_request')
|
||||||
const SearchRequest = messages.SearchRequest
|
const SearchEntry = require('./search_entry')
|
||||||
const SearchEntry = messages.SearchResultEntry
|
const SearchReference = require('./search_reference')
|
||||||
const SearchReference = messages.SearchResultReference
|
|
||||||
const SearchResponse = require('./search_response')
|
const SearchResponse = require('./search_response')
|
||||||
const UnbindRequest = messages.UnbindRequest
|
const UnbindRequest = require('./unbind_request')
|
||||||
const LDAPResult = messages.LdapResult
|
// var UnbindResponse = require('./unbind_response')
|
||||||
|
|
||||||
const Protocol = require('@ldapjs/protocol')
|
const LDAPResult = require('./result')
|
||||||
|
// var Message = require('./message')
|
||||||
|
|
||||||
|
const Protocol = require('../protocol')
|
||||||
|
|
||||||
/// --- Globals
|
/// --- Globals
|
||||||
|
|
||||||
|
// var Ber = asn1.Ber
|
||||||
const BerReader = asn1.BerReader
|
const BerReader = asn1.BerReader
|
||||||
|
|
||||||
/// --- API
|
/// --- API
|
||||||
|
@ -48,13 +52,6 @@ function Parser (options = {}) {
|
||||||
}
|
}
|
||||||
util.inherits(Parser, EventEmitter)
|
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) {
|
Parser.prototype.write = function (data) {
|
||||||
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
|
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
|
||||||
|
|
||||||
|
@ -67,9 +64,9 @@ Parser.prototype.write = function (data) {
|
||||||
return true
|
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
|
let foundSeq = false
|
||||||
try {
|
try {
|
||||||
|
@ -83,22 +80,9 @@ Parser.prototype.write = function (data) {
|
||||||
return false
|
return false
|
||||||
} else if (ber.remain > ber.length) {
|
} else if (ber.remain > ber.length) {
|
||||||
// ETOOMUCH
|
// ETOOMUCH
|
||||||
|
// This is sort of ugly, but allows us to make miminal copies
|
||||||
// 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.
|
|
||||||
nextMessage = self.buffer.slice(ber.offset + ber.length)
|
nextMessage = self.buffer.slice(ber.offset + ber.length)
|
||||||
|
ber._size = ber.offset + ber.length
|
||||||
const currOffset = ber.offset
|
|
||||||
ber = new BerReader(ber.buffer.subarray(0, currOffset + ber.length))
|
|
||||||
ber.readSequence()
|
|
||||||
|
|
||||||
assert.equal(ber.remain, ber.length)
|
assert.equal(ber.remain, ber.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,25 +92,13 @@ Parser.prototype.write = function (data) {
|
||||||
|
|
||||||
let message
|
let message
|
||||||
try {
|
try {
|
||||||
if (Object.prototype.toString.call(ber) === '[object BerReader]') {
|
// Bail here if peer isn't speaking protocol at all
|
||||||
// Parse the BER into a JavaScript object representation. The message
|
message = this.getMessage(ber)
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!message) {
|
if (!message) {
|
||||||
return end()
|
return end()
|
||||||
}
|
}
|
||||||
|
message.parse(ber)
|
||||||
// TODO: find a better way to handle logging now that messages and the
|
|
||||||
// server are decoupled. ~ jsumners 2023-02-17
|
|
||||||
message.log = this.log
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.emit('error', e, message)
|
this.emit('error', e, message)
|
||||||
return false
|
return false
|
||||||
|
@ -141,88 +113,88 @@ Parser.prototype.getMessage = function (ber) {
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
const messageId = ber.readInt()
|
const messageID = ber.readInt()
|
||||||
const type = ber.readSequence()
|
const type = ber.readSequence()
|
||||||
|
|
||||||
let Message
|
let Message
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Protocol.operations.LDAP_REQ_ABANDON:
|
case Protocol.LDAP_REQ_ABANDON:
|
||||||
Message = AbandonRequest
|
Message = AbandonRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_ADD:
|
case Protocol.LDAP_REQ_ADD:
|
||||||
Message = AddRequest
|
Message = AddRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_ADD:
|
case Protocol.LDAP_REP_ADD:
|
||||||
Message = AddResponse
|
Message = AddResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_BIND:
|
case Protocol.LDAP_REQ_BIND:
|
||||||
Message = BindRequest
|
Message = BindRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_BIND:
|
case Protocol.LDAP_REP_BIND:
|
||||||
Message = BindResponse
|
Message = BindResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_COMPARE:
|
case Protocol.LDAP_REQ_COMPARE:
|
||||||
Message = CompareRequest
|
Message = CompareRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_COMPARE:
|
case Protocol.LDAP_REP_COMPARE:
|
||||||
Message = CompareResponse
|
Message = CompareResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_DELETE:
|
case Protocol.LDAP_REQ_DELETE:
|
||||||
Message = DeleteRequest
|
Message = DeleteRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_DELETE:
|
case Protocol.LDAP_REP_DELETE:
|
||||||
Message = DeleteResponse
|
Message = DeleteResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_EXTENSION:
|
case Protocol.LDAP_REQ_EXTENSION:
|
||||||
Message = ExtendedRequest
|
Message = ExtendedRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_EXTENSION:
|
case Protocol.LDAP_REP_EXTENSION:
|
||||||
Message = ExtendedResponse
|
Message = ExtendedResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_MODIFY:
|
case Protocol.LDAP_REQ_MODIFY:
|
||||||
Message = ModifyRequest
|
Message = ModifyRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_MODIFY:
|
case Protocol.LDAP_REP_MODIFY:
|
||||||
Message = ModifyResponse
|
Message = ModifyResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_MODRDN:
|
case Protocol.LDAP_REQ_MODRDN:
|
||||||
Message = ModifyDNRequest
|
Message = ModifyDNRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_MODRDN:
|
case Protocol.LDAP_REP_MODRDN:
|
||||||
Message = ModifyDNResponse
|
Message = ModifyDNResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_SEARCH:
|
case Protocol.LDAP_REQ_SEARCH:
|
||||||
Message = SearchRequest
|
Message = SearchRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_SEARCH_ENTRY:
|
case Protocol.LDAP_REP_SEARCH_ENTRY:
|
||||||
Message = SearchEntry
|
Message = SearchEntry
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_SEARCH_REF:
|
case Protocol.LDAP_REP_SEARCH_REF:
|
||||||
Message = SearchReference
|
Message = SearchReference
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_RES_SEARCH:
|
case Protocol.LDAP_REP_SEARCH:
|
||||||
Message = SearchResponse
|
Message = SearchResponse
|
||||||
break
|
break
|
||||||
|
|
||||||
case Protocol.operations.LDAP_REQ_UNBIND:
|
case Protocol.LDAP_REQ_UNBIND:
|
||||||
Message = UnbindRequest
|
Message = UnbindRequest
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -231,15 +203,15 @@ Parser.prototype.getMessage = function (ber) {
|
||||||
new Error('Op 0x' + (type ? type.toString(16) : '??') +
|
new Error('Op 0x' + (type ? type.toString(16) : '??') +
|
||||||
' not supported'),
|
' not supported'),
|
||||||
new LDAPResult({
|
new LDAPResult({
|
||||||
messageId,
|
messageID: messageID,
|
||||||
protocolOp: type || Protocol.operations.LDAP_RES_EXTENSION
|
protocolOp: type || Protocol.LDAP_REP_EXTENSION
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Message({
|
return new Message({
|
||||||
messageId,
|
messageID: messageID,
|
||||||
log: self.log
|
log: self.log
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,31 +1,31 @@
|
||||||
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
|
// Copyright 2011 Mark Cavage, Inc. All rights reserved.
|
||||||
|
|
||||||
const assert = require('assert-plus')
|
const assert = require('assert-plus')
|
||||||
|
const util = require('util')
|
||||||
|
|
||||||
const Attribute = require('@ldapjs/attribute')
|
const LDAPResult = require('./result')
|
||||||
const {
|
const SearchEntry = require('./search_entry')
|
||||||
SearchResultEntry: SearchEntry,
|
const SearchReference = require('./search_reference')
|
||||||
SearchResultReference: SearchReference,
|
|
||||||
SearchResultDone
|
|
||||||
} = require('@ldapjs/messages')
|
|
||||||
|
|
||||||
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
|
/// --- API
|
||||||
|
|
||||||
class SearchResponse extends SearchResultDone {
|
function SearchResponse (options) {
|
||||||
attributes
|
options = options || {}
|
||||||
notAttributes
|
assert.object(options)
|
||||||
sentEntries
|
|
||||||
|
|
||||||
constructor (options = {}) {
|
options.protocolOp = Protocol.LDAP_REP_SEARCH
|
||||||
super(options)
|
LDAPResult.call(this, options)
|
||||||
|
|
||||||
this.attributes = options.attributes ? options.attributes.slice() : []
|
this.attributes = options.attributes ? options.attributes.slice() : []
|
||||||
this.notAttributes = []
|
this.notAttributes = []
|
||||||
this.sentEntries = 0
|
this.sentEntries = 0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
util.inherits(SearchResponse, LDAPResult)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows you to send a SearchEntry back to the client.
|
* Allows you to send a SearchEntry back to the client.
|
||||||
|
@ -42,18 +42,14 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
|
||||||
const self = this
|
const self = this
|
||||||
|
|
||||||
const savedAttrs = {}
|
const savedAttrs = {}
|
||||||
let save = null
|
const save = null
|
||||||
if (entry instanceof SearchEntry || entry instanceof SearchReference) {
|
if (entry instanceof SearchEntry || entry instanceof SearchReference) {
|
||||||
if (!entry.messageId) { entry.messageId = this.messageId }
|
if (!entry.messageID) { entry.messageID = this.messageID }
|
||||||
if (entry.messageId !== this.messageId) {
|
if (entry.messageID !== this.messageID) { throw new Error('SearchEntry messageID mismatch') }
|
||||||
throw new Error('SearchEntry messageId mismatch')
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!entry.attributes) { throw new Error('entry.attributes required') }
|
if (!entry.attributes) { throw new Error('entry.attributes required') }
|
||||||
|
|
||||||
const all = (self.attributes.indexOf('*') !== -1)
|
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) {
|
Object.keys(entry.attributes).forEach(function (a) {
|
||||||
const _a = a.toLowerCase()
|
const _a = a.toLowerCase()
|
||||||
if (!nofiltering && _a.length && _a[0] === '_') {
|
if (!nofiltering && _a.length && _a[0] === '_') {
|
||||||
|
@ -70,27 +66,42 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
save = entry
|
const save = entry
|
||||||
entry = new SearchEntry({
|
entry = new SearchEntry({
|
||||||
objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn,
|
objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn,
|
||||||
messageId: self.messageId,
|
messageID: self.messageID,
|
||||||
attributes: Attribute.fromObject(entry.attributes)
|
log: self.log
|
||||||
})
|
})
|
||||||
|
entry.fromObject(save)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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++
|
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
|
// Restore attributes
|
||||||
Object.keys(savedAttrs).forEach(function (k) {
|
Object.keys(savedAttrs).forEach(function (k) {
|
||||||
save.attributes[k] = savedAttrs[k]
|
save.attributes[k] = savedAttrs[k]
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.log.warn(e, '%s failure to write message %j',
|
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)
|
assert.object(object)
|
||||||
|
|
||||||
const entry = new SearchEntry({
|
const entry = new SearchEntry({
|
||||||
messageId: this.messageId,
|
messageID: this.messageID,
|
||||||
objectName: object.objectName || object.dn,
|
log: this.log,
|
||||||
attributes: object.attributes ?? []
|
objectName: object.objectName || object.dn
|
||||||
})
|
})
|
||||||
|
entry.fromObject((object.attributes || object))
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,10 +122,15 @@ SearchResponse.prototype.createSearchReference = function (uris) {
|
||||||
|
|
||||||
if (!Array.isArray(uris)) { uris = [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
|
const self = this
|
||||||
return new SearchReference({
|
return new SearchReference({
|
||||||
messageId: self.messageId,
|
messageID: self.messageID,
|
||||||
uri: uris
|
log: self.log,
|
||||||
|
uris: uris
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -77,7 +77,7 @@ function getOperationType (requestType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEntryChangeNotificationControl (req, obj) {
|
function getEntryChangeNotificationControl (req, obj, callback) {
|
||||||
// if we want to return a ECNC
|
// if we want to return a ECNC
|
||||||
if (req.persistentSearch.value.returnECs) {
|
if (req.persistentSearch.value.returnECs) {
|
||||||
const attrs = obj.attributes
|
const attrs = obj.attributes
|
||||||
|
@ -89,7 +89,7 @@ function getEntryChangeNotificationControl (req, obj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
value.changeNumber = attrs.changenumber
|
value.changeNumber = attrs.changenumber
|
||||||
return new EntryChangeNotificationControl({ value })
|
return new EntryChangeNotificationControl({ value: value })
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,6 @@ function checkChangeType (req, requestType) {
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
PersistentSearchCache: PersistentSearch,
|
PersistentSearchCache: PersistentSearch,
|
||||||
checkChangeType,
|
checkChangeType: checkChangeType,
|
||||||
getEntryChangeNotificationControl
|
getEntryChangeNotificationControl: getEntryChangeNotificationControl
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
394
lib/server.js
394
lib/server.js
|
@ -6,33 +6,33 @@ const net = require('net')
|
||||||
const tls = require('tls')
|
const tls = require('tls')
|
||||||
const util = require('util')
|
const util = require('util')
|
||||||
|
|
||||||
// var asn1 = require('@ldapjs/asn1')
|
// var asn1 = require('asn1')
|
||||||
const VError = require('verror').VError
|
const VError = require('verror').VError
|
||||||
|
|
||||||
const { DN, RDN } = require('@ldapjs/dn')
|
const dn = require('./dn')
|
||||||
|
const dtrace = require('./dtrace')
|
||||||
const errors = require('./errors')
|
const errors = require('./errors')
|
||||||
const Protocol = require('@ldapjs/protocol')
|
const Protocol = require('./protocol')
|
||||||
|
|
||||||
const messages = require('@ldapjs/messages')
|
|
||||||
|
|
||||||
const Parser = require('./messages').Parser
|
const Parser = require('./messages').Parser
|
||||||
const LdapResult = messages.LdapResult
|
const AbandonResponse = require('./messages/abandon_response')
|
||||||
const AbandonResponse = messages.AbandonResponse
|
const AddResponse = require('./messages/add_response')
|
||||||
const AddResponse = messages.AddResponse
|
const BindResponse = require('./messages/bind_response')
|
||||||
const BindResponse = messages.BindResponse
|
const CompareResponse = require('./messages/compare_response')
|
||||||
const CompareResponse = messages.CompareResponse
|
const DeleteResponse = require('./messages/del_response')
|
||||||
const DeleteResponse = messages.DeleteResponse
|
const ExtendedResponse = require('./messages/ext_response')
|
||||||
const ExtendedResponse = messages.ExtensionResponse
|
// var LDAPResult = require('./messages/result')
|
||||||
const ModifyResponse = messages.ModifyResponse
|
const ModifyResponse = require('./messages/modify_response')
|
||||||
const ModifyDnResponse = messages.ModifyDnResponse
|
const ModifyDNResponse = require('./messages/moddn_response')
|
||||||
const SearchRequest = messages.SearchRequest
|
const SearchRequest = require('./messages/search_request')
|
||||||
const SearchResponse = require('./messages/search_response')
|
const SearchResponse = require('./messages/search_response')
|
||||||
|
const UnbindResponse = require('./messages/unbind_response')
|
||||||
|
|
||||||
/// --- Globals
|
/// --- Globals
|
||||||
|
|
||||||
// var Ber = asn1.Ber
|
// var Ber = asn1.Ber
|
||||||
// var BerReader = asn1.BerReader
|
// var BerReader = asn1.BerReader
|
||||||
// const DN = dn.DN
|
const DN = dn.DN
|
||||||
|
|
||||||
// var sprintf = util.format
|
// var sprintf = util.format
|
||||||
|
|
||||||
|
@ -47,15 +47,15 @@ function mergeFunctionArgs (argv, start, end) {
|
||||||
const handlers = []
|
const handlers = []
|
||||||
|
|
||||||
for (let i = start; i < end; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
if (Array.isArray(argv[i])) {
|
if (argv[i] instanceof Array) {
|
||||||
const arr = argv[i]
|
const arr = argv[i]
|
||||||
for (let j = 0; j < arr.length; j++) {
|
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]))
|
throw new TypeError('Invalid argument type: ' + typeof (arr[j]))
|
||||||
}
|
}
|
||||||
handlers.push(arr[j])
|
handlers.push(arr[j])
|
||||||
}
|
}
|
||||||
} else if (typeof argv[i] === 'function') {
|
} else if (argv[i] instanceof Function) {
|
||||||
handlers.push(argv[i])
|
handlers.push(argv[i])
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError('Invalid argument type: ' + typeof (argv[i]))
|
throw new TypeError('Invalid argument type: ' + typeof (argv[i]))
|
||||||
|
@ -71,42 +71,35 @@ function getResponse (req) {
|
||||||
let Response
|
let Response
|
||||||
|
|
||||||
switch (req.protocolOp) {
|
switch (req.protocolOp) {
|
||||||
case Protocol.operations.LDAP_REQ_BIND:
|
case Protocol.LDAP_REQ_BIND:
|
||||||
Response = BindResponse
|
Response = BindResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_ABANDON:
|
case Protocol.LDAP_REQ_ABANDON:
|
||||||
Response = AbandonResponse
|
Response = AbandonResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_ADD:
|
case Protocol.LDAP_REQ_ADD:
|
||||||
Response = AddResponse
|
Response = AddResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_COMPARE:
|
case Protocol.LDAP_REQ_COMPARE:
|
||||||
Response = CompareResponse
|
Response = CompareResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_DELETE:
|
case Protocol.LDAP_REQ_DELETE:
|
||||||
Response = DeleteResponse
|
Response = DeleteResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_EXTENSION:
|
case Protocol.LDAP_REQ_EXTENSION:
|
||||||
Response = ExtendedResponse
|
Response = ExtendedResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_MODIFY:
|
case Protocol.LDAP_REQ_MODIFY:
|
||||||
Response = ModifyResponse
|
Response = ModifyResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_MODRDN:
|
case Protocol.LDAP_REQ_MODRDN:
|
||||||
Response = ModifyDnResponse
|
Response = ModifyDNResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_SEARCH:
|
case Protocol.LDAP_REQ_SEARCH:
|
||||||
Response = SearchResponse
|
Response = SearchResponse
|
||||||
break
|
break
|
||||||
case Protocol.operations.LDAP_REQ_UNBIND:
|
case Protocol.LDAP_REQ_UNBIND:
|
||||||
// TODO: when the server receives an unbind request this made up response object was returned.
|
Response = UnbindResponse
|
||||||
// Instead, we need to just terminate the connection. ~ jsumners
|
|
||||||
Response = class extends LdapResult {
|
|
||||||
status = 0
|
|
||||||
end () {
|
|
||||||
req.connection.end()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
|
@ -114,83 +107,16 @@ function getResponse (req) {
|
||||||
assert.ok(Response)
|
assert.ok(Response)
|
||||||
|
|
||||||
const res = new Response({
|
const res = new Response({
|
||||||
messageId: req.messageId,
|
messageID: req.messageID,
|
||||||
|
log: req.log,
|
||||||
attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
|
attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
|
||||||
})
|
})
|
||||||
res.log = req.log
|
|
||||||
res.connection = req.connection
|
res.connection = req.connection
|
||||||
res.logId = req.logId
|
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
|
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) {
|
function defaultHandler (req, res, next) {
|
||||||
assert.ok(req)
|
assert.ok(req)
|
||||||
assert.ok(res)
|
assert.ok(res)
|
||||||
|
@ -231,6 +157,69 @@ function noExOpHandler (req, res, next) {
|
||||||
return 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
|
/// --- API
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -272,6 +261,8 @@ function Server (options) {
|
||||||
|
|
||||||
this._chain = []
|
this._chain = []
|
||||||
this.log = options.log
|
this.log = options.log
|
||||||
|
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
|
||||||
|
|
||||||
const log = this.log
|
const log = this.log
|
||||||
|
|
||||||
function setupConnection (c) {
|
function setupConnection (c) {
|
||||||
|
@ -286,12 +277,12 @@ function Server (options) {
|
||||||
c.remotePort = c.socket.remotePort
|
c.remotePort = c.socket.remotePort
|
||||||
}
|
}
|
||||||
|
|
||||||
const rdn = new RDN({ cn: 'anonymous' })
|
const rdn = new dn.RDN({ cn: 'anonymous' })
|
||||||
|
|
||||||
c.ldap = {
|
c.ldap = {
|
||||||
id: c.remoteAddress + ':' + c.remotePort,
|
id: c.remoteAddress + ':' + c.remotePort,
|
||||||
config: options,
|
config: options,
|
||||||
_bindDN: new DN({ rdns: [rdn] })
|
_bindDN: new DN([rdn])
|
||||||
}
|
}
|
||||||
c.addListener('timeout', function () {
|
c.addListener('timeout', function () {
|
||||||
log.trace('%s timed out', c.ldap.id)
|
log.trace('%s timed out', c.ldap.id)
|
||||||
|
@ -314,9 +305,7 @@ function Server (options) {
|
||||||
return c.ldap._bindDN
|
return c.ldap._bindDN
|
||||||
})
|
})
|
||||||
c.ldap.__defineSetter__('bindDN', function (val) {
|
c.ldap.__defineSetter__('bindDN', function (val) {
|
||||||
if (Object.prototype.toString.call(val) !== '[object LdapDn]') {
|
if (!(val instanceof DN)) { throw new TypeError('DN required') }
|
||||||
throw new TypeError('DN required')
|
|
||||||
}
|
|
||||||
|
|
||||||
c.ldap._bindDN = val
|
c.ldap._bindDN = val
|
||||||
return val
|
return val
|
||||||
|
@ -324,78 +313,62 @@ function Server (options) {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
self.newConnection = function (conn) {
|
function newConnection (c) {
|
||||||
// TODO: make `newConnection` available on the `Server` prototype
|
setupConnection(c)
|
||||||
// https://github.com/ldapjs/node-ldapjs/pull/727/files#r636572294
|
log.trace('new connection from %s', c.ldap.id)
|
||||||
setupConnection(conn)
|
|
||||||
log.trace('new connection from %s', conn.ldap.id)
|
|
||||||
|
|
||||||
conn.parser = new Parser({
|
dtrace.fire('server-connection', function () {
|
||||||
|
return [c.remoteAddress]
|
||||||
|
})
|
||||||
|
|
||||||
|
c.parser = new Parser({
|
||||||
log: options.log
|
log: options.log
|
||||||
})
|
})
|
||||||
conn.parser.on('message', function (req) {
|
c.parser.on('message', function (req) {
|
||||||
// TODO: this is mutating the `@ldapjs/message` objects.
|
req.connection = c
|
||||||
// We should avoid doing that. ~ jsumners 2023-02-16
|
req.logId = c.ldap.id + '::' + req.messageID
|
||||||
req.connection = conn
|
|
||||||
req.logId = conn.ldap.id + '::' + req.messageId
|
|
||||||
req.startTime = new Date().getTime()
|
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)
|
const res = getResponse(req)
|
||||||
if (!res) {
|
if (!res) {
|
||||||
log.warn('Unimplemented server method: %s', req.type)
|
log.warn('Unimplemented server method: %s', req.type)
|
||||||
conn.destroy()
|
c.destroy()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse string DNs for routing/etc
|
// parse string DNs for routing/etc
|
||||||
try {
|
try {
|
||||||
switch (req.protocolOp) {
|
switch (req.protocolOp) {
|
||||||
case Protocol.operations.LDAP_REQ_BIND: {
|
case Protocol.LDAP_REQ_BIND:
|
||||||
req.name = DN.fromString(req.name)
|
req.name = dn.parse(req.name)
|
||||||
break
|
break
|
||||||
}
|
case Protocol.LDAP_REQ_ADD:
|
||||||
|
case Protocol.LDAP_REQ_COMPARE:
|
||||||
case Protocol.operations.LDAP_REQ_ADD:
|
case Protocol.LDAP_REQ_DELETE:
|
||||||
case Protocol.operations.LDAP_REQ_COMPARE:
|
req.entry = dn.parse(req.entry)
|
||||||
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')
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
case Protocol.LDAP_REQ_MODIFY:
|
||||||
|
req.object = dn.parse(req.object)
|
||||||
case Protocol.operations.LDAP_REQ_MODIFY: {
|
|
||||||
req.object = DN.fromString(req.object)
|
|
||||||
break
|
break
|
||||||
}
|
case Protocol.LDAP_REQ_MODRDN:
|
||||||
|
req.entry = dn.parse(req.entry)
|
||||||
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')
|
|
||||||
}
|
|
||||||
// TODO: handle newRdn/Superior
|
// TODO: handle newRdn/Superior
|
||||||
break
|
break
|
||||||
}
|
case Protocol.LDAP_REQ_SEARCH:
|
||||||
|
req.baseObject = dn.parse(req.baseObject)
|
||||||
case Protocol.operations.LDAP_REQ_SEARCH: {
|
|
||||||
break
|
break
|
||||||
}
|
default:
|
||||||
|
|
||||||
default: {
|
|
||||||
break
|
break
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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.logId = req.logId
|
||||||
res.requestDN = req.dn
|
res.requestDN = req.dn
|
||||||
|
|
||||||
|
@ -403,10 +376,10 @@ function Server (options) {
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
return (function messageIIFE (err) {
|
return (function messageIIFE (err) {
|
||||||
function sendError (sendErr) {
|
function sendError (err) {
|
||||||
res.status = sendErr.code || errors.LDAP_OPERATIONS_ERROR
|
res.status = err.code || errors.LDAP_OPERATIONS_ERROR
|
||||||
res.matchedDN = req.suffix ? req.suffix.toString() : ''
|
res.matchedDN = req.suffix ? req.suffix.toString() : ''
|
||||||
res.errorMessage = sendErr.message || ''
|
res.errorMessage = err.message || ''
|
||||||
return res.end()
|
return res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -415,8 +388,8 @@ function Server (options) {
|
||||||
|
|
||||||
function next () {} // stub out next for the post chain
|
function next () {} // stub out next for the post chain
|
||||||
|
|
||||||
self._postChain.forEach(function (cb) {
|
self._postChain.forEach(function (c) {
|
||||||
cb.call(self, req, res, next)
|
c.call(self, req, res, next)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,20 +404,7 @@ function Server (options) {
|
||||||
const next = messageIIFE
|
const next = messageIIFE
|
||||||
if (chain.handlers[i]) { return chain.handlers[i++].call(chain.backend, req, res, next) }
|
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) {
|
if (req.protocolOp === Protocol.LDAP_REQ_BIND && res.status === 0) { c.ldap.bindDN = req.dn }
|
||||||
// 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' })] })
|
|
||||||
}
|
|
||||||
|
|
||||||
return after()
|
return after()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -455,32 +415,32 @@ function Server (options) {
|
||||||
}())
|
}())
|
||||||
})
|
})
|
||||||
|
|
||||||
conn.parser.on('error', function (err, message) {
|
c.parser.on('error', function (err, message) {
|
||||||
self.emit('error', new VError(err, 'Parser error for %s', conn.ldap.id))
|
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)
|
const res = getResponse(message)
|
||||||
if (!res) { return conn.destroy() }
|
if (!res) { return c.destroy() }
|
||||||
|
|
||||||
res.status = 0x02 // protocol error
|
res.status = 0x02 // protocol error
|
||||||
res.errorMessage = err.toString()
|
res.errorMessage = err.toString()
|
||||||
return conn.end(res.toBer())
|
return c.end(res.toBer())
|
||||||
})
|
})
|
||||||
|
|
||||||
conn.on('data', function (data) {
|
c.on('data', function (data) {
|
||||||
log.trace('data on %s: %s', conn.ldap.id, util.inspect(data))
|
log.trace('data on %s: %s', c.ldap.id, util.inspect(data))
|
||||||
|
|
||||||
conn.parser.write(data)
|
c.parser.write(data)
|
||||||
})
|
})
|
||||||
} // end newConnection
|
} // end newConnection
|
||||||
|
|
||||||
this.routes = {}
|
this.routes = {}
|
||||||
if ((options.cert || options.certificate) && options.key) {
|
if ((options.cert || options.certificate) && options.key) {
|
||||||
options.cert = options.cert || options.certificate
|
options.cert = options.cert || options.certificate
|
||||||
this.server = tls.createServer(options, options.connectionRouter ? options.connectionRouter : self.newConnection)
|
this.server = tls.createServer(options, newConnection)
|
||||||
} else {
|
} else {
|
||||||
this.server = net.createServer(options.connectionRouter ? options.connectionRouter : self.newConnection)
|
this.server = net.createServer(newConnection)
|
||||||
}
|
}
|
||||||
this.server.log = options.log
|
this.server.log = options.log
|
||||||
this.server.ldap = {
|
this.server.ldap = {
|
||||||
|
@ -533,15 +493,7 @@ Object.defineProperties(Server.prototype, {
|
||||||
} else {
|
} else {
|
||||||
str = 'ldap://'
|
str = 'ldap://'
|
||||||
}
|
}
|
||||||
|
str += this.host + ':' + this.port
|
||||||
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
|
|
||||||
return str
|
return str
|
||||||
},
|
},
|
||||||
configurable: false
|
configurable: false
|
||||||
|
@ -561,7 +513,7 @@ module.exports = Server
|
||||||
*/
|
*/
|
||||||
Server.prototype.add = function (name) {
|
Server.prototype.add = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.bind = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.compare = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.del = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.exop = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.modify = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.modifyDN = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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) {
|
Server.prototype.search = function (name) {
|
||||||
const args = Array.prototype.slice.call(arguments, 1)
|
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 () {
|
Server.prototype.unbind = function () {
|
||||||
const args = Array.prototype.slice.call(arguments, 0)
|
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 () {
|
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) {
|
Server.prototype.listen = function (port, host, callback) {
|
||||||
if (typeof (port) !== 'number' && typeof (port) !== 'string') { throw new TypeError('port (number or path) required') }
|
if (typeof (port) !== 'number' && typeof (port) !== 'string') { throw new TypeError('port (number or path) required') }
|
||||||
|
|
||||||
if (typeof (host) === 'function') {
|
if (typeof (host) === 'function') {
|
||||||
callback = host
|
callback = host
|
||||||
host = '127.0.0.1'
|
host = '0.0.0.0'
|
||||||
}
|
}
|
||||||
if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
|
if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
|
||||||
// Disambiguate between string ports and file paths
|
// Disambiguate between string ports and file paths
|
||||||
|
@ -750,10 +702,12 @@ Server.prototype.getConnections = function (callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Server.prototype._getRoute = function (_dn, backend) {
|
Server.prototype._getRoute = function (_dn, backend) {
|
||||||
|
assert.ok(dn)
|
||||||
|
|
||||||
if (!backend) { backend = this }
|
if (!backend) { backend = this }
|
||||||
|
|
||||||
let name
|
let name
|
||||||
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') {
|
if (_dn instanceof dn.DN) {
|
||||||
name = _dn.toString()
|
name = _dn.toString()
|
||||||
} else {
|
} else {
|
||||||
name = _dn
|
name = _dn
|
||||||
|
@ -780,10 +734,10 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
|
||||||
Object.keys(this.routes).forEach(function (key) {
|
Object.keys(this.routes).forEach(function (key) {
|
||||||
const _dn = self.routes[key].dn
|
const _dn = self.routes[key].dn
|
||||||
// Ignore non-DN routes such as exop or unbind
|
// 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()
|
const reversed = _dn.clone()
|
||||||
reversed.reverse()
|
reversed.rdns.reverse()
|
||||||
reversedRDNsToKeys[reversed.toString()] = key
|
reversedRDNsToKeys[reversed.format()] = key
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const output = []
|
const output = []
|
||||||
|
@ -800,15 +754,17 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
|
||||||
return this._routeKeyCache
|
return this._routeKeyCache
|
||||||
}
|
}
|
||||||
|
|
||||||
Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
Server.prototype._getHandlerChain = function _getHandlerChain (req, res) {
|
||||||
assert.ok(req)
|
assert.ok(req)
|
||||||
|
|
||||||
|
fireDTraceProbe(req, res)
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
const routes = this.routes
|
const routes = this.routes
|
||||||
let route
|
let route
|
||||||
|
|
||||||
// check anonymous bind
|
// check anonymous bind
|
||||||
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND &&
|
if (req.protocolOp === Protocol.LDAP_REQ_BIND &&
|
||||||
req.dn.toString() === '' &&
|
req.dn.toString() === '' &&
|
||||||
req.credentials === '') {
|
req.credentials === '') {
|
||||||
return {
|
return {
|
||||||
|
@ -820,7 +776,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
||||||
const op = '0x' + req.protocolOp.toString(16)
|
const op = '0x' + req.protocolOp.toString(16)
|
||||||
|
|
||||||
// Special cases are exops, unbinds and abandons. Handle those first.
|
// 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]
|
route = routes[req.requestName]
|
||||||
if (route) {
|
if (route) {
|
||||||
return {
|
return {
|
||||||
|
@ -833,7 +789,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
||||||
handlers: [noExOpHandler]
|
handlers: [noExOpHandler]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND) {
|
} else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
|
||||||
route = routes.unbind
|
route = routes.unbind
|
||||||
if (route) {
|
if (route) {
|
||||||
return {
|
return {
|
||||||
|
@ -846,7 +802,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
||||||
handlers: [defaultNoOpHandler]
|
handlers: [defaultNoOpHandler]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_ABANDON) {
|
} else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) {
|
||||||
return {
|
return {
|
||||||
backend: self,
|
backend: self,
|
||||||
handlers: [defaultNoOpHandler]
|
handlers: [defaultNoOpHandler]
|
||||||
|
@ -854,11 +810,11 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, match via DN rules
|
// Otherwise, match via DN rules
|
||||||
|
assert.ok(req.dn)
|
||||||
const keys = this._sortedRouteKeys()
|
const keys = this._sortedRouteKeys()
|
||||||
let fallbackHandler = [noSuffixHandler]
|
let fallbackHandler = [noSuffixHandler]
|
||||||
// invalid DNs in non-strict mode are routed to the default handler
|
// invalid DNs in non-strict mode are routed to the default handler
|
||||||
const testDN = (typeof (req.dn) === 'string') ? DN.fromString(req.dn) : req.dn
|
const testDN = (typeof (req.dn) === 'string') ? '' : req.dn
|
||||||
assert.ok(testDN)
|
|
||||||
|
|
||||||
for (let i = 0; i < keys.length; i++) {
|
for (let i = 0; i < keys.length; i++) {
|
||||||
const suffix = keys[i]
|
const suffix = keys[i]
|
||||||
|
@ -905,7 +861,7 @@ Server.prototype._mount = function (op, name, argv, notDN) {
|
||||||
backend = argv[0]
|
backend = argv[0]
|
||||||
index = 1
|
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()
|
const chain = this._chain.slice()
|
||||||
argv.slice(index).forEach(function (a) {
|
argv.slice(index).forEach(function (a) {
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
const querystring = require('querystring')
|
const querystring = require('querystring')
|
||||||
const url = require('url')
|
const url = require('url')
|
||||||
const { DN } = require('@ldapjs/dn')
|
const dn = require('./dn')
|
||||||
const filter = require('@ldapjs/filter')
|
const filter = require('./filters/')
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ module.exports = {
|
||||||
|
|
||||||
if (u.pathname) {
|
if (u.pathname) {
|
||||||
u.pathname = querystring.unescape(u.pathname.substr(1))
|
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) {
|
if (u.search) {
|
||||||
|
|
67
package.json
67
package.json
|
@ -3,57 +3,50 @@
|
||||||
"name": "ldapjs",
|
"name": "ldapjs",
|
||||||
"homepage": "http://ldapjs.org",
|
"homepage": "http://ldapjs.org",
|
||||||
"description": "LDAP client and server APIs",
|
"description": "LDAP client and server APIs",
|
||||||
"version": "3.0.7",
|
"version": "2.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/ldapjs/node-ldapjs.git"
|
"url": "git://github.com/ldapjs/node-ldapjs.git"
|
||||||
},
|
},
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
"directories": {
|
||||||
|
"lib": "./lib"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ldapjs/asn1": "^2.0.0",
|
"abstract-logging": "^2.0.0",
|
||||||
"@ldapjs/attribute": "^1.0.0",
|
"asn1": "^0.2.4",
|
||||||
"@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",
|
|
||||||
"assert-plus": "^1.0.0",
|
"assert-plus": "^1.0.0",
|
||||||
"backoff": "^2.5.0",
|
"backoff": "^2.5.0",
|
||||||
|
"ldap-filter": "^0.3.3",
|
||||||
"once": "^1.4.0",
|
"once": "^1.4.0",
|
||||||
"vasync": "^2.2.1",
|
"vasync": "^2.2.0",
|
||||||
"verror": "^1.10.1"
|
"verror": "^1.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"get-port": "^5.1.1",
|
||||||
"highlight.js": "^11.7.0",
|
"husky": "^4.2.5",
|
||||||
"marked": "^4.2.12",
|
"snazzy": "^9.0.0",
|
||||||
"tap": "^16.3.7"
|
"standard": "^16.0.0",
|
||||||
|
"tap": "14.10.8"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "tap --no-cov -R terse",
|
"test": "tap --no-cov",
|
||||||
"test:ci": "tap --coverage-report=lcovonly -R terse",
|
"test:ci": "tap --coverage-report=lcovonly",
|
||||||
"test:cov": "tap -R terse",
|
"test:cov": "tap",
|
||||||
"test:cov:html": "tap --coverage-report=html -R terse",
|
"test:cov:html": "tap --coverage-report=html",
|
||||||
"test:watch": "tap -n -w --no-coverage-report -R terse",
|
"test:watch": "tap -n -w --no-coverage-report",
|
||||||
"test:integration": "tap --no-cov -R terse 'test-integration/**/*.test.js'",
|
"test:integration": "tap --no-cov 'test-integration/**/*.test.js'",
|
||||||
"test:integration:local": "docker-compose up -d --wait && npm run test:integration ; docker-compose down",
|
"test:integration:local": "docker-compose up -d && npm run test:integration && docker-compose down",
|
||||||
"lint": "eslint . --fix",
|
"lint": "standard | snazzy",
|
||||||
"lint:ci": "eslint .",
|
"lint:ci": "standard"
|
||||||
"docs": "node scripts/build-docs.js"
|
|
||||||
},
|
},
|
||||||
"pre-commit": [
|
"husky": {
|
||||||
"lint:ci",
|
"hooks": {
|
||||||
"test"
|
"pre-commit": "npm run lint && npm run test"
|
||||||
]
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
})
|
|
|
@ -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
Loading…
Reference in New Issue