changeset 0:3ac7bd7495fd draft

Initial commit
author Ivo Smits <Ivo@UCIS.nl>
date Sat, 08 Nov 2014 22:22:42 +0100
parents
children caa68b502313
files anoclaims.php marccore.php marcus.php
diffstat 3 files changed, 977 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/anoclaims.php	Sat Nov 08 22:22:42 2014 +0100
@@ -0,0 +1,83 @@
+<?php
+require_once './marccore.php';
+error_reporting(E_ALL);
+if (!isset($argv)) $argv = $_SERVER['argv'];
+$argi = 1;
+$database = new MARCDatabaseFlatFile('anoclaims.db');
+$key = NULL;
+if (file_exists('anoclaims.key')) {
+	$key = file_get_contents('anoclaims.key');
+	if (strlen($key) != 32) $key = NULL;
+}
+switch (strtoupper($argv[$argi++])) {
+	case 'REGISTER':
+		if (is_null($key)) $key = randombytes(32);
+		$label = chr(0).nacl_crypto_sign_ed25519_keypair($key, $key);
+		$resource = array('label' => $label, 'value' => array('owner' => $argv[$argi++]));
+		if (!$database->UpdateResource($resource, $key)) throw new Exception('Could not update resource');
+		break;
+	case 'CLAIM':
+		if (is_null($key)) throw new Exception('Key not found');
+		$label = argtolabel($argv, $argi);
+		$resource = $database->GetResource($label);
+		if (!$resource) $resource = array('label' => $label, 'value' => array());
+		if (!$database->UpdateResource($resource, $key)) throw new Exception('Could not update resource');
+		break;
+	case 'SETNS':
+		if (is_null($key)) throw new Exception('Key not found');
+		$label = argtolabel($argv, $argi);
+		$resource = $database->GetResource($label);
+		if (!$resource) throw new Exception('Resource is not registered');
+		if (!is_array($resource['value'])) $resource['value'] = array();
+		if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
+		if (!isset($resource['value']['ns']) || !is_array($resource['value']['ns'])) $resource['value']['ns'] = array();
+		$nsname = $argv[$argi++];
+		if (strlen($nsname) && $nsname[strlen($nsname)-1] != '.') $resource['value']['ns'] = array($nsname => array());
+		else $resource['value']['ns'] = array($nsname => $argv[$argi++]);
+		if (!$database->UpdateResource($resource, $key)) throw new Exception('Could not update resource');
+		break;
+	case 'SYNC':
+		$database->SyncHTTP($argv[$argi++]);
+		break;
+	case 'HELP':
+		print_help();
+		break;
+	default:
+		throw new Exception('Unknown operation '.$argv[$argi-1]);
+}
+$database->Save();
+$database->Close();
+
+function argtolabel($argv, &$argi) {
+	$t = $argv[$argi++];
+	if (preg_match('/^AS[0-9]{1-9}$/', $t)) return chr(3).marc_decode_int32be(substr($argv[$argi++], 2));
+	if (preg_match('_^[0-9]{1-3}\.[0-9]{1-3}\.[0-9]{1-3}\.[0-9]{1-3}/[0-9]{1-2}$_', $t)) return ipv4tolabel($t);
+	if (preg_match('_^(((?=.*(::))(?!.*\3.+\3))\3?|([\dA-F]{1,4}(\3|:\b|$)|\2))(?4){5}((?4){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})\z/[0-9]{1-3}_i', $t)) return ipv6tolabel($t);
+	if (preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$/i', $t)) return chr(4).strtolower(trim($t, '.'));
+	throw new Exception('Could not detect label type for '.$t);
+}
+function ipnettolabel($s) {
+	$ip = inet_pton(strtok($s, '/'));
+	$pl = intval(strtok('/'));
+	if ($pl == 0) throw new Exception('Invalid IP network specified');
+	if (strlen($ip) == 4) return chr(1).$ip.chr($pl);
+	if (strlen($ip) == 16) return chr(2).$ip.chr($pl);
+}
+function randombytes($n) {
+	$b = '';
+	$file = fopen('/dev/urandom', 'r');
+	for ($i = 0; $i < $n; $i++) $b .= fgetc($file);
+	fclose($file);
+	return $b;
+}
+
+function print_help() {
+	echo 'Usage: anoclaims.php [operation] [arguments]
+register [ownername] - generate a key pair and register it with specified owner name
+claim [resource] - claim a resource (eg 1.2.3.0/24, fd63:1e39:6f73:0203::/64, test.ano, AS1234)
+setns [resource] [nsname]. - define an external DNS server for a domain name or IP network (don\'t forget the .)
+setns [resource] [nsname] [nsglue] - define an in-zone DNS server for a domain name or IP network with glue record
+sync [server] - synchronize the local database with a remote HTTP server (eg http://marc.ucis.ano)
+';
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/marccore.php	Sat Nov 08 22:22:42 2014 +0100
@@ -0,0 +1,523 @@
+<?php
+if (!function_exists('hex2bin')) { function hex2bin($h) { return pack("H*", $h); } }
+
+class MARCUpdate implements ArrayAccess {
+	private $_version, $_key, $_serial, $_label, $_extensions, $_value = FALSE, $_valueoffset = -1, $_message, $_tags = array();
+	public function __get($name) {
+		switch (strtolower($name)) {
+			case 'version': return $this->_version;
+			case 'key': return $this->_key;
+			case 'serial': return $this->_serial;
+			case 'label': return $this->_label;
+			case 'expiration': return (isset($this->_extensions[4]) && strlen($this->_extensions[4]) >= 4) ? marc_decode_int32be($this->_extensions[4]) : NULL;
+			case 'transfer': return isset($this->_extensions[1]) ? $this->_extensions[1] : NULL;
+			case 'value':
+				if ($this->_value === FALSE) $this->_value = self::DecodeValue($this->_message, $this->_valueoffset);
+				return $this->_value;
+			case 'updatemessage': return $this->_message;
+			case 'tags': return $this->_tags; break;
+			default: throw new Exception('Property '.$name.' does not exist');
+		}
+	}
+	public function __set($name, $value) {
+		switch (strtolower($name)) {
+			case 'version': case 'key': case 'serial': case 'label': case 'value': case 'expiration': case 'transfer': case 'updatemessage': case 'tags': throw new Exception('Property '.$name.' is read only');
+			default: throw new Exception('Property '.$name.' does not exist or is read-only');
+		}
+	}
+	public function __isset($name) {
+		switch (strtolower($name)) {
+			case 'version': case 'key': case 'serial': case 'label': case 'value': case 'updatemessage': case 'tags': return TRUE;
+			case 'expiration': return isset($this->_extensions[4]);
+			case 'transfer': return isset($this->_extensions[1]);
+			default: return FALSE;
+		}
+	}
+	public function __unset($name) {
+		switch (strtolower($name)) {
+			case 'version': case 'key': case 'serial': case 'label': case 'value': case 'expiration': case 'transfer': case 'updatemessage': case 'tags': throw new Exception('Property '.$name.' is read only');
+			default: throw new Exception('Property '.$name.' does not exist');
+		}
+	}
+	public function offsetSet($offset, $value) { $this->__set($offset, $value); }
+	public function offsetExists($offset) { return $this->__isset($offset); }
+	public function offsetUnset($offset) { $this->__unset($offset); }
+	public function offsetGet($offset) { return $this->__get($offset); }
+
+	private function __construct() { }
+	public static function Decode($data) {
+		$upd = new MARCUpdate();
+		$upd->_message = $data;
+		if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE;
+		$upd->_version = ord($data[0]);
+		if ($upd->_version != 2) return FALSE;
+		$upd->_key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES);
+		$i = NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 64 + 1;
+		$l = strlen($data);
+		if (!$l || $l < $i+4+1+1) return FALSE;
+		$upd->_serial = marc_decode_int32be($data, $i); $i += 4;
+		$labellen = ord($data[$i++]);
+		if ($l < $i+$labellen+4) return FALSE;
+		$upd->_label = substr($data, $i, $labellen); $i += $labellen;
+		$upd->_extensions = array();
+		for ($numext = ord($data[$i++]); $numext > 0; $numext--) {
+			if ($l < $i+1+2) return FALSE;
+			$extid = ord($data[$i++]);
+			$extlen = marc_decode_int16be($data, $i); $i += 2;
+			if ($l < $i + $extlen) return FALSE;
+			$upd->_extensions[$extid] = substr($data, $i, $extlen);
+			$i += $extlen;
+		}
+		$upd->_valueoffset = $i;
+		return $upd;
+	}
+	public function Verify() {
+		$data = $this->_message;
+		if (strlen($data) < 1 + NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES) return FALSE;
+		$version = ord($data[0]);
+		if ($version != 2) return FALSE;
+		$key = substr($data, 1, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES);
+		$data = substr($data, NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES + 1);
+		$data = nacl_crypto_sign_ed25519_open($data, $key);
+		return $data !== NULL && $data !== FALSE;
+	}
+	public function Create($upd, $seckey, $current = NULL) {
+		if (strlen($seckey) == 32) nacl_crypto_sign_ed25519_keypair($seckey, $seckey);
+		if (strlen($seckey) < 64) throw new Exception('Signing key is not valid');
+		if (!isset($upd['key'])) $upd['key'] = substr($seckey, 32, 32);
+		if ($upd['key'] != substr($seckey, 32, 32)) throw new Exception('Resource key is not valid');
+		if (!isset($upd['label'])) throw new Exception('Resource label not set');
+		if (strlen($upd['label']) > 255) throw new Exception('Resource label too long');
+		if (!isset($upd['serial'])) $upd['serial'] = time();
+		if ($current) {
+			if ($upd['serial'] <= $current['serial']) $upd['serial'] = $current['serial'] + 1;
+			if (!self::CanImport($upd, $current)) throw new Exception('Can not update resource');
+		}
+		if (isset($upd['transfer']) && (strlen($upd['transfer']) != 0 && strlen($upd['transfer']) != NACL_CRYPTO_SIGN_ed25519_PUBLICKEYBYTES)) throw new Exception('Transfer recipient key is not valid');
+		$data = marc_encode_int32be($upd['serial']);
+		$data .= chr(strlen($upd['label'])).$upd['label'];
+		$value = array();
+		if (isset($upd->_extensions)) foreach ($upd->_extensions as $identifier => $item) $value[$identifier] = $item;
+		if (isset($upd['transfer'])) $value[1] = $upd['transfer'];
+		if (isset($upd['expiration'])) $value[4] = marc_encode_int32be($upd['expiration']);
+		if (count($value) > 0xff) throw new Exception('Too many extensions used');
+		$data .= chr(count($value));
+		foreach ($value as $identifier => $item) {
+			$item = (string)$item;
+			if (strlen($item) > 0xffff) throw new Exception('Extension data too big');
+			$data .= chr($identifier).marc_encode_int16be(strlen($item)).$item;
+		}
+		if (isset($upd['value'])) $data .= self::EncodeValue($upd['value']);
+		$data = nacl_crypto_sign_ed25519($data, $seckey);
+		if (!strlen($data)) throw new Exception('Failed to sign data');
+		if (!strlen(nacl_crypto_sign_ed25519_open($data, $upd['key']))) throw new Exception('Key pair is not valid');
+		$data = chr(2).$upd['key'].$data;
+		return self::Decode($data);
+	}
+	public function ToArray() {
+		$arr = array('version' => $this->version, 'key' => $this->key, 'serial' => $this->serial, 'label' => $this->label, 'value' => $this->value);
+		if (isset($this->expiration)) $arr['expiration'] = $this->expiration;
+		if (isset($this->transfer)) $arr['transfer'] = $this->transfer;
+		return $arr;
+	}
+
+	private static function EncodeValue($data) {
+		if (is_null($data)) {
+			return chr(0);
+		} else if (is_scalar($data)) {
+			return chr(1).(string)$data;
+		} else if (is_array($data)) {
+			$iscollection = TRUE;
+			foreach ($data as $key => $value) if (!is_int($key) || $key < 0) $iscollection = FALSE;
+			$ret = $iscollection ? chr(2) : chr(3);
+			foreach ($data as $key => $value) {
+				if (!$iscollection) {
+					if (strlen($key) > 0xff) throw new Exception('Dictionary key is too long');
+					$ret .= chr(strlen($key)).$key;
+				}
+				$bytes = self::EncodeValue($value);
+				$ret .= marc_encode_int32be(strlen($bytes)).$bytes;
+			}
+			return $ret;
+		} else {
+			throw new Exception('Unable to encode type '.gettype($data));
+		}
+	}
+	private static function DecodeValue($data, $offset = 0, $length = -1) {
+		if ($length == -1) $length = strlen($data) - $offset;
+		if ($length < 0) throw new Exception('Truncated');
+		if ($length == 0) return NULL;
+		$type = ord($data[$offset]); $offset++; $length--;
+		switch ($type) {
+			case 0: return NULL;
+			case 1: return substr($data, $offset, $length);
+			case 2:
+				$value = array();
+				while ($length > 0) {
+					if (4 > $length) throw new Exception('Truncated');
+					$len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4;
+					if ($len > $length) throw new Exception('Truncated');
+					$value[] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len;
+				}
+				return $value;
+			case 3:
+				$value = array();
+				while ($length > 0) {
+					if (1 > $length) throw new Exception('Truncated');
+					$len = ord($data[$offset]); $offset++; $length--;
+					if ($len + 4 > $length) throw new Exception('Truncated');
+					$key = substr($data, $offset, $len); $offset += $len; $length -= $len;
+					$len = marc_decode_int32be($data, $offset); $offset += 4; $length -= 4;
+					if ($len > $length) throw new Exception('Truncated');
+					$value[$key] = self::DecodeValue($data, $offset, $len); $offset += $len; $length -= $len;
+				}
+				return $value;
+			default: throw new Exception('Unsupported type code '.$type);
+		}
+	}
+	public static function CanImport($nw, $cu = NULL) {
+		if (!$nw || !isset($nw['label'])) return FALSE;
+		if ($cu) {
+			if ($nw['label'] != $cu['label']) return FALSE;
+			if ($cu['serial'] >= $nw['serial']) return FALSE;
+			if ($cu['key'] == $nw['key']) return TRUE;
+			if (isset($cu['expiration']) && $cu['expiration'] < time()) return TRUE;
+			if ($cu['serial'] < time() - 365*24*60*60) return TRUE;
+			if (isset($cu['transfer']) && (!strlen($cu['transfer']) || $cu['transfer'] == $nw['key'])) return TRUE;
+			return FALSE;
+		} else {
+			if ($nw['serial'] < time() - 365*24*60*60) return FALSE;
+			if (isset($nw['expiration']) && $nw['expiration'] < time()) return FALSE;
+		}
+		return TRUE;
+	}
+}
+abstract class MARCDatabase {
+	private $_importResourceFilterCallbacks = array(), $_importResourceCallbacks = array();
+	public abstract function GetResource($label);
+	protected abstract function ImportInternal($resource);
+	public abstract function GetResources($labelprefix = NULL, $mints = NULL);
+	public abstract function DeleteResource($label);
+	public function Import($resource, $force = FALSE) {
+		if (is_string($resource)) $resource = MARCUpdate::Decode($resource);
+		if (!$resource) return FALSE;
+		$current = $this->GetResource($resource['label']);
+		if (!$force) foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE;
+		if (!$force && !MARCUpdate::CanImport($resource, $current)) return FALSE;
+		if (!$resource->Verify()) return FALSE;
+		if (!$this->ImportInternal($resource)) return FALSE;
+		$this->ResourceImported($resource);
+		return $resource;
+	}
+	public function UpdateResource($resource, $seckey, $force = FALSE) {
+		$res = MARCUpdate::Create($resource, $seckey, $force ? NULL : $this->GetResource($resource['label']));
+		if (!$res) return FALSE;
+		return $this->Import($res, $force);
+	}
+	protected function CanImportResource(&$resource, $force = FALSE) {
+		if (is_string($resource)) $resource = MARCUpdate::Decode($resource);
+		if (!$resource) return FALSE;
+		$current = $this->GetResource($resource['label']);
+		if (!$force) foreach ($this->_importResourceFilterCallbacks as $callback) if (!call_user_func($callback, $this, $resource, $current)) return FALSE;
+		if (!$force && !MARCUpdate::CanImport($resource, $current)) return FALSE;
+		if (!$resource->Verify()) return FALSE;
+		return TRUE;
+	}
+	protected function ResourceImported($resource) {
+		foreach ($this->_importResourceCallbacks as $callback) call_user_func($callback, $this, $resource);
+	}
+	public function RegisterResourceFilterCallback($callback) {
+		$this->_importResourceFilterCallbacks[] = $callback;
+	}
+	public function RegisterResourceImportCallback($callback) {
+		$this->_importResourceCallbacks[] = $callback;
+	}
+	public function SyncHTTP($server, $options = array()) {
+		$log = isset($options['log']) ? $options['log'] : TRUE;
+		$result = array();
+		$method = isset($options['method']) ? $options['method'] : 'PUT';
+		if ($method != 'POST' && $method != 'PUT' && $method != 'GET') throw new Exception('Unsupported method '.$method);
+		$result['exported'] = 0;
+		$post = NULL;
+		if ($method != 'GET' && (!isset($options['noexport']) || !$options['noexport'])) {
+			$post = $method == 'POST' ? array() : '';
+			$updates = $this->GetResources(NULL, isset($options['exporttimestamp']) ? $options['exporttimestamp'] : NULL);
+			foreach ($updates as $update) {
+				if (!is_string($update)) $update = $update['updatemessage'];
+				if ($method == 'PUT') {
+					$post .= marc_encode_int32be(strlen($update)).$update;
+				} else {
+					$post[] = 'update[]='.urlencode($update);
+				}
+				$result['exported']++;
+			}
+			$updates = NULL;
+			if ($method == 'POST') $post = implode('&', $post);
+		}
+		if ($log) echo "Sending ".strlen($post)." bytes... ";
+		$result['bytessent'] = strlen($post);
+		$ch = curl_init();
+		$timestamp = isset($options['importtimestamp']) ? intval($options['importtimestamp']) : 0;
+		curl_setopt($ch, CURLOPT_URL, $server.'?version=3&get='.$timestamp);
+		if ($method == 'PUT') curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");
+		else if ($method == 'POST') curl_setopt($ch, CURLOPT_POST, TRUE);
+		if ($post != NULL) curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
+		$post = NULL;
+		curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
+		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
+		curl_setopt($ch, CURLOPT_TIMEOUT, 60);
+		$response = curl_exec($ch);
+		if ($response === FALSE) throw new Exception('HTTP request failed');
+		if ($log) echo "received ".strlen($response)." bytes.\n";
+		$result['bytesreceived'] = strlen($response);
+		if (($httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE)) != 200) throw new Exception('Received unexpected HTTP code '.$httpcode.' - '.(strlen($response)?var_export($response,TRUE):''));
+		if (($contenttype = curl_getinfo($ch, CURLINFO_CONTENT_TYPE)) != 'application/octet-stream') throw new Exception('Received unexpected content type '.$contenttype.' - '.(strlen($response)?var_export($response,TRUE):''));
+		curl_close($ch);
+		$i = 0;
+		if ($i + 2 > strlen($response)) throw new Exception('Data has been truncated');
+		$version = ord($response[$i++]);
+		if ($version != 3) throw new Exception('Unsupported protocol version '.$version);
+		for ($numext = ord($response[$i++]); $numext > 0; $numext--) {
+			if ($i + 3 > strlen($response)) throw new Exception('Truncated');
+			$extid = ord($response[$i++]);
+			$extlen = marc_decode_int16be($response, $i); $i += 2;
+			if ($i + $extlen > strlen($response)) throw new Exception('Truncated');
+			switch ($extid) {
+				case 2:
+					if ($extlen >= 4 * 3) {
+						$result['remotereceived'] = marc_decode_int32be($response, $i + 0);
+						$result['remoteimported'] = marc_decode_int32be($response, $i + 4);
+						$result['remoteexported'] = marc_decode_int32be($response, $i + 8);
+					}
+					break;
+				case 3: if ($extlen >= 4) $result['importtimestamp'] = marc_decode_int32be($response, $i); break;
+			}
+			$i += $extlen;
+		}
+		if ($log) echo "Exported ".$result['exported']." updates of which ".(isset($result['remoteimported']) ? $result['remoteimported'] : 'none')." were imported.\n";
+		$result['updates'] = array();
+		$result['imported'] = 0;
+		$result['updatesreceived'] = 0;
+		while ($i < strlen($response)) {
+			if ($i + 4 > strlen($response)) throw new Exception('Truncated');
+			$len = marc_decode_int32be($response, $i); $i += 4;
+			if ($i + $len > strlen($response)) throw new Exception('Truncated');
+			$result['updatesreceived']++;
+			$value = substr($response, $i, $len); $i += $len;
+			$res = MARCUpdate::Decode($value);
+			$result['updates'][] = $res;
+			if (!$this->Import($value)) continue;
+			$result['imported']++;
+		}
+		if ($log) echo "Imported ".$result['imported']." out of ".$result['updatesreceived']." updates.\n";
+		return $result;
+	}
+}
+class MARCDatabaseFlatFile extends MARCDatabase {
+	private $dbfile = NULL;
+	private $resources = array();
+	private $changed = FALSE;
+	public function GetResource($label) {
+		if (!isset($this->resources[$label])) return NULL;
+		return $this->resources[$label]['update'];
+	}
+	public function DeleteResource($label) {
+		if (!isset($this->resources[$label])) return FALSE;
+		unset($this->resources[$label]);
+		$this->changed = TRUE;
+		return TRUE;
+	}
+	protected function ImportInternal($update) {
+		$this->resources[$update['label']] = array('timestamp' => time(), 'update' => $update);
+		$this->changed = TRUE;
+		return TRUE;
+	}
+	public function GetResources($labelprefix = NULL, $mints = NULL) {
+		return new MARCDatabaseFlatFileFilteredResourceIterator(new ArrayIterator($this->resources), $labelprefix, $mints);
+	}
+	public function __construct($filename = FALSE) {
+		if ($filename) $this->Open($filename);
+	}
+	private function OpenFile($filename) {
+		$this->Close();
+		$this->dbfile = fopen($filename, 'c+');
+		if (!$this->dbfile) throw new Exception('Could not open database file');
+		if (!flock($this->dbfile, LOCK_EX)) throw new Exception('Could not lock database file');
+	}
+	public function Open($filename) {
+		$this->OpenFile($filename);
+		$this->resources = array();
+		$this->changed = FALSE;
+		rewind($this->dbfile);
+		while (true) {
+			$data = fread($this->dbfile, 8);
+			if (strlen($data) == 0) break;
+			if (strlen($data) != 8) throw new Exception('Database truncated');
+			$ts = marc_decode_int32be($data, 0);
+			$len = marc_decode_int32be($data, 4);
+			$data = fread($this->dbfile, $len);
+			if (strlen($data) != $len) throw new Exception('Database truncated');
+			$res = MARCUpdate::Decode($data);
+			if (!$res) continue;
+			$this->resources[$res['label']] = array('timestamp' => $ts, 'update' => $res);
+		}
+	}
+	public function Save() {
+		if (!$this->dbfile) throw new Exception('No database file is open');
+		rewind($this->dbfile);
+		ftruncate($this->dbfile, 0);
+		foreach ($this->resources as $res) {
+			fwrite($this->dbfile, marc_encode_int32be($res['timestamp']));
+			$u = (string)$res['update']['updatemessage'];
+			fwrite($this->dbfile, marc_encode_int32be(strlen($u)));
+			fwrite($this->dbfile, $u);
+		}
+		$this->changed = FALSE;
+	}
+	public function SaveAs($filename) {
+		$this->OpenFile($filename);
+		$this->Save();
+	}
+	public function Close() {
+		if ($this->dbfile) {
+			flock($this->dbfile, LOCK_UN);
+			fclose($this->dbfile);
+			$this->dbfile = NULL;
+		}
+	}
+	public function IsChanged() {
+		return $this->changed;
+	}
+}
+class MARCDatabaseFlatFileFilteredResourceIterator implements Iterator {
+	private $source, $labelprefixlength, $labelprefix, $mints;
+	public function __construct($source, $labelprefix, $mints) {
+		$this->source = $source;
+		$this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix);
+		$this->labelprefix = $labelprefix;
+		$this->mints = intval($mints);
+	}
+	public function current() { $r = $this->source->current(); return $r ? $r['update'] : NULL; }
+	public function key() { return $this->source->key(); }
+	public function valid() { return $this->source->valid(); }
+	public function next() { $this->source->next(); $this->findnext(); }
+	public function rewind() { $this->source->rewind(); $this->findnext(); }
+	private function findnext() {
+		while ($this->source->valid()) {
+			$c = $this->source->current();
+			if ($c['timestamp'] >= $this->mints && (!$this->labelprefixlength || substr($c['update']['label'], 0, $this->labelprefixlength) == $this->labelprefix)) break;
+			$this->source->next();
+		}
+	}
+}
+class MARCDatabaseSQLite extends MARCDatabase {
+	private $pdo = NULL;
+	protected function DBPrepareStatement($query, $args = NULL) {
+		$stmt = $this->pdo->prepare($query);
+		if (!is_array($args) && !is_null($args)) $args = array($args);
+		$stmt->execute($args);
+		return $stmt;
+	}
+	private function DBUpdate($query, $args = NULL) {
+		$stmt = $this->DBPrepareStatement($query, $args);
+		$cnt = $stmt->rowCount();
+		$stmt->closeCursor();
+		return $cnt;
+	}
+	public function __construct($filename) {
+		$this->pdo = new PDO('sqlite:'.$filename);
+		$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+		$this->DBUpdate('CREATE TABLE IF NOT EXISTS `resources` (`timestamp` INTEGER, `label` BLOB PRIMARY KEY, `update` BLOB)');
+	}
+	public function GetResource($label) {
+		$stmt = $this->DBPrepareStatement('SELECT `update` FROM `resources` WHERE `label` = ?', $label);
+		$res = $stmt->fetchColumn();
+		$stmt->closeCursor();
+		if ($res === FALSE) return NULL;
+		return MARCUpdate::Decode($res);
+	}
+	public function DeleteResource($label) {
+		return $this->DBUpdate('DELETE FROM `resources` WHERE `label` = ?', $label) != 0;
+	}
+	protected function ImportInternal($update) {
+		$args = array('d' => $update['updatemessage'], 't' => time(), 'l' => $update['label']);
+		if ($this->DBUpdate('UPDATE `resources` SET `update` = :d, `timestamp` = :t WHERE `label` = :l', $args) == 0)
+			$this->DBUpdate('INSERT OR IGNORE INTO `resources` (`label`, `update`, `timestamp`) VALUES (:l, :d, :t)', $args);
+		return TRUE;
+	}
+	public function GetResources($labelprefix = NULL, $mints = NULL) {
+		$labelprefix = is_null($labelprefix) ? '' : (string)$labelprefix;
+		return $this->GetResourcesFromQuery('SELECT `update` FROM `resources` WHERE `timestamp` >= ? AND SUBSTR(`label`, 1, ?) = ? ORDER BY `label`', array(intval($mints), strlen($labelprefix), $labelprefix));
+	}
+	public function GetResourcesFromQuery($query, $args = NULL) {
+		return $this->GetResourcesFromStatement($this->DBPrepareStatement($query, $args));
+	}
+	public function GetResourcesFromStatement($statement) {
+		return new MARCDatabaseSQLiteResourceIterator($statement);
+	}
+	public function IsChanged() {
+		return FALSE;
+	}
+	public function Close() {
+	}
+}
+class MARCDatabaseSQLiteResourceIterator implements Iterator {
+	private $source, $current = FALSE;
+	public function __construct($source) { $this->source = $source; }
+	public function current() { return $this->current === FALSE ? NULL : MARCUpdate::Decode($this->current); }
+	public function key() { return NULL; }
+	public function valid() { return $this->current !== FALSE; }
+	public function next() { $this->current = $this->source->fetchColumn(); if ($this->current === FALSE) $this->source->closeCursor(); }
+	public function rewind() { $this->next(); }
+}
+class MARCDatabaseDBA extends MARCDatabase {
+	private $db = NULL;
+	public function __construct($path, $mode = 'cd', $handler = 'qdbm') {
+		$this->db = dba_open($path, $mode, $handler);
+		if ($this->db === FALSE) throw new Exception('Could not open database');
+	}
+	public function GetResource($label) {
+		$r = dba_fetch($label, $this->db);
+		if ($r === FALSE) return NULL;
+		return MARCUpdate::Decode($r);
+	}
+	public function DeleteResource($label) {
+		return dba_delete($label, $this->db);
+	}
+	protected function ImportInternal($update) {
+		return dba_replace($update['label'], $update['updatemessage'], $this->db);
+	}
+	public function GetResources($labelprefix = NULL, $mints = NULL) {
+		return new MARCDatabaseDBAFilteredResourceIterator($this->db, $labelprefix, $mints);
+	}
+	public function Save() {
+		dba_sync($this->db);
+	}
+	public function Close() {
+		dba_close($this->db);
+	}
+	public function IsChanged() {
+		return FALSE;
+	}
+}
+class MARCDatabaseDBAFilteredResourceIterator implements Iterator {
+	private $db, $currentkey = FALSE, $labelprefixlength, $labelprefix, $mints;
+	public function __construct($db, $labelprefix, $mints) {
+		$this->db = $db;
+		$this->labelprefixlength = is_null($labelprefix) ? 0 : strlen($labelprefix);
+		$this->labelprefix = $labelprefix;
+		$this->mints = intval($mints);
+	}
+	public function current() { return MARCUpdate::Decode(dba_fetch($this->currentkey, $this->db)); }
+	public function key() { return $this->currentkey; }
+	public function valid() { return strlen($this->currentkey); }
+	public function next() { $this->currentkey = dba_nextkey($this->db); $this->findnext(); }
+	public function rewind() { $this->currentkey = dba_firstkey($this->db); $this->findnext(); }
+	private function findnext() { while ($this->currentkey !== FALSE && ($this->labelprefixlength && substr($this->currentkey, 0, $this->labelprefixlength) != $this->labelprefix)) $this->currentkey = dba_nextkey($this->db); }
+}
+
+function marc_decode_int32be($data, $i = 0) { return (ord($data[$i]) << 24) | (ord($data[$i+1]) << 16) | (ord($data[$i+2]) << 8) | ord($data[$i+3]); }
+function marc_decode_int16be($data, $i = 0) { return (ord($data[$i+0]) << 8) | ord($data[$i+1]); }
+function marc_encode_int32be($v) { return pack("N", $v); }
+function marc_encode_int16be($v) { return pack("n", $v); }
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/marcus.php	Sat Nov 08 22:22:42 2014 +0100
@@ -0,0 +1,371 @@
+<?php
+require_once './marccore.php';
+
+error_reporting(E_ALL);
+
+if (!isset($argv)) $argv = $_SERVER['argv'];
+$argi = 1;
+
+class FilteredResourceIterator implements Iterator {
+	private $source, $filtertype, $filtervalue;
+	public function __construct($source, $filtertype, $filtervalue) {
+		$this->source = $source;
+		$this->filtertype = $filtertype;
+		$this->filtervalue = $filtervalue;
+	}
+	public function current() { $r = $this->source->current(); return $r; }
+	public function key() { return $this->source->key(); }
+	public function valid() { return $this->source->valid(); }
+	public function next() { $this->source->next(); $this->findnext(); }
+	public function rewind() { $this->source->rewind(); $this->findnext(); }
+	private function findnext() {
+		while ($this->source->valid()) {
+			$c = $this->source->current();
+			if ($this->filter($c)) break;
+			$this->source->next();
+		}
+	}
+	private function filter($c) {
+		switch ($this->filtertype) {
+			case 'OWNER': return isset($c['value']['owner']) && is_scalar($c['value']['owner']) && strcasecmp($c['value']['owner'], $this->filtervalue) == 0;
+			case 'KEY': return $c['key'] == hex2bin($this->filtervalue);
+			case 'DOMEXT': return ord($c['label'][0]) == 4 && substr_compare($c['label'], $this->filtervalue, -strlen($this->filtervalue), strlen($this->filtervalue), TRUE) == 0;
+			default: return FALSE;
+		}
+	}
+}
+
+$database = new MARCDatabaseFlatFile();
+$key = NULL;
+$resource = NULL;
+$reschanged = FALSE;
+
+while ($argi < count($argv)) {
+	switch (strtoupper($argv[$argi++])) {
+		case 'OPEN':
+			if ($reschanged) echo "Warning: selected resource has not been updated.\n";
+			if ($database->IsChanged()) echo "Warning: database has unsaved changes.\n";
+			$database->Open($argv[$argi++]);
+			$reschanged = FALSE;
+			break;
+		case 'OPENSQLITE':
+			$database = new MARCDatabaseSQLite($argv[$argi++]);
+			break;
+		case 'OPENDBA':
+			$database = new MARCDatabaseDBA($argv[$argi++]);
+			break;
+		case 'SAVE':
+			$database->Save();
+			break;
+		case 'SAVEAS':
+			$database->SaveAs($argv[$argi]);
+			break;
+		case 'SYNC':
+			$database->SyncHTTP($argv[$argi++]);
+			break;
+		case 'KEY':
+			switch (strtoupper($argv[$argi++])) {
+				case 'CREATE':
+					$key = array('store' => TRUE);
+					$key['pk'] = nacl_crypto_sign_ed25519_keypair($key['sk'], randombytes(32));
+					$dbchanged = TRUE;
+					echo 'Created public key '.bin2hex($key['pk'])."\n";
+					break;
+				case 'FORGET':
+					$key['store'] = FALSE;
+					$dbchanged = TRUE;
+					break;
+				case 'STORE':
+					$key['store'] = TRUE;
+					$dbchanged = TRUE;
+					break;
+				case 'USE':
+					$key = array('store' => FALSE, 'pk' => $resource['key']);
+					if (isset($resource['value']['seckey'])) $key['sk'] = $resource['value']['seckey'];
+					if (isset($resource['value']['seckeyenc'])) $key['locked'] = $resource['value']['seckeyenc'];
+					break;
+				case 'IMPORT':
+					$key = array('store' => FALSE);
+					$key['pk'] = nacl_crypto_sign_ed25519_keypair($key['sk'], hex2bin($argv[$argi++]));
+					$dbchanged = TRUE;
+					break;
+				case 'UNLOCK':
+					if (!isset($key['locked'])) throw new Exception('The key is not locked');
+					$ret = hash('sha512', $key['pk'].$argv[$argi++], TRUE);
+					$key['sk'] = '';
+					for ($i = 0; $i < 32; $i++) $key['sk'] .= chr(ord($key['locked'][$i]) ^ ord($ret[$i]));
+					$ret = nacl_crypto_sign_ed25519_keypair($key['sk'], $key['sk']);
+					if ($ret != $key['pk']) throw new Exception('Key password is not valid');
+					echo 'Unlocked public key '.bin2hex($key['pk'])."\n";
+					break;
+				default:
+					throw new Exception('Unknown key operation '.$argv[$argi-1]);
+			}
+			break;
+		case 'LIST':
+			foreach ($database->GetResources() as $ret) echo labeltoname($ret['label'])."\n";
+			break;
+		case 'FIND':
+			foreach (filterresources($database->GetResources(), $argv, $argi) as $ret) echo labeltoname($ret['label'])."\n";
+			break;
+		case 'DELETE':
+			$database->DeleteResource($resource['label']);
+			break;
+		case 'DUMP':
+			dumpresource($resource);
+			break;
+		case 'UPDATE':
+			if (!isset($resource['key'])) $resource['key'] = $key['pk'];
+			unset($resource['serial']);
+			$res = $database->UpdateResource($resource, $key['sk']);
+			if (!$res) throw new Exception('Could not update resource');
+			$resource = $res->ToArray();
+			$reschanged = FALSE;
+			break;
+		case 'SET':
+			switch (strtoupper($argv[$argi++])) {
+				case 'OWNER':
+					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
+					$resource['value']['owner'] = $argv[$argi++];
+					$reschanged = TRUE;
+					break;
+				case 'DESC':
+				case 'DESCR':
+				case 'DESCRIPTION':
+					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
+					$resource['value']['descr'] = $argv[$argi++];
+					$reschanged = TRUE;
+					break;
+				case 'PWAUTH':
+					if (!isset($key['sk'])) throw new Exception('The key is not available');
+					$ret = hash('sha512', $key['pk'].$argv[$argi++], TRUE);
+					$key['locked'] = '';
+					for ($i = 0; $i < 32; $i++) $key['locked'] .= chr(ord($key['sk'][$i]) ^ ord($ret[$i]));
+					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
+					$resource['value']['seckeyenc'] = $key['locked'];
+					$reschanged = TRUE;
+					break;
+				case 'TRANSFER':
+					$ret = $argv[$argi++];
+					$resource['value']['transfer'] = (strtolower($ret) == 'any') ? '' : hex2bin($ret);
+					$reschanged = TRUE;
+					break;
+				default:
+					throw new Exception('Unknown set operation '.$argv[$argi-1]);
+			}
+			break;
+		case 'ADD':
+			switch (strtoupper($argv[$argi++])) {
+				case 'NS':
+					if (!isset($resource['value']) || !is_array($resource['value'])) $resource['value'] = array();
+					if (!isset($resource['value']['ns']) || !is_array($resource['value']['ns'])) $resource['value']['ns'] = array();
+					$nsname = $argv[$argi++];
+					$nsglue = (strlen($nsname) && $nsname[strlen($nsname)-1] != '.') ? $argv[$argi++] : NULL;
+					if (!isset($resource['value']['ns'][$nsname]) || !is_array($resource['value']['ns'][$nsname])) $resource['value']['ns'][$nsname] = array();
+					if ($nsglue !== NULL) $resource['value']['ns'][$nsname][] = $nsglue;
+					$reschanged = TRUE;
+					break;
+				default:
+					throw new Exception('Unknown add operation '.$argv[$argi-1]);
+			}
+			break;
+		case 'RESET':
+			switch (strtoupper($argv[$argi++])) {
+				case 'OWNER':
+					if (!is_array($resource['value'])) $resource['value'] = array();
+					unset($resource['value']['owner']);
+					$reschanged = TRUE;
+					break;
+				case 'DESC':
+				case 'DESCR':
+				case 'DESCRIPTION':
+					if (!is_array($resource['value'])) $resource['value'] = array();
+					unset($resource['value']['descr']);
+					$reschanged = TRUE;
+					break;
+				case 'PWAUTH':
+					if (!is_array($resource['value'])) $resource['value'] = array();
+					unset($resource['value']['seckeyenc']);
+					$reschanged = TRUE;
+					break;
+				case 'NS':
+					if (!is_array($resource['value'])) $resource['value'] = array();
+					unset($resource['value']['ns']);
+					$reschanged = TRUE;
+					break;
+				case 'VALUE':
+					$resource['value'] = array();
+					$reschanged = TRUE;
+					break;
+				case 'TRANSFER':
+					unset($resource['transfer']);
+					$reschanged = TRUE;
+					break;
+				case 'EXPIRATION':
+					unset($resource['expiration']);
+					$reschanged = TRUE;
+					break;
+				default:
+					throw new Exception('Unknown reset operation '.$argv[$argi-1]);
+			}
+			break;
+		case 'CREATE':
+			if ($reschanged) echo "Warning: selected resource has not been updated.\n";
+			$reschanged = TRUE;
+			$resource = array('label' => argtolabel($argv, $argi));
+			break;
+		case 'SELECT':
+			if ($reschanged) echo "Warning: selected resource has not been updated.\n";
+			$reschanged = FALSE;
+			$label = argtolabel($argv, $argi);
+			$resource = $database->GetResource($label);
+			if (!$resource) echo "Warning: resource ".labeltoname($label)." does not exist.\n";
+			else $resource = $resource->ToArray();
+			break;
+		case 'HELP':
+			print_help();
+			break;
+		default:
+			throw new Exception('Unknown operation '.$argv[$argi-1]);
+	}
+}
+if ($reschanged) echo "Warning: selected resource has not been updated.\n";
+if ($database->IsChanged()) echo "Warning: database has unsaved changes.\n";
+$database->Close();
+
+function filterresources($iterator, $argv, &$argi) {
+	$filtertype = strtoupper($argv[$argi++]);
+	switch ($filtertype) {
+		case 'OWNER':
+		case 'KEY':
+		case 'DOMEXT':
+			$filtervalue = $argv[$argi++];
+			break;
+		default:
+			throw new Exception('Unknown filter type '.$t);
+	}
+	return new FilteredResourceIterator($iterator, $filtertype, $filtervalue);
+}
+function argtolabel($argv, &$argi) {
+	$t = $argv[$argi++];
+	switch (strtoupper($t)) {
+		case 'LABEL': return hex2bin($argv[$argi++]);
+		case 'CURRENTKEY': return chr(0).$GLOBALS['key']['pk'];
+		case 'RESOURCEKEY': return chr(0).$GLOBALS['resource']['key'];
+		case 'KEY': return chr(0).hex2bin($argv[$argi++]);
+		case 'IP':
+		case 'IP4':
+		case 'IPV4':
+		case 'IP6':
+		case 'IPV6': return ipnettolabel($argv[$argi++]);
+		case 'AS': return chr(3).marc_decode_int32be($argv[$argi++]);
+		case 'DOM':
+		case 'DOMAIN': return chr(4).strtolower(trim($argv[$argi++], '.'));
+		default:
+			if (preg_match('/^AS[0-9]{1-9}$/', $t)) return chr(3).marc_decode_int32be(substr($argv[$argi++], 2));
+			if (preg_match('_^[0-9]{1-3}\.[0-9]{1-3}\.[0-9]{1-3}\.[0-9]{1-3}/[0-9]{1-2}$_', $t)) return ipv4tolabel($t);
+			if (preg_match('_^(((?=.*(::))(?!.*\3.+\3))\3?|([\dA-F]{1,4}(\3|:\b|$)|\2))(?4){5}((?4){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})\z/[0-9]{1-3}_i', $t)) return ipv6tolabel($t);
+			if (preg_match('/^[a-f0-9]{64}$/i', $t)) return chr(0).hex2bin($t);
+			if (preg_match('/^[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,6}$/i', $t)) return chr(4).strtolower(trim($t, '.'));
+			throw new Exception('Could not detect label type for '.$t);
+	}
+}
+function ipnettolabel($s) {
+	$ip = inet_pton(strtok($s, '/'));
+	$pl = intval(strtok('/'));
+	if ($pl == 0) throw new Exception('Invalid IP network specified');
+	if (strlen($ip) == 4) return chr(1).$ip.chr($pl);
+	if (strlen($ip) == 16) return chr(2).$ip.chr($pl);
+}
+function labeltoname($l) {
+	switch (ord($l)) {
+		case 0: return 'KEY '.bin2hex(substr($l, 1));
+		case 1: if (strlen($l) == 6) return 'IPv4 '.inet_ntop(substr($l, 1, 4)).'/'.ord($l[5]); else return 'LABEL '.bin2hex($l);
+		case 2: if (strlen($l) == 18) return 'IPv6 '.inet_ntop(substr($l, 1, 16)).'/'.ord($l[17]); else return 'LABEL '.bin2hex($l);
+		case 3: if (strlen($l) == 5) return 'AS '.marc_decode_int32be(substr($l, 1, 4)); else return 'LABEL '.bin2hex($l);
+		case 4: if (strlen($l) > 1) return 'DOM '.substr($l, 1); else return 'LABEL '.bin2hex($l);
+		default: return 'LABEL '.bin2hex($l);
+	}
+}
+function dumpresource($r, $p = '') {
+	if (is_null($r)) {
+		echo "NULL\n";
+	} else if (is_string($r)) {
+		$bin = FALSE;
+		for ($i = 0; $i < strlen($r); $i++) $bin |= ord($r[$i]) < 32 || ord($r[$i]) > 126;
+		if ($bin) echo '0x'.bin2hex($r)."\n";
+		else echo '"'.$r.'"'."\n";
+	} else if (is_scalar($r)) {
+		echo $r."\n";
+	} else if (is_array($r)) {
+		echo "array(\n";
+		foreach ($r as $key => $value) {
+			echo $p.'    ['.$key.'] => ';
+			dumpresource($value, $p.'    ');
+		}
+		echo $p."  )\n";
+	} else {
+		print_r($r);
+	}
+}
+function randombytes($n) {
+	$b = '';
+	$file = fopen('/dev/urandom', 'r');
+	for ($i = 0; $i < $n; $i++) $b .= fgetc($file);
+	fclose($file);
+	return $b;
+}
+
+function print_help() {
+	echo 'Usage: marcus.php [operation] [arguments] [operation] [arguments]...
+open [filename.mdb] - opens the specified database file
+save - saves the current database file
+saveas [filename.mdb] - saves the current data to a new databse file
+sync [url] - synchronize database with remote server
+key create - create a new key pair
+key forget - do not store the current key pair in the local database
+key store - store the current key pair in the local database
+key use - use the key pair from the currently selected resource
+key import [secretkey] - import the key pair defined by the given secret key
+key unlock [password] - unlock a password protected key pair
+list - list registered resources
+find [type] [value] - list registered sources matching filter (type=OWNER|KEY|DOMEXT)
+create [identifier] - create given resource
+create currentkey - create resource for current key pair
+create ip|ip4|ipv4 [ipv4network] - create resource for IPv4 network
+create ip|ip6|ipv6 [ipv6network] - create resource for IPv6 network
+create dom|domain [ipv6network] - create resource for domain name
+select [identifier] - select resource given by identifier
+select currentkey - select key resource for current key pair
+select resourcekey - select key resource for the key that signed the currently selected resource
+select label [identifier] - select resource by hexadecimal label
+select key [publickey] - select key resource (hexadecimal)
+select ip|ip4|ipv4 [ipv4network] - select resource for IPv4 network
+select ip|ip6|ipv6 [ipv6network] - select resource for IPv6 network
+select dom|domain [ipv6network] - select resource for domain name
+delete - delete currently selected resource
+dump - display currently selected resource
+update - update selected resource in the local database
+set owner [name] - set the owner name for the selected resource
+set descr|desc|description [text] - set the description for the selected resource
+set pwauth [password] - store the current key pair in the selected resource for password authentication
+set transfer any - allow anyone to take over the resource
+set transfer [key] - transfer the resource to given key
+TODO: set expiration [value]
+add ns [name] [glue] - add in-zone nameserver with glue record (name is the part of the nameserver name before the domain name, glue is an IPv4 or IPv6 address)
+add ns [name]. - add an external nameserver
+reset owner - clear the owner
+reset descr|desc|description - clear the description
+reset pwauth - remove the key pair from the recourse, disabling password authentication
+reset ns - clear the nameserver records
+reset transfer - disable resource transfers
+reset expiration - disable explicit expiration
+reset value - clear the owner, description, password authentication and nameserver records
+
+Examples:
+OPEN marc.mdb KEY create CREATE currentkey SET owner "Your name" SET pwauth yourpassword UPDATE KEY forget CREATE yourdomain.ano SET owner "Your name" UPDATE SAVE
+OPEN marc.mdb SELECT yourdomain.ano SELECT resourcekey KEY use KEY unlock yourpassword SELECT yourdomain.ano ADD ns ns1 1.2.3.4 UPDATE SAVE
+OPEN marc.mdb SELECT yourdomain.ano SELECT resourcekey KEY use KEY unlock yourpassword CREATE 1.2.3.0/24 SET owner "Your name" ADD ns ns1.yourdomain.ano. UPDATE SAVE
+OPEN marc.mdb SYNC http://marc.ucis.ano/ SAVE
+';
+}