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
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
|
||||
jobs:
|
||||
baseline:
|
||||
name: Baseline Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
openldap:
|
||||
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30
|
||||
ports:
|
||||
- 389:389
|
||||
- 636:636
|
||||
options: >
|
||||
--health-cmd "ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn"
|
||||
# services:
|
||||
# openldap:
|
||||
# image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:1.0
|
||||
# ports:
|
||||
# - 389:389
|
||||
# - 636:636
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
|
||||
# Hack way to start service since GitHub doesn't integrate with its own services
|
||||
- name: Docker login
|
||||
run: docker login docker.pkg.github.com -u ${GITHUB_ACTOR} -p ${GITHUB_TOKEN}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
- name: Pull Docker image
|
||||
run: docker pull "docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest"
|
||||
- name: Start OpenLDAP service
|
||||
run: docker run -it -d --name openldap -p 389:389 -p 636:636 docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
|
||||
|
||||
- name: Install Packages
|
||||
run: npm install
|
||||
|
|
|
@ -4,21 +4,17 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- next
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Install Packages
|
||||
run: npm install
|
||||
- name: Lint Code
|
||||
|
@ -32,16 +28,27 @@ jobs:
|
|||
- ubuntu-latest
|
||||
- windows-latest
|
||||
node:
|
||||
- 16
|
||||
- 18
|
||||
- 20
|
||||
- 10.13.0
|
||||
- 10.x
|
||||
- 12.x
|
||||
- 14.x
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- name: Install Packages
|
||||
run: npm install
|
||||
- name: Run Tests
|
||||
run: npm run test:ci
|
||||
- name: Coveralls Parallel
|
||||
uses: coverallsapp/github-action@v1.1.2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel: true
|
||||
- name: Coveralls Finished
|
||||
uses: coverallsapp/github-action@v1.1.2
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
parallel-finished: true
|
||||
|
|
|
@ -75,6 +75,3 @@ xcuserdata
|
|||
*.pem
|
||||
*.env.json
|
||||
*.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
|
||||
languishing without any maintenance as it filled a need in the ecosystem and
|
||||
I had built things at a prior organization that depended upon this project.
|
||||
I spent a lot of time triaging issues and reworking things toward a path
|
||||
that could be more easily maintained by a community of volunteers. But I have
|
||||
not had the time to dedicate to this project in quite a while. There are
|
||||
outstanding issues that would take me at least a week of dedicated development
|
||||
time to solve, and I cannot afford to take time off of work to do that.
|
||||
Particularly considering that the aforementioned organization was two
|
||||
jobs ago, and it is extremely unlikely that I will transition to a role again
|
||||
that will need this project.
|
||||
[](https://github.com/ldapjs/node-ldapjs/actions)
|
||||
[](https://coveralls.io/github/ldapjs/node-ldapjs/)
|
||||
|
||||
So, why am I just now deciding to decomission this project? Because today,
|
||||
2024-05-14, I received the following email:
|
||||
LDAPjs makes the LDAP protocol a first class citizen in Node.js.
|
||||
|
||||

|
||||
## Usage
|
||||
|
||||
I will not tolerate abuse, and I especially will not tolerate tacit death
|
||||
threats, over a hobby. You can thank the author of that email for the
|
||||
decomissioning on this project.
|
||||
For full docs, head on over to <http://ldapjs.org>.
|
||||
|
||||
My recommendation to you in regard to LDAP operations: write a gateway in a
|
||||
language that is more suited to these types of operations. I'd suggest
|
||||
[Go](https://go.dev).
|
||||
```javascript
|
||||
var ldap = require('ldapjs');
|
||||
|
||||
👋
|
||||
var server = ldap.createServer();
|
||||
|
||||
P.S.: if I ever do need this project again, I might revive it. But I'd fight
|
||||
hard for my suggestion above. Also, I will consider turning it over to an
|
||||
interested party, but I will require at least one recommendation from a
|
||||
Node.js core contributor that I can vet with the people that I know on that
|
||||
team.
|
||||
server.search('dc=example', function(req, res, next) {
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['organization', 'top'],
|
||||
o: 'example'
|
||||
}
|
||||
};
|
||||
|
||||
if (req.filter.matches(obj.attributes))
|
||||
res.send(obj);
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
server.listen(1389, function() {
|
||||
console.log('ldapjs listening at ' + server.url);
|
||||
});
|
||||
```
|
||||
|
||||
To run that, assuming you've got the [OpenLDAP](http://www.openldap.org/)
|
||||
client on your system:
|
||||
|
||||
ldapsearch -H ldap://localhost:1389 -x -b dc=example objectclass=*
|
||||
|
||||
## Installation
|
||||
|
||||
npm install ldapjs
|
||||
|
||||
DTrace support is included in ldapjs. To enable it, `npm install dtrace-provider`.
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
|
||||
## Bugs
|
||||
|
||||
See <https://github.com/ldapjs/node-ldapjs/issues>.
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
version: '3'
|
||||
|
||||
services:
|
||||
openldap:
|
||||
image: ghcr.io/ldapjs/docker-test-openldap/openldap:2023-10-30
|
||||
image: docker.pkg.github.com/ldapjs/docker-test-openldap/openldap:latest
|
||||
ports:
|
||||
- 389:389
|
||||
- 636:636
|
||||
healthcheck:
|
||||
start_period: 3s
|
||||
test: >
|
||||
/usr/bin/ldapsearch -Y EXTERNAL -Q -H ldapi:// -b ou=people,dc=planetexpress,dc=com -LLL '(cn=Turanga Leela)' cn 1>/dev/null
|
||||
|
|
|
@ -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>
|
||||
<title>%(title)s</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<link rel="stylesheet" type="text/css" href="media/css/style.css">
|
||||
<link rel="stylesheet" type="text/css" href="media/css/highlight.css">
|
||||
<link rel="stylesheet" type="text/css" href="%(mediaroot)s/css/restdown.css">
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
|
@ -30,10 +30,3 @@
|
|||
</span>
|
||||
%(toc_html)s
|
||||
</div>
|
||||
|
||||
<div id="content">
|
||||
%(content)s
|
||||
</div><!-- end #content -->
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -7,7 +7,7 @@ body {
|
|||
color: #4a3f2d;
|
||||
}
|
||||
|
||||
:focus:not(:focus-visible) {
|
||||
:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,22 @@ h4 {
|
|||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ---- custom classes */
|
||||
|
||||
pre.shell,
|
||||
pre.shell code {
|
||||
background:#444;
|
||||
color:#fff;
|
||||
border-width:0px;
|
||||
}
|
||||
pre.shell code::before {
|
||||
content: '$ ';
|
||||
display: inline;
|
||||
}
|
||||
|
||||
|
||||
/* ---- header and sidebar */
|
||||
|
||||
#header {
|
||||
|
@ -134,6 +150,15 @@ h4 {
|
|||
z-index:0;
|
||||
}
|
||||
|
||||
#sidebar .vertical_divider {
|
||||
background-color:#FFFFFF;
|
||||
bottom:0px;
|
||||
position:absolute;
|
||||
top:0px;
|
||||
right:0px;
|
||||
width:1px;
|
||||
}
|
||||
|
||||
#sidebar h1 {
|
||||
font-size:1.2em;
|
||||
padding:0px;
|
||||
|
@ -162,6 +187,25 @@ h4 {
|
|||
padding:1px 0px 1px 2px;
|
||||
}
|
||||
|
||||
#sidebar li span.verb {
|
||||
color:#aaa;
|
||||
padding:2px 3px 0px;
|
||||
width:30px;
|
||||
display:block;
|
||||
float:left;
|
||||
font-size:9px;
|
||||
font-family:verdana;
|
||||
-moz-border-radius:3px;
|
||||
-webkit-border-radius:3px;
|
||||
margin-left:0px;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
#sidebar li.current {
|
||||
background-repeat: no-repeat;
|
||||
background-position:right;
|
||||
}
|
||||
|
||||
|
||||
/* ---- intro */
|
||||
|
||||
|
@ -175,6 +219,13 @@ h4 {
|
|||
border-radius: 5px;
|
||||
margin-bottom:40px;
|
||||
}
|
||||
.intro pre.base {
|
||||
background:#444;
|
||||
color:#29231A;
|
||||
color:#fff;
|
||||
border-color:#fff;
|
||||
font-size:1.5em;
|
||||
}
|
||||
.intro h1 {
|
||||
color: #1C313C;
|
||||
}
|
||||
|
@ -208,6 +259,14 @@ h1 + h2 {
|
|||
margin-top: 0px;
|
||||
}
|
||||
|
||||
h2.fixed {
|
||||
position:fixed;
|
||||
margin-top: 0;
|
||||
border-top:none;
|
||||
right:45px;
|
||||
top:66px;
|
||||
}
|
||||
|
||||
h2 span {
|
||||
background: #979592;
|
||||
float:right;
|
|
@ -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
|
||||
markdown2extras: tables
|
||||
---
|
||||
|
||||
# ldapjs Client API
|
||||
|
||||
<div class="intro">
|
||||
|
||||
This document covers the ldapjs client API and assumes that you are familiar
|
||||
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||
|
||||
</div>
|
||||
|
||||
# Create a client
|
||||
|
||||
The code to create a new client looks like:
|
||||
|
||||
```js
|
||||
const ldap = require('ldapjs');
|
||||
|
||||
const client = ldap.createClient({
|
||||
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
|
||||
});
|
||||
|
||||
client.on('connectError', (err) => {
|
||||
// handle connection error
|
||||
})
|
||||
```
|
||||
var ldap = require('ldapjs');
|
||||
var client = ldap.createClient({
|
||||
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
|
||||
});
|
||||
|
||||
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
|
||||
that this will not use the LDAP TLS extended operation, but literally an SSL
|
||||
|
@ -41,6 +31,7 @@ client is:
|
|||
|connectTimeout |Milliseconds client should wait before timing out on TCP connections (Default: OS default)|
|
||||
|tlsOptions |Additional options passed to TLS connection layer when connecting via `ldaps://` (See: The TLS docs for node.js)|
|
||||
|idleTimeout |Milliseconds after last activity before client emits idle event|
|
||||
|strictDN |Force strict DN parsing for client methods (Default is true)|
|
||||
|reconnect |Try to reconnect when the connection gets lost (Default is false)|
|
||||
|
||||
### url
|
||||
|
@ -62,13 +53,6 @@ Known compatible loggers are:
|
|||
+ [Bunyan](https://www.npmjs.com/package/bunyan)
|
||||
+ [Pino](https://www.npmjs.com/package/pino)
|
||||
|
||||
|
||||
### Note On Error Handling
|
||||
|
||||
The client is an `EventEmitter`. If you don't register an error handler and
|
||||
e.g. a connection error occurs, Node.js will print a stack trace and exit the
|
||||
process ([reference](https://nodejs.org/api/events.html#error-events)).
|
||||
|
||||
## Connection management
|
||||
|
||||
As LDAP is a stateful protocol (as opposed to HTTP), having connections torn
|
||||
|
@ -83,25 +67,6 @@ more sophisticated control, you can provide an Object with the properties
|
|||
`failAfter` (default: `Infinity`).
|
||||
After the reconnect you maybe need to [bind](#bind) again.
|
||||
|
||||
## Client events
|
||||
|
||||
The client is an `EventEmitter` and can emit the following events:
|
||||
|
||||
|Event |Description |
|
||||
|---------------|----------------------------------------------------------|
|
||||
|error |General error |
|
||||
|connectRefused |Server refused connection. Most likely bad authentication |
|
||||
|connectTimeout |Server timeout |
|
||||
|connectError |Socket connection error |
|
||||
|setupError |Setup error after successful connection |
|
||||
|socketTimeout |Socket timeout |
|
||||
|resultError |Search result error |
|
||||
|timeout |Search result timeout |
|
||||
|destroy |After client is disconnected |
|
||||
|end |Socket end event |
|
||||
|close |Socket closed |
|
||||
|connect |Client connected |
|
||||
|idle |Idle timeout reached |
|
||||
|
||||
## Common patterns
|
||||
|
||||
|
@ -113,6 +78,9 @@ Almost every operation has the callback form of `function(err, res)` where err
|
|||
will be an instance of an `LDAPError` (you can use `instanceof` to switch).
|
||||
You probably won't need to check the `res` parameter, but it's there if you do.
|
||||
|
||||
|
||||
|
||||
|
||||
# bind
|
||||
`bind(dn, password, controls, callback)`
|
||||
|
||||
|
@ -124,11 +92,9 @@ of `Control` objects. You probably don't need them though...
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
client.bind('cn=root', 'secret', (err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
```
|
||||
client.bind('cn=root', 'secret', function(err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
||||
# add
|
||||
`add(dn, entry, controls, callback)`
|
||||
|
@ -140,17 +106,15 @@ controls are optional.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const entry = {
|
||||
cn: 'foo',
|
||||
sn: 'bar',
|
||||
email: ['foo@bar.com', 'foo1@bar.com'],
|
||||
objectclass: 'fooPerson'
|
||||
};
|
||||
client.add('cn=foo, o=example', entry, (err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
```
|
||||
var entry = {
|
||||
cn: 'foo',
|
||||
sn: 'bar',
|
||||
email: ['foo@bar.com', 'foo1@bar.com'],
|
||||
objectclass: 'fooPerson'
|
||||
};
|
||||
client.add('cn=foo, o=example', entry, function(err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
||||
# compare
|
||||
`compare(dn, attribute, value, controls, callback)`
|
||||
|
@ -160,13 +124,11 @@ the entry referenced by dn.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
client.compare('cn=foo, o=example', 'sn', 'bar', (err, matched) => {
|
||||
assert.ifError(err);
|
||||
client.compare('cn=foo, o=example', 'sn', 'bar', function(err, matched) {
|
||||
assert.ifError(err);
|
||||
|
||||
console.log('matched: ' + matched);
|
||||
});
|
||||
```
|
||||
console.log('matched: ' + matched);
|
||||
});
|
||||
|
||||
# del
|
||||
`del(dn, controls, callback)`
|
||||
|
@ -176,11 +138,9 @@ Deletes an entry from the LDAP server.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
client.del('cn=foo, o=example', (err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
```
|
||||
client.del('cn=foo, o=example', function(err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
||||
# exop
|
||||
`exop(name, value, controls, callback)`
|
||||
|
@ -192,13 +152,11 @@ should be.
|
|||
|
||||
Example (performs an LDAP 'whois' extended op):
|
||||
|
||||
```js
|
||||
client.exop('1.3.6.1.4.1.4203.1.11.3', (err, value, res) => {
|
||||
assert.ifError(err);
|
||||
client.exop('1.3.6.1.4.1.4203.1.11.3', function(err, value, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
console.log('whois: ' + value);
|
||||
});
|
||||
```
|
||||
console.log('whois: ' + value);
|
||||
});
|
||||
|
||||
# modify
|
||||
`modify(name, changes, controls, callback)`
|
||||
|
@ -209,19 +167,16 @@ pass in a single `Change` or an array of `Change` objects.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const change = new ldap.Change({
|
||||
operation: 'add',
|
||||
modification: {
|
||||
type: 'pets',
|
||||
values: ['cat', 'dog']
|
||||
}
|
||||
});
|
||||
var change = new ldap.Change({
|
||||
operation: 'add',
|
||||
modification: {
|
||||
pets: ['cat', 'dog']
|
||||
}
|
||||
});
|
||||
|
||||
client.modify('cn=foo, o=example', change, (err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
```
|
||||
client.modify('cn=foo, o=example', change, function(err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
||||
## Change
|
||||
|
||||
|
@ -235,13 +190,7 @@ must be one of:
|
|||
| add | Adds the attribute value(s) referenced in `modification`. The attribute may or may not already exist. |
|
||||
| delete | Deletes the attribute (and all values) referenced in `modification`. |
|
||||
|
||||
`modification` is just a plain old JS object with the required type and values you want.
|
||||
|
||||
| Operation | Description |
|
||||
|-----------|-------------|
|
||||
| type | String that defines the attribute type for the modification. |
|
||||
| values | Defines the values for modification. |
|
||||
|
||||
`modification` is just a plain old JS object with the values you want.
|
||||
|
||||
# modifyDN
|
||||
`modifyDN(dn, newDN, controls, callback)`
|
||||
|
@ -257,11 +206,9 @@ as opposed to just renaming the leaf).
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
client.modifyDN('cn=foo, o=example', 'cn=bar', (err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
```
|
||||
client.modifyDN('cn=foo, o=example', 'cn=bar', function(err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
||||
# search
|
||||
`search(base, options, controls, callback)`
|
||||
|
@ -289,45 +236,40 @@ containing the following fields:
|
|||
|timeLimit |the maximum amount of time the server should take in responding, in seconds. Defaults to 10. Lots of servers will ignore this.|
|
||||
|paged |enable and/or configure automatic result paging|
|
||||
|
||||
Responses inside callback of the `search` method are an `EventEmitter` where you will get a notification for
|
||||
each `searchEntry` that comes back from the server. You will additionally be able to listen for a `searchRequest`
|
||||
, `searchReference`, `error` and `end` event.
|
||||
`searchRequest` is emitted immediately after every `SearchRequest` is sent with a `SearchRequest` parameter. You can do operations
|
||||
like `client.abandon` with `searchRequest.messageId` to abandon this search request. Note that the `error` event will
|
||||
only be for client/TCP errors, not LDAP error codes like the other APIs. You'll want to check the LDAP status code
|
||||
(likely for `0`) on the `end` event to assert success. LDAP search results can give you a lot of status codes, such as
|
||||
time or size exceeded, busy, inappropriate matching, etc., which is why this method doesn't try to wrap up the code
|
||||
matching.
|
||||
Responses from the `search` method are an `EventEmitter` where you will get a
|
||||
notification for each `searchEntry` that comes back from the server. You will
|
||||
additionally be able to listen for a `searchReference`, `error` and `end` event.
|
||||
Note that the `error` event will only be for client/TCP errors, not LDAP error
|
||||
codes like the other APIs. You'll want to check the LDAP status code
|
||||
(likely for `0`) on the `end` event to assert success. LDAP search results
|
||||
can give you a lot of status codes, such as time or size exceeded, busy,
|
||||
inappropriate matching, etc., which is why this method doesn't try to wrap up
|
||||
the code matching.
|
||||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const opts = {
|
||||
filter: '(&(l=Seattle)(email=*@foo.com))',
|
||||
scope: 'sub',
|
||||
attributes: ['dn', 'sn', 'cn']
|
||||
};
|
||||
var opts = {
|
||||
filter: '(&(l=Seattle)(email=*@foo.com))',
|
||||
scope: 'sub',
|
||||
attributes: ['dn', 'sn', 'cn']
|
||||
};
|
||||
|
||||
client.search('o=example', opts, (err, res) => {
|
||||
assert.ifError(err);
|
||||
client.search('o=example', opts, function(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
res.on('searchRequest', (searchRequest) => {
|
||||
console.log('searchRequest: ', searchRequest.messageId);
|
||||
});
|
||||
res.on('searchEntry', (entry) => {
|
||||
console.log('entry: ' + JSON.stringify(entry.pojo));
|
||||
});
|
||||
res.on('searchReference', (referral) => {
|
||||
console.log('referral: ' + referral.uris.join());
|
||||
});
|
||||
res.on('error', (err) => {
|
||||
console.error('error: ' + err.message);
|
||||
});
|
||||
res.on('end', (result) => {
|
||||
console.log('status: ' + result.status);
|
||||
});
|
||||
});
|
||||
```
|
||||
res.on('searchEntry', function(entry) {
|
||||
console.log('entry: ' + JSON.stringify(entry.object));
|
||||
});
|
||||
res.on('searchReference', function(referral) {
|
||||
console.log('referral: ' + referral.uris.join());
|
||||
});
|
||||
res.on('error', function(err) {
|
||||
console.error('error: ' + err.message);
|
||||
});
|
||||
res.on('end', function(result) {
|
||||
console.log('status: ' + result.status);
|
||||
});
|
||||
});
|
||||
|
||||
## Filter Strings
|
||||
|
||||
|
@ -342,31 +284,25 @@ in prefix notation. For example, let's start simple, and build up a complicated
|
|||
filter. The most basic filter is equality, so let's assume you want to search
|
||||
for an attribute `email` with a value of `foo@bar.com`. The syntax would be:
|
||||
|
||||
```
|
||||
(email=foo@bar.com)
|
||||
```
|
||||
(email=foo@bar.com)
|
||||
|
||||
ldapjs requires all filters to be surrounded by '()' blocks. Ok, that was easy.
|
||||
Let's now assume that you want to find all records where the email is actually
|
||||
just anything in the "@bar.com" domain and the location attribute is set to
|
||||
Seattle:
|
||||
|
||||
```
|
||||
(&(email=*@bar.com)(l=Seattle))
|
||||
```
|
||||
(&(email=*@bar.com)(l=Seattle))
|
||||
|
||||
Now our filter is actually three LDAP filters. We have an `and` filter (single
|
||||
amp `&`), an `equality` filter `(the l=Seattle)`, and a `substring` filter.
|
||||
Substrings are wildcard filters. They use `*` as the wildcard. You can put more
|
||||
than one wildcard for a given string. For example you could do `(email=*@*bar.com)`
|
||||
to match any email of @bar.com or its subdomains like `"example@foo.bar.com"`.
|
||||
to match any email of @bar.com or its subdomains like "example@foo.bar.com".
|
||||
|
||||
Now, let's say we also want to set our filter to include a
|
||||
specification that either the employeeType *not* be a manager nor a secretary:
|
||||
|
||||
```
|
||||
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
|
||||
```
|
||||
(&(email=*@bar.com)(l=Seattle)(!(|(employeeType=manager)(employeeType=secretary))))
|
||||
|
||||
The `not` character is represented as a `!`, the `or` as a single pipe `|`.
|
||||
It gets a little bit complicated, but it's actually quite powerful, and lets you
|
||||
|
@ -381,29 +317,27 @@ While callers could choose to do this manually via the `controls` parameter to
|
|||
most simple way to use the paging automation is to set the `paged` option to
|
||||
true when performing a search:
|
||||
|
||||
```js
|
||||
const opts = {
|
||||
filter: '(objectclass=commonobject)',
|
||||
scope: 'sub',
|
||||
paged: true,
|
||||
sizeLimit: 200
|
||||
};
|
||||
client.search('o=largedir', opts, (err, res) => {
|
||||
assert.ifError(err);
|
||||
res.on('searchEntry', (entry) => {
|
||||
// do per-entry processing
|
||||
});
|
||||
res.on('page', (result) => {
|
||||
console.log('page end');
|
||||
});
|
||||
res.on('error', (resErr) => {
|
||||
assert.ifError(resErr);
|
||||
});
|
||||
res.on('end', (result) => {
|
||||
console.log('done ');
|
||||
});
|
||||
});
|
||||
```
|
||||
var opts = {
|
||||
filter: '(objectclass=commonobject)',
|
||||
scope: 'sub',
|
||||
paged: true,
|
||||
sizeLimit: 200
|
||||
};
|
||||
client.search('o=largedir', opts, function(err, res) {
|
||||
assert.ifError(err);
|
||||
res.on('searchEntry', function(entry) {
|
||||
// do per-entry processing
|
||||
});
|
||||
res.on('page', function(result) {
|
||||
console.log('page end');
|
||||
});
|
||||
res.on('error', function(resErr) {
|
||||
assert.ifError(resErr);
|
||||
});
|
||||
res.on('end', function(result) {
|
||||
console.log('done ');
|
||||
});
|
||||
});
|
||||
|
||||
This will enable paging with a default page size of 199 (`sizeLimit` - 1) and
|
||||
will output all of the resulting objects via the `searchEntry` event. At the
|
||||
|
@ -421,34 +355,32 @@ client will wait to request the next page until that callback is executed.
|
|||
|
||||
Here is an example where both of those parameters are used:
|
||||
|
||||
```js
|
||||
const queue = new MyWorkQueue(someSlowWorkFunction);
|
||||
const opts = {
|
||||
filter: '(objectclass=commonobject)',
|
||||
scope: 'sub',
|
||||
paged: {
|
||||
pageSize: 250,
|
||||
pagePause: true
|
||||
},
|
||||
};
|
||||
client.search('o=largerdir', opts, (err, res) => {
|
||||
assert.ifError(err);
|
||||
res.on('searchEntry', (entry) => {
|
||||
// Submit incoming objects to queue
|
||||
queue.push(entry);
|
||||
});
|
||||
res.on('page', (result, cb) => {
|
||||
// Allow the queue to flush before fetching next page
|
||||
queue.cbWhenFlushed(cb);
|
||||
});
|
||||
res.on('error', (resErr) => {
|
||||
assert.ifError(resErr);
|
||||
});
|
||||
res.on('end', (result) => {
|
||||
console.log('done');
|
||||
});
|
||||
});
|
||||
```
|
||||
var queue = new MyWorkQueue(someSlowWorkFunction);
|
||||
var opts = {
|
||||
filter: '(objectclass=commonobject)',
|
||||
scope: 'sub',
|
||||
paged: {
|
||||
pageSize: 250,
|
||||
pagePause: true
|
||||
},
|
||||
};
|
||||
client.search('o=largerdir', opts, function(err, res) {
|
||||
assert.ifError(err);
|
||||
res.on('searchEntry', function(entry) {
|
||||
// Submit incoming objects to queue
|
||||
queue.push(entry);
|
||||
});
|
||||
res.on('page', function(result, cb) {
|
||||
// Allow the queue to flush before fetching next page
|
||||
queue.cbWhenFlushed(cb);
|
||||
});
|
||||
res.on('error', function(resErr) {
|
||||
assert.ifError(resErr);
|
||||
});
|
||||
res.on('end', function(result) {
|
||||
console.log('done');
|
||||
});
|
||||
});
|
||||
|
||||
# starttls
|
||||
`starttls(options, controls, callback)`
|
||||
|
@ -457,17 +389,15 @@ Attempt to secure existing LDAP connection via STARTTLS.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
const opts = {
|
||||
ca: [fs.readFileSync('mycacert.pem')]
|
||||
};
|
||||
var opts = {
|
||||
ca: [fs.readFileSync('mycacert.pem')]
|
||||
};
|
||||
|
||||
client.starttls(opts, (err, res) => {
|
||||
assert.ifError(err);
|
||||
client.starttls(opts, function(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
// Client communication now TLS protected
|
||||
});
|
||||
```
|
||||
// Client communication now TLS protected
|
||||
});
|
||||
|
||||
|
||||
# unbind
|
||||
|
@ -484,8 +414,6 @@ not have a response.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
client.unbind((err) => {
|
||||
assert.ifError(err);
|
||||
});
|
||||
```
|
||||
client.unbind(function(err) {
|
||||
assert.ifError(err);
|
||||
});
|
||||
|
|
70
docs/dn.md
70
docs/dn.md
|
@ -4,13 +4,9 @@ title: DN API | ldapjs
|
|||
|
||||
# ldapjs DN API
|
||||
|
||||
<div class="intro">
|
||||
|
||||
This document covers the ldapjs DN API and assumes that you are familiar
|
||||
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||
|
||||
</div>
|
||||
|
||||
DNs are LDAP distinguished names, and are composed of a set of RDNs (relative
|
||||
distinguished names). [RFC2253](http://www.ietf.org/rfc/rfc2253.txt) has the
|
||||
complete specification, but basically an RDN is an attribute value assertion
|
||||
|
@ -26,12 +22,10 @@ The `parseDN` API converts a string representation of a DN into an ldapjs DN
|
|||
object; in most cases this will be handled for you under the covers of the
|
||||
ldapjs framework, but if you need it, it's there.
|
||||
|
||||
```js
|
||||
const parseDN = require('ldapjs').parseDN;
|
||||
var parseDN = require('ldapjs').parseDN;
|
||||
|
||||
const dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
|
||||
console.log(dn.toString());
|
||||
```
|
||||
var dn = parseDN('cn=foo+sn=bar, ou=people, o=example');
|
||||
console.log(dn.toString());
|
||||
|
||||
# DN
|
||||
|
||||
|
@ -43,46 +37,40 @@ APIs are setup to give you a DN object.
|
|||
Returns a boolean indicating whether 'this' is a child of the passed in dn. The
|
||||
`dn` argument can be either a string or a DN.
|
||||
|
||||
```js
|
||||
server.add('o=example', (req, res, next) => {
|
||||
if (req.dn.childOf('ou=people, o=example')) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
});
|
||||
```
|
||||
server.add('o=example', function(req, res, next) {
|
||||
if (req.dn.childOf('ou=people, o=example')) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
});
|
||||
|
||||
## parentOf(dn)
|
||||
|
||||
The inverse of `childOf`; returns a boolean on whether or not `this` is a parent
|
||||
of the passed in dn. Like `childOf`, can take either a string or a DN.
|
||||
|
||||
```js
|
||||
server.add('o=example', (req, res, next) => {
|
||||
const dn = parseDN('ou=people, o=example');
|
||||
if (dn.parentOf(req.dn)) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
});
|
||||
```
|
||||
server.add('o=example', function(req, res, next) {
|
||||
var dn = parseDN('ou=people, o=example');
|
||||
if (dn.parentOf(req.dn)) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
});
|
||||
|
||||
## equals(dn)
|
||||
|
||||
Returns a boolean indicating whether `this` is equivalent to the passed in `dn`
|
||||
argument. `dn` can be a string or a DN.
|
||||
|
||||
```js
|
||||
server.add('o=example', (req, res, next) => {
|
||||
if (req.dn.equals('cn=foo, ou=people, o=example')) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
});
|
||||
```
|
||||
server.add('o=example', function(req, res, next) {
|
||||
if (req.dn.equals('cn=foo, ou=people, o=example')) {
|
||||
...
|
||||
} else {
|
||||
...
|
||||
}
|
||||
});
|
||||
|
||||
## parent()
|
||||
|
||||
|
@ -120,8 +108,6 @@ It accepts the same parameters as `format`.
|
|||
|
||||
Returns the string representation of `this`.
|
||||
|
||||
```js
|
||||
server.add('o=example', (req, res, next) => {
|
||||
console.log(req.dn.toString());
|
||||
});
|
||||
```
|
||||
server.add('o=example', function(req, res, next) {
|
||||
console.log(req.dn.toString());
|
||||
});
|
||||
|
|
|
@ -4,36 +4,30 @@ title: Errors API | ldapjs
|
|||
|
||||
# ldapjs Errors API
|
||||
|
||||
<div class="intro">
|
||||
|
||||
This document covers the ldapjs errors API and assumes that you are familiar
|
||||
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||
|
||||
</div>
|
||||
|
||||
All errors in the ldapjs framework extend from an abstract error type called
|
||||
`LDAPError`. In addition to the properties listed below, all errors will have
|
||||
a `stack` property correctly set.
|
||||
|
||||
In general, you'll be using the errors in ldapjs like:
|
||||
|
||||
```js
|
||||
const ldap = require('ldapjs');
|
||||
var ldap = require('ldapjs');
|
||||
|
||||
const db = {};
|
||||
var db = {};
|
||||
|
||||
server.add('o=example', (req, res, next) => {
|
||||
const parent = req.dn.parent();
|
||||
if (parent) {
|
||||
if (!db[parent.toString()])
|
||||
return next(new ldap.NoSuchObjectError(parent.toString()));
|
||||
}
|
||||
if (db[req.dn.toString()])
|
||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||
server.add('o=example', function(req, res, next) {
|
||||
var parent = req.dn.parent();
|
||||
if (parent) {
|
||||
if (!db[parent.toString()])
|
||||
return next(new ldap.NoSuchObjectError(parent.toString()));
|
||||
}
|
||||
if (db[req.dn.toString()])
|
||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||
|
||||
...
|
||||
});
|
||||
```
|
||||
...
|
||||
});
|
||||
|
||||
I.e., if you just pass them into the `next()` handler, ldapjs will automatically
|
||||
return the appropriate LDAP error message, and stop the handler chain.
|
||||
|
|
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
|
||||
|
||||
<div class="intro">
|
||||
|
||||
This document covers the ldapjs filters API and assumes that you are familiar
|
||||
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||
|
||||
</div>
|
||||
|
||||
LDAP search filters are really the backbone of LDAP search operations, and
|
||||
ldapjs tries to get you in "easy" with them if your dataset is small, and also
|
||||
lets you introspect them if you want to write a "query planner". For reference,
|
||||
|
@ -34,17 +30,13 @@ Parses an [RFC2254](http://www.ietf.org/rfc/rfc2254.txt) filter string into an
|
|||
ldapjs object(s). If the filter is "complex", it will be a "tree" of objects.
|
||||
For example:
|
||||
|
||||
```js
|
||||
const parseFilter = require('ldapjs').parseFilter;
|
||||
var parseFilter = require('ldapjs').parseFilter;
|
||||
|
||||
const f = parseFilter('(objectclass=*)');
|
||||
```
|
||||
var f = parseFilter('(objectclass=*)');
|
||||
|
||||
Is a "simple" filter, and would just return a `PresenceFilter` object. However,
|
||||
|
||||
```js
|
||||
const f = parseFilter('(&(employeeType=manager)(l=Seattle))');
|
||||
```
|
||||
var f = parseFilter('(&(employeeType=manager)(l=Seattle))');
|
||||
|
||||
Would return an `AndFilter`, which would have a `filters` array of two
|
||||
`EqualityFilter` objects.
|
||||
|
@ -56,22 +48,20 @@ syntactically invalid string).
|
|||
|
||||
The equality filter is used to check exact matching of attribute/value
|
||||
assertions. This object will have an `attribute` and `value` property, and the
|
||||
`name` property will be `equal`.
|
||||
`name` proerty will be `equal`.
|
||||
|
||||
The string syntax for an equality filter is `(attr=value)`.
|
||||
|
||||
The `matches()` method will return true IFF the passed in object has a
|
||||
key matching `attribute` and a value matching `value`.
|
||||
|
||||
```js
|
||||
const f = new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
});
|
||||
var f = new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
});
|
||||
|
||||
f.matches({cn: 'foo'}); => true
|
||||
f.matches({cn: 'bar'}); => false
|
||||
```
|
||||
f.matches({cn: 'foo'}); => true
|
||||
f.matches({cn: 'bar'}); => false
|
||||
|
||||
Equality matching uses "strict" type JavaScript comparison, and by default
|
||||
everything in ldapjs (and LDAP) is a UTF-8 string. If you want comparison
|
||||
|
@ -89,14 +79,12 @@ The string syntax for a presence filter is `(attr=*)`.
|
|||
The `matches()` method will return true IFF the passed in object has a
|
||||
key matching `attribute`.
|
||||
|
||||
```js
|
||||
const f = new PresenceFilter({
|
||||
attribute: 'cn'
|
||||
});
|
||||
var f = new PresenceFilter({
|
||||
attribute: 'cn'
|
||||
});
|
||||
|
||||
f.matches({cn: 'foo'}); => true
|
||||
f.matches({sn: 'foo'}); => false
|
||||
```
|
||||
f.matches({cn: 'foo'}); => true
|
||||
f.matches({sn: 'foo'}); => false
|
||||
|
||||
# SubstringFilter
|
||||
|
||||
|
@ -110,28 +98,24 @@ optional. The `name` property will be `substring`.
|
|||
The string syntax for a presence filter is `(attr=foo*bar*cat*dog)`, which would
|
||||
map to:
|
||||
|
||||
```js
|
||||
{
|
||||
initial: 'foo',
|
||||
any: ['bar', 'cat'],
|
||||
final: 'dog'
|
||||
}
|
||||
```
|
||||
{
|
||||
initial: 'foo',
|
||||
any: ['bar', 'cat'],
|
||||
final: 'dog'
|
||||
}
|
||||
|
||||
The `matches()` method will return true IFF the passed in object has a
|
||||
key matching `attribute` and the "regex" matches the value
|
||||
|
||||
```js
|
||||
const f = new SubstringFilter({
|
||||
attribute: 'cn',
|
||||
initial: 'foo',
|
||||
any: ['bar'],
|
||||
final: 'baz'
|
||||
});
|
||||
var f = new SubstringFilter({
|
||||
attribute: 'cn',
|
||||
initial: 'foo',
|
||||
any: ['bar'],
|
||||
final: 'baz'
|
||||
});
|
||||
|
||||
f.matches({cn: 'foobigbardogbaz'}); => true
|
||||
f.matches({sn: 'fobigbardogbaz'}); => false
|
||||
```
|
||||
f.matches({cn: 'foobigbardogbaz'}); => true
|
||||
f.matches({sn: 'fobigbardogbaz'}); => false
|
||||
|
||||
# GreaterThanEqualsFilter
|
||||
|
||||
|
@ -147,22 +131,18 @@ property and the `name` property will be `ge`.
|
|||
|
||||
The string syntax for a ge filter is:
|
||||
|
||||
```
|
||||
(cn>=foo)
|
||||
```
|
||||
(cn>=foo)
|
||||
|
||||
The `matches()` method will return true IFF the passed in object has a
|
||||
key matching `attribute` and the value is `>=` this filter's `value`.
|
||||
|
||||
```js
|
||||
const f = new GreaterThanEqualsFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo',
|
||||
});
|
||||
var f = new GreaterThanEqualsFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo',
|
||||
});
|
||||
|
||||
f.matches({cn: 'foobar'}); => true
|
||||
f.matches({cn: 'abc'}); => false
|
||||
```
|
||||
f.matches({cn: 'foobar'}); => true
|
||||
f.matches({cn: 'abc'}); => false
|
||||
|
||||
# LessThanEqualsFilter
|
||||
|
||||
|
@ -175,9 +155,7 @@ Note that the ldapjs schema middleware will do this.
|
|||
|
||||
The string syntax for a le filter is:
|
||||
|
||||
```
|
||||
(cn<=foo)
|
||||
```
|
||||
(cn<=foo)
|
||||
|
||||
The LessThanEqualsFilter will have an `attribute` property, a `value`
|
||||
property and the `name` property will be `le`.
|
||||
|
@ -185,15 +163,13 @@ property and the `name` property will be `le`.
|
|||
The `matches()` method will return true IFF the passed in object has a
|
||||
key matching `attribute` and the value is `<=` this filter's `value`.
|
||||
|
||||
```js
|
||||
const f = new LessThanEqualsFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo',
|
||||
});
|
||||
var f = new LessThanEqualsFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo',
|
||||
});
|
||||
|
||||
f.matches({cn: 'abc'}); => true
|
||||
f.matches({cn: 'foobar'}); => false
|
||||
```
|
||||
f.matches({cn: 'abc'}); => true
|
||||
f.matches({cn: 'foobar'}); => false
|
||||
|
||||
# AndFilter
|
||||
|
||||
|
@ -204,30 +180,26 @@ object will have a `filters` property which is an array of `Filter` objects. The
|
|||
The string syntax for an and filter is (assuming below we're and'ing two
|
||||
equality filters):
|
||||
|
||||
```
|
||||
(&(cn=foo)(sn=bar))
|
||||
```
|
||||
(&(cn=foo)(sn=bar))
|
||||
|
||||
The `matches()` method will return true IFF the passed in object matches all
|
||||
the filters in the `filters` array.
|
||||
|
||||
```js
|
||||
const f = new AndFilter({
|
||||
filters: [
|
||||
new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
}),
|
||||
new EqualityFilter({
|
||||
attribute: 'sn',
|
||||
value: 'bar'
|
||||
})
|
||||
]
|
||||
});
|
||||
var f = new AndFilter({
|
||||
filters: [
|
||||
new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
}),
|
||||
new EqualityFilter({
|
||||
attribute: 'sn',
|
||||
value: 'bar'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
f.matches({cn: 'foo', sn: 'bar'}); => true
|
||||
f.matches({cn: 'foo', sn: 'baz'}); => false
|
||||
```
|
||||
f.matches({cn: 'foo', sn: 'bar'}); => true
|
||||
f.matches({cn: 'foo', sn: 'baz'}); => false
|
||||
|
||||
# OrFilter
|
||||
|
||||
|
@ -238,30 +210,26 @@ object will have a `filters` property which is an array of `Filter` objects. The
|
|||
The string syntax for an or filter is (assuming below we're or'ing two
|
||||
equality filters):
|
||||
|
||||
```
|
||||
(|(cn=foo)(sn=bar))
|
||||
```
|
||||
(|(cn=foo)(sn=bar))
|
||||
|
||||
The `matches()` method will return true IFF the passed in object matches *any*
|
||||
of the filters in the `filters` array.
|
||||
|
||||
```js
|
||||
const f = new OrFilter({
|
||||
filters: [
|
||||
new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
}),
|
||||
new EqualityFilter({
|
||||
attribute: 'sn',
|
||||
value: 'bar'
|
||||
})
|
||||
]
|
||||
});
|
||||
var f = new OrFilter({
|
||||
filters: [
|
||||
new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
}),
|
||||
new EqualityFilter({
|
||||
attribute: 'sn',
|
||||
value: 'bar'
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
f.matches({cn: 'foo', sn: 'baz'}); => true
|
||||
f.matches({cn: 'bar', sn: 'baz'}); => false
|
||||
```
|
||||
f.matches({cn: 'foo', sn: 'baz'}); => true
|
||||
f.matches({cn: 'bar', sn: 'baz'}); => false
|
||||
|
||||
# NotFilter
|
||||
|
||||
|
@ -272,30 +240,26 @@ The `name` property will be `not`.
|
|||
The string syntax for a not filter is (assuming below we're not'ing an
|
||||
equality filter):
|
||||
|
||||
```
|
||||
(!(cn=foo))
|
||||
```
|
||||
(!(cn=foo))
|
||||
|
||||
The `matches()` method will return true IFF the passed in object does not match
|
||||
the filter in the `filter` property.
|
||||
|
||||
```js
|
||||
const f = new NotFilter({
|
||||
filter: new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
})
|
||||
});
|
||||
var f = new NotFilter({
|
||||
filter: new EqualityFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
})
|
||||
});
|
||||
|
||||
f.matches({cn: 'bar'}); => true
|
||||
f.matches({cn: 'foo'}); => false
|
||||
```
|
||||
f.matches({cn: 'bar'}); => true
|
||||
f.matches({cn: 'foo'}); => false
|
||||
|
||||
# ApproximateFilter
|
||||
|
||||
The approximate filter is used to check "approximate" matching of
|
||||
attribute/value assertions. This object will have an `attribute` and
|
||||
`value` property, and the `name` property will be `approx`.
|
||||
`value` property, and the `name` proerty will be `approx`.
|
||||
|
||||
As a side point, this is a useless filter. It's really only here if you have
|
||||
some whacky client that's sending this. It just does an exact match (which
|
||||
|
@ -306,12 +270,11 @@ The string syntax for an equality filter is `(attr~=value)`.
|
|||
The `matches()` method will return true IFF the passed in object has a
|
||||
key matching `attribute` and a value exactly matching `value`.
|
||||
|
||||
```js
|
||||
const f = new ApproximateFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
});
|
||||
var f = new ApproximateFilter({
|
||||
attribute: 'cn',
|
||||
value: 'foo'
|
||||
});
|
||||
|
||||
f.matches({cn: 'foo'}); => true
|
||||
f.matches({cn: 'bar'}); => false
|
||||
|
||||
f.matches({cn: 'foo'}); => true
|
||||
f.matches({cn: 'bar'}); => false
|
||||
```
|
||||
|
|
548
docs/guide.md
548
docs/guide.md
|
@ -1,11 +1,10 @@
|
|||
---
|
||||
title: LDAP Guide | ldapjs
|
||||
markdown2extras: tables
|
||||
---
|
||||
|
||||
# LDAP Guide
|
||||
|
||||
<div class="intro">
|
||||
|
||||
This guide was written assuming that you (1) don't know anything about ldapjs,
|
||||
and perhaps more importantly (2) know little, if anything about LDAP. If you're
|
||||
already an LDAP whiz, please don't read this and feel it's condescending. Most
|
||||
|
@ -15,8 +14,6 @@ password."
|
|||
By the end of this guide, we'll have a simple LDAP server that accomplishes a
|
||||
"real" task.
|
||||
|
||||
</div>
|
||||
|
||||
# What exactly is LDAP?
|
||||
|
||||
If you haven't already read the
|
||||
|
@ -31,26 +28,23 @@ Access Protocol". A directory service basically breaks down as follows:
|
|||
|
||||
It might be helpful to visualize:
|
||||
|
||||
```
|
||||
o=example
|
||||
/ \
|
||||
ou=users ou=groups
|
||||
/ | | \
|
||||
cn=john cn=jane cn=dudes cn=dudettes
|
||||
/
|
||||
keyid=foo
|
||||
```
|
||||
o=example
|
||||
/ \
|
||||
ou=users ou=groups
|
||||
/ | | \
|
||||
cn=john cn=jane cn=dudes cn=dudettes
|
||||
/
|
||||
keyid=foo
|
||||
|
||||
|
||||
Let's say we wanted to look at the record cn=john:
|
||||
|
||||
```shell
|
||||
dn: cn=john, ou=users, o=example
|
||||
cn: john
|
||||
sn: smith
|
||||
email: john@example.com
|
||||
email: john.smith@example.com
|
||||
objectClass: person
|
||||
```
|
||||
dn: cn=john, ou=users, o=example
|
||||
cn: john
|
||||
sn: smith
|
||||
email: john@example.com
|
||||
email: john.smith@example.com
|
||||
objectClass: person
|
||||
|
||||
A few things to note:
|
||||
|
||||
|
@ -94,7 +88,7 @@ Want to run schema-less in ldapjs, or wire it up with some mongoose models? No
|
|||
problem. Want to back it to redis? Should be able to get some basics up in a
|
||||
day or two.
|
||||
|
||||
Basically, the ldapjs philosophy is to deal with the "muck" of LDAP, and then
|
||||
Basically, the ldapjs philospohy is to deal with the "muck" of LDAP, and then
|
||||
get out of the way so you can just use the "good parts."
|
||||
|
||||
# Ok, cool. Learn me some LDAP!
|
||||
|
@ -114,9 +108,7 @@ If you don't already have node.js and npm, clearly you need those, so follow
|
|||
the steps at [nodejs.org](http://nodejs.org) and [npmjs.org](http://npmjs.org),
|
||||
respectively. After that, run:
|
||||
|
||||
```shell
|
||||
$ npm install ldapjs
|
||||
```
|
||||
$ npm install ldapjs
|
||||
|
||||
Rather than overload you with client-side programming for now, we'll use
|
||||
the OpenLDAP CLI to interact with our server. It's almost certainly already
|
||||
|
@ -126,22 +118,18 @@ package manager here.
|
|||
To get started, open some file, and let's get the library loaded and a server
|
||||
created:
|
||||
|
||||
```js
|
||||
const ldap = require('ldapjs');
|
||||
var ldap = require('ldapjs');
|
||||
|
||||
const server = ldap.createServer();
|
||||
var server = ldap.createServer();
|
||||
|
||||
server.listen(1389, () => {
|
||||
console.log('/etc/passwd LDAP server up at: %s', server.url);
|
||||
});
|
||||
```
|
||||
server.listen(1389, function() {
|
||||
console.log('/etc/passwd LDAP server up at: %s', server.url);
|
||||
});
|
||||
|
||||
And run that. Doing anything will give you errors (LDAP "No Such Object")
|
||||
since we haven't added any support in yet, but go ahead and try it anyway:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -b "o=myhost" objectclass=*
|
||||
```
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -b "o=myhost" objectclass=*
|
||||
|
||||
Before we go any further, note that the complete code for the server we are
|
||||
about to build up is on the [examples](examples.html) page.
|
||||
|
@ -162,15 +150,13 @@ has no correspondence to our Unix root user, it's just something we're making up
|
|||
and going to use for allowing an (LDAP) admin to do anything. To do so, add
|
||||
this code into your file:
|
||||
|
||||
```js
|
||||
server.bind('cn=root', (req, res, next) => {
|
||||
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
|
||||
return next(new ldap.InvalidCredentialsError());
|
||||
server.bind('cn=root', function(req, res, next) {
|
||||
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
|
||||
return next(new ldap.InvalidCredentialsError());
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
```
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
Not very secure, but this is a demo. What we did there was "mount" a tree in
|
||||
the ldapjs server, and add a handler for the _bind_ method. If you've ever used
|
||||
|
@ -179,9 +165,7 @@ handlers in, as we'll see later.
|
|||
|
||||
On to the meat of the method. What's up with this?
|
||||
|
||||
```js
|
||||
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
|
||||
```
|
||||
if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
|
||||
|
||||
The first part `req.dn.toString() !== 'cn=root'`: you're probably thinking
|
||||
"WTF?!? Does ldapjs allow something other than cn=root into this handler?" Sort
|
||||
|
@ -205,22 +189,18 @@ add another handler in later you won't get bit by it not being invoked.
|
|||
|
||||
Blah blah, let's try running the ldap client again, first with a bad password:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w foo -b "o=myhost" objectclass=*
|
||||
|
||||
ldap_bind: Invalid credentials (49)
|
||||
matched DN: cn=root
|
||||
additional info: Invalid Credentials
|
||||
```
|
||||
ldap_bind: Invalid credentials (49)
|
||||
matched DN: cn=root
|
||||
additional info: Invalid Credentials
|
||||
|
||||
And again with the correct one:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
|
||||
|
||||
No such object (32)
|
||||
Additional information: No tree found for: o=myhost
|
||||
```
|
||||
No such object (32)
|
||||
Additional information: No tree found for: o=myhost
|
||||
|
||||
Don't worry about all the flags we're passing into OpenLDAP, that's just to make
|
||||
their CLI less annonyingly noisy. This time, we got another `No such object`
|
||||
|
@ -234,14 +214,12 @@ what if the remote end doesn't authenticate at all? Right, nothing says they
|
|||
*have to* bind, that's just what the common clients do. Let's add a quick
|
||||
authorization handler that we'll use in all our subsequent routes:
|
||||
|
||||
```js
|
||||
function authorize(req, res, next) {
|
||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||
return next(new ldap.InsufficientAccessRightsError());
|
||||
function authorize(req, res, next) {
|
||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||
return next(new ldap.InsufficientAccessRightsError());
|
||||
|
||||
return next();
|
||||
}
|
||||
```
|
||||
return next();
|
||||
}
|
||||
|
||||
Should be pretty self-explanatory, but as a reminder, LDAP is connection
|
||||
oriented, so we check that the connection remote user was indeed our `cn=root`
|
||||
|
@ -252,9 +230,7 @@ oriented, so we check that the connection remote user was indeed our `cn=root`
|
|||
We said we wanted to allow LDAP operations over /etc/passwd, so let's detour
|
||||
for a moment to explain an /etc/passwd record.
|
||||
|
||||
```shell
|
||||
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
|
||||
```
|
||||
jsmith:x:1001:1000:Joe Smith,Room 1007,(234)555-8910,(234)555-0044,email:/home/jsmith:/bin/sh
|
||||
|
||||
The sample record above maps to:
|
||||
|
||||
|
@ -269,86 +245,77 @@ The sample record above maps to:
|
|||
|/bin/sh |Shell |
|
||||
|
||||
Let's write some handlers to parse that and transform it into an LDAP search
|
||||
record (note, you'll need to add `const fs = require('fs');` at the top of the
|
||||
record (note, you'll need to add `var fs = require('fs');` at the top of the
|
||||
source file).
|
||||
|
||||
First, make a handler that just loads the "user database" in a "pre" handler:
|
||||
|
||||
```js
|
||||
function loadPasswdFile(req, res, next) {
|
||||
fs.readFile('/etc/passwd', 'utf8', (err, data) => {
|
||||
if (err)
|
||||
return next(new ldap.OperationsError(err.message));
|
||||
function loadPasswdFile(req, res, next) {
|
||||
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
|
||||
if (err)
|
||||
return next(new ldap.OperationsError(err.message));
|
||||
|
||||
req.users = {};
|
||||
req.users = {};
|
||||
|
||||
const lines = data.split('\n');
|
||||
for (const line of lines) {
|
||||
if (!line || /^#/.test(line))
|
||||
continue;
|
||||
var lines = data.split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (!lines[i] || /^#/.test(lines[i]))
|
||||
continue;
|
||||
|
||||
const record = line.split(':');
|
||||
if (!record || !record.length)
|
||||
continue;
|
||||
var record = lines[i].split(':');
|
||||
if (!record || !record.length)
|
||||
continue;
|
||||
|
||||
req.users[record[0]] = {
|
||||
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
||||
attributes: {
|
||||
cn: record[0],
|
||||
uid: record[2],
|
||||
gid: record[3],
|
||||
description: record[4],
|
||||
homedirectory: record[5],
|
||||
shell: record[6] || '',
|
||||
objectclass: 'unixUser'
|
||||
req.users[record[0]] = {
|
||||
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
||||
attributes: {
|
||||
cn: record[0],
|
||||
uid: record[2],
|
||||
gid: record[3],
|
||||
description: record[4],
|
||||
homedirectory: record[5],
|
||||
shell: record[6] || '',
|
||||
objectclass: 'unixUser'
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
}
|
||||
```
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
||||
Ok, all that did is tack the /etc/passwd records onto req.users so that any
|
||||
subsequent handler doesn't have to reload the file. Next, let's write a search
|
||||
handler to process that:
|
||||
|
||||
```js
|
||||
const pre = [authorize, loadPasswdFile];
|
||||
var pre = [authorize, loadPasswdFile];
|
||||
|
||||
server.search('o=myhost', pre, (req, res, next) => {
|
||||
const keys = Object.keys(req.users);
|
||||
for (const k of keys) {
|
||||
if (req.filter.matches(req.users[k].attributes))
|
||||
res.send(req.users[k]);
|
||||
}
|
||||
server.search('o=myhost', pre, function(req, res, next) {
|
||||
Object.keys(req.users).forEach(function(k) {
|
||||
if (req.filter.matches(req.users[k].attributes))
|
||||
res.send(req.users[k]);
|
||||
});
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
```
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
And try running:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
|
||||
dn: cn=root, ou=users, o=myhost
|
||||
cn: root
|
||||
uid: 0
|
||||
gid: 0
|
||||
description: System Administrator
|
||||
homedirectory: /var/root
|
||||
shell: /bin/sh
|
||||
objectclass: unixUser
|
||||
```
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
|
||||
dn: cn=root, ou=users, o=myhost
|
||||
cn: root
|
||||
uid: 0
|
||||
gid: 0
|
||||
description: System Administrator
|
||||
homedirectory: /var/root
|
||||
shell: /bin/sh
|
||||
objectclass: unixUser
|
||||
|
||||
Sweet! Try this out too:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
|
||||
...
|
||||
```
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" objectclass=*
|
||||
...
|
||||
|
||||
You should have seen an entry for every record in /etc/passwd with the second.
|
||||
What all did we do here? A lot. Let's break this down...
|
||||
|
@ -357,9 +324,7 @@ What all did we do here? A lot. Let's break this down...
|
|||
|
||||
Let's start with looking at what you even asked for:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
|
||||
```
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" cn=root
|
||||
|
||||
We can throw away `ldapsearch -H -x -D -w -LLL`, as those just specify the URL
|
||||
to connect to, the bind credentials and the `-LLL` just quiets down OpenLDAP.
|
||||
|
@ -392,20 +357,18 @@ and made the cheesiest transform ever, which is making up a "search entry." A
|
|||
search entry _must_ have a DN so the client knows what record it is, and a set
|
||||
of attributes. So that's why we did this:
|
||||
|
||||
```js
|
||||
const entry = {
|
||||
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
||||
attributes: {
|
||||
cn: record[0],
|
||||
uid: record[2],
|
||||
gid: record[3],
|
||||
description: record[4],
|
||||
homedirectory: record[5],
|
||||
shell: record[6] || '',
|
||||
objectclass: 'unixUser'
|
||||
}
|
||||
};
|
||||
```
|
||||
var entry = {
|
||||
dn: 'cn=' + record[0] + ', ou=users, o=myhost',
|
||||
attributes: {
|
||||
cn: record[0],
|
||||
uid: record[2],
|
||||
gid: record[3],
|
||||
description: record[4],
|
||||
homedirectory: record[5],
|
||||
shell: record[6] || '',
|
||||
objectclass: 'unixUser'
|
||||
}
|
||||
};
|
||||
|
||||
Next, we let ldapjs do all the hard work of figuring out LDAP search filters
|
||||
for us by calling `req.filter.matches`. If it matched, we return the whole
|
||||
|
@ -420,133 +383,120 @@ shell set to `/bin/false` and whose name starts with `p` (I'm doing this
|
|||
on Ubuntu). Then, let's say we only care about their login name and primary
|
||||
group id. We'd do this:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" "(&(shell=/bin/false)(cn=p*))" cn gid
|
||||
dn: cn=proxy, ou=users, o=myhost
|
||||
cn: proxy
|
||||
gid: 13
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -D cn=root -w secret -LLL -b "o=myhost" "(&(shell=/bin/false)(cn=p*))" cn gid
|
||||
dn: cn=proxy, ou=users, o=myhost
|
||||
cn: proxy
|
||||
gid: 13
|
||||
|
||||
dn: cn=pulse, ou=users, o=myhost
|
||||
cn: pulse
|
||||
gid: 114
|
||||
```
|
||||
dn: cn=pulse, ou=users, o=myhost
|
||||
cn: pulse
|
||||
gid: 114
|
||||
|
||||
## Add
|
||||
|
||||
This is going to be a little bit ghetto, since what we're going to do is just
|
||||
use node's child process module to spawn calls to `adduser`. Go ahead and add
|
||||
the following code in as another handler (you'll need a
|
||||
`const { spawn } = require('child_process');` at the top of your file):
|
||||
`var spawn = require('child_process').spawn;` at the top of your file):
|
||||
|
||||
```js
|
||||
server.add('ou=users, o=myhost', pre, (req, res, next) => {
|
||||
if (!req.dn.rdns[0].attrs.cn)
|
||||
return next(new ldap.ConstraintViolationError('cn required'));
|
||||
server.add('ou=users, o=myhost', pre, function(req, res, next) {
|
||||
if (!req.dn.rdns[0].cn)
|
||||
return next(new ldap.ConstraintViolationError('cn required'));
|
||||
|
||||
if (req.users[req.dn.rdns[0].attrs.cn.value])
|
||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||
if (req.users[req.dn.rdns[0].cn])
|
||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||
|
||||
const entry = req.toObject().attributes;
|
||||
var entry = req.toObject().attributes;
|
||||
|
||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
||||
|
||||
const opts = ['-m'];
|
||||
if (entry.description) {
|
||||
opts.push('-c');
|
||||
opts.push(entry.description[0]);
|
||||
}
|
||||
if (entry.homedirectory) {
|
||||
opts.push('-d');
|
||||
opts.push(entry.homedirectory[0]);
|
||||
}
|
||||
if (entry.gid) {
|
||||
opts.push('-g');
|
||||
opts.push(entry.gid[0]);
|
||||
}
|
||||
if (entry.shell) {
|
||||
opts.push('-s');
|
||||
opts.push(entry.shell[0]);
|
||||
}
|
||||
if (entry.uid) {
|
||||
opts.push('-u');
|
||||
opts.push(entry.uid[0]);
|
||||
}
|
||||
opts.push(entry.cn[0]);
|
||||
const useradd = spawn('useradd', opts);
|
||||
var opts = ['-m'];
|
||||
if (entry.description) {
|
||||
opts.push('-c');
|
||||
opts.push(entry.description[0]);
|
||||
}
|
||||
if (entry.homedirectory) {
|
||||
opts.push('-d');
|
||||
opts.push(entry.homedirectory[0]);
|
||||
}
|
||||
if (entry.gid) {
|
||||
opts.push('-g');
|
||||
opts.push(entry.gid[0]);
|
||||
}
|
||||
if (entry.shell) {
|
||||
opts.push('-s');
|
||||
opts.push(entry.shell[0]);
|
||||
}
|
||||
if (entry.uid) {
|
||||
opts.push('-u');
|
||||
opts.push(entry.uid[0]);
|
||||
}
|
||||
opts.push(entry.cn[0]);
|
||||
var useradd = spawn('useradd', opts);
|
||||
|
||||
const messages = [];
|
||||
var messages = [];
|
||||
|
||||
useradd.stdout.on('data', (data) => {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
useradd.stderr.on('data', (data) => {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
useradd.stdout.on('data', function(data) {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
useradd.stderr.on('data', function(data) {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
|
||||
useradd.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
let msg = '' + code;
|
||||
if (messages.length)
|
||||
msg += ': ' + messages.join();
|
||||
return next(new ldap.OperationsError(msg));
|
||||
}
|
||||
useradd.on('exit', function(code) {
|
||||
if (code !== 0) {
|
||||
var msg = '' + code;
|
||||
if (messages.length)
|
||||
msg += ': ' + messages.join();
|
||||
return next(new ldap.OperationsError(msg));
|
||||
}
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
```
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
Then, you'll need to be root to have this running, so start your server with
|
||||
`sudo` (or be root, whatever). Now, go ahead and create a file called
|
||||
`user.ldif` with the following contents:
|
||||
|
||||
```shell
|
||||
dn: cn=ldapjs, ou=users, o=myhost
|
||||
objectClass: unixUser
|
||||
cn: ldapjs
|
||||
shell: /bin/bash
|
||||
description: Created via ldapadd
|
||||
```
|
||||
dn: cn=ldapjs, ou=users, o=myhost
|
||||
objectClass: unixUser
|
||||
cn: ldapjs
|
||||
shell: /bin/bash
|
||||
description: Created via ldapadd
|
||||
|
||||
Now go ahead and invoke with:
|
||||
|
||||
```shell
|
||||
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
|
||||
adding new entry "cn=ldapjs, ou=users, o=myhost"
|
||||
```
|
||||
$ ldapadd -H ldap://localhost:1389 -x -D cn=root -w secret -f ./user.ldif
|
||||
adding new entry "cn=ldapjs, ou=users, o=myhost"
|
||||
|
||||
Let's confirm he got added with an ldapsearch:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs
|
||||
dn: cn=ldapjs, ou=users, o=myhost
|
||||
cn: ldapjs
|
||||
uid: 1001
|
||||
gid: 1001
|
||||
description: Created via ldapadd
|
||||
homedirectory: /home/ldapjs
|
||||
shell: /bin/bash
|
||||
objectclass: unixUser
|
||||
```
|
||||
$ ldapsearch -H ldap://localhost:1389 -LLL -x -D cn=root -w secret -b "ou=users, o=myhost" cn=ldapjs
|
||||
dn: cn=ldapjs, ou=users, o=myhost
|
||||
cn: ldapjs
|
||||
uid: 1001
|
||||
gid: 1001
|
||||
description: Created via ldapadd
|
||||
homedirectory: /home/ldapjs
|
||||
shell: /bin/bash
|
||||
objectclass: unixUser
|
||||
|
||||
As before, here's a breakdown of the code:
|
||||
|
||||
```js
|
||||
server.add('ou=users, o=myhost', pre, (req, res, next) => {
|
||||
if (!req.dn.rdns[0].attrs.cn)
|
||||
return next(new ldap.ConstraintViolationError('cn required'));
|
||||
server.add('ou=users, o=myhost', pre, function(req, res, next) {
|
||||
if (!req.dn.rdns[0].cn)
|
||||
return next(new ldap.ConstraintViolationError('cn required'));
|
||||
|
||||
if (req.users[req.dn.rdns[0].attrs.cn.value])
|
||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||
if (req.users[req.dn.rdns[0].cn])
|
||||
return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));
|
||||
|
||||
const entry = req.toObject().attributes;
|
||||
var entry = req.toObject().attributes;
|
||||
|
||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
||||
});
|
||||
```
|
||||
if (entry.objectclass.indexOf('unixUser') === -1)
|
||||
return next(new ldap.ConstraintViolationError('entry must be a unixUser'));
|
||||
|
||||
A few new things:
|
||||
|
||||
|
@ -581,43 +531,42 @@ Unlike HTTP, "partial" document updates are fully specified as part of the
|
|||
RFC, so appending, removing, or replacing a single attribute is pretty natural.
|
||||
Go ahead and add the following code into your source file:
|
||||
|
||||
```js
|
||||
server.modify('ou=users, o=myhost', pre, (req, res, next) => {
|
||||
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
|
||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
server.modify('ou=users, o=myhost', pre, function(req, res, next) {
|
||||
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
if (!req.changes.length)
|
||||
return next(new ldap.ProtocolError('changes required'));
|
||||
if (!req.changes.length)
|
||||
return next(new ldap.ProtocolError('changes required'));
|
||||
|
||||
const user = req.users[req.dn.rdns[0].attrs.cn.value].attributes;
|
||||
let mod;
|
||||
var user = req.users[req.dn.rdns[0].cn].attributes;
|
||||
var mod;
|
||||
|
||||
for (const i = 0; i < req.changes.length; i++) {
|
||||
mod = req.changes[i].modification;
|
||||
switch (req.changes[i].operation) {
|
||||
case 'replace':
|
||||
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
|
||||
return next(new ldap.UnwillingToPerformError('only password updates ' +
|
||||
'allowed'));
|
||||
break;
|
||||
case 'add':
|
||||
case 'delete':
|
||||
return next(new ldap.UnwillingToPerformError('only replace allowed'));
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < req.changes.length; i++) {
|
||||
mod = req.changes[i].modification;
|
||||
switch (req.changes[i].operation) {
|
||||
case 'replace':
|
||||
if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
|
||||
return next(new ldap.UnwillingToPerformError('only password updates ' +
|
||||
'allowed'));
|
||||
break;
|
||||
case 'add':
|
||||
case 'delete':
|
||||
return next(new ldap.UnwillingToPerformError('only replace allowed'));
|
||||
}
|
||||
}
|
||||
|
||||
const passwd = spawn('chpasswd', ['-c', 'MD5']);
|
||||
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
|
||||
var passwd = spawn('chpasswd', ['-c', 'MD5']);
|
||||
passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');
|
||||
|
||||
passwd.on('exit', (code) => {
|
||||
if (code !== 0)
|
||||
return next(new ldap.OperationsError(code));
|
||||
passwd.on('exit', function(code) {
|
||||
if (code !== 0)
|
||||
return next(new ldap.OperationsError(code));
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Basically, we made sure the remote client was targeting an entry that exists,
|
||||
ensuring that they were asking to "replace" the `userPassword` attribute (which
|
||||
|
@ -626,19 +575,15 @@ is the 'standard' LDAP attribute for passwords; if you think it's easier to use
|
|||
command (which lets you change a user's password over stdin). Next, go ahead
|
||||
and create a `passwd.ldif` file:
|
||||
|
||||
```shell
|
||||
dn: cn=ldapjs, ou=users, o=myhost
|
||||
changetype: modify
|
||||
replace: userPassword
|
||||
userPassword: secret
|
||||
-
|
||||
```
|
||||
dn: cn=ldapjs, ou=users, o=myhost
|
||||
changetype: modify
|
||||
replace: userPassword
|
||||
userPassword: secret
|
||||
-
|
||||
|
||||
And then run the OpenLDAP CLI:
|
||||
|
||||
```shell
|
||||
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
|
||||
```
|
||||
$ ldapmodify -H ldap://localhost:1389 -x -D cn=root -w secret -f ./passwd.ldif
|
||||
|
||||
You should now be able to login to your box as the ldapjs user. Let's get
|
||||
the last "mainline" piece of work out of the way, and delete the user.
|
||||
|
@ -648,40 +593,37 @@ the last "mainline" piece of work out of the way, and delete the user.
|
|||
Delete is pretty straightforward. The client gives you a dn to delete, and you
|
||||
delete it :). Add the following code into your server:
|
||||
|
||||
```js
|
||||
server.del('ou=users, o=myhost', pre, (req, res, next) => {
|
||||
if (!req.dn.rdns[0].attrs.cn || !req.users[req.dn.rdns[0].attrs.cn.value])
|
||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
server.del('ou=users, o=myhost', pre, function(req, res, next) {
|
||||
if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
|
||||
return next(new ldap.NoSuchObjectError(req.dn.toString()));
|
||||
|
||||
const userdel = spawn('userdel', ['-f', req.dn.rdns[0].attrs.cn.value]);
|
||||
var userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);
|
||||
|
||||
const messages = [];
|
||||
userdel.stdout.on('data', (data) => {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
userdel.stderr.on('data', (data) => {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
var messages = [];
|
||||
userdel.stdout.on('data', function(data) {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
userdel.stderr.on('data', function(data) {
|
||||
messages.push(data.toString());
|
||||
});
|
||||
|
||||
userdel.on('exit', (code) => {
|
||||
if (code !== 0) {
|
||||
let msg = '' + code;
|
||||
if (messages.length)
|
||||
msg += ': ' + messages.join();
|
||||
return next(new ldap.OperationsError(msg));
|
||||
}
|
||||
userdel.on('exit', function(code) {
|
||||
if (code !== 0) {
|
||||
var msg = '' + code;
|
||||
if (messages.length)
|
||||
msg += ': ' + messages.join();
|
||||
return next(new ldap.OperationsError(msg));
|
||||
}
|
||||
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
```
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
And then run the following command:
|
||||
|
||||
```shell
|
||||
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
|
||||
```
|
||||
$ ldapdelete -H ldap://localhost:1389 -x -D cn=root -w secret "cn=ldapjs, ou=users, o=myhost"
|
||||
|
||||
|
||||
# Where to go from here
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: ldapjs
|
||||
markdown2extras: tables
|
||||
---
|
||||
|
||||
<div id="indextagline">
|
||||
|
@ -8,45 +9,37 @@ Reimagining <a href="http://tools.ietf.org/html/rfc4510" id="indextaglink">LDAP<
|
|||
|
||||
# Overview
|
||||
|
||||
<div class="intro">
|
||||
|
||||
ldapjs is a pure JavaScript, from-scratch framework for implementing
|
||||
[LDAP](http://tools.ietf.org/html/rfc4510) clients and servers in
|
||||
[Node.js](http://nodejs.org). It is intended for developers used to interacting
|
||||
with HTTP services in node and [restify](http://restify.com).
|
||||
|
||||
</div>
|
||||
var ldap = require('ldapjs');
|
||||
|
||||
```js
|
||||
const ldap = require('ldapjs');
|
||||
var server = ldap.createServer();
|
||||
|
||||
const server = ldap.createServer();
|
||||
server.search('o=example', function(req, res, next) {
|
||||
var obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['organization', 'top'],
|
||||
o: 'example'
|
||||
}
|
||||
};
|
||||
|
||||
server.search('o=example', (req, res, next) => {
|
||||
const obj = {
|
||||
dn: req.dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['organization', 'top'],
|
||||
o: 'example'
|
||||
}
|
||||
};
|
||||
if (req.filter.matches(obj.attributes))
|
||||
res.send(obj);
|
||||
|
||||
if (req.filter.matches(obj.attributes))
|
||||
res.send(obj);
|
||||
res.end();
|
||||
});
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
server.listen(1389, () => {
|
||||
console.log('LDAP server listening at %s', server.url);
|
||||
});
|
||||
```
|
||||
server.listen(1389, function() {
|
||||
console.log('LDAP server listening at %s', server.url);
|
||||
});
|
||||
|
||||
Try hitting that with:
|
||||
|
||||
```shell
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
|
||||
```
|
||||
$ ldapsearch -H ldap://localhost:1389 -x -b o=example objectclass=*
|
||||
|
||||
# Features
|
||||
|
||||
|
@ -59,9 +52,7 @@ that you can build LDAP over anything you want, not just traditional databases.
|
|||
|
||||
# Getting started
|
||||
|
||||
```shell
|
||||
$ npm install ldapjs
|
||||
```
|
||||
$ npm install ldapjs
|
||||
|
||||
If you're new to LDAP, check out the [guide](guide.html). Otherwise, the
|
||||
API documentation is:
|
||||
|
@ -79,7 +70,9 @@ API documentation is:
|
|||
# More information
|
||||
|
||||
- License:[MIT](http://opensource.org/licenses/mit-license.php)
|
||||
- Code: [ldapjs/node-ldapjs](https://github.com/ldapjs/node-ldapjs)
|
||||
- Code: [mcavage/node-ldapjs](https://github.com/mcavage/node-ldapjs)
|
||||
- node.js version: >=0.8
|
||||
- Twitter: [@pfmooney](http://twitter.com/pfmooney)
|
||||
|
||||
# What's not in the box?
|
||||
|
||||
|
@ -93,3 +86,5 @@ Specifically:
|
|||
* Extensible matching
|
||||
|
||||
There are a few others, but those are the "big" ones.
|
||||
|
||||
|
||||
|
|
269
docs/server.md
269
docs/server.md
|
@ -1,23 +1,18 @@
|
|||
---
|
||||
title: Server API | ldapjs
|
||||
markdown2extras: wiki-tables
|
||||
---
|
||||
|
||||
# ldapjs Server API
|
||||
|
||||
<div class="intro">
|
||||
|
||||
This document covers the ldapjs server API and assumes that you are familiar
|
||||
with LDAP. If you're not, read the [guide](guide.html) first.
|
||||
|
||||
</div>
|
||||
|
||||
# Create a server
|
||||
|
||||
The code to create a new server looks like:
|
||||
|
||||
```js
|
||||
const server = ldap.createServer();
|
||||
```
|
||||
var server = ldap.createServer();
|
||||
|
||||
The full list of options is:
|
||||
|
||||
|
@ -70,25 +65,17 @@ available.
|
|||
|
||||
Example:
|
||||
|
||||
```js
|
||||
server.listen(389, '127.0.0.1', function() {
|
||||
console.log('LDAP server listening at: ' + server.url);
|
||||
});
|
||||
```
|
||||
server.listen(389, '127.0.0.1', function() {
|
||||
console.log('LDAP server listening at: ' + server.url);
|
||||
});
|
||||
|
||||
|
||||
### Port and Host
|
||||
`listen(port, [host], [callback])`
|
||||
|
||||
Begin accepting connections on the specified port and host. If the host is
|
||||
omitted, the server will accept connections directed to the IPv4 address
|
||||
`127.0.0.1`. To listen on any other address, supply said address as the `host`
|
||||
parameter. For example, to listen on all available IPv6 addresses supply
|
||||
`::` as the `host` (note, this _may_ also result in listening on all
|
||||
available IPv4 addresses, depending on operating system behavior).
|
||||
|
||||
We highly recommend being as explicit as possible with the `host` parameter.
|
||||
Listening on all available addresses (through `::` or `0.0.0.0`) can lead
|
||||
to potential security issues.
|
||||
omitted, the server will accept connections directed to any IPv4 address
|
||||
(INADDR\_ANY).
|
||||
|
||||
This function is asynchronous. The last parameter callback will be called when
|
||||
the server has been bound.
|
||||
|
@ -125,16 +112,14 @@ paradigm of programming. Essentially every method is of the form
|
|||
handlers together by calling `next()` and ordering your functions in the
|
||||
definition of the route. For example:
|
||||
|
||||
```js
|
||||
function authorize(req, res, next) {
|
||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||
return next(new ldap.InsufficientAccessRightsError());
|
||||
function authorize(req, res, next) {
|
||||
if (!req.connection.ldap.bindDN.equals('cn=root'))
|
||||
return next(new ldap.InsufficientAccessRightsError());
|
||||
|
||||
return next();
|
||||
}
|
||||
return next();
|
||||
}
|
||||
|
||||
server.search('o=example', authorize, function(req, res, next) { ... });
|
||||
```
|
||||
server.search('o=example', authorize, function(req, res, next) { ... });
|
||||
|
||||
Note that ldapjs is also slightly different, since it's often going to be backed
|
||||
to a DB-like entity, in that it also has an API where you can pass in a
|
||||
|
@ -146,25 +131,23 @@ complete implementation of the LDAP protocol over
|
|||
[Riak](https://github.com/basho/riak). Getting an LDAP server up with riak
|
||||
looks like:
|
||||
|
||||
```js
|
||||
const ldap = require('ldapjs');
|
||||
const ldapRiak = require('ldapjs-riak');
|
||||
var ldap = require('ldapjs');
|
||||
var ldapRiak = require('ldapjs-riak');
|
||||
|
||||
const server = ldap.createServer();
|
||||
const backend = ldapRiak.createBackend({
|
||||
"host": "localhost",
|
||||
"port": 8098,
|
||||
"bucket": "example",
|
||||
"indexes": ["l", "cn"],
|
||||
"uniqueIndexes": ["uid"],
|
||||
"numConnections": 5
|
||||
});
|
||||
var server = ldap.createServer();
|
||||
var backend = ldapRiak.createBackend({
|
||||
"host": "localhost",
|
||||
"port": 8098,
|
||||
"bucket": "example",
|
||||
"indexes": ["l", "cn"],
|
||||
"uniqueIndexes": ["uid"],
|
||||
"numConnections": 5
|
||||
});
|
||||
|
||||
server.add("o=example",
|
||||
backend,
|
||||
backend.add());
|
||||
...
|
||||
```
|
||||
server.add("o=example",
|
||||
backend,
|
||||
backend.add());
|
||||
...
|
||||
|
||||
The first parameter to an ldapjs route is always the point in the
|
||||
tree to mount the handler chain at. The second argument is _optionally_ a
|
||||
|
@ -177,12 +160,10 @@ operation requires specific methods/fields on the request/response
|
|||
objects. However, there is a `.use()` method availabe, similar to
|
||||
that on express/connect, allowing you to chain up "middleware":
|
||||
|
||||
```js
|
||||
server.use(function(req, res, next) {
|
||||
console.log('hello world');
|
||||
return next();
|
||||
});
|
||||
```
|
||||
server.use(function(req, res, next) {
|
||||
console.log('hello world');
|
||||
return next();
|
||||
});
|
||||
|
||||
## Common Request Elements
|
||||
|
||||
|
@ -204,7 +185,7 @@ authenticated as on this connection. If the client didn't bind, then a DN object
|
|||
will be there defaulted to `cn=anonymous`.
|
||||
|
||||
Additionally, request will have a `logId` parameter you can use to uniquely
|
||||
identify the request/connection pair in logs (includes the LDAP messageId).
|
||||
identify the request/connection pair in logs (includes the LDAP messageID).
|
||||
|
||||
## Common Response Elements
|
||||
|
||||
|
@ -226,13 +207,11 @@ the paradigm is something defined like CONSTRAINT\_VIOLATION in the RFC would be
|
|||
`ConstraintViolationError` in ldapjs. Upon calling `next(new LDAPError())`,
|
||||
ldapjs will _stop_ calling your handler chain. For example:
|
||||
|
||||
```js
|
||||
server.search('o=example',
|
||||
(req, res, next) => { return next(); },
|
||||
(req, res, next) => { return next(new ldap.OperationsError()); },
|
||||
(req, res, next) => { res.end(); }
|
||||
);
|
||||
```
|
||||
server.search('o=example',
|
||||
function(req, res, next) { return next(); },
|
||||
function(req, res, next) { return next(new ldap.OperationsError()); },
|
||||
function(req, res, next) { res.end(); }
|
||||
);
|
||||
|
||||
In the code snipped above, the third handler would never get invoked.
|
||||
|
||||
|
@ -240,13 +219,11 @@ In the code snipped above, the third handler would never get invoked.
|
|||
|
||||
Adds a mount in the tree to perform LDAP binds with. Example:
|
||||
|
||||
```js
|
||||
server.bind('ou=people, o=example', (req, res, next) => {
|
||||
console.log('bind DN: ' + req.dn.toString());
|
||||
console.log('bind PW: ' + req.credentials);
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
server.bind('ou=people, o=example', function(req, res, next) {
|
||||
console.log('bind DN: ' + req.dn.toString());
|
||||
console.log('bind PW: ' + req.credentials);
|
||||
res.end();
|
||||
});
|
||||
|
||||
## BindRequest
|
||||
|
||||
|
@ -279,13 +256,11 @@ No extra methods above an `LDAPResult` API call.
|
|||
|
||||
Adds a mount in the tree to perform LDAP adds with.
|
||||
|
||||
```js
|
||||
server.add('ou=people, o=example', (req, res, next) => {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('Entry attributes: ' + req.toObject().attributes);
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
server.add('ou=people, o=example', function(req, res, next) {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('Entry attributes: ' + req.toObject().attributes);
|
||||
res.end();
|
||||
});
|
||||
|
||||
## AddRequest
|
||||
|
||||
|
@ -309,16 +284,14 @@ a standard JavaScript object.
|
|||
This operation will return a plain JavaScript object from the request that looks
|
||||
like:
|
||||
|
||||
```js
|
||||
{
|
||||
dn: 'cn=foo, o=example', // string, not DN object
|
||||
attributes: {
|
||||
cn: ['foo'],
|
||||
sn: ['bar'],
|
||||
objectclass: ['person', 'top']
|
||||
}
|
||||
}
|
||||
```
|
||||
{
|
||||
dn: 'cn=foo, o=example', // string, not DN object
|
||||
attributes: {
|
||||
cn: ['foo'],
|
||||
sn: ['bar'],
|
||||
objectclass: ['person', 'top']
|
||||
}
|
||||
}
|
||||
|
||||
## AddResponse
|
||||
|
||||
|
@ -328,14 +301,12 @@ No extra methods above an `LDAPResult` API call.
|
|||
|
||||
Adds a handler for the LDAP search operation.
|
||||
|
||||
```js
|
||||
server.search('o=example', (req, res, next) => {
|
||||
console.log('base object: ' + req.dn.toString());
|
||||
console.log('scope: ' + req.scope);
|
||||
console.log('filter: ' + req.filter.toString());
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
server.search('o=example', function(req, res, next) {
|
||||
console.log('base object: ' + req.dn.toString());
|
||||
console.log('scope: ' + req.scope);
|
||||
console.log('filter: ' + req.filter.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
## SearchRequest
|
||||
|
||||
|
@ -393,38 +364,34 @@ explicitly pass in a `SearchEntry` object, and can instead just send a plain
|
|||
JavaScript object that matches the format used from `AddRequest.toObject()`.
|
||||
|
||||
|
||||
```js
|
||||
server.search('o=example', (req, res, next) => {
|
||||
const obj = {
|
||||
dn: 'o=example',
|
||||
attributes: {
|
||||
objectclass: ['top', 'organization'],
|
||||
o: ['example']
|
||||
}
|
||||
};
|
||||
server.search('o=example', function(req, res, next) {
|
||||
var obj = {
|
||||
dn: 'o=example',
|
||||
attributes: {
|
||||
objectclass: ['top', 'organization'],
|
||||
o: ['example']
|
||||
}
|
||||
};
|
||||
|
||||
if (req.filter.matches(obj))
|
||||
res.send(obj)
|
||||
if (req.filter.matches(obj))
|
||||
res.send(obj)
|
||||
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
res.end();
|
||||
});
|
||||
|
||||
# modify
|
||||
|
||||
Allows you to handle an LDAP modify operation.
|
||||
|
||||
```js
|
||||
server.modify('o=example', (req, res, next) => {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('changes:');
|
||||
for (const c of req.changes) {
|
||||
console.log(' operation: ' + c.operation);
|
||||
console.log(' modification: ' + c.modification.toString());
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
server.modify('o=example', function(req, res, next) {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('changes:');
|
||||
req.changes.forEach(function(c) {
|
||||
console.log(' operation: ' + c.operation);
|
||||
console.log(' modification: ' + c.modification.toString());
|
||||
});
|
||||
res.end();
|
||||
});
|
||||
|
||||
## ModifyRequest
|
||||
|
||||
|
@ -461,12 +428,10 @@ No extra methods above an `LDAPResult` API call.
|
|||
|
||||
Allows you to handle an LDAP delete operation.
|
||||
|
||||
```js
|
||||
server.del('o=example', (req, res, next) => {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
server.del('o=example', function(req, res, next) {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
## DeleteRequest
|
||||
|
||||
|
@ -483,14 +448,12 @@ No extra methods above an `LDAPResult` API call.
|
|||
|
||||
Allows you to handle an LDAP compare operation.
|
||||
|
||||
```js
|
||||
server.compare('o=example', (req, res, next) => {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('attribute name: ' + req.attribute);
|
||||
console.log('attribute value: ' + req.value);
|
||||
res.end(req.value === 'foo');
|
||||
});
|
||||
```
|
||||
server.compare('o=example', function(req, res, next) {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('attribute name: ' + req.attribute);
|
||||
console.log('attribute value: ' + req.value);
|
||||
res.end(req.value === 'foo');
|
||||
});
|
||||
|
||||
## CompareRequest
|
||||
|
||||
|
@ -517,17 +480,15 @@ that, there are no extra methods above an `LDAPResult` API call.
|
|||
|
||||
Allows you to handle an LDAP modifyDN operation.
|
||||
|
||||
```js
|
||||
server.modifyDN('o=example', (req, res, next) => {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('new RDN: ' + req.newRdn.toString());
|
||||
console.log('deleteOldRDN: ' + req.deleteOldRdn);
|
||||
console.log('new superior: ' +
|
||||
(req.newSuperior ? req.newSuperior.toString() : ''));
|
||||
server.modifyDN('o=example', function(req, res, next) {
|
||||
console.log('DN: ' + req.dn.toString());
|
||||
console.log('new RDN: ' + req.newRdn.toString());
|
||||
console.log('deleteOldRDN: ' + req.deleteOldRdn);
|
||||
console.log('new superior: ' +
|
||||
(req.newSuperior ? req.newSuperior.toString() : ''));
|
||||
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
res.end();
|
||||
});
|
||||
|
||||
## ModifyDNRequest
|
||||
|
||||
|
@ -561,16 +522,14 @@ OID, but ldapjs makes no such restrictions; it just needs to be a string.
|
|||
Unlike the other operations, extended operations don't map to any location in
|
||||
the tree, so routing here will be exact match, as opposed to subtree.
|
||||
|
||||
```js
|
||||
// LDAP whoami
|
||||
server.exop('1.3.6.1.4.1.4203.1.11.3', (req, res, next) => {
|
||||
console.log('name: ' + req.name);
|
||||
console.log('value: ' + req.value);
|
||||
res.value = 'u:xxyyz@EXAMPLE.NET';
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
```
|
||||
// LDAP whoami
|
||||
server.exop('1.3.6.1.4.1.4203.1.11.3', function(req, res, next) {
|
||||
console.log('name: ' + req.name);
|
||||
console.log('value: ' + req.value);
|
||||
res.value = 'u:xxyyz@EXAMPLE.NET';
|
||||
res.end();
|
||||
return next();
|
||||
});
|
||||
|
||||
## ExtendedRequest
|
||||
|
||||
|
@ -601,11 +560,9 @@ and cleans up any internals (in ldapjs core). You can override this handler
|
|||
if you need to clean up any items in your backend, or perform any other cleanup
|
||||
tasks you need to.
|
||||
|
||||
```js
|
||||
server.unbind((req, res, next) => {
|
||||
res.end();
|
||||
});
|
||||
```
|
||||
server.unbind(function(req, res, next) {
|
||||
res.end();
|
||||
});
|
||||
|
||||
Note that the LDAP unbind operation actually doesn't send any response (by
|
||||
definition in the RFC), so the UnbindResponse is really just a stub that
|
||||
|
|
|
@ -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':
|
||||
if (req.filter.matches(db[dn])) {
|
||||
res.send({
|
||||
dn,
|
||||
dn: 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 VError = require('verror').VError
|
||||
|
||||
const Attribute = require('@ldapjs/attribute')
|
||||
const Change = require('@ldapjs/change')
|
||||
const Attribute = require('../attribute')
|
||||
const Change = require('../change')
|
||||
const Control = require('../controls/index').Control
|
||||
const { Control: LdapControl } = require('@ldapjs/controls')
|
||||
const SearchPager = require('./search_pager')
|
||||
const Protocol = require('@ldapjs/protocol')
|
||||
const { DN } = require('@ldapjs/dn')
|
||||
const Protocol = require('../protocol')
|
||||
const dn = require('../dn')
|
||||
const errors = require('../errors')
|
||||
const filters = require('@ldapjs/filter')
|
||||
const Parser = require('../messages/parser')
|
||||
const filters = require('../filters')
|
||||
const messages = require('../messages')
|
||||
const url = require('../url')
|
||||
const CorkedEmitter = require('../corked_emitter')
|
||||
|
||||
/// --- Globals
|
||||
|
||||
const messages = require('@ldapjs/messages')
|
||||
const {
|
||||
AbandonRequest,
|
||||
AddRequest,
|
||||
BindRequest,
|
||||
CompareRequest,
|
||||
DeleteRequest,
|
||||
ExtensionRequest: ExtendedRequest,
|
||||
ModifyRequest,
|
||||
ModifyDnRequest: ModifyDNRequest,
|
||||
SearchRequest,
|
||||
UnbindRequest,
|
||||
LdapResult: LDAPResult,
|
||||
SearchResultEntry: SearchEntry,
|
||||
SearchResultReference: SearchReference
|
||||
} = messages
|
||||
const AbandonRequest = messages.AbandonRequest
|
||||
const AddRequest = messages.AddRequest
|
||||
const BindRequest = messages.BindRequest
|
||||
const CompareRequest = messages.CompareRequest
|
||||
const DeleteRequest = messages.DeleteRequest
|
||||
const ExtendedRequest = messages.ExtendedRequest
|
||||
const ModifyRequest = messages.ModifyRequest
|
||||
const ModifyDNRequest = messages.ModifyDNRequest
|
||||
const SearchRequest = messages.SearchRequest
|
||||
const UnbindRequest = messages.UnbindRequest
|
||||
const UnbindResponse = messages.UnbindResponse
|
||||
|
||||
const LDAPResult = messages.LDAPResult
|
||||
const SearchEntry = messages.SearchEntry
|
||||
const SearchReference = messages.SearchReference
|
||||
// var SearchResponse = messages.SearchResponse
|
||||
const Parser = messages.Parser
|
||||
|
||||
const PresenceFilter = filters.PresenceFilter
|
||||
|
||||
|
@ -67,9 +67,9 @@ function nextClientId () {
|
|||
function validateControls (controls) {
|
||||
if (Array.isArray(controls)) {
|
||||
controls.forEach(function (c) {
|
||||
if (!(c instanceof Control) && !(c instanceof LdapControl)) { throw new TypeError('controls must be [Control]') }
|
||||
if (!(c instanceof Control)) { throw new TypeError('controls must be [Control]') }
|
||||
})
|
||||
} else if (controls instanceof Control || controls instanceof LdapControl) {
|
||||
} else if (controls instanceof Control) {
|
||||
controls = [controls]
|
||||
} else {
|
||||
throw new TypeError('controls must be [Control]')
|
||||
|
@ -78,11 +78,13 @@ function validateControls (controls) {
|
|||
return controls
|
||||
}
|
||||
|
||||
function ensureDN (input) {
|
||||
if (DN.isDn(input)) {
|
||||
return input
|
||||
function ensureDN (input, strict) {
|
||||
if (dn.DN.isDN(input)) {
|
||||
return dn
|
||||
} else if (strict) {
|
||||
return dn.parse(input)
|
||||
} else if (typeof (input) === 'string') {
|
||||
return DN.fromString(input)
|
||||
return input
|
||||
} else {
|
||||
throw new Error('invalid DN')
|
||||
}
|
||||
|
@ -134,6 +136,7 @@ function Client (options) {
|
|||
failAfter: parseInt(rOpts.failAfter, 10) || Infinity
|
||||
}
|
||||
}
|
||||
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
|
||||
|
||||
this.queue = requestQueueFactory({
|
||||
size: parseInt((options.queueSize || 0), 10),
|
||||
|
@ -174,13 +177,13 @@ module.exports = Client
|
|||
* The callback will be invoked as soon as the data is flushed out to the
|
||||
* network, as there is never a response from abandon.
|
||||
*
|
||||
* @param {Number} messageId the messageId to abandon.
|
||||
* @param {Number} messageID the messageID to abandon.
|
||||
* @param {Control} controls (optional) either a Control or [Control].
|
||||
* @param {Function} callback of the form f(err).
|
||||
* @throws {TypeError} on invalid input.
|
||||
*/
|
||||
Client.prototype.abandon = function abandon (messageId, controls, callback) {
|
||||
assert.number(messageId, 'messageId')
|
||||
Client.prototype.abandon = function abandon (messageID, controls, callback) {
|
||||
assert.number(messageID, 'messageID')
|
||||
if (typeof (controls) === 'function') {
|
||||
callback = controls
|
||||
controls = []
|
||||
|
@ -190,8 +193,8 @@ Client.prototype.abandon = function abandon (messageId, controls, callback) {
|
|||
assert.func(callback, 'callback')
|
||||
|
||||
const req = new AbandonRequest({
|
||||
abandonId: messageId,
|
||||
controls
|
||||
abandonID: messageID,
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req, 'abandon', null, callback)
|
||||
|
@ -235,8 +238,6 @@ Client.prototype.add = function add (name, entry, controls, callback) {
|
|||
save[k].forEach(function (v) {
|
||||
attr.addValue(v.toString())
|
||||
})
|
||||
} else if (Buffer.isBuffer(save[k])) {
|
||||
attr.addValue(save[k])
|
||||
} else {
|
||||
attr.addValue(save[k].toString())
|
||||
}
|
||||
|
@ -245,9 +246,9 @@ Client.prototype.add = function add (name, entry, controls, callback) {
|
|||
}
|
||||
|
||||
const req = new AddRequest({
|
||||
entry: ensureDN(name),
|
||||
entry: ensureDN(name, this.strictDN),
|
||||
attributes: entry,
|
||||
controls
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
||||
|
@ -267,12 +268,7 @@ Client.prototype.bind = function bind (name,
|
|||
controls,
|
||||
callback,
|
||||
_bypass) {
|
||||
if (
|
||||
typeof (name) !== 'string' &&
|
||||
Object.prototype.toString.call(name) !== '[object LdapDn]'
|
||||
) {
|
||||
throw new TypeError('name (string) required')
|
||||
}
|
||||
if (typeof (name) !== 'string' && !(name instanceof dn.DN)) { throw new TypeError('name (string) required') }
|
||||
assert.optionalString(credentials, 'credentials')
|
||||
if (typeof (controls) === 'function') {
|
||||
callback = controls
|
||||
|
@ -286,7 +282,7 @@ Client.prototype.bind = function bind (name,
|
|||
name: name || '',
|
||||
authentication: 'Simple',
|
||||
credentials: credentials || '',
|
||||
controls
|
||||
controls: controls
|
||||
})
|
||||
|
||||
// Connection errors will be reported to the bind callback too (useful when the LDAP server is not available)
|
||||
|
@ -327,10 +323,10 @@ Client.prototype.compare = function compare (name,
|
|||
assert.func(callback, 'callback')
|
||||
|
||||
const req = new CompareRequest({
|
||||
entry: ensureDN(name),
|
||||
entry: ensureDN(name, this.strictDN),
|
||||
attribute: attr,
|
||||
value,
|
||||
controls
|
||||
value: value,
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req, CMP_EXPECT, null, function (err, res) {
|
||||
|
@ -359,8 +355,8 @@ Client.prototype.del = function del (name, controls, callback) {
|
|||
assert.func(callback, 'callback')
|
||||
|
||||
const req = new DeleteRequest({
|
||||
entry: ensureDN(name),
|
||||
controls
|
||||
entry: ensureDN(name, this.strictDN),
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
||||
|
@ -397,7 +393,7 @@ Client.prototype.exop = function exop (name, value, controls, callback) {
|
|||
const req = new ExtendedRequest({
|
||||
requestName: name,
|
||||
requestValue: value,
|
||||
controls
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req, [errors.LDAP_SUCCESS], null, function (err, res) {
|
||||
|
@ -422,25 +418,25 @@ Client.prototype.modify = function modify (name, change, controls, callback) {
|
|||
|
||||
const changes = []
|
||||
|
||||
function changeFromObject (obj) {
|
||||
if (!obj.operation && !obj.type) { throw new Error('change.operation required') }
|
||||
if (typeof (obj.modification) !== 'object') { throw new Error('change.modification (object) required') }
|
||||
function changeFromObject (change) {
|
||||
if (!change.operation && !change.type) { throw new Error('change.operation required') }
|
||||
if (typeof (change.modification) !== 'object') { throw new Error('change.modification (object) required') }
|
||||
|
||||
if (Object.keys(obj.modification).length === 2 &&
|
||||
typeof (obj.modification.type) === 'string' &&
|
||||
Array.isArray(obj.modification.vals)) {
|
||||
if (Object.keys(change.modification).length === 2 &&
|
||||
typeof (change.modification.type) === 'string' &&
|
||||
Array.isArray(change.modification.vals)) {
|
||||
// Use modification directly if it's already normalized:
|
||||
changes.push(new Change({
|
||||
operation: obj.operation || obj.type,
|
||||
modification: obj.modification
|
||||
operation: change.operation || change.type,
|
||||
modification: change.modification
|
||||
}))
|
||||
} else {
|
||||
// Normalize the modification object
|
||||
Object.keys(obj.modification).forEach(function (k) {
|
||||
Object.keys(change.modification).forEach(function (k) {
|
||||
const mod = {}
|
||||
mod[k] = obj.modification[k]
|
||||
mod[k] = change.modification[k]
|
||||
changes.push(new Change({
|
||||
operation: obj.operation || obj.type,
|
||||
operation: change.operation || change.type,
|
||||
modification: mod
|
||||
}))
|
||||
})
|
||||
|
@ -470,9 +466,9 @@ Client.prototype.modify = function modify (name, change, controls, callback) {
|
|||
assert.func(callback, 'callback')
|
||||
|
||||
const req = new ModifyRequest({
|
||||
object: ensureDN(name),
|
||||
changes,
|
||||
controls
|
||||
object: ensureDN(name, this.strictDN),
|
||||
changes: changes,
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req, [errors.LDAP_SUCCESS], null, callback)
|
||||
|
@ -506,16 +502,18 @@ Client.prototype.modifyDN = function modifyDN (name,
|
|||
}
|
||||
assert.func(callback)
|
||||
|
||||
const newDN = DN.fromString(newName)
|
||||
const DN = ensureDN(name)
|
||||
// TODO: is non-strict handling desired here?
|
||||
const newDN = dn.parse(newName)
|
||||
|
||||
const req = new ModifyDNRequest({
|
||||
entry: DN.fromString(name),
|
||||
entry: DN,
|
||||
deleteOldRdn: true,
|
||||
controls
|
||||
controls: controls
|
||||
})
|
||||
|
||||
if (newDN.length !== 1) {
|
||||
req.newRdn = DN.fromString(newDN.shift().toString())
|
||||
req.newRdn = dn.parse(newDN.rdns.shift().toString())
|
||||
req.newSuperior = newDN
|
||||
} else {
|
||||
req.newRdn = newDN
|
||||
|
@ -571,7 +569,7 @@ Client.prototype.search = function search (base,
|
|||
options.filter = filters.parseString(options.filter)
|
||||
} else if (!options.filter) {
|
||||
options.filter = new PresenceFilter({ attribute: 'objectclass' })
|
||||
} else if (Object.prototype.toString.call(options.filter) !== '[object FilterString]') {
|
||||
} else if (!filters.isFilter(options.filter)) {
|
||||
throw new TypeError('options.filter (Filter) required')
|
||||
}
|
||||
if (typeof (controls) === 'function') {
|
||||
|
@ -593,14 +591,14 @@ Client.prototype.search = function search (base,
|
|||
}
|
||||
|
||||
const self = this
|
||||
const baseDN = ensureDN(base)
|
||||
const baseDN = ensureDN(base, this.strictDN)
|
||||
|
||||
function sendRequest (ctrls, emitter, cb) {
|
||||
const req = new SearchRequest({
|
||||
baseObject: baseDN,
|
||||
scope: options.scope || 'base',
|
||||
filter: options.filter,
|
||||
derefAliases: options.derefAliases || Protocol.search.NEVER_DEREF_ALIASES,
|
||||
derefAliases: options.derefAliases || Protocol.NEVER_DEREF_ALIASES,
|
||||
sizeLimit: options.sizeLimit || 0,
|
||||
timeLimit: options.timeLimit || 10,
|
||||
typesOnly: options.typesOnly || false,
|
||||
|
@ -629,12 +627,12 @@ Client.prototype.search = function search (base,
|
|||
}
|
||||
|
||||
const pager = new SearchPager({
|
||||
callback,
|
||||
controls,
|
||||
callback: callback,
|
||||
controls: controls,
|
||||
pageSize: size,
|
||||
pagePause: pageOpts.pagePause,
|
||||
sendRequest
|
||||
pagePause: pageOpts.pagePause
|
||||
})
|
||||
pager.on('search', sendRequest)
|
||||
pager.begin()
|
||||
} else {
|
||||
sendRequest(controls, new CorkedEmitter(), callback)
|
||||
|
@ -681,9 +679,9 @@ Client.prototype.starttls = function starttls (options,
|
|||
return callback(new Error('STARTTLS already in progress or active'))
|
||||
}
|
||||
|
||||
function onSend (sendErr, emitter) {
|
||||
if (sendErr) {
|
||||
callback(sendErr)
|
||||
function onSend (err, emitter) {
|
||||
if (err) {
|
||||
callback(err)
|
||||
return
|
||||
}
|
||||
/*
|
||||
|
@ -699,7 +697,7 @@ Client.prototype.starttls = function starttls (options,
|
|||
self._starttls = null
|
||||
callback(err)
|
||||
})
|
||||
emitter.on('end', function (_res) {
|
||||
emitter.on('end', function (res) {
|
||||
const sock = self._socket
|
||||
/*
|
||||
* Unplumb socket data during SSL negotiation.
|
||||
|
@ -723,7 +721,7 @@ Client.prototype.starttls = function starttls (options,
|
|||
self._tracker.parser.write(data)
|
||||
})
|
||||
secure.on('error', function (err) {
|
||||
self.log.trace({ err }, 'error event: %s', new Error().stack)
|
||||
self.log.trace({ err: err }, 'error event: %s', new Error().stack)
|
||||
|
||||
self.emit('error', err)
|
||||
sock.destroy()
|
||||
|
@ -744,7 +742,7 @@ Client.prototype.starttls = function starttls (options,
|
|||
const req = new ExtendedRequest({
|
||||
requestName: '1.3.6.1.4.1.1466.20037',
|
||||
requestValue: null,
|
||||
controls
|
||||
controls: controls
|
||||
})
|
||||
|
||||
return this._send(req,
|
||||
|
@ -776,11 +774,9 @@ Client.prototype.destroy = function destroy (err) {
|
|||
})
|
||||
if (this.connected) {
|
||||
this.unbind()
|
||||
}
|
||||
if (this._socket) {
|
||||
} else if (this._socket) {
|
||||
this._socket.destroy()
|
||||
}
|
||||
|
||||
this.emit('destroy', err)
|
||||
}
|
||||
|
||||
|
@ -854,10 +850,10 @@ Client.prototype.connect = function connect () {
|
|||
}
|
||||
|
||||
// Initialize socket events and LDAP parser.
|
||||
function initSocket (server) {
|
||||
function initSocket (url) {
|
||||
tracker = messageTrackerFactory({
|
||||
id: server ? server.href : self.socketPath,
|
||||
parser: new Parser({ log })
|
||||
id: url ? url.href : self.socketPath,
|
||||
parser: new Parser({ log: log })
|
||||
})
|
||||
|
||||
// This won't be set on TLS. So. Very. Annoying.
|
||||
|
@ -876,52 +872,15 @@ Client.prototype.connect = function connect () {
|
|||
})
|
||||
|
||||
// The "router"
|
||||
//
|
||||
// This is invoked after the incoming BER has been parsed into a JavaScript
|
||||
// object.
|
||||
tracker.parser.on('message', function onMessage (message) {
|
||||
message.connection = self._socket
|
||||
const trackedObject = tracker.fetch(message.messageId)
|
||||
if (!trackedObject) {
|
||||
log.error({ message: message.pojo }, 'unmatched server message received')
|
||||
return false
|
||||
}
|
||||
|
||||
const { message: trackedMessage, callback } = trackedObject
|
||||
const callback = tracker.fetch(message.messageID)
|
||||
|
||||
if (!callback) {
|
||||
log.error({ message: message.pojo }, 'unsolicited message')
|
||||
log.error({ message: message.json }, 'unsolicited message')
|
||||
return false
|
||||
}
|
||||
|
||||
// Some message types have narrower implementations and require extra
|
||||
// parsing to be complete. In particular, ExtensionRequest messages will
|
||||
// return responses that do not identify the request that generated them.
|
||||
// Therefore, we have to match the response to the request and handle
|
||||
// the extra processing accordingly.
|
||||
switch (trackedMessage.type) {
|
||||
case 'ExtensionRequest': {
|
||||
const extensionType = ExtendedRequest.recognizedOIDs().lookupName(trackedMessage.requestName)
|
||||
switch (extensionType) {
|
||||
case 'PASSWORD_MODIFY': {
|
||||
message = messages.PasswordModifyResponse.fromResponse(message)
|
||||
break
|
||||
}
|
||||
|
||||
case 'WHO_AM_I': {
|
||||
message = messages.WhoAmIResponse.fromResponse(message)
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
return callback(message)
|
||||
})
|
||||
|
||||
|
@ -979,7 +938,7 @@ Client.prototype.connect = function connect () {
|
|||
f(basicClient, callback)
|
||||
},
|
||||
inputs: self.listeners('setup')
|
||||
}, function (err, _res) {
|
||||
}, function (err, res) {
|
||||
if (err) {
|
||||
self.emit('setupError', err)
|
||||
}
|
||||
|
@ -1002,7 +961,7 @@ Client.prototype.connect = function connect () {
|
|||
socket.end()
|
||||
})
|
||||
socket.on('error', function onSocketError (err) {
|
||||
log.trace({ err }, 'error event: %s', new Error().stack)
|
||||
log.trace({ err: err }, 'error event: %s', new Error().stack)
|
||||
|
||||
self.emit('error', err)
|
||||
socket.destroy()
|
||||
|
@ -1042,7 +1001,7 @@ Client.prototype.connect = function connect () {
|
|||
}
|
||||
retry.failAfter(failAfter)
|
||||
|
||||
retry.on('ready', function (num, _delay) {
|
||||
retry.on('ready', function (num, delay) {
|
||||
if (self.destroyed) {
|
||||
// Cease connection attempts if destroyed
|
||||
return
|
||||
|
@ -1070,9 +1029,9 @@ Client.prototype.connect = function connect () {
|
|||
self.log.debug('failed to connect after %d attempts', failAfter)
|
||||
// Communicate the last-encountered error
|
||||
if (err instanceof ConnectionError) {
|
||||
self.emitError('connectTimeout', err)
|
||||
self.emit('connectTimeout', err)
|
||||
} else if (err.code === 'ECONNREFUSED') {
|
||||
self.emitError('connectRefused', err)
|
||||
self.emit('connectRefused', err)
|
||||
} else {
|
||||
self.emit('error', err)
|
||||
}
|
||||
|
@ -1120,16 +1079,8 @@ Client.prototype._onClose = function _onClose (closeError) {
|
|||
return cb(new ConnectionError(tracker.id + ' closed'))
|
||||
} else {
|
||||
// Unbinds will be communicated as a success since we're closed
|
||||
// TODO: we are faking this "UnbindResponse" object in order to make
|
||||
// tests pass. There is no such thing as an "unbind response" in the LDAP
|
||||
// protocol. When the client is revamped, this logic should be removed.
|
||||
// ~ jsumners 2023-02-16
|
||||
const Unbind = class extends LDAPResult {
|
||||
messageID = msgid
|
||||
messageId = msgid
|
||||
status = 'unbind'
|
||||
}
|
||||
const unbind = new Unbind()
|
||||
const unbind = new UnbindResponse({ messageID: msgid })
|
||||
unbind.status = 'unbind'
|
||||
return cb(unbind)
|
||||
}
|
||||
})
|
||||
|
@ -1247,23 +1198,21 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
|||
function messageCallback (msg) {
|
||||
if (timer) { clearTimeout(timer) }
|
||||
|
||||
log.trace({ msg: msg ? msg.pojo : null }, 'response received')
|
||||
log.trace({ msg: msg ? msg.json : null }, 'response received')
|
||||
|
||||
if (expect === 'abandon') { return sendResult('end', null) }
|
||||
|
||||
if (msg instanceof SearchEntry || msg instanceof SearchReference) {
|
||||
let event = msg.constructor.name
|
||||
// Generate the event name for the event emitter, i.e. "searchEntry"
|
||||
// and "searchReference".
|
||||
event = (event[0].toLowerCase() + event.slice(1)).replaceAll('Result', '')
|
||||
event = event[0].toLowerCase() + event.slice(1)
|
||||
return sendResult(event, msg)
|
||||
} else {
|
||||
tracker.remove(message.messageId)
|
||||
tracker.remove(message.messageID)
|
||||
// Potentially mark client as idle
|
||||
self._updateIdle()
|
||||
|
||||
if (msg instanceof LDAPResult) {
|
||||
if (msg.status !== 0 && expect.indexOf(msg.status) === -1) {
|
||||
if (expect.indexOf(msg.status) === -1) {
|
||||
return sendResult('error', errors.getError(msg))
|
||||
}
|
||||
return sendResult('end', msg)
|
||||
|
@ -1277,7 +1226,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
|||
|
||||
function onRequestTimeout () {
|
||||
self.emit('timeout', message)
|
||||
const { callback: cb } = tracker.fetch(message.messageId)
|
||||
const cb = tracker.fetch(message.messageID)
|
||||
if (cb) {
|
||||
// FIXME: the timed-out request should be abandoned
|
||||
cb(new errors.TimeoutError('request timeout (client interrupt)'))
|
||||
|
@ -1286,8 +1235,8 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
|||
|
||||
function writeCallback () {
|
||||
if (expect === 'abandon') {
|
||||
// Mark the messageId specified as abandoned
|
||||
tracker.abandon(message.abandonId)
|
||||
// Mark the messageID specified as abandoned
|
||||
tracker.abandon(message.abandonID)
|
||||
// No need to track the abandon request itself
|
||||
tracker.remove(message.id)
|
||||
return callback(null)
|
||||
|
@ -1302,9 +1251,7 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
|||
conn.end()
|
||||
} else if (emitter) {
|
||||
sentEmitter = true
|
||||
callback(null, emitter)
|
||||
emitter.emit('searchRequest', message)
|
||||
return
|
||||
return callback(null, emitter)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -1319,11 +1266,10 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
|||
timer = setTimeout(onRequestTimeout, self.timeout)
|
||||
}
|
||||
|
||||
log.trace('sending request %j', message.pojo)
|
||||
log.trace('sending request %j', message.json)
|
||||
|
||||
try {
|
||||
const messageBer = message.toBer()
|
||||
return conn.write(messageBer.buffer, writeCallback)
|
||||
return conn.write(message.toBer(), writeCallback)
|
||||
} catch (e) {
|
||||
if (timer) { clearTimeout(timer) }
|
||||
|
||||
|
@ -1331,15 +1277,3 @@ Client.prototype._sendSocket = function _sendSocket (message,
|
|||
return callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
Client.prototype.emitError = function emitError (event, err) {
|
||||
if (event !== 'error' && err && this.listenerCount(event) === 0) {
|
||||
if (typeof err === 'string') {
|
||||
err = event + ': ' + err
|
||||
} else if (err.message) {
|
||||
err.message = event + ': ' + err.message
|
||||
}
|
||||
this.emit('error', err)
|
||||
}
|
||||
this.emit(event, err)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ const logger = require('../logger')
|
|||
const Client = require('./client')
|
||||
|
||||
module.exports = {
|
||||
Client,
|
||||
Client: Client,
|
||||
createClient: function createClient (options) {
|
||||
if (isObject(options) === false) throw TypeError('options (object) required')
|
||||
if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')
|
||||
|
|
|
@ -16,8 +16,8 @@ const { MAX_MSGID } = require('../constants')
|
|||
module.exports = function idGeneratorFactory (start = 0) {
|
||||
let currentID = start
|
||||
return function nextID () {
|
||||
const id = currentID + 1
|
||||
currentID = (id >= MAX_MSGID) ? 1 : id
|
||||
const nextID = currentID + 1
|
||||
currentID = (nextID >= MAX_MSGID) ? 1 : nextID
|
||||
return currentID
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,23 +62,13 @@ module.exports = function messageTrackerFactory (options) {
|
|||
*/
|
||||
tracker.abandon = function abandonMessage (msgID) {
|
||||
if (messages.has(msgID) === false) return false
|
||||
const toAbandon = messages.get(msgID)
|
||||
abandoned.set(msgID, {
|
||||
age: currentID,
|
||||
message: toAbandon.message,
|
||||
cb: toAbandon.callback
|
||||
cb: messages.get(msgID)
|
||||
})
|
||||
return messages.delete(msgID)
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Tracked
|
||||
* @property {object} message The tracked message. Usually the outgoing
|
||||
* request object.
|
||||
* @property {Function} callback The handler to use when receiving a
|
||||
* response to the tracked message.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieves the message handler for a message. Removes abandoned messages
|
||||
* that have been given time to be resolved.
|
||||
|
@ -89,10 +79,10 @@ module.exports = function messageTrackerFactory (options) {
|
|||
* @method fetch
|
||||
*/
|
||||
tracker.fetch = function fetchMessage (msgID) {
|
||||
const tracked = messages.get(msgID)
|
||||
if (tracked) {
|
||||
const messageCB = messages.get(msgID)
|
||||
if (messageCB) {
|
||||
purgeAbandoned(msgID, abandoned)
|
||||
return tracked
|
||||
return messageCB
|
||||
}
|
||||
|
||||
// We sent an abandon request but the server either wasn't able to process
|
||||
|
@ -101,7 +91,7 @@ module.exports = function messageTrackerFactory (options) {
|
|||
// to be processed normally.
|
||||
const abandonedMsg = abandoned.get(msgID)
|
||||
if (abandonedMsg) {
|
||||
return { message: abandonedMsg, callback: abandonedMsg.cb }
|
||||
return abandonedMsg.cb
|
||||
}
|
||||
|
||||
return null
|
||||
|
@ -120,7 +110,7 @@ module.exports = function messageTrackerFactory (options) {
|
|||
messages.forEach((val, key) => {
|
||||
purgeAbandoned(key, abandoned)
|
||||
tracker.remove(key)
|
||||
cb(key, val.callback)
|
||||
cb(key, val)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -142,7 +132,7 @@ module.exports = function messageTrackerFactory (options) {
|
|||
* Add a message handler to be tracked.
|
||||
*
|
||||
* @param {object} message The message object to be tracked. This object will
|
||||
* have a new property added to it: `messageId`.
|
||||
* have a new property added to it: `messageID`.
|
||||
* @param {function} callback The handler for the message.
|
||||
*
|
||||
* @memberof MessageTracker
|
||||
|
@ -153,8 +143,8 @@ module.exports = function messageTrackerFactory (options) {
|
|||
// This side effect is not ideal but the client doesn't attach the tracker
|
||||
// to itself until after the `.connect` method has fired. If this can be
|
||||
// refactored later, then we can possibly get rid of this side effect.
|
||||
message.messageId = currentID
|
||||
messages.set(currentID, { callback, message })
|
||||
message.messageID = currentID
|
||||
messages.set(currentID, callback)
|
||||
}
|
||||
|
||||
return tracker
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
* is not accepting any requests.
|
||||
*/
|
||||
module.exports = function enqueue (message, expect, emitter, cb) {
|
||||
if (this._queue.size >= this.size || this._frozen) {
|
||||
if (this._queue.length >= this.size || this._frozen) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,13 @@
|
|||
|
||||
const EventEmitter = require('events').EventEmitter
|
||||
const util = require('util')
|
||||
|
||||
const assert = require('assert-plus')
|
||||
const { PagedResultsControl } = require('@ldapjs/controls')
|
||||
const CorkedEmitter = require('../corked_emitter.js')
|
||||
|
||||
// var dn = require('../dn')
|
||||
// var messages = require('../messages/index')
|
||||
// var Protocol = require('../protocol')
|
||||
const PagedControl = require('../controls/paged_results_control.js')
|
||||
|
||||
/// --- API
|
||||
|
||||
|
@ -25,23 +29,22 @@ const CorkedEmitter = require('../corked_emitter.js')
|
|||
* will be emitted (and 'end' will not be). By listening to
|
||||
* 'pageError', a successful search that lacks paging will be
|
||||
* able to emit 'end'.
|
||||
* 3. search - Emitted as an internal event to trigger another client search.
|
||||
*/
|
||||
function SearchPager (opts) {
|
||||
assert.object(opts)
|
||||
assert.func(opts.callback)
|
||||
assert.number(opts.pageSize)
|
||||
assert.func(opts.sendRequest)
|
||||
|
||||
CorkedEmitter.call(this, {})
|
||||
EventEmitter.call(this, {})
|
||||
|
||||
this.callback = opts.callback
|
||||
this.controls = opts.controls
|
||||
this.pageSize = opts.pageSize
|
||||
this.pagePause = opts.pagePause
|
||||
this.sendRequest = opts.sendRequest
|
||||
|
||||
this.controls.forEach(function (control) {
|
||||
if (control.type === PagedResultsControl.OID) {
|
||||
if (control.type === PagedControl.OID) {
|
||||
// The point of using SearchPager is not having to do this.
|
||||
// Toss an error if the pagedResultsControl is present
|
||||
throw new Error('redundant pagedResultControl')
|
||||
|
@ -52,13 +55,12 @@ function SearchPager (opts) {
|
|||
this.started = false
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
emitter.on('searchRequest', this.emit.bind(this, 'searchRequest'))
|
||||
emitter.on('searchEntry', this.emit.bind(this, 'searchEntry'))
|
||||
emitter.on('end', this._onEnd.bind(this))
|
||||
emitter.on('error', this._onError.bind(this))
|
||||
this.childEmitter = emitter
|
||||
}
|
||||
util.inherits(SearchPager, CorkedEmitter)
|
||||
util.inherits(SearchPager, EventEmitter)
|
||||
module.exports = SearchPager
|
||||
|
||||
/**
|
||||
|
@ -73,7 +75,7 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
|
|||
const self = this
|
||||
let cookie = null
|
||||
res.controls.forEach(function (control) {
|
||||
if (control.type === PagedResultsControl.OID) {
|
||||
if (control.type === PagedControl.OID) {
|
||||
cookie = control.value.cookie
|
||||
}
|
||||
})
|
||||
|
@ -89,13 +91,13 @@ SearchPager.prototype._onEnd = function _onEnd (res) {
|
|||
if (this.listeners('pageError').length > 0) {
|
||||
this.emit('pageError', err)
|
||||
// If the consumer as subscribed to pageError, SearchPager is absolved
|
||||
// from delivering the fault via the 'error' event. Emitting an 'end'
|
||||
// from deliverying the fault via the 'error' event. Emitting an 'end'
|
||||
// event after 'error' breaks the contract that the standard client
|
||||
// provides, so it's only a possibility if 'pageError' is used instead.
|
||||
this.emit('end', res)
|
||||
} else {
|
||||
this.emit('error', err)
|
||||
// No end event possible per explanation above.
|
||||
// No end event possible per explaination above.
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -134,20 +136,21 @@ SearchPager.prototype._onError = function _onError (err) {
|
|||
*/
|
||||
SearchPager.prototype._nextPage = function _nextPage (cookie) {
|
||||
const controls = this.controls.slice(0)
|
||||
controls.push(new PagedResultsControl({
|
||||
controls.push(new PagedControl({
|
||||
value: {
|
||||
size: this.pageSize,
|
||||
cookie
|
||||
cookie: cookie
|
||||
}
|
||||
}))
|
||||
|
||||
this.sendRequest(controls, this.childEmitter, this._sendCallback.bind(this))
|
||||
this.emit('search', controls, this.childEmitter,
|
||||
this._sendCallback.bind(this))
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback provided to the client API for successful transmission.
|
||||
*/
|
||||
SearchPager.prototype._sendCallback = function _sendCallback (err) {
|
||||
SearchPager.prototype._sendCallback = function _sendCallback (err, res) {
|
||||
if (err) {
|
||||
this.finished = true
|
||||
if (!this.started) {
|
||||
|
|
|
@ -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.
|
||||
|
||||
const controls = require('@ldapjs/controls')
|
||||
module.exports = controls
|
||||
const assert = require('assert')
|
||||
const Ber = require('asn1').Ber
|
||||
|
||||
const Control = require('./control')
|
||||
const EntryChangeNotificationControl =
|
||||
require('./entry_change_notification_control')
|
||||
const PersistentSearchControl = require('./persistent_search_control')
|
||||
const PagedResultsControl = require('./paged_results_control')
|
||||
const ServerSideSortingRequestControl =
|
||||
require('./server_side_sorting_request_control.js')
|
||||
const ServerSideSortingResponseControl =
|
||||
require('./server_side_sorting_response_control.js')
|
||||
const VirtualListViewRequestControl =
|
||||
require('./virtual_list_view_request_control.js')
|
||||
const VirtualListViewResponseControl =
|
||||
require('./virtual_list_view_response_control.js')
|
||||
|
||||
/// --- API
|
||||
|
||||
module.exports = {
|
||||
|
||||
getControl: function getControl (ber) {
|
||||
assert.ok(ber)
|
||||
|
||||
if (ber.readSequence() === null) { return null }
|
||||
|
||||
let type
|
||||
const opts = {
|
||||
criticality: false,
|
||||
value: null
|
||||
}
|
||||
|
||||
if (ber.length) {
|
||||
const end = ber.offset + ber.length
|
||||
|
||||
type = ber.readString()
|
||||
if (ber.offset < end) {
|
||||
if (ber.peek() === Ber.Boolean) { opts.criticality = ber.readBoolean() }
|
||||
}
|
||||
|
||||
if (ber.offset < end) { opts.value = ber.readString(Ber.OctetString, true) }
|
||||
}
|
||||
|
||||
let control
|
||||
switch (type) {
|
||||
case PersistentSearchControl.OID:
|
||||
control = new PersistentSearchControl(opts)
|
||||
break
|
||||
case EntryChangeNotificationControl.OID:
|
||||
control = new EntryChangeNotificationControl(opts)
|
||||
break
|
||||
case PagedResultsControl.OID:
|
||||
control = new PagedResultsControl(opts)
|
||||
break
|
||||
case ServerSideSortingRequestControl.OID:
|
||||
control = new ServerSideSortingRequestControl(opts)
|
||||
break
|
||||
case ServerSideSortingResponseControl.OID:
|
||||
control = new ServerSideSortingResponseControl(opts)
|
||||
break
|
||||
case VirtualListViewRequestControl.OID:
|
||||
control = new VirtualListViewRequestControl(opts)
|
||||
break
|
||||
case VirtualListViewResponseControl.OID:
|
||||
control = new VirtualListViewResponseControl(opts)
|
||||
break
|
||||
default:
|
||||
opts.type = type
|
||||
control = new Control(opts)
|
||||
break
|
||||
}
|
||||
|
||||
return control
|
||||
},
|
||||
|
||||
Control: Control,
|
||||
EntryChangeNotificationControl: EntryChangeNotificationControl,
|
||||
PagedResultsControl: PagedResultsControl,
|
||||
PersistentSearchControl: PersistentSearchControl,
|
||||
ServerSideSortingRequestControl: ServerSideSortingRequestControl,
|
||||
ServerSideSortingResponseControl: ServerSideSortingResponseControl,
|
||||
VirtualListViewRequestControl: VirtualListViewRequestControl,
|
||||
VirtualListViewResponseControl: VirtualListViewResponseControl
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
return this.lde_message || this.name
|
||||
},
|
||||
set: function setMessage (message) {
|
||||
this.lde_message = message
|
||||
},
|
||||
configurable: false
|
||||
},
|
||||
dn: {
|
||||
|
@ -86,7 +83,7 @@ Object.keys(CODES).forEach(function (code) {
|
|||
})
|
||||
|
||||
ERRORS[CODES[code]] = {
|
||||
err,
|
||||
err: err,
|
||||
message: msg
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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 client = require('./client')
|
||||
const Attribute = require('@ldapjs/attribute')
|
||||
const Change = require('@ldapjs/change')
|
||||
const Protocol = require('@ldapjs/protocol')
|
||||
const Attribute = require('./attribute')
|
||||
const Change = require('./change')
|
||||
const Protocol = require('./protocol')
|
||||
const Server = require('./server')
|
||||
|
||||
const controls = require('./controls')
|
||||
const persistentSearch = require('./persistent_search')
|
||||
const dn = require('@ldapjs/dn')
|
||||
const dn = require('./dn')
|
||||
const errors = require('./errors')
|
||||
const filters = require('@ldapjs/filter')
|
||||
const filters = require('./filters')
|
||||
const messages = require('./messages')
|
||||
const url = require('./url')
|
||||
|
||||
|
@ -24,7 +24,7 @@ module.exports = {
|
|||
Client: client.Client,
|
||||
createClient: client.createClient,
|
||||
|
||||
Server,
|
||||
Server: Server,
|
||||
createServer: function (options) {
|
||||
if (options === undefined) { options = {} }
|
||||
|
||||
|
@ -37,21 +37,21 @@ module.exports = {
|
|||
return new Server(options)
|
||||
},
|
||||
|
||||
Attribute,
|
||||
Change,
|
||||
Attribute: Attribute,
|
||||
Change: Change,
|
||||
|
||||
dn,
|
||||
dn: dn,
|
||||
DN: dn.DN,
|
||||
RDN: dn.RDN,
|
||||
parseDN: dn.DN.fromString,
|
||||
parseDN: dn.parse,
|
||||
|
||||
persistentSearch,
|
||||
persistentSearch: persistentSearch,
|
||||
PersistentSearchCache: persistentSearch.PersistentSearchCache,
|
||||
|
||||
filters,
|
||||
filters: filters,
|
||||
parseFilter: filters.parseString,
|
||||
|
||||
url,
|
||||
url: url,
|
||||
parseURL: url.parse
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
const messages = require('@ldapjs/messages')
|
||||
|
||||
const LDAPMessage = require('./message')
|
||||
const LDAPResult = require('./result')
|
||||
const Parser = require('./parser')
|
||||
|
||||
const AbandonRequest = require('./abandon_request')
|
||||
const AbandonResponse = require('./abandon_response')
|
||||
const AddRequest = require('./add_request')
|
||||
const AddResponse = require('./add_response')
|
||||
const BindRequest = require('./bind_request')
|
||||
const BindResponse = require('./bind_response')
|
||||
const CompareRequest = require('./compare_request')
|
||||
const CompareResponse = require('./compare_response')
|
||||
const DeleteRequest = require('./del_request')
|
||||
const DeleteResponse = require('./del_response')
|
||||
const ExtendedRequest = require('./ext_request')
|
||||
const ExtendedResponse = require('./ext_response')
|
||||
const ModifyRequest = require('./modify_request')
|
||||
const ModifyResponse = require('./modify_response')
|
||||
const ModifyDNRequest = require('./moddn_request')
|
||||
const ModifyDNResponse = require('./moddn_response')
|
||||
const SearchRequest = require('./search_request')
|
||||
const SearchEntry = require('./search_entry')
|
||||
const SearchReference = require('./search_reference')
|
||||
const SearchResponse = require('./search_response')
|
||||
const UnbindRequest = require('./unbind_request')
|
||||
const UnbindResponse = require('./unbind_response')
|
||||
|
||||
/// --- API
|
||||
|
||||
module.exports = {
|
||||
|
||||
LDAPMessage: messages.LdapMessage,
|
||||
LDAPResult: messages.LdapResult,
|
||||
Parser,
|
||||
LDAPMessage: LDAPMessage,
|
||||
LDAPResult: LDAPResult,
|
||||
Parser: Parser,
|
||||
|
||||
AbandonRequest: messages.AbandonRequest,
|
||||
AbandonResponse: messages.AbandonResponse,
|
||||
AddRequest: messages.AddRequest,
|
||||
AddResponse: messages.AddResponse,
|
||||
BindRequest: messages.BindRequest,
|
||||
BindResponse: messages.BindResponse,
|
||||
CompareRequest: messages.CompareRequest,
|
||||
CompareResponse: messages.CompareResponse,
|
||||
DeleteRequest: messages.DeleteRequest,
|
||||
DeleteResponse: messages.DeleteResponse,
|
||||
ExtendedRequest: messages.ExtensionRequest,
|
||||
ExtendedResponse: messages.ExtensionResponse,
|
||||
ModifyRequest: messages.ModifyRequest,
|
||||
ModifyResponse: messages.ModifyResponse,
|
||||
ModifyDNRequest: messages.ModifyDnRequest,
|
||||
ModifyDNResponse: messages.ModifyDnResponse,
|
||||
SearchRequest: messages.SearchRequest,
|
||||
SearchEntry: messages.SearchResultEntry,
|
||||
SearchReference: messages.SearchResultReference,
|
||||
SearchResponse,
|
||||
UnbindRequest: messages.UnbindRequest
|
||||
AbandonRequest: AbandonRequest,
|
||||
AbandonResponse: AbandonResponse,
|
||||
AddRequest: AddRequest,
|
||||
AddResponse: AddResponse,
|
||||
BindRequest: BindRequest,
|
||||
BindResponse: BindResponse,
|
||||
CompareRequest: CompareRequest,
|
||||
CompareResponse: CompareResponse,
|
||||
DeleteRequest: DeleteRequest,
|
||||
DeleteResponse: DeleteResponse,
|
||||
ExtendedRequest: ExtendedRequest,
|
||||
ExtendedResponse: ExtendedResponse,
|
||||
ModifyRequest: ModifyRequest,
|
||||
ModifyResponse: ModifyResponse,
|
||||
ModifyDNRequest: ModifyDNRequest,
|
||||
ModifyDNResponse: ModifyDNResponse,
|
||||
SearchRequest: SearchRequest,
|
||||
SearchEntry: SearchEntry,
|
||||
SearchReference: SearchReference,
|
||||
SearchResponse: SearchResponse,
|
||||
UnbindRequest: UnbindRequest,
|
||||
UnbindResponse: UnbindResponse
|
||||
|
||||
}
|
||||
|
|
|
@ -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 assert = require('assert-plus')
|
||||
const asn1 = require('@ldapjs/asn1')
|
||||
const asn1 = require('asn1')
|
||||
// var VError = require('verror').VError
|
||||
const logger = require('../logger')
|
||||
|
||||
const messages = require('@ldapjs/messages')
|
||||
const AbandonRequest = messages.AbandonRequest
|
||||
const AddRequest = messages.AddRequest
|
||||
const AddResponse = messages.AddResponse
|
||||
const BindRequest = messages.BindRequest
|
||||
const BindResponse = messages.BindResponse
|
||||
const CompareRequest = messages.CompareRequest
|
||||
const CompareResponse = messages.CompareResponse
|
||||
const DeleteRequest = messages.DeleteRequest
|
||||
const DeleteResponse = messages.DeleteResponse
|
||||
const ExtendedRequest = messages.ExtensionRequest
|
||||
const ExtendedResponse = messages.ExtensionResponse
|
||||
const ModifyRequest = messages.ModifyRequest
|
||||
const ModifyResponse = messages.ModifyResponse
|
||||
const ModifyDNRequest = messages.ModifyDnRequest
|
||||
const ModifyDNResponse = messages.ModifyDnResponse
|
||||
const SearchRequest = messages.SearchRequest
|
||||
const SearchEntry = messages.SearchResultEntry
|
||||
const SearchReference = messages.SearchResultReference
|
||||
const AbandonRequest = require('./abandon_request')
|
||||
const AddRequest = require('./add_request')
|
||||
const AddResponse = require('./add_response')
|
||||
const BindRequest = require('./bind_request')
|
||||
const BindResponse = require('./bind_response')
|
||||
const CompareRequest = require('./compare_request')
|
||||
const CompareResponse = require('./compare_response')
|
||||
const DeleteRequest = require('./del_request')
|
||||
const DeleteResponse = require('./del_response')
|
||||
const ExtendedRequest = require('./ext_request')
|
||||
const ExtendedResponse = require('./ext_response')
|
||||
const ModifyRequest = require('./modify_request')
|
||||
const ModifyResponse = require('./modify_response')
|
||||
const ModifyDNRequest = require('./moddn_request')
|
||||
const ModifyDNResponse = require('./moddn_response')
|
||||
const SearchRequest = require('./search_request')
|
||||
const SearchEntry = require('./search_entry')
|
||||
const SearchReference = require('./search_reference')
|
||||
const SearchResponse = require('./search_response')
|
||||
const UnbindRequest = messages.UnbindRequest
|
||||
const LDAPResult = messages.LdapResult
|
||||
const UnbindRequest = require('./unbind_request')
|
||||
// var UnbindResponse = require('./unbind_response')
|
||||
|
||||
const Protocol = require('@ldapjs/protocol')
|
||||
const LDAPResult = require('./result')
|
||||
// var Message = require('./message')
|
||||
|
||||
const Protocol = require('../protocol')
|
||||
|
||||
/// --- Globals
|
||||
|
||||
// var Ber = asn1.Ber
|
||||
const BerReader = asn1.BerReader
|
||||
|
||||
/// --- API
|
||||
|
@ -48,13 +52,6 @@ function Parser (options = {}) {
|
|||
}
|
||||
util.inherits(Parser, EventEmitter)
|
||||
|
||||
/**
|
||||
* The LDAP server/client implementations will receive data from a stream and feed
|
||||
* it into this method. This method will collect that data into an internal
|
||||
* growing buffer. As that buffer fills with enough data to constitute a valid
|
||||
* LDAP message, the data will be parsed, emitted as a message object, and
|
||||
* reset the buffer to account for any next message in the stream.
|
||||
*/
|
||||
Parser.prototype.write = function (data) {
|
||||
if (!data || !Buffer.isBuffer(data)) { throw new TypeError('data (buffer) required') }
|
||||
|
||||
|
@ -67,9 +64,9 @@ Parser.prototype.write = function (data) {
|
|||
return true
|
||||
}
|
||||
|
||||
self.buffer = self.buffer ? Buffer.concat([self.buffer, data]) : data
|
||||
self.buffer = (self.buffer ? Buffer.concat([self.buffer, data]) : data)
|
||||
|
||||
let ber = new BerReader(self.buffer)
|
||||
const ber = new BerReader(self.buffer)
|
||||
|
||||
let foundSeq = false
|
||||
try {
|
||||
|
@ -83,22 +80,9 @@ Parser.prototype.write = function (data) {
|
|||
return false
|
||||
} else if (ber.remain > ber.length) {
|
||||
// ETOOMUCH
|
||||
|
||||
// This is an odd branch. Basically, it is setting `nextMessage` to
|
||||
// a buffer that represents data part of a message subsequent to the one
|
||||
// being processed. It then re-creates `ber` as a representation of
|
||||
// the message being processed and advances its offset to the value
|
||||
// position of the TLV.
|
||||
|
||||
// Set `nextMessage` to the bytes subsequent to the current message's
|
||||
// value bytes. That is, slice from the byte immediately following the
|
||||
// current message's value bytes until the end of the buffer.
|
||||
// This is sort of ugly, but allows us to make miminal copies
|
||||
nextMessage = self.buffer.slice(ber.offset + ber.length)
|
||||
|
||||
const currOffset = ber.offset
|
||||
ber = new BerReader(ber.buffer.subarray(0, currOffset + ber.length))
|
||||
ber.readSequence()
|
||||
|
||||
ber._size = ber.offset + ber.length
|
||||
assert.equal(ber.remain, ber.length)
|
||||
}
|
||||
|
||||
|
@ -108,25 +92,13 @@ Parser.prototype.write = function (data) {
|
|||
|
||||
let message
|
||||
try {
|
||||
if (Object.prototype.toString.call(ber) === '[object BerReader]') {
|
||||
// Parse the BER into a JavaScript object representation. The message
|
||||
// objects require the full sequence in order to construct the object.
|
||||
// At this point, we have already read the sequence tag and length, so
|
||||
// we need to rewind the buffer a bit. The `.sequenceToReader` method
|
||||
// does this for us.
|
||||
message = messages.LdapMessage.parse(ber.sequenceToReader())
|
||||
} else {
|
||||
// Bail here if peer isn't speaking protocol at all
|
||||
message = this.getMessage(ber)
|
||||
}
|
||||
// Bail here if peer isn't speaking protocol at all
|
||||
message = this.getMessage(ber)
|
||||
|
||||
if (!message) {
|
||||
return end()
|
||||
}
|
||||
|
||||
// TODO: find a better way to handle logging now that messages and the
|
||||
// server are decoupled. ~ jsumners 2023-02-17
|
||||
message.log = this.log
|
||||
message.parse(ber)
|
||||
} catch (e) {
|
||||
this.emit('error', e, message)
|
||||
return false
|
||||
|
@ -141,88 +113,88 @@ Parser.prototype.getMessage = function (ber) {
|
|||
|
||||
const self = this
|
||||
|
||||
const messageId = ber.readInt()
|
||||
const messageID = ber.readInt()
|
||||
const type = ber.readSequence()
|
||||
|
||||
let Message
|
||||
switch (type) {
|
||||
case Protocol.operations.LDAP_REQ_ABANDON:
|
||||
case Protocol.LDAP_REQ_ABANDON:
|
||||
Message = AbandonRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_ADD:
|
||||
case Protocol.LDAP_REQ_ADD:
|
||||
Message = AddRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_ADD:
|
||||
case Protocol.LDAP_REP_ADD:
|
||||
Message = AddResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_BIND:
|
||||
case Protocol.LDAP_REQ_BIND:
|
||||
Message = BindRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_BIND:
|
||||
case Protocol.LDAP_REP_BIND:
|
||||
Message = BindResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_COMPARE:
|
||||
case Protocol.LDAP_REQ_COMPARE:
|
||||
Message = CompareRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_COMPARE:
|
||||
case Protocol.LDAP_REP_COMPARE:
|
||||
Message = CompareResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_DELETE:
|
||||
case Protocol.LDAP_REQ_DELETE:
|
||||
Message = DeleteRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_DELETE:
|
||||
case Protocol.LDAP_REP_DELETE:
|
||||
Message = DeleteResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_EXTENSION:
|
||||
case Protocol.LDAP_REQ_EXTENSION:
|
||||
Message = ExtendedRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_EXTENSION:
|
||||
case Protocol.LDAP_REP_EXTENSION:
|
||||
Message = ExtendedResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_MODIFY:
|
||||
case Protocol.LDAP_REQ_MODIFY:
|
||||
Message = ModifyRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_MODIFY:
|
||||
case Protocol.LDAP_REP_MODIFY:
|
||||
Message = ModifyResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_MODRDN:
|
||||
case Protocol.LDAP_REQ_MODRDN:
|
||||
Message = ModifyDNRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_MODRDN:
|
||||
case Protocol.LDAP_REP_MODRDN:
|
||||
Message = ModifyDNResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_SEARCH:
|
||||
case Protocol.LDAP_REQ_SEARCH:
|
||||
Message = SearchRequest
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_SEARCH_ENTRY:
|
||||
case Protocol.LDAP_REP_SEARCH_ENTRY:
|
||||
Message = SearchEntry
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_SEARCH_REF:
|
||||
case Protocol.LDAP_REP_SEARCH_REF:
|
||||
Message = SearchReference
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_RES_SEARCH:
|
||||
case Protocol.LDAP_REP_SEARCH:
|
||||
Message = SearchResponse
|
||||
break
|
||||
|
||||
case Protocol.operations.LDAP_REQ_UNBIND:
|
||||
case Protocol.LDAP_REQ_UNBIND:
|
||||
Message = UnbindRequest
|
||||
break
|
||||
|
||||
|
@ -231,15 +203,15 @@ Parser.prototype.getMessage = function (ber) {
|
|||
new Error('Op 0x' + (type ? type.toString(16) : '??') +
|
||||
' not supported'),
|
||||
new LDAPResult({
|
||||
messageId,
|
||||
protocolOp: type || Protocol.operations.LDAP_RES_EXTENSION
|
||||
messageID: messageID,
|
||||
protocolOp: type || Protocol.LDAP_REP_EXTENSION
|
||||
}))
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return new Message({
|
||||
messageId,
|
||||
messageID: messageID,
|
||||
log: self.log
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
const assert = require('assert-plus')
|
||||
const util = require('util')
|
||||
|
||||
const Attribute = require('@ldapjs/attribute')
|
||||
const {
|
||||
SearchResultEntry: SearchEntry,
|
||||
SearchResultReference: SearchReference,
|
||||
SearchResultDone
|
||||
} = require('@ldapjs/messages')
|
||||
const LDAPResult = require('./result')
|
||||
const SearchEntry = require('./search_entry')
|
||||
const SearchReference = require('./search_reference')
|
||||
|
||||
const parseDN = require('@ldapjs/dn').DN.fromString
|
||||
const dtrace = require('../dtrace')
|
||||
const parseDN = require('../dn').parse
|
||||
const parseURL = require('../url').parse
|
||||
const Protocol = require('../protocol')
|
||||
|
||||
/// --- API
|
||||
|
||||
class SearchResponse extends SearchResultDone {
|
||||
attributes
|
||||
notAttributes
|
||||
sentEntries
|
||||
function SearchResponse (options) {
|
||||
options = options || {}
|
||||
assert.object(options)
|
||||
|
||||
constructor (options = {}) {
|
||||
super(options)
|
||||
options.protocolOp = Protocol.LDAP_REP_SEARCH
|
||||
LDAPResult.call(this, options)
|
||||
|
||||
this.attributes = options.attributes ? options.attributes.slice() : []
|
||||
this.notAttributes = []
|
||||
this.sentEntries = 0
|
||||
}
|
||||
this.attributes = options.attributes ? options.attributes.slice() : []
|
||||
this.notAttributes = []
|
||||
this.sentEntries = 0
|
||||
}
|
||||
util.inherits(SearchResponse, LDAPResult)
|
||||
|
||||
/**
|
||||
* Allows you to send a SearchEntry back to the client.
|
||||
|
@ -42,18 +42,14 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
|
|||
const self = this
|
||||
|
||||
const savedAttrs = {}
|
||||
let save = null
|
||||
const save = null
|
||||
if (entry instanceof SearchEntry || entry instanceof SearchReference) {
|
||||
if (!entry.messageId) { entry.messageId = this.messageId }
|
||||
if (entry.messageId !== this.messageId) {
|
||||
throw new Error('SearchEntry messageId mismatch')
|
||||
}
|
||||
if (!entry.messageID) { entry.messageID = this.messageID }
|
||||
if (entry.messageID !== this.messageID) { throw new Error('SearchEntry messageID mismatch') }
|
||||
} else {
|
||||
if (!entry.attributes) { throw new Error('entry.attributes required') }
|
||||
|
||||
const all = (self.attributes.indexOf('*') !== -1)
|
||||
// Filter attributes in a plain object according to the magic `_` prefix
|
||||
// and presence in `notAttributes`.
|
||||
Object.keys(entry.attributes).forEach(function (a) {
|
||||
const _a = a.toLowerCase()
|
||||
if (!nofiltering && _a.length && _a[0] === '_') {
|
||||
|
@ -70,27 +66,42 @@ SearchResponse.prototype.send = function (entry, nofiltering) {
|
|||
}
|
||||
})
|
||||
|
||||
save = entry
|
||||
const save = entry
|
||||
entry = new SearchEntry({
|
||||
objectName: typeof (save.dn) === 'string' ? parseDN(save.dn) : save.dn,
|
||||
messageId: self.messageId,
|
||||
attributes: Attribute.fromObject(entry.attributes)
|
||||
messageID: self.messageID,
|
||||
log: self.log
|
||||
})
|
||||
entry.fromObject(save)
|
||||
}
|
||||
|
||||
try {
|
||||
this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.pojo)
|
||||
this.log.debug('%s: sending: %j', this.connection.ldap.id, entry.json)
|
||||
|
||||
this.connection.write(entry.toBer().buffer)
|
||||
this.connection.write(entry.toBer())
|
||||
this.sentEntries++
|
||||
|
||||
if (self._dtraceOp && self._dtraceId) {
|
||||
dtrace.fire('server-search-entry', function () {
|
||||
const c = self.connection || { ldap: {} }
|
||||
return [
|
||||
self._dtraceId || 0,
|
||||
(c.remoteAddress || ''),
|
||||
c.ldap.bindDN ? c.ldap.bindDN.toString() : '',
|
||||
(self.requestDN ? self.requestDN.toString() : ''),
|
||||
entry.objectName.toString(),
|
||||
entry.attributes.length
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
// Restore attributes
|
||||
Object.keys(savedAttrs).forEach(function (k) {
|
||||
save.attributes[k] = savedAttrs[k]
|
||||
})
|
||||
} catch (e) {
|
||||
this.log.warn(e, '%s failure to write message %j',
|
||||
this.connection.ldap.id, this.pojo)
|
||||
this.connection.ldap.id, this.json)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,10 +109,11 @@ SearchResponse.prototype.createSearchEntry = function (object) {
|
|||
assert.object(object)
|
||||
|
||||
const entry = new SearchEntry({
|
||||
messageId: this.messageId,
|
||||
objectName: object.objectName || object.dn,
|
||||
attributes: object.attributes ?? []
|
||||
messageID: this.messageID,
|
||||
log: this.log,
|
||||
objectName: object.objectName || object.dn
|
||||
})
|
||||
entry.fromObject((object.attributes || object))
|
||||
return entry
|
||||
}
|
||||
|
||||
|
@ -110,10 +122,15 @@ SearchResponse.prototype.createSearchReference = function (uris) {
|
|||
|
||||
if (!Array.isArray(uris)) { uris = [uris] }
|
||||
|
||||
for (let i = 0; i < uris.length; i++) {
|
||||
if (typeof (uris[i]) === 'string') { uris[i] = parseURL(uris[i]) }
|
||||
}
|
||||
|
||||
const self = this
|
||||
return new SearchReference({
|
||||
messageId: self.messageId,
|
||||
uri: uris
|
||||
messageID: self.messageID,
|
||||
log: self.log,
|
||||
uris: uris
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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 (req.persistentSearch.value.returnECs) {
|
||||
const attrs = obj.attributes
|
||||
|
@ -89,7 +89,7 @@ function getEntryChangeNotificationControl (req, obj) {
|
|||
}
|
||||
|
||||
value.changeNumber = attrs.changenumber
|
||||
return new EntryChangeNotificationControl({ value })
|
||||
return new EntryChangeNotificationControl({ value: value })
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
@ -104,6 +104,6 @@ function checkChangeType (req, requestType) {
|
|||
|
||||
module.exports = {
|
||||
PersistentSearchCache: PersistentSearch,
|
||||
checkChangeType,
|
||||
getEntryChangeNotificationControl
|
||||
checkChangeType: checkChangeType,
|
||||
getEntryChangeNotificationControl: getEntryChangeNotificationControl
|
||||
}
|
||||
|
|
|
@ -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 util = require('util')
|
||||
|
||||
// var asn1 = require('@ldapjs/asn1')
|
||||
// var asn1 = require('asn1')
|
||||
const VError = require('verror').VError
|
||||
|
||||
const { DN, RDN } = require('@ldapjs/dn')
|
||||
const dn = require('./dn')
|
||||
const dtrace = require('./dtrace')
|
||||
const errors = require('./errors')
|
||||
const Protocol = require('@ldapjs/protocol')
|
||||
|
||||
const messages = require('@ldapjs/messages')
|
||||
const Protocol = require('./protocol')
|
||||
|
||||
const Parser = require('./messages').Parser
|
||||
const LdapResult = messages.LdapResult
|
||||
const AbandonResponse = messages.AbandonResponse
|
||||
const AddResponse = messages.AddResponse
|
||||
const BindResponse = messages.BindResponse
|
||||
const CompareResponse = messages.CompareResponse
|
||||
const DeleteResponse = messages.DeleteResponse
|
||||
const ExtendedResponse = messages.ExtensionResponse
|
||||
const ModifyResponse = messages.ModifyResponse
|
||||
const ModifyDnResponse = messages.ModifyDnResponse
|
||||
const SearchRequest = messages.SearchRequest
|
||||
const AbandonResponse = require('./messages/abandon_response')
|
||||
const AddResponse = require('./messages/add_response')
|
||||
const BindResponse = require('./messages/bind_response')
|
||||
const CompareResponse = require('./messages/compare_response')
|
||||
const DeleteResponse = require('./messages/del_response')
|
||||
const ExtendedResponse = require('./messages/ext_response')
|
||||
// var LDAPResult = require('./messages/result')
|
||||
const ModifyResponse = require('./messages/modify_response')
|
||||
const ModifyDNResponse = require('./messages/moddn_response')
|
||||
const SearchRequest = require('./messages/search_request')
|
||||
const SearchResponse = require('./messages/search_response')
|
||||
const UnbindResponse = require('./messages/unbind_response')
|
||||
|
||||
/// --- Globals
|
||||
|
||||
// var Ber = asn1.Ber
|
||||
// var BerReader = asn1.BerReader
|
||||
// const DN = dn.DN
|
||||
const DN = dn.DN
|
||||
|
||||
// var sprintf = util.format
|
||||
|
||||
|
@ -47,15 +47,15 @@ function mergeFunctionArgs (argv, start, end) {
|
|||
const handlers = []
|
||||
|
||||
for (let i = start; i < end; i++) {
|
||||
if (Array.isArray(argv[i])) {
|
||||
if (argv[i] instanceof Array) {
|
||||
const arr = argv[i]
|
||||
for (let j = 0; j < arr.length; j++) {
|
||||
if (typeof arr[j] !== 'function') {
|
||||
if (!(arr[j] instanceof Function)) {
|
||||
throw new TypeError('Invalid argument type: ' + typeof (arr[j]))
|
||||
}
|
||||
handlers.push(arr[j])
|
||||
}
|
||||
} else if (typeof argv[i] === 'function') {
|
||||
} else if (argv[i] instanceof Function) {
|
||||
handlers.push(argv[i])
|
||||
} else {
|
||||
throw new TypeError('Invalid argument type: ' + typeof (argv[i]))
|
||||
|
@ -71,42 +71,35 @@ function getResponse (req) {
|
|||
let Response
|
||||
|
||||
switch (req.protocolOp) {
|
||||
case Protocol.operations.LDAP_REQ_BIND:
|
||||
case Protocol.LDAP_REQ_BIND:
|
||||
Response = BindResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_ABANDON:
|
||||
case Protocol.LDAP_REQ_ABANDON:
|
||||
Response = AbandonResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_ADD:
|
||||
case Protocol.LDAP_REQ_ADD:
|
||||
Response = AddResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_COMPARE:
|
||||
case Protocol.LDAP_REQ_COMPARE:
|
||||
Response = CompareResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_DELETE:
|
||||
case Protocol.LDAP_REQ_DELETE:
|
||||
Response = DeleteResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_EXTENSION:
|
||||
case Protocol.LDAP_REQ_EXTENSION:
|
||||
Response = ExtendedResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_MODIFY:
|
||||
case Protocol.LDAP_REQ_MODIFY:
|
||||
Response = ModifyResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_MODRDN:
|
||||
Response = ModifyDnResponse
|
||||
case Protocol.LDAP_REQ_MODRDN:
|
||||
Response = ModifyDNResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_SEARCH:
|
||||
case Protocol.LDAP_REQ_SEARCH:
|
||||
Response = SearchResponse
|
||||
break
|
||||
case Protocol.operations.LDAP_REQ_UNBIND:
|
||||
// TODO: when the server receives an unbind request this made up response object was returned.
|
||||
// Instead, we need to just terminate the connection. ~ jsumners
|
||||
Response = class extends LdapResult {
|
||||
status = 0
|
||||
end () {
|
||||
req.connection.end()
|
||||
}
|
||||
}
|
||||
case Protocol.LDAP_REQ_UNBIND:
|
||||
Response = UnbindResponse
|
||||
break
|
||||
default:
|
||||
return null
|
||||
|
@ -114,83 +107,16 @@ function getResponse (req) {
|
|||
assert.ok(Response)
|
||||
|
||||
const res = new Response({
|
||||
messageId: req.messageId,
|
||||
messageID: req.messageID,
|
||||
log: req.log,
|
||||
attributes: ((req instanceof SearchRequest) ? req.attributes : undefined)
|
||||
})
|
||||
res.log = req.log
|
||||
res.connection = req.connection
|
||||
res.logId = req.logId
|
||||
|
||||
if (typeof res.end !== 'function') {
|
||||
// This is a hack to re-add the original tight coupling of the message
|
||||
// objects and the server connection.
|
||||
// TODO: remove this during server refactoring ~ jsumners 2023-02-16
|
||||
switch (res.protocolOp) {
|
||||
case 0: {
|
||||
res.end = abandonResponseEnd
|
||||
break
|
||||
}
|
||||
|
||||
case Protocol.operations.LDAP_RES_COMPARE: {
|
||||
res.end = compareResponseEnd
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
res.end = defaultResponseEnd
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Response connection end handler for most responses.
|
||||
*
|
||||
* @param {number} status
|
||||
*/
|
||||
function defaultResponseEnd (status) {
|
||||
if (typeof status === 'number') { this.status = status }
|
||||
|
||||
const ber = this.toBer()
|
||||
this.log.debug('%s: sending: %j', this.connection.ldap.id, this.pojo)
|
||||
|
||||
try {
|
||||
this.connection.write(ber.buffer)
|
||||
} catch (error) {
|
||||
this.log.warn(
|
||||
error,
|
||||
'%s failure to write message %j',
|
||||
this.connection.ldap.id,
|
||||
this.pojo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Response connection end handler for ABANDON responses.
|
||||
*/
|
||||
function abandonResponseEnd () {}
|
||||
|
||||
/**
|
||||
* Response connection end handler for COMPARE responses.
|
||||
*
|
||||
* @param {number | boolean} status
|
||||
*/
|
||||
function compareResponseEnd (status) {
|
||||
let result = 0x06
|
||||
if (typeof status === 'boolean') {
|
||||
if (status === false) {
|
||||
result = 0x05
|
||||
}
|
||||
} else {
|
||||
result = status
|
||||
}
|
||||
return defaultResponseEnd.call(this, result)
|
||||
}
|
||||
|
||||
function defaultHandler (req, res, next) {
|
||||
assert.ok(req)
|
||||
assert.ok(res)
|
||||
|
@ -231,6 +157,69 @@ function noExOpHandler (req, res, next) {
|
|||
return next()
|
||||
}
|
||||
|
||||
function fireDTraceProbe (req, res) {
|
||||
assert.ok(req)
|
||||
|
||||
req._dtraceId = res._dtraceId = dtrace._nextId()
|
||||
const probeArgs = [
|
||||
req._dtraceId,
|
||||
req.connection.remoteAddress || 'localhost',
|
||||
req.connection.ldap.bindDN.toString(),
|
||||
req.dn.toString()
|
||||
]
|
||||
|
||||
let op
|
||||
switch (req.protocolOp) {
|
||||
case Protocol.LDAP_REQ_ABANDON:
|
||||
op = 'abandon'
|
||||
break
|
||||
case Protocol.LDAP_REQ_ADD:
|
||||
op = 'add'
|
||||
probeArgs.push(req.attributes.length)
|
||||
break
|
||||
case Protocol.LDAP_REQ_BIND:
|
||||
op = 'bind'
|
||||
break
|
||||
case Protocol.LDAP_REQ_COMPARE:
|
||||
op = 'compare'
|
||||
probeArgs.push(req.attribute)
|
||||
probeArgs.push(req.value)
|
||||
break
|
||||
case Protocol.LDAP_REQ_DELETE:
|
||||
op = 'delete'
|
||||
break
|
||||
case Protocol.LDAP_REQ_EXTENSION:
|
||||
op = 'exop'
|
||||
probeArgs.push(req.name)
|
||||
probeArgs.push(req.value)
|
||||
break
|
||||
case Protocol.LDAP_REQ_MODIFY:
|
||||
op = 'modify'
|
||||
probeArgs.push(req.changes.length)
|
||||
break
|
||||
case Protocol.LDAP_REQ_MODRDN:
|
||||
op = 'modifydn'
|
||||
probeArgs.push(req.newRdn.toString())
|
||||
probeArgs.push((req.newSuperior ? req.newSuperior.toString() : ''))
|
||||
break
|
||||
case Protocol.LDAP_REQ_SEARCH:
|
||||
op = 'search'
|
||||
probeArgs.push(req.scope)
|
||||
probeArgs.push(req.filter.toString())
|
||||
break
|
||||
case Protocol.LDAP_REQ_UNBIND:
|
||||
op = 'unbind'
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
res._dtraceOp = op
|
||||
dtrace.fire('server-' + op + '-start', function () {
|
||||
return probeArgs
|
||||
})
|
||||
}
|
||||
|
||||
/// --- API
|
||||
|
||||
/**
|
||||
|
@ -272,6 +261,8 @@ function Server (options) {
|
|||
|
||||
this._chain = []
|
||||
this.log = options.log
|
||||
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
|
||||
|
||||
const log = this.log
|
||||
|
||||
function setupConnection (c) {
|
||||
|
@ -286,12 +277,12 @@ function Server (options) {
|
|||
c.remotePort = c.socket.remotePort
|
||||
}
|
||||
|
||||
const rdn = new RDN({ cn: 'anonymous' })
|
||||
const rdn = new dn.RDN({ cn: 'anonymous' })
|
||||
|
||||
c.ldap = {
|
||||
id: c.remoteAddress + ':' + c.remotePort,
|
||||
config: options,
|
||||
_bindDN: new DN({ rdns: [rdn] })
|
||||
_bindDN: new DN([rdn])
|
||||
}
|
||||
c.addListener('timeout', function () {
|
||||
log.trace('%s timed out', c.ldap.id)
|
||||
|
@ -314,9 +305,7 @@ function Server (options) {
|
|||
return c.ldap._bindDN
|
||||
})
|
||||
c.ldap.__defineSetter__('bindDN', function (val) {
|
||||
if (Object.prototype.toString.call(val) !== '[object LdapDn]') {
|
||||
throw new TypeError('DN required')
|
||||
}
|
||||
if (!(val instanceof DN)) { throw new TypeError('DN required') }
|
||||
|
||||
c.ldap._bindDN = val
|
||||
return val
|
||||
|
@ -324,78 +313,62 @@ function Server (options) {
|
|||
return c
|
||||
}
|
||||
|
||||
self.newConnection = function (conn) {
|
||||
// TODO: make `newConnection` available on the `Server` prototype
|
||||
// https://github.com/ldapjs/node-ldapjs/pull/727/files#r636572294
|
||||
setupConnection(conn)
|
||||
log.trace('new connection from %s', conn.ldap.id)
|
||||
function newConnection (c) {
|
||||
setupConnection(c)
|
||||
log.trace('new connection from %s', c.ldap.id)
|
||||
|
||||
conn.parser = new Parser({
|
||||
dtrace.fire('server-connection', function () {
|
||||
return [c.remoteAddress]
|
||||
})
|
||||
|
||||
c.parser = new Parser({
|
||||
log: options.log
|
||||
})
|
||||
conn.parser.on('message', function (req) {
|
||||
// TODO: this is mutating the `@ldapjs/message` objects.
|
||||
// We should avoid doing that. ~ jsumners 2023-02-16
|
||||
req.connection = conn
|
||||
req.logId = conn.ldap.id + '::' + req.messageId
|
||||
c.parser.on('message', function (req) {
|
||||
req.connection = c
|
||||
req.logId = c.ldap.id + '::' + req.messageID
|
||||
req.startTime = new Date().getTime()
|
||||
|
||||
log.debug('%s: message received: req=%j', conn.ldap.id, req.pojo)
|
||||
log.debug('%s: message received: req=%j', c.ldap.id, req.json)
|
||||
|
||||
const res = getResponse(req)
|
||||
if (!res) {
|
||||
log.warn('Unimplemented server method: %s', req.type)
|
||||
conn.destroy()
|
||||
c.destroy()
|
||||
return false
|
||||
}
|
||||
|
||||
// parse string DNs for routing/etc
|
||||
try {
|
||||
switch (req.protocolOp) {
|
||||
case Protocol.operations.LDAP_REQ_BIND: {
|
||||
req.name = DN.fromString(req.name)
|
||||
case Protocol.LDAP_REQ_BIND:
|
||||
req.name = dn.parse(req.name)
|
||||
break
|
||||
}
|
||||
|
||||
case Protocol.operations.LDAP_REQ_ADD:
|
||||
case Protocol.operations.LDAP_REQ_COMPARE:
|
||||
case Protocol.operations.LDAP_REQ_DELETE: {
|
||||
if (typeof req.entry === 'string') {
|
||||
req.entry = DN.fromString(req.entry)
|
||||
} else if (Object.prototype.toString.call(req.entry) !== '[object LdapDn]') {
|
||||
throw Error('invalid entry object for operation')
|
||||
}
|
||||
case Protocol.LDAP_REQ_ADD:
|
||||
case Protocol.LDAP_REQ_COMPARE:
|
||||
case Protocol.LDAP_REQ_DELETE:
|
||||
req.entry = dn.parse(req.entry)
|
||||
break
|
||||
}
|
||||
|
||||
case Protocol.operations.LDAP_REQ_MODIFY: {
|
||||
req.object = DN.fromString(req.object)
|
||||
case Protocol.LDAP_REQ_MODIFY:
|
||||
req.object = dn.parse(req.object)
|
||||
break
|
||||
}
|
||||
|
||||
case Protocol.operations.LDAP_REQ_MODRDN: {
|
||||
if (typeof req.entry === 'string') {
|
||||
req.entry = DN.fromString(req.entry)
|
||||
} else if (Object.prototype.toString.call(req.entry) !== '[object LdapDn]') {
|
||||
throw Error('invalid entry object for operation')
|
||||
}
|
||||
case Protocol.LDAP_REQ_MODRDN:
|
||||
req.entry = dn.parse(req.entry)
|
||||
// TODO: handle newRdn/Superior
|
||||
break
|
||||
}
|
||||
|
||||
case Protocol.operations.LDAP_REQ_SEARCH: {
|
||||
case Protocol.LDAP_REQ_SEARCH:
|
||||
req.baseObject = dn.parse(req.baseObject)
|
||||
break
|
||||
}
|
||||
|
||||
default: {
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return res.end(errors.LDAP_INVALID_DN_SYNTAX)
|
||||
if (self.strictDN) {
|
||||
return res.end(errors.LDAP_INVALID_DN_SYNTAX)
|
||||
}
|
||||
}
|
||||
|
||||
res.connection = conn
|
||||
res.connection = c
|
||||
res.logId = req.logId
|
||||
res.requestDN = req.dn
|
||||
|
||||
|
@ -403,10 +376,10 @@ function Server (options) {
|
|||
|
||||
let i = 0
|
||||
return (function messageIIFE (err) {
|
||||
function sendError (sendErr) {
|
||||
res.status = sendErr.code || errors.LDAP_OPERATIONS_ERROR
|
||||
function sendError (err) {
|
||||
res.status = err.code || errors.LDAP_OPERATIONS_ERROR
|
||||
res.matchedDN = req.suffix ? req.suffix.toString() : ''
|
||||
res.errorMessage = sendErr.message || ''
|
||||
res.errorMessage = err.message || ''
|
||||
return res.end()
|
||||
}
|
||||
|
||||
|
@ -415,8 +388,8 @@ function Server (options) {
|
|||
|
||||
function next () {} // stub out next for the post chain
|
||||
|
||||
self._postChain.forEach(function (cb) {
|
||||
cb.call(self, req, res, next)
|
||||
self._postChain.forEach(function (c) {
|
||||
c.call(self, req, res, next)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -431,20 +404,7 @@ function Server (options) {
|
|||
const next = messageIIFE
|
||||
if (chain.handlers[i]) { return chain.handlers[i++].call(chain.backend, req, res, next) }
|
||||
|
||||
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND && res.status === 0) {
|
||||
// 0 length == anonymous bind
|
||||
if (req.dn.length === 0 && req.credentials === '') {
|
||||
conn.ldap.bindDN = new DN({ rdns: [new RDN({ cn: 'anonymous' })] })
|
||||
} else {
|
||||
conn.ldap.bindDN = DN.fromString(req.dn)
|
||||
}
|
||||
}
|
||||
|
||||
// unbind clear bindDN for safety
|
||||
// conn should terminate on unbind (RFC4511 4.3)
|
||||
if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND && res.status === 0) {
|
||||
conn.ldap.bindDN = new DN({ rdns: [new RDN({ cn: 'anonymous' })] })
|
||||
}
|
||||
if (req.protocolOp === Protocol.LDAP_REQ_BIND && res.status === 0) { c.ldap.bindDN = req.dn }
|
||||
|
||||
return after()
|
||||
} catch (e) {
|
||||
|
@ -455,32 +415,32 @@ function Server (options) {
|
|||
}())
|
||||
})
|
||||
|
||||
conn.parser.on('error', function (err, message) {
|
||||
self.emit('error', new VError(err, 'Parser error for %s', conn.ldap.id))
|
||||
c.parser.on('error', function (err, message) {
|
||||
self.emit('error', new VError(err, 'Parser error for %s', c.ldap.id))
|
||||
|
||||
if (!message) { return conn.destroy() }
|
||||
if (!message) { return c.destroy() }
|
||||
|
||||
const res = getResponse(message)
|
||||
if (!res) { return conn.destroy() }
|
||||
if (!res) { return c.destroy() }
|
||||
|
||||
res.status = 0x02 // protocol error
|
||||
res.errorMessage = err.toString()
|
||||
return conn.end(res.toBer())
|
||||
return c.end(res.toBer())
|
||||
})
|
||||
|
||||
conn.on('data', function (data) {
|
||||
log.trace('data on %s: %s', conn.ldap.id, util.inspect(data))
|
||||
c.on('data', function (data) {
|
||||
log.trace('data on %s: %s', c.ldap.id, util.inspect(data))
|
||||
|
||||
conn.parser.write(data)
|
||||
c.parser.write(data)
|
||||
})
|
||||
} // end newConnection
|
||||
|
||||
this.routes = {}
|
||||
if ((options.cert || options.certificate) && options.key) {
|
||||
options.cert = options.cert || options.certificate
|
||||
this.server = tls.createServer(options, options.connectionRouter ? options.connectionRouter : self.newConnection)
|
||||
this.server = tls.createServer(options, newConnection)
|
||||
} else {
|
||||
this.server = net.createServer(options.connectionRouter ? options.connectionRouter : self.newConnection)
|
||||
this.server = net.createServer(newConnection)
|
||||
}
|
||||
this.server.log = options.log
|
||||
this.server.ldap = {
|
||||
|
@ -533,15 +493,7 @@ Object.defineProperties(Server.prototype, {
|
|||
} else {
|
||||
str = 'ldap://'
|
||||
}
|
||||
|
||||
let host = this.host
|
||||
// Node 18 switched family from returning a string to returning a number
|
||||
// https://nodejs.org/api/net.html#serveraddress
|
||||
if (addr.family === 'IPv6' || addr.family === 6) {
|
||||
host = '[' + this.host + ']'
|
||||
}
|
||||
|
||||
str += host + ':' + this.port
|
||||
str += this.host + ':' + this.port
|
||||
return str
|
||||
},
|
||||
configurable: false
|
||||
|
@ -561,7 +513,7 @@ module.exports = Server
|
|||
*/
|
||||
Server.prototype.add = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_ADD, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_ADD, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -576,7 +528,7 @@ Server.prototype.add = function (name) {
|
|||
*/
|
||||
Server.prototype.bind = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_BIND, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_BIND, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -591,7 +543,7 @@ Server.prototype.bind = function (name) {
|
|||
*/
|
||||
Server.prototype.compare = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_COMPARE, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_COMPARE, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -606,7 +558,7 @@ Server.prototype.compare = function (name) {
|
|||
*/
|
||||
Server.prototype.del = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_DELETE, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_DELETE, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -621,7 +573,7 @@ Server.prototype.del = function (name) {
|
|||
*/
|
||||
Server.prototype.exop = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_EXTENSION, name, args, true)
|
||||
return this._mount(Protocol.LDAP_REQ_EXTENSION, name, args, true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -636,7 +588,7 @@ Server.prototype.exop = function (name) {
|
|||
*/
|
||||
Server.prototype.modify = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_MODIFY, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_MODIFY, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -651,7 +603,7 @@ Server.prototype.modify = function (name) {
|
|||
*/
|
||||
Server.prototype.modifyDN = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_MODRDN, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_MODRDN, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -666,7 +618,7 @@ Server.prototype.modifyDN = function (name) {
|
|||
*/
|
||||
Server.prototype.search = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_SEARCH, name, args)
|
||||
return this._mount(Protocol.LDAP_REQ_SEARCH, name, args)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -680,7 +632,7 @@ Server.prototype.search = function (name) {
|
|||
*/
|
||||
Server.prototype.unbind = function () {
|
||||
const args = Array.prototype.slice.call(arguments, 0)
|
||||
return this._mount(Protocol.operations.LDAP_REQ_UNBIND, 'unbind', args, true)
|
||||
return this._mount(Protocol.LDAP_REQ_UNBIND, 'unbind', args, true)
|
||||
}
|
||||
|
||||
Server.prototype.use = function use () {
|
||||
|
@ -701,13 +653,13 @@ Server.prototype.after = function () {
|
|||
})
|
||||
}
|
||||
|
||||
// All these just re-expose the requisite net.Server APIs
|
||||
// All these just reexpose the requisite net.Server APIs
|
||||
Server.prototype.listen = function (port, host, callback) {
|
||||
if (typeof (port) !== 'number' && typeof (port) !== 'string') { throw new TypeError('port (number or path) required') }
|
||||
|
||||
if (typeof (host) === 'function') {
|
||||
callback = host
|
||||
host = '127.0.0.1'
|
||||
host = '0.0.0.0'
|
||||
}
|
||||
if (typeof (port) === 'string' && /^[0-9]+$/.test(port)) {
|
||||
// Disambiguate between string ports and file paths
|
||||
|
@ -750,10 +702,12 @@ Server.prototype.getConnections = function (callback) {
|
|||
}
|
||||
|
||||
Server.prototype._getRoute = function (_dn, backend) {
|
||||
assert.ok(dn)
|
||||
|
||||
if (!backend) { backend = this }
|
||||
|
||||
let name
|
||||
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') {
|
||||
if (_dn instanceof dn.DN) {
|
||||
name = _dn.toString()
|
||||
} else {
|
||||
name = _dn
|
||||
|
@ -780,10 +734,10 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
|
|||
Object.keys(this.routes).forEach(function (key) {
|
||||
const _dn = self.routes[key].dn
|
||||
// Ignore non-DN routes such as exop or unbind
|
||||
if (Object.prototype.toString.call(_dn) === '[object LdapDn]') {
|
||||
if (_dn instanceof dn.DN) {
|
||||
const reversed = _dn.clone()
|
||||
reversed.reverse()
|
||||
reversedRDNsToKeys[reversed.toString()] = key
|
||||
reversed.rdns.reverse()
|
||||
reversedRDNsToKeys[reversed.format()] = key
|
||||
}
|
||||
})
|
||||
const output = []
|
||||
|
@ -800,15 +754,17 @@ Server.prototype._sortedRouteKeys = function _sortedRouteKeys () {
|
|||
return this._routeKeyCache
|
||||
}
|
||||
|
||||
Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
||||
Server.prototype._getHandlerChain = function _getHandlerChain (req, res) {
|
||||
assert.ok(req)
|
||||
|
||||
fireDTraceProbe(req, res)
|
||||
|
||||
const self = this
|
||||
const routes = this.routes
|
||||
let route
|
||||
|
||||
// check anonymous bind
|
||||
if (req.protocolOp === Protocol.operations.LDAP_REQ_BIND &&
|
||||
if (req.protocolOp === Protocol.LDAP_REQ_BIND &&
|
||||
req.dn.toString() === '' &&
|
||||
req.credentials === '') {
|
||||
return {
|
||||
|
@ -820,7 +776,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
|||
const op = '0x' + req.protocolOp.toString(16)
|
||||
|
||||
// Special cases are exops, unbinds and abandons. Handle those first.
|
||||
if (req.protocolOp === Protocol.operations.LDAP_REQ_EXTENSION) {
|
||||
if (req.protocolOp === Protocol.LDAP_REQ_EXTENSION) {
|
||||
route = routes[req.requestName]
|
||||
if (route) {
|
||||
return {
|
||||
|
@ -833,7 +789,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
|||
handlers: [noExOpHandler]
|
||||
}
|
||||
}
|
||||
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_UNBIND) {
|
||||
} else if (req.protocolOp === Protocol.LDAP_REQ_UNBIND) {
|
||||
route = routes.unbind
|
||||
if (route) {
|
||||
return {
|
||||
|
@ -846,7 +802,7 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
|||
handlers: [defaultNoOpHandler]
|
||||
}
|
||||
}
|
||||
} else if (req.protocolOp === Protocol.operations.LDAP_REQ_ABANDON) {
|
||||
} else if (req.protocolOp === Protocol.LDAP_REQ_ABANDON) {
|
||||
return {
|
||||
backend: self,
|
||||
handlers: [defaultNoOpHandler]
|
||||
|
@ -854,11 +810,11 @@ Server.prototype._getHandlerChain = function _getHandlerChain (req) {
|
|||
}
|
||||
|
||||
// Otherwise, match via DN rules
|
||||
assert.ok(req.dn)
|
||||
const keys = this._sortedRouteKeys()
|
||||
let fallbackHandler = [noSuffixHandler]
|
||||
// invalid DNs in non-strict mode are routed to the default handler
|
||||
const testDN = (typeof (req.dn) === 'string') ? DN.fromString(req.dn) : req.dn
|
||||
assert.ok(testDN)
|
||||
const testDN = (typeof (req.dn) === 'string') ? '' : req.dn
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const suffix = keys[i]
|
||||
|
@ -905,7 +861,7 @@ Server.prototype._mount = function (op, name, argv, notDN) {
|
|||
backend = argv[0]
|
||||
index = 1
|
||||
}
|
||||
const route = this._getRoute(notDN ? name : DN.fromString(name), backend)
|
||||
const route = this._getRoute(notDN ? name : dn.parse(name), backend)
|
||||
|
||||
const chain = this._chain.slice()
|
||||
argv.slice(index).forEach(function (a) {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
const querystring = require('querystring')
|
||||
const url = require('url')
|
||||
const { DN } = require('@ldapjs/dn')
|
||||
const filter = require('@ldapjs/filter')
|
||||
const dn = require('./dn')
|
||||
const filter = require('./filters/')
|
||||
|
||||
module.exports = {
|
||||
|
||||
|
@ -38,7 +38,7 @@ module.exports = {
|
|||
|
||||
if (u.pathname) {
|
||||
u.pathname = querystring.unescape(u.pathname.substr(1))
|
||||
u.DN = parseDN ? DN.fromString(u.pathname) : u.pathname
|
||||
u.DN = parseDN ? dn.parse(u.pathname) : u.pathname
|
||||
}
|
||||
|
||||
if (u.search) {
|
||||
|
|
67
package.json
67
package.json
|
@ -3,57 +3,50 @@
|
|||
"name": "ldapjs",
|
||||
"homepage": "http://ldapjs.org",
|
||||
"description": "LDAP client and server APIs",
|
||||
"version": "3.0.7",
|
||||
"version": "2.2.1",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/ldapjs/node-ldapjs.git"
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"directories": {
|
||||
"lib": "./lib"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ldapjs/asn1": "^2.0.0",
|
||||
"@ldapjs/attribute": "^1.0.0",
|
||||
"@ldapjs/change": "^1.0.0",
|
||||
"@ldapjs/controls": "^2.1.0",
|
||||
"@ldapjs/dn": "^1.1.0",
|
||||
"@ldapjs/filter": "^2.1.1",
|
||||
"@ldapjs/messages": "^1.3.0",
|
||||
"@ldapjs/protocol": "^1.2.1",
|
||||
"abstract-logging": "^2.0.1",
|
||||
"abstract-logging": "^2.0.0",
|
||||
"asn1": "^0.2.4",
|
||||
"assert-plus": "^1.0.0",
|
||||
"backoff": "^2.5.0",
|
||||
"ldap-filter": "^0.3.3",
|
||||
"once": "^1.4.0",
|
||||
"vasync": "^2.2.1",
|
||||
"verror": "^1.10.1"
|
||||
"vasync": "^2.2.0",
|
||||
"verror": "^1.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fastify/pre-commit": "^2.0.2",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.0.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"front-matter": "^4.0.2",
|
||||
"get-port": "^5.1.1",
|
||||
"highlight.js": "^11.7.0",
|
||||
"marked": "^4.2.12",
|
||||
"tap": "^16.3.7"
|
||||
"husky": "^4.2.5",
|
||||
"snazzy": "^9.0.0",
|
||||
"standard": "^16.0.0",
|
||||
"tap": "14.10.8"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "tap --no-cov -R terse",
|
||||
"test:ci": "tap --coverage-report=lcovonly -R terse",
|
||||
"test:cov": "tap -R terse",
|
||||
"test:cov:html": "tap --coverage-report=html -R terse",
|
||||
"test:watch": "tap -n -w --no-coverage-report -R terse",
|
||||
"test:integration": "tap --no-cov -R terse 'test-integration/**/*.test.js'",
|
||||
"test:integration:local": "docker-compose up -d --wait && npm run test:integration ; docker-compose down",
|
||||
"lint": "eslint . --fix",
|
||||
"lint:ci": "eslint .",
|
||||
"docs": "node scripts/build-docs.js"
|
||||
"test": "tap --no-cov",
|
||||
"test:ci": "tap --coverage-report=lcovonly",
|
||||
"test:cov": "tap",
|
||||
"test:cov:html": "tap --coverage-report=html",
|
||||
"test:watch": "tap -n -w --no-coverage-report",
|
||||
"test:integration": "tap --no-cov 'test-integration/**/*.test.js'",
|
||||
"test:integration:local": "docker-compose up -d && npm run test:integration && docker-compose down",
|
||||
"lint": "standard | snazzy",
|
||||
"lint:ci": "standard"
|
||||
},
|
||||
"pre-commit": [
|
||||
"lint:ci",
|
||||
"test"
|
||||
]
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint && npm run test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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