-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.php
471 lines (386 loc) · 19 KB
/
index.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
<?php
if (!function_exists('error_get_last_message')) { function error_get_last_message() { $error = error_get_last(); return $error['message']; }}
if (!function_exists('is_dn')) { function is_dn($dn) {
if (!is_string($dn) || (false==($parts = preg_split('/(?<!\\\\),/', $dn)))) return false;
foreach($parts as $part) if (!preg_match('#^\w+=([^,+"\\\\<>;=/]|\\\\[,+"\\\\<>;=/])+$#',$part)) return false;
return true;
}}
/**
* LDAPaaS - LDAP as a Service
*
* This compact implementation allows simple orchestration of LDAP instances
*
* @author MrTrick
* @copyright 2015 MrTrick
* @license MIT
* @version 0.0.1
* @url http://github.com/mrtrick/ldapaas
* @dependency ZendFramework 1.12
*/
class LDAPaaS {
const VERSION = '0.0.1';
const STATUS_RUNNING = "running"; //PID file exists, and corresponding process is running
const STATUS_DEAD = "dead"; //PID file exists, but no corresponding process is running
const STATUS_STOPPED = "stopped"; //No PID file exists
/** @var Zend_Log */
protected $log = null;
/** @var Zend_Config */
protected $config;
/** @var string */
protected $path;
//------------------------------------------------------------------------------
// Constructor
//------------------------------------------------------------------------------
public function __construct() {
//Configuration
require_once('Zend/Config/Ini.php');
$config = new Zend_Config_Ini('defaults.ini', null, true);
$config->merge(new Zend_Config_Ini('config.ini', null, false));
$config->setReadOnly();
$this->config = $config;
//Sanity check; that all required configuration items have been defined
$_conf = $config->toArray();
$_check = function($v, $k) {if ($v === "REQUIRED") throw new UnexpectedValueException("Must override config item \"$k\"", 500);};
array_walk_recursive($_conf, $_check);
//Logging
require_once('Zend/Log.php');
$log = Zend_Log::factory($config->log);
//Check access to the LDAP instance directory
if (!is_writable($config->ldap->path)) throw new RuntimeException("Not enough access to LDAP instance directory", 500);
$this->path = realpath($config->ldap->path);
}
//------------------------------------------------------------------------------
// Routes
//------------------------------------------------------------------
protected static $routes = array(
'GET /' => 'routeIndex',
'DELETE /' => 'routeReset',
'PUT /' => 'routeCreate',
'GET /(?P<name>\w+)' => 'routeRead',
'DELETE /(?P<name>\w+)' => 'routeDelete',
'POST /(?P<name>\w+)/restart' => 'routeRestart',
'POST /(?P<name>\w+)/purge' => 'routePurge'
);
public function route(Zend_Controller_Request_Http $request) {
//Require a user for all routes
$user = $request->getServer('REMOTE_USER', $request->getServer('PHP_AUTH_USER'));
if (!$user or !preg_match("/^\\w+$/", $user)) throw new InvalidArgumentException("Invalid user", 403);
$request->setParam('user', $user);
//Allow the method determination to be overridden by a 'method' parameter
$method = $request->getParam('method', $request->getMethod());
//Find a route handler for the method and path
$route = $method.' /'.trim($request->getPathInfo(), '/');
foreach(self::$routes as $pattern=>$method) {
if (preg_match('#^'.$pattern.'$#', $route, $params)) {
//Push any parameters into the request
$request->setParams($params);
//And run it
return $this->$method($request);
}
}
//No route matched?
throw new InvalidArgumentException("Route not found", 404);
}
protected function routeCreate(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user'); //Authenticated user
$port = $this->getNextPort(); //Next available port
$host = $this->config->ldap->host ? $this->config->ldap->host : gethostname();
//The LDAP server, which due to proxies etc may be different to the HTTP server
$name = $user.$port; //Create name as userPORT
$base_dn = $request->get('base_dn'); //Using the given base_dn
return $this->create($name, $user, $host, $port, $base_dn);
}
protected function routeRead(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user');
$name = $request->getParam('name');
$instance = $this->read($name);
if ($user !== $instance->user) throw new InvalidArgumentException("Access to this instance is forbidden", 403);
return $instance;
}
protected function routeDelete(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user');
$name = $request->getParam('name');
$instance = $this->read($name);
if ($user !== $instance->user) throw new InvalidArgumentException("Access to this instance is forbidden", 403);
return $this->delete($name);
}
protected function routeIndex(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user');
return $this->readMany($user);
}
protected function routeReset(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user');
return $this->deleteMany($user);
}
protected function routeRestart(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user');
$name = $request->getparam('name');
$instance = $this->read($name);
if ($user !== $instance->user) throw new InvalidArgumentException("Access to this instance is forbidden", 403);
return $this->restart($name);
}
protected function routePurge(Zend_Controller_Request_Http $request) {
$user = $request->getParam('user');
$name = $request->getParam('name');
$instance = $this->read($name);
if ($user !== $instance->user) throw new InvalidArgumentException("Access to this instance is forbidden", 403);
return $this->purge($name);
}
//------------------------------------------------------------------------------
// Utility Functions
//------------------------------------------------------------------------------
public function getLog() { return $this->log; }
/**
* Find the next TCP port not in use
* @throws RuntimeException If a port cannot be obtained
* @return int Port number
*/
protected function getNextPort() {
$start = $this->config->ldap->ports->start;
$finish = $start + $this->config->ldap->ports->max;
//Get a list of all ports in use in the range
exec('netstat -ntl', $outputs, $ret);
if ($ret !== 0) throw new RuntimeException("Could not check ports", 500, new Exception(implode("\n", $outputs)));
$_extract = function($line) { return preg_match('/:(\\d+)\s+/',$line,$m) ? $m[1] : false; };
$_filter = function($port) use ($start, $finish) { return $port && $port >= $start && $port <= $finish; };
$ports = array_flip(array_filter(array_unique(array_map($_extract, $outputs)), $_filter));
//Return the first available port
for($port=$start; $port<$finish; $port++) if (!array_key_exists($port, $ports)) return $port;
//Couldn't find any within the allowable range?
throw new RuntimeException("Could not find an available port", 500, new Exception("All ports in use between $start and $finish"));
}
/**
* Create an LDAP instance
* @param string $name Instance name
* @param string $user
* @param string $host
* @param int $port
* @param string $base_dn
* @param string $admin_password
* @throws InvalidArgumentException If any parameters are invalid
* @throws RuntimeException If creating the instance fails
* @return StdClass The instance details
*/
protected function create($name, $user, $host, $port, $base_dn, $admin_password=null) {
//Validate inputs
if (!$name or !preg_match("/^\\w+$/", $name)) throw new InvalidArgumentException("Invalid name '$name'", 403);
if (!$user or !preg_match("/^\\w+$/", $user)) throw new InvalidArgumentException("Invalid user", 403);
if (!$host) throw new InvalidArgumentException("Invalid host", 403);
if (!$port or !is_numeric($port)) throw new InvalidArgumentException("Invalid port", 403);
if (!$base_dn or !is_dn($base_dn)) throw new InvalidArgumentException("Invalid base_dn parameter", 403);
//Get / generate other details
$path = $this->path.'/'.$name;
$password = is_null($admin_password) ? substr(base64_encode(md5(microtime())),0,15) : $admin_password;
//Create a directory for that instance
if (file_exists($path)) throw new RuntimeException("Folder for '$name' already exists", 500);
else if (!@mkdir($path)) throw new RuntimeException("Could not create folder for '$name'", 500, new Exception(error_get_last_message()));
//Create an install file for that instance
$inf = <<<INF
[General]
FullMachineName=$host
ServerRoot=$path
ConfigDirectoryAdminID=admin
ConfigDirectoryAdminPwd=$password
[slapd]
ServerPort=$port
ServerIdentifier=$name
Suffix=$base_dn
RootDN=cn=Directory Manager
RootDNPwd=$password
sysconfdir=$path/etc
localstatedir=$path/var
inst_dir=$path/slapd-$name
config_dir=$path/etc/dirsrv/slapd-$name
datadir=$path/usr/share
initconfig_dir=$path
run_dir=$path/run
sbin_dir=$path
db_dir=$path/db
ldif_dir=$path/ldif
bak_dir=$path/bak
INF;
if (!@file_put_contents($path.'/install.inf', $inf))
throw new RuntimeException("Could not create install file for '$name'", 500, new Exception(error_get_last_message()));
//Store the instance details
$details = (object)compact('name','user','host','port','base_dn','password');
if (!@file_put_contents($path.'/details.json', json_encode($details)))
throw new RuntimeException("Could not store details for '$name'", 500, new Exception(error_get_last_message()));
//Run the installer
exec("setsid /usr/sbin/setup-ds.pl --file=$path/install.inf --silent --logfile=$path/setup.log 2>&1", $output, $res);
if ($res !== 0)
throw new RuntimeException("Could not create instance", 500, new Exception(implode("\n",$output)));
//Determine service status
$status = $this->status($name);
if ($status)
$details->status = $status;
return $details;
}
/**
* Read the instance details
* @param string $name Instance name
* @throws InvalidArgumentException If the name is invalid
* @throws RuntimeException If the instance cannot be read
* @return StdClass Instance details
*/
protected function read($name) {
//Validate inputs
if (!$name or !preg_match("/^\\w+$/", $name)) throw new InvalidArgumentException("Invalid name '$name'", 403);
//Find the instance
$path = $this->path.'/'.$name;
if (!file_exists($path)) throw new RuntimeException("'$name' not found", 404);
//Read the details
if (!is_readable($path.'/details.json')) throw new RuntimeException("Could not read '$name'", 500, new Exception("File missing or unreadable"));
$content = @file_get_contents($path.'/details.json');
if (!$content) throw new RuntimeException("Could not read '$name'", 500, new Exception(error_get_last_message()));
$details = json_decode($content);
if ($details === false) throw new RuntimeException("Could not read '$name'", 500, new Exception(json_last_error_msg()));
//Determine service status
$status = $this->status($name);
if ($status)
$details->status = $status;
return $details;
}
protected function status($name) {
//Validate inputs
if (!$name or !preg_match("/^\\w+$/", $name)) throw new InvalidArgumentException("Invalid name '$name'", 403);
//Find the instance
$path = $this->path.'/'.$name;
if (!file_exists($path)) throw new RuntimeException("'$name' not found", 404);
//Determine service status
$pidfile = "$path/run/slapd-$name.pid";
if (file_exists($pidfile)) {
$pid = file_get_contents($pidfile);
exec("kill -0 $pid", $output, $res);
if ($res === 0)
$status = self::STATUS_RUNNING;
else if ($res === 1)
$status = self::STATUS_DEAD;
}
else {
$status = self::STATUS_STOPPED;
}
return $status;
}
/**
* Delete the instance
* @param string $name Instance name
* @throws InvalidArgumentException If the name is invalid
* @throws RuntimeException If the instance cannot be deleted
*/
protected function delete($name) {
//Validate inputs
if (!$name or !preg_match("/^\\w+$/", $name)) throw new InvalidArgumentException("Invalid name '$name'", 403);
$ret = array();
//Find the instance
$path = $this->path.'/'.$name;
if (!file_exists($path)) throw new RuntimeException("Could not find folder for '$name'", 500);
$status = $this->status($name);
if ($status === self::STATUS_RUNNING) {
//Stop it
exec("/usr/sbin/stop-dirsrv -d $path $name 2>&1", $output, $res);
if ($res !== 0 && $res !== 2) //Server was stopped (0) or was not already running (2)
throw new RuntimeException("Could not stop instance", 500, new Exception(implode("\n",$output)));
}
else {
$ret['message'] = "Instance not stopped: status before deletion was '".$status."'";
}
//Remove the instance folder
exec("rm $path -r", $output, $res);
if ($res !== 0)
throw new RuntimeException("Could not remove instance", 500, new Exception(implode("\n",$output)));
$ret['success'] = true;
return (object)$ret;
}
/**
* Restart the instance
* @param string $name Instance name
* @throws InvalidArgumentException If the name is invalid
* @throws RuntimeException If the instance cannot be restarted
*/
protected function restart($name) {
//Validate inputs
if (!$name or !preg_match("/^\\w+$/", $name)) throw new InvalidArgumentException("Invalid name '$name'", 403);
//Find the instance
$path = $this->path.'/'.$name;
if (!file_exists($path)) throw new RuntimeException("Could not find folder for '$name'", 500);
//Stop it
exec("setsid /usr/sbin/restart-dirsrv -d $path $name 2>&1", $output, $res);
//Result:
// 0 - Process has been successfully halted and started again
// 1 - Process has been successfully halted, but could not
// 2 - Process was not running, but has successfully been started
// 3 - Process could not be stopped
if ($res !== 0 and $res !== 2)
throw new RuntimeException("Could not restart instance", 500, new Exception(implode("\n",$output)));
return (object)array('success'=>true);
}
/**
* Purge the instance (delete and recreate with the same settings)
* @param string $name Instance name
* @throws InvalidArgumentException If the name is invalid
* @return StdClass Instance details @see create()
*/
protected function purge($name) {
//Validate inputs
if (!$name or !preg_match("/^\\w+$/", $name)) throw new InvalidArgumentException("Invalid name '$name'", 403);
//Read the instance configuration
$instance = $this->read($name);
//Delete the existing instance - throws on failure, continues on success
$deleteResult = $this->delete($name);
//Create a new instance with the original configuration
$createResult = $this->create($instance->name, $instance->user, $instance->host, $instance->port, $instance->base_dn, $instance->password);
if (!empty($deleteResult->message) && empty($createResult->message)) $createResult->message = $deleteResult->message;
return $createResult;
}
protected function readMany($filter = '') {
//Validate inputs
if ($filter and !preg_match("/^\\w+$/", $filter)) throw new InvalidArgumentException("Invalid filter", 403);
//Get the list of instance names matching the filter
$offset = strlen($this->path) + 1;
$names = array_map(function($path) use ($offset) { return substr($path, $offset); }, glob($this->path.'/'.$filter.'*'));
//Read each instance
$results = new stdClass;
foreach($names as $name) $results->$name = $this->read($name);
return $results;
}
protected function deleteMany($filter = '') {
//Validate inputs
if ($filter and !preg_match("/^\\w+$/", $filter)) throw new InvalidArgumentException("Invalid filter", 403);
//Get the list of instance names matching the filter
$offset = strlen($this->path) + 1;
$names = array_map(function($path) use ($offset) { return substr($path, $offset); }, glob($this->path.'/'.$filter.'*'));
//Delete each instance
$results = new stdClass;
foreach($names as $name) $results->$name = $this->delete($name);
return $results;
}
}
//-----------------------------------------------------------------------------
//Set up request/response objects
require_once('Zend/Controller/Request/Http.php');
require_once('Zend/Controller/Response/Http.php');
$request = new Zend_Controller_Request_Http();
$response = new Zend_Controller_Response_Http();
$response->setHeader('Content-Type', 'application/json');
//Route the incoming request, and send back the response
try {
chdir(__DIR__);
$ldapaas = new LDAPaaS();
$output = $ldapaas->route($request);
$response->setBody(json_encode($output));
$response->sendResponse();
}
//If anything goes wrong, log it, and return an error!
catch(Exception $e) {
//What kind of error occurred?
$code = $e->getCode();
$code = ($code >= 400 && $code <= 599) ? $code : 500;
//Log it
$log = isset($ldapaas) && $ldapaas->getLog();
if ($log) $log->err($e);
else error_log( (string)$e );
//Return it
$response->setHttpResponseCode($code);
$response->setBody(json_encode((object)array("code"=>$code, "error"=>$e->getMessage())));
$response->sendResponse();
}