# HG changeset patch # User Ivo Smits # Date 1415481762 -3600 # Node ID 3ac7bd7495fd6bb6a192d8a4ba4b5339144e5e11 Initial commit diff -r 000000000000 -r 3ac7bd7495fd anoclaims.php --- /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 @@ + $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) +'; +} + diff -r 000000000000 -r 3ac7bd7495fd marccore.php --- /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 @@ +_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); } + diff -r 000000000000 -r 3ac7bd7495fd marcus.php --- /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 @@ +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 +'; +}