isundil 4 years ago
parent
commit
df003d19f8

+ 13 - 0
.gitignore

@@ -0,0 +1,13 @@
+/config.js
+/src/*.js
+/src/*.js.map
+/routes/*.js
+/routes/*.js.map
+/app.js
+/app.js.map
+/.vs
+/bin
+/node_modules
+/obj
+/package-lock.json
+*.swp

+ 119 - 0
LDAPManager.njsproj

@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
+  <PropertyGroup>
+    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
+    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
+    <Name>LDAPManager</Name>
+    <RootNamespace>LDAPManager</RootNamespace>
+  </PropertyGroup>
+  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
+  <PropertyGroup>
+    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
+    <SchemaVersion>2.0</SchemaVersion>
+    <ProjectGuid>82f43a22-9411-4b31-ac19-35fafe99caa9</ProjectGuid>
+    <ProjectHome>.</ProjectHome>
+    <StartupFile>app.js</StartupFile>
+    <SearchPath>
+    </SearchPath>
+    <WorkingDirectory>.</WorkingDirectory>
+    <OutputPath>.</OutputPath>
+    <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
+    <ProjectTypeGuids>{3AF33F2E-1136-4D97-BBB7-1795711AC8B8};{349c5851-65df-11da-9384-00065b846f21};{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}</ProjectTypeGuids>
+    <NodejsPort>1337</NodejsPort>
+    <EnableTypeScript>true</EnableTypeScript>
+    <StartWebBrowser>true</StartWebBrowser>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
+    <DebugSymbols>true</DebugSymbols>
+  </PropertyGroup>
+  <ItemGroup>
+    <Content Include="public\javascripts\category.js">
+      <SubType>Code</SubType>
+    </Content>
+    <Content Include="views\category.pug">
+      <SubType>Code</SubType>
+    </Content>
+    <None Include="app.ts" />
+    <None Include="routes\index.ts" />
+    <None Include="routes\category.ts" />
+    <Content Include="config.js">
+      <SubType>Code</SubType>
+    </Content>
+    <Content Include="libman.json" />
+    <Content Include="tsconfig.json" />
+    <Content Include="package.json" />
+    <Content Include="public\stylesheets\main.css" />
+    <Content Include="README.md" />
+    <Content Include="views\login.pug" />
+    <Content Include="views\index.pug" />
+    <Content Include="views\layout.pug" />
+    <Content Include="views\error.pug" />
+  </ItemGroup>
+  <ItemGroup>
+    <Folder Include="public\" />
+    <Folder Include="public\images\" />
+    <Folder Include="public\javascripts\" />
+    <Folder Include="public\stylesheets\" />
+    <Folder Include="routes\" />
+    <Folder Include="src\" />
+    <Folder Include="views\" />
+  </ItemGroup>
+  <ItemGroup>
+    <TypeScriptCompile Include="routes\login.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\ConfigLoader.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\LDAPInterface.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+    <TypeScriptCompile Include="src\RouterUtils.ts" />
+    <TypeScriptCompile Include="src\Security.ts" />
+    <TypeScriptCompile Include="src\LDAPEntry.ts">
+      <SubType>Code</SubType>
+    </TypeScriptCompile>
+  </ItemGroup>
+  <Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
+  <ProjectExtensions>
+    <VisualStudio>
+      <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
+        <WebProjectProperties>
+          <UseIIS>False</UseIIS>
+          <AutoAssignPort>True</AutoAssignPort>
+          <DevelopmentServerPort>0</DevelopmentServerPort>
+          <DevelopmentServerVPath>/</DevelopmentServerVPath>
+          <IISUrl>http://localhost:48022/</IISUrl>
+          <NTLMAuthentication>False</NTLMAuthentication>
+          <UseCustomServer>True</UseCustomServer>
+          <CustomServerUrl>http://localhost:1337</CustomServerUrl>
+          <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
+        </WebProjectProperties>
+      </FlavorProperties>
+      <FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}" User="">
+        <WebProjectProperties>
+          <StartPageUrl>
+          </StartPageUrl>
+          <StartAction>CurrentPage</StartAction>
+          <AspNetDebugging>True</AspNetDebugging>
+          <SilverlightDebugging>False</SilverlightDebugging>
+          <NativeDebugging>False</NativeDebugging>
+          <SQLDebugging>False</SQLDebugging>
+          <ExternalProgram>
+          </ExternalProgram>
+          <StartExternalURL>
+          </StartExternalURL>
+          <StartCmdLineArguments>
+          </StartCmdLineArguments>
+          <StartWorkingDirectory>
+          </StartWorkingDirectory>
+          <EnableENC>False</EnableENC>
+          <AlwaysStartWebServerOnDebug>False</AlwaysStartWebServerOnDebug>
+        </WebProjectProperties>
+      </FlavorProperties>
+    </VisualStudio>
+  </ProjectExtensions>
+</Project>

+ 6 - 0
LDAPManager.njsproj.user

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
+  </PropertyGroup>
+</Project>

+ 25 - 0
LDAPManager.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.31205.134
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9092AA53-FB77-4645-B42D-1CCCA6BD08BD}") = "LDAPManager", "LDAPManager.njsproj", "{82F43A22-9411-4B31-AC19-35FAFE99CAA9}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{82F43A22-9411-4B31-AC19-35FAFE99CAA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{82F43A22-9411-4B31-AC19-35FAFE99CAA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{82F43A22-9411-4B31-AC19-35FAFE99CAA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{82F43A22-9411-4B31-AC19-35FAFE99CAA9}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {115F6147-14AD-462E-B9C1-9CCFAFD0D8B7}
+	EndGlobalSection
+EndGlobal

+ 3 - 0
README.md_

@@ -0,0 +1,3 @@
+# LDAPManager
+
+

+ 75 - 0
app.ts

@@ -0,0 +1,75 @@
+import * as express from 'express';
+import { AddressInfo } from "net";
+import * as path from 'path';
+import * as bodyParser from 'body-parser';
+import route_category from './routes/category';
+import { route_login, route_logout } from './routes/login';
+import LDAPFactory from './src/ldapInterface';
+
+const debug = require('debug')('my express app');
+const app = express();
+
+declare global {
+    namespace Express {
+        interface Request {
+            ldapManager: LDAPFactory
+        }
+    }
+}
+
+// view engine setup
+app.set('views', path.join(__dirname, 'views'));
+app.set('view engine', 'pug');
+
+app.use(express.static(path.join(__dirname, 'public')));
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: true }));
+
+app.use((req, res, next) => {
+    req.ldapManager = new LDAPFactory();
+    res.socket && res.socket.once('close', () => {
+        req.ldapManager.Release();
+    });
+    next();
+});
+
+//app.use('/', route_index);
+app.use('/', route_category);
+app.use('/login', route_login);
+app.use('/logout', route_logout);
+
+// catch 404 and forward to error handler
+app.use((req, res, next) => {
+    const err: any = new Error('Not Found');
+    err['status'] = 404;
+    next(err);
+});
+// error handlers
+
+// development error handler
+// will print stacktrace
+if (app.get('env') === 'development') {
+    app.use((err: any, req: Express.Request, res: any, next: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
+        res.status(err[ 'status' ] || 500);
+        res.render('error', {
+            message: err.message,
+            error: err
+        });
+    });
+}
+
+// production error handler
+// no stacktraces leaked to user
+app.use((err: any, req: any, res: any, next: any) => { // eslint-disable-line @typescript-eslint/no-unused-vars
+    res.status(err.status || 500);
+    res.render('error', {
+        message: err.message,
+        error: {}
+    });
+});
+
+app.set('port', process.env.PORT || 3000);
+
+const server = app.listen(app.get('port'), function () {
+    debug(`Express server listening on port ${(server.address() as AddressInfo).port}`);
+});

+ 5 - 0
libman.json

@@ -0,0 +1,5 @@
+{
+  "version": "1.0",
+  "defaultProvider": "cdnjs",
+  "libraries": []
+}

+ 31 - 0
package.json

@@ -0,0 +1,31 @@
+{
+  "name": "ldapmanager",
+  "version": "0.0.0",
+  "private": true,
+  "scripts": {
+    "build": "tsc --build",
+    "clean": "tsc --build --clean",
+    "start": "node app"
+  },
+  "description": "LDAPManager",
+  "author": {
+    "name": ""
+  },
+  "main": "app.js",
+  "dependencies": {
+    "body-parser": "^1.19.0",
+    "debug": "^2.2.0",
+    "express": "^4.14.0",
+    "ldapjs": "^2.2.4",
+    "pug": "^2.0.0-rc.3"
+  },
+  "devDependencies": {
+    "@types/debug": "0.0.30",
+    "@types/express": "^4.0.37",
+    "@types/express-serve-static-core": "^4.0.50",
+    "@types/mime": "^1.3.1",
+    "@types/serve-static": "^1.7.32",
+    "@types/node": "^14.14.7",
+    "typescript": "^4.0.5"
+  }
+}

+ 8 - 0
public/javascripts/category.js

@@ -0,0 +1,8 @@
+(() => {
+	let selection = null;
+
+	if (document.location.hash !== null &&
+		(selection = document.getElementById(document.location.hash.replace("#", ""))) !== null) {
+		selection.classList.add("selected");
+	}
+})();

+ 11 - 0
public/stylesheets/main.css

@@ -0,0 +1,11 @@
+body {
+    padding: 50px;
+    font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+
+a {
+    color: #00B7FF;
+}
+.category-list .category.selected {
+    background-color: #dbfffd;
+}

+ 59 - 0
routes/category.ts

@@ -0,0 +1,59 @@
+/*
+ * GET users listing.
+ */
+import express = require('express');
+const router = express.Router();
+import Security from '../src/Security';
+import { ILDAPManager } from '../src/ldapInterface';
+import LDAPEntry from '../src/LDAPEntry';
+import ConfigManager, { LDAPAttribute, LDAPCategory } from '../src/ConfigLoader';
+
+function GetCurrentCategory(req: express.Request, defaultResult: LDAPCategory): LDAPCategory {
+    if (!req.query["category"])
+        return defaultResult;
+    let query = Array.isArray(req.query["category"]) ? req.query["category"][0] : req.query["category"];
+    return LinkToCategory(query.toString()) || defaultResult;
+}
+
+function LinkToCategory(query: string): LDAPCategory | null {
+    return ConfigManager.GetInstance().GetCategoryByName(query);
+}
+
+function CategoryToLink(category: LDAPCategory): string {
+    return category.GetName();
+}
+
+function DnToLink(dn: string): string {
+    return dn.replace(new RegExp("=", "g"), "-").replace(new RegExp("[^a-zA-Z\-]", "g"), "_");
+}
+
+router.get('/', (req: express.Request, res: express.Response) => {
+    if (!Security.requireLoggedUser(req, res))
+        return;
+    req.ldapManager.GetInstance().then((ldap: ILDAPManager): void => {
+        let categories = ConfigManager.GetInstance().GetCategories();
+        let category = GetCurrentCategory(req, categories[0]);
+        ldap.ListEntries(category).then(items => {
+            res.render("category", {
+                categoryName: category.GetName(),
+                allCategories: categories.map((i) => { return { name: i.GetName(), lnk: "?category="+CategoryToLink(i) }}),
+                items: items,
+                attributes: category.GetAttributes(),
+                DnToLnk: DnToLink,
+                GetAttributeLink: function (attr: LDAPAttribute, entityDn: string) {
+                    if (attr.type.startsWith("entry")) {
+                        let link = attr.type.split("#", 2)[1];
+                        if (link)
+                            return "?category=" + link + "#" + DnToLink(entityDn);
+                    }
+                    return null;
+				},
+                GetAttributeValue: function (user: LDAPEntry, attribute: LDAPAttribute) {
+                    return user.GetAttributeByName(attribute.name);
+				}
+            });
+        });
+    });
+});
+
+export default router;

+ 11 - 0
routes/index.ts

@@ -0,0 +1,11 @@
+/*
+ * GET home page.
+ */
+import express = require('express');
+const router = express.Router();
+
+router.get('/', (req: express.Request, res: express.Response) => {
+    res.render('index', { title: 'Express' });
+});
+
+export default router;

+ 55 - 0
routes/login.ts

@@ -0,0 +1,55 @@
+/*
+ * GET home page.
+ */
+import express = require('express');
+import RouterUtils from '../src/RouterUtils';
+import Security from '../src/Security';
+import { ILDAPManager } from './../src/ldapInterface';
+
+const route_login = express.Router();
+const route_logout = express.Router();
+
+function ManageLogin(req: express.Request, res: express.Response, ldap: ILDAPManager|null, postData: { username: string, password: string }|null) {
+	let session = Security.GetOrCreateSession(req);
+
+	if (session.IsLoggedIn()) {
+		RouterUtils.Redirect(res, "/");
+		return;
+	}
+	let prevUsername = session.GetPreviousUsername() || "";
+
+	if (postData && postData.username && postData.password && postData.username.length && postData.password.length && ldap) {
+		prevUsername = postData.username;
+		Security.TryLogin(ldap, postData.username, postData.password).then(user => {
+			if (user) {
+				session.Login(user, postData.username);
+				RouterUtils.Redirect(res, "/");
+				return;
+			}
+			res.render('login', { previousUsername: prevUsername, loginFail: true });
+		}).catch(() => {
+			res.render('login', { previousUsername: prevUsername, loginFail: true });
+		});
+	} else {
+		res.render('login', { previousUsername: prevUsername, loginFail: false });
+	}
+}
+
+route_login.get('/', (req: express.Request, res: express.Response) => {
+	ManageLogin(req, res, null, null);
+});
+route_login.post('/', (req: express.Request, res: express.Response) => {
+	req.ldapManager.GetInstance().then(ldap => {
+		ManageLogin(req, res, ldap, { username: req.body.username, password: req.body.password });
+	});
+});
+route_logout.get('/', (req: express.Request, res: express.Response) => {
+	let session = Security.GetSession(req);
+	if (session)
+		session.Logout();
+	res.clearCookie("sessId");
+	RouterUtils.Redirect(res, "/");
+});
+
+export { route_login };
+export { route_logout };

+ 99 - 0
src/ConfigLoader.ts

@@ -0,0 +1,99 @@
+
+export class LDAPAttribute {
+	constructor(config: any) {
+		this.name = config["name"];
+		this.type = config["type"];
+		this.mapped = config["mapped"];
+		this.filter = config["filter"] ? new RegExp(config["filter"]) : null;
+	}
+	public readonly name: string;
+	public readonly type: string;
+	public readonly mapped: string;
+	public readonly filter: RegExp | null;
+}
+
+export class LDAPCategory {
+	constructor(config: any) {
+		this.fName = config["name"];
+		this.fBase = config["base"];
+		this.fScope = config["scope"];
+		this.fAttributes = new Array();
+		this.fFilter = config["filter"] || null;
+		for (let i of config["attributes"])
+			this.fAttributes.push(new LDAPAttribute(i));
+	}
+
+	public GetBaseDn(): string { return this.fBase; }
+	public GetScope(): string { return this.fScope; }
+	public GetName(): string { return this.fName; }
+	public GetFilter(): string|null { return this.fFilter; }
+	public GetAttributes(): Array<LDAPAttribute> { return this.fAttributes; }
+
+	private fName: string;
+	private fBase: string;
+	private fScope: string;
+	private fFilter: string|null;
+	private fAttributes: Array<LDAPAttribute>;
+}
+
+export interface IConfigLoader {
+	GetCategories(): Array<LDAPCategory>;
+	GetCategoryByName(name: string): LDAPCategory | null;
+	GetLDAPUrls(): Array<string>;
+	GetBindDn(): string;
+	GetBindPassword(): string;
+	GetLoginBase(): string;
+	GetLoginScope(): string;
+	GetLoginFilter(username: string): string;
+}
+
+class ConfigLoader implements IConfigLoader {
+	public constructor(config: any) {
+		this.fCategories = new Array();
+		for (let i of config.LDAP_CATEGORIES) {
+			this.fCategories.push(new LDAPCategory(i));
+		}
+		this.fServer.urls = typeof config["LDAP_URL"] === "string" ? [config["LDAP_URL"]] : config["LDAP_URL"];
+		this.fServer.bindDn = config["LDAP_BIND_DN"];
+		this.fServer.bindPassword = config["LDAP_BIND_PASSWD"];
+
+		this.fLogin.fBase = config["LDAP_LOGIN_BASE"];
+		this.fLogin.fScope = config["LDAP_LOGIN_SCOPE"];
+		this.fLogin.fFilter = config["LDAP_LOGIN_FILTER"];
+	}
+
+	public GetLDAPUrls(): Array<string> { return this.fServer.urls; }
+	public GetBindDn(): string { return this.fServer.bindDn; }
+	public GetBindPassword(): string { return this.fServer.bindPassword; }
+
+	public GetCategories(): Array<LDAPCategory> { return this.fCategories; }
+	public GetCategoryByName(name: string): LDAPCategory | null {
+		for (let i of this.fCategories)
+			if (i.GetName() === name)
+				return i;
+		return null;
+	}
+
+	public GetLoginBase(): string { return this.fLogin.fBase; }
+	public GetLoginScope(): string { return this.fLogin.fScope; }
+	public GetLoginFilter(login: string): string { return this.fLogin.fFilter(login); }
+
+	private fCategories: Array<LDAPCategory>;
+	private fServer = {
+		urls: [],
+		bindDn: "",
+		bindPassword: ""
+	};
+	private fLogin = {
+		fBase: "",
+		fScope: "one",
+		fFilter: (username: string):string => ""
+	};
+}
+let _instance: IConfigLoader = new ConfigLoader(require("../config.js"));
+
+export default class ConfigManager {
+	public static GetInstance(): IConfigLoader {
+		return _instance;
+	}
+}

+ 66 - 0
src/LDAPEntry.ts

@@ -0,0 +1,66 @@
+
+import { LDAPAttribute } from './ConfigLoader';
+
+class AttributeValue {
+	public stringValue: string|null = null;
+	public stringArrayValue: Array<string>|null = null;
+
+	public constructor(def: LDAPAttribute, item: any) {
+		if (def.type === "stringArray" || def.type.startsWith("entryArray#")) {
+			this.stringArrayValue = new Array();
+			if (item instanceof Array)
+				for (let i of item) {
+					if (!this.IsFiltered(def, i))
+						this.stringArrayValue.push(i);
+				}
+			else if (typeof (item) === "string") {
+				if (!this.IsFiltered(def, item))
+					this.stringArrayValue.push(item);
+			}
+		} else if (def.type === "string") {
+			if (item instanceof Array) {
+				this.stringValue = item[0];
+			}
+			else if (typeof (item) === "string") {
+				this.stringValue = item;
+			}
+		}
+	}
+
+	private IsFiltered(def: LDAPAttribute, value: string): boolean {
+		return !!(def.filter && !def.filter.test(value));
+	}
+}
+
+export default class LDAPEntry {
+	protected address: string;
+	protected attributes: Map<LDAPAttribute, AttributeValue|null>;
+
+	public constructor(attributes: Iterable<LDAPAttribute>, addr: string, userdata: Map<string, string>) {
+		this.address = addr;
+		this.attributes = new Map<LDAPAttribute, AttributeValue>();
+		for (var i of attributes)
+			this.attributes.set(i, new AttributeValue(i, userdata.get(i.mapped)));
+	}
+
+	private PutAttributeByName(attrName: string, attrValue: any): boolean {
+		for (let i of this.attributes.keys()) {
+			if (i.name === attrName) {
+				this.attributes.set(i, new AttributeValue(i, attrValue));
+				return true;
+			}
+		}
+		return false;
+	}
+
+	public GetAttributeByName(attrName: string): AttributeValue|null {
+		for (let [key, val] of this.attributes.entries())
+			if (key.name === attrName && val)
+				return val;
+		return null;
+	}
+
+	public GetAttributes(): Map<LDAPAttribute, AttributeValue|null> { return this.attributes; }
+
+	public GetAddress(): string { return this.address; }
+}

+ 20 - 0
src/RouterUtils.ts

@@ -0,0 +1,20 @@
+import express = require('express');
+
+export default class RouterUtil {
+	public static Redirect(res: express.Response, location: string) {
+		res.status(302);
+		res.setHeader("Location", location);
+		res.send("<!DOCTYPE html><html><body><a href='" + location + "'>" + location + "</a><script>document.location.href='" + location + "';</script></html>");
+	}
+
+	public static GetCookies(req: express.Request): Map<string, string> {
+		let cookies = req.headers.cookie;
+		let result = new Map<string, string>();
+		if (cookies)
+			cookies.split(";").forEach(i => {
+				let keyValue = i.split("=", 2);
+				result.set(keyValue[0].trim(), keyValue[1].trim());
+			});
+		return result;
+	}
+}

+ 109 - 0
src/Security.ts

@@ -0,0 +1,109 @@
+
+import express = require('express');
+import RouterUtils from './RouterUtils';
+import { ILDAPManager } from './ldapInterface';
+const crypto = require('crypto');
+
+const START_DATE_TS = (new Date()).getTime();
+
+class Session {
+	protected loggedAddress: string|null;
+	protected previousUsername: string|null;
+	protected key: string;
+
+	constructor(req: express.Request) {
+		let key = (new Date()).toISOString() + "__salt__" + req.connection.remoteAddress + "" + req.connection.remotePort + "" + START_DATE_TS;
+		this.key = crypto.createHash('sha1').update(key).digest('hex');
+		this.loggedAddress = this.previousUsername = null;
+	}
+
+	public Login(loggedAddress: string, username: string) {
+		this.loggedAddress = loggedAddress;
+		this.previousUsername = username;
+	}
+
+	public Expired(): Boolean {
+		return false; // FIXME
+	}
+
+	public IsLoggedIn(): boolean {
+		return !!this.loggedAddress;
+	}
+
+	public GetKey(): string {
+		return this.key;
+	}
+
+	public Logout() {
+		this.loggedAddress = null;
+	}
+
+	public GetPreviousUsername(): string|null {
+		return this.previousUsername;
+	}
+}
+
+class Security {
+	private static sessions: Map<string, Session> = new Map<string, Session>();
+
+	private CreateSession(req: express.Request): Session {
+		let session = new Session(req);
+		if (req.res)
+			req.res.cookie("sessId", session.GetKey());
+		Security.sessions.set(session.GetKey(), session);
+		console.info("Created session with key: " + session.GetKey());
+		return session;
+	}
+
+	public GetOrCreateSession(req: express.Request): Session {
+		let sess = this.GetSession(req);
+		if (sess !== null)
+			return sess;
+		return this.CreateSession(req);
+	}
+
+	private GetSessionById(sessId: string|null): Session|null {
+		if (!sessId || sessId.length == 0 || !Security.sessions.has(sessId))
+			return null;
+		let session = Security.sessions.get(sessId);
+		if (!session || session.Expired())
+			return null;
+		return session;
+	}
+
+	public GetSession(req: express.Request): Session|null {
+		let cookies = RouterUtils.GetCookies(req);
+		return cookies.has("sessId") ? this.GetSessionById(cookies.get("sessId") || null) : null;
+	}
+
+	public IsUserLogged(req: express.Request): boolean {
+		let session = this.GetOrCreateSession(req);
+		return session !== null && session.IsLoggedIn();
+	}
+
+	public requireLoggedUser(req: express.Request, res: express.Response) {
+		if (this.IsUserLogged(req))
+			return true;
+		RouterUtils.Redirect(res, "/login");
+		return false;
+	}
+
+	public TryLogin(ldap: ILDAPManager, username: string, password: string): Promise<string> {
+		return new Promise((ok, ko) => {
+			ldap.CheckLoginExists(username).then(userDn => {
+				if (userDn == null) {
+					ko();
+					return;
+				}
+				ldap.TryBind(userDn, password).then((success) => {
+					if (!success)
+						ko();
+					else
+						ok(userDn);
+				});
+			}).catch(ko);
+		});
+	}
+}
+
+export default new Security() as Security;

+ 155 - 0
src/ldapInterface.ts

@@ -0,0 +1,155 @@
+const ldapjs = require("ldapjs");
+import ConfigManager, { LDAPCategory } from "./ConfigLoader";
+import LDAPEntry from "./LDAPEntry";
+
+interface ILDAPManager {
+	Release(): Promise<void>;
+	/**
+	 * Try to login as user to check password
+	 * @param dn
+	 * @param password
+	 */
+	TryBind(dn: string, password: string): Promise<boolean>;
+	CheckLoginExists(user: string): Promise<string>;
+	ListEntries(category: LDAPCategory): Promise<Array<LDAPEntry>>;
+}
+
+class LDAPManager implements ILDAPManager {
+	protected cli: any;
+
+	public constructor(cli: any) {
+		this.cli = cli;
+	}
+
+	public static Create(): Promise<LDAPManager> {
+		return new Promise((ok, ko) => {
+			let config = ConfigManager.GetInstance();
+
+			let cli = ldapjs.createClient({
+				url: config.GetLDAPUrls()
+			});
+			cli.on('error', (err: any) => {
+				console.error("LDAP general error: ", err);
+				ko(err);
+			});
+			cli.bind(config.GetBindDn(), config.GetBindPassword(), (err: any) => {
+				if (err) {
+					console.error("LDAP bind error: ", err);
+					ko(err);
+				} else {
+					ok(new LDAPManager(cli));
+				}
+			});
+		});
+	}
+
+	public TryBind(userDn: string, password: string): Promise<boolean> {
+		return new Promise((ok, ko) => {
+			let cli = ldapjs.createClient({
+				url: ConfigManager.GetInstance().GetLDAPUrls()
+			});
+			cli.on('error', (err: any) => {
+				console.error("LDAP general error: ", err);
+				ko(err);
+			});
+			cli.bind(userDn, password, (err: any) => {
+				if (err) {
+					ok(false);
+				} else {
+					ok(true);
+				}
+			});
+		});
+	}
+
+	public Search(base: string, scope: string, filter: string|undefined, attributes: Array<string>): Promise<Map<string, Map<string, any>>> {
+		return new Promise((ok, ko) => {
+			this.cli.search(base, { scope: scope, filter: filter, attributes: attributes, paged: false }, (err: any, res: any) => {
+				if (err) {
+					console.error("Search error: ", { base: base, scope: scope, filter: filter, attributes: attributes });
+					ko(err);
+					return;
+				}
+				let result = new Map<string, any>();
+				let error = false;
+
+				res.on('searchEntry', (i: any) => {
+					if (error) return;
+					let LDAPEntry = new Map<string, any>();
+					for (let attr in i.object) {
+						let value = i.object[attr];
+						LDAPEntry.set(attr, value);
+					}
+					result.set(i.dn, LDAPEntry);
+				});
+				res.on('error', (err: any) => {
+					error = true;
+					console.error("Search error: ", { base: base, scope: scope, filter: filter, attributes: attributes });
+					ko(err);
+				});
+				res.on('end', () => {
+					if (error) return;
+					ok(result);
+				});
+			});
+		});
+	}
+
+	public CheckLoginExists(user: string): Promise<string> {
+		return new Promise((ok, ko) => {
+			let config = ConfigManager.GetInstance();
+			return this.Search(config.GetLoginBase(), config.GetLoginScope(), config.GetLoginFilter(user), []).then(result => {
+				if (!result || result.size !== 1)
+					ko();
+				for (let [addr, _] of result) {
+					ok(addr);
+				}
+			});
+		});
+	}
+
+	public ListEntries(category: LDAPCategory): Promise<Array<LDAPEntry>> {
+		return new Promise((ok, ko) => {
+			this.Search(category.GetBaseDn(), category.GetScope(), category.GetFilter() || undefined, category.GetAttributes().map(i => i.mapped)).then(result => {
+				let userArray = new Array();
+				for (let [addr, val] of result)
+					userArray.push(new LDAPEntry(category.GetAttributes(), addr, val));
+				ok(userArray);
+			});
+		});
+	}
+
+	public Release():Promise<void> {
+		return new Promise(ok => {
+			this.cli.unbind((err: any) => {
+				if (err)
+					console.error("LDAP unbind error: ", err);
+				ok();
+			});
+		});
+	}
+}
+
+export default class LDAPFactory {
+	private _instance: ILDAPManager|null = null;
+
+	public GetInstance(): Promise<ILDAPManager> {
+		if (!this._instance) {
+			return new Promise((ok, ko) => {
+				LDAPManager.Create().then(inst => {
+					this._instance = inst;
+					ok(this._instance);
+				});
+			});
+		}
+		return Promise.resolve(this._instance);
+	}
+
+	public Release(): Promise<null> {
+		if (this._instance)
+			return this._instance.Release().then(() => this._instance = null);
+		return Promise.resolve(null);
+	}
+}
+
+export { ILDAPManager };

+ 12 - 0
tsconfig.json

@@ -0,0 +1,12 @@
+{
+    "compilerOptions": {
+        "module": "commonjs",
+        "target": "es6",
+        "lib": [ "es6" ],
+        "sourceMap": true,
+        "strict": true
+    },
+    "exclude": [
+        "node_modules"
+    ]
+}

+ 33 - 0
views/category.pug

@@ -0,0 +1,33 @@
+extends layout
+
+block content
+  h1= categoryName
+  ul
+    for i in allCategories
+      li
+        a(href=i.lnk)= i.name
+  table(class='category-list')
+    tr
+      th Address
+      for i in attributes
+        th= i.name
+    for item in items
+      tr(id=DnToLnk(item.GetAddress()) class='category')
+        td= item.GetAddress()
+        for i in attributes
+          - var value = GetAttributeValue(item, i)
+          if value && value.stringValue
+              td= value.stringValue
+          else if value && value.stringArrayValue && value.stringArrayValue.length > 0
+              td
+                ul
+                  for j in value.stringArrayValue
+                    - var link = GetAttributeLink(i, j)
+                    if link
+                      li
+                        a(href=link)= j
+                    else
+                      li= j
+          else
+              td (empty)
+  script(src='/javascripts/category.js', language='javascript')

+ 6 - 0
views/error.pug

@@ -0,0 +1,6 @@
+extends layout 
+
+block content 
+  h1= message 
+  h2= error.status 
+  pre #{error.stack}

+ 5 - 0
views/index.pug

@@ -0,0 +1,5 @@
+extends layout
+
+block content
+  h1= title
+  p Welcome to #{title}

+ 7 - 0
views/layout.pug

@@ -0,0 +1,7 @@
+doctype html
+html
+  head
+    title= title
+    link(rel='stylesheet', href='/stylesheets/main.css')
+  body
+    block content

+ 10 - 0
views/login.pug

@@ -0,0 +1,10 @@
+extends layout
+
+block content
+  h1= title
+  form(method='post')
+    if loginFail
+      p Login fail
+    input(type='text' name='username' value=previousUsername placeholder='Username' required)
+    input(type='password' name='password' placeholder='Password' required)
+    input(type='submit' value='Login')