diff --git a/.travis.yml b/.travis.yml index e4b5980..5a31794 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,12 +12,31 @@ matrix: allow_failures: - php: "nightly" +services: + - mysql + - postgresql + +before_install: + - mysql -e 'CREATE DATABASE IF NOT EXISTS test;' + - psql -c 'create database travis_ci_test;' -U postgres + install: - composer self-update - composer update - - php bin/install.php - - php bin/make-tables.php script: + # Test SQLite + - php bin/install.php + - php bin/make-tables.php + - composer test + - composer static-analysis + # Test MySQL + - php bin/install.php --mysql --host 127.0.0.1 -u root --database test + - php bin/make-tables.php + - composer test + - composer static-analysis + # Test PostgreSQL + - php bin/install.php --pgsql -u postgres --database travis_ci_test + - php bin/make-tables.php - composer test - composer static-analysis diff --git a/README.md b/README.md index 547749c..d44fe4a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ as part of our continued efforts to make the Internet more secure. * [How to write (publish) to your Chronicle](docs/02-publish.md) * [How to setup cross-signing to other Chronicles](docs/03-cross-signing.md) * [How to replicate other Chronicles](docs/04-replication.md) +* [Concurrent Instances](docs/05-instances.md) +* [Internal Developer Documentation](docs/internals) + * [Design Philosophy](docs/internals/01-design-philosophy.md) + * [SQL Tables](docs/internals/02-sql-tables.md) ### Client-Side Software that Interacts with Chronicle diff --git a/bin/create-client.php b/bin/create-client.php index 820a3af..a54ca7d 100644 --- a/bin/create-client.php +++ b/bin/create-client.php @@ -36,6 +36,9 @@ $settings['database']['options'] ?? [] ); +// Pass database instance to Chronicle +Chronicle::setDatabase($db); + /** * @var Getopt $getopt * @@ -110,9 +113,13 @@ /** @var string $newPublicId */ $newPublicId = Base64UrlSafe::encode(\random_bytes(24)); +// Disable escaping for SQLite +/** @var boolean $isSQLite */ +$isSQLite = strpos($settings['database']['dsn'] ?? '', 'sqlite:') !== false; + $db->beginTransaction(); $db->insert( - Chronicle::getTableName('clients'), + Chronicle::getTableName('clients', $isSQLite), [ 'isAdmin' => !empty($admin), 'publicid' => $newPublicId, diff --git a/bin/install.php b/bin/install.php index d2ada81..634d9a1 100644 --- a/bin/install.php +++ b/bin/install.php @@ -1,6 +1,10 @@ getString() ); +/** + * @var Getopt $getopt + * + * This defines the Command Line options. + * + * These are many examples: + * php install.php + * php install.php --mysql + * php install.php --pgsql + * php install.php --sqlite + * php install.php --mysql --host localhost --port 3306 --username mysql_user --password mysql_password + * php install.php --pgsql --host=localhost --port=5432 --username=pgsql_user --password=pgsql_password + * php install.php --mysql --h localhost --port 3306 --u mysql_user --p mysql_password + * php install.php --pgsql --h=localhost --port=5432 --u=pgsql_user --p=pgsql_password + * php install.php --sqlite --database chronicle + * php install.php --sqlite --database=chronicle --extension db + */ +$getopt = new Getopt([ + new Option(null, 'mysql', Getopt::OPTIONAL_ARGUMENT), + new Option(null, 'pgsql', Getopt::OPTIONAL_ARGUMENT), + new Option(null, 'sqlite', Getopt::OPTIONAL_ARGUMENT), + new Option('h', 'host', Getopt::OPTIONAL_ARGUMENT), + new Option(null, 'port', Getopt::OPTIONAL_ARGUMENT), + new Option('d', 'database', Getopt::OPTIONAL_ARGUMENT), + new Option('e', 'extension', Getopt::OPTIONAL_ARGUMENT), + new Option('u', 'username', Getopt::OPTIONAL_ARGUMENT), + new Option('p', 'password', Getopt::OPTIONAL_ARGUMENT), +]); +$getopt->process(); + +/** @var string $mysql */ +$mysql = $getopt->getOption('mysql') ?? false; +/** @var string $pgsql */ +$pgsql = $getopt->getOption('pgsql') ?? false; +/** @var string $sqlite */ +$sqlite = $getopt->getOption('sqlite') ?? (!$mysql && !$pgsql); +/** @var string $host */ +$host = $getopt->getOption('host') ?? 'localhost'; +/** @var string $port */ +$port = $getopt->getOption('port') ?? ($mysql ? '3306' : ($pgsql ? '5432' : '')); +/** @var string $database */ +$database = $getopt->getOption('database') ?? 'chronicle'; +/** @var string $extension */ +$extension = $getopt->getOption('extension') ?? 'db'; +/** @var string $username */ +$username = $getopt->getOption('username') ?? ($mysql ? 'mysqluser' : ($pgsql ? 'pgsqluser' : '')); +/** @var string $password */ +$password = $getopt->getOption('password') ?? ''; + +// default SQLite +$databaseConfig = [ + 'dsn' => 'sqlite:' . $root . '/local/' . $database . '.' . $extension, +]; + +if(!$sqlite){ + + $dbType = $mysql ? 'mysql' : 'pgsql'; + + $databaseConfig = [ + 'dsn' => $dbType . ':host=' . $host . ';port=' . $port . ';dbname=' . $database, + 'username' => $username, + 'password' => $password, + ]; +} + // Write the default settings to the local settings file. $localSettings = [ - 'database' => [ - 'dsn' => 'sqlite:' . $root . '/local/chronicle.sql' - ], + 'database' => $databaseConfig, // Map 'channel-name' => 'table_prefix' 'instances' => [ '' => '' diff --git a/docs/01-setup.md b/docs/01-setup.md index 6ad62ca..f036208 100644 --- a/docs/01-setup.md +++ b/docs/01-setup.md @@ -9,10 +9,10 @@ General process: 1. Clone this repository: `git clone https://github.com/paragonie/chronicle.git` 2. Run `composer install` * If you don't have Composer, [go here for **Composer installation** instructions](https://getcomposer.org/download/). -3. Run `bin/install.php` to generate a keypair and basic configuration file. +3. Run `php bin/install.php` to generate a keypair and basic configuration file. 4. Edit `local/settings.json` to configure your Chronicle. For example, you can choose a MySQL, PostgreSQL, or SQLite backend. [See below](#configuring-localsettingsjson). -5. Run `bin/make-tables.php` to setup the database tables +5. Run `php bin/make-tables.php` to setup the database tables 6. Configure a new virtual host for Apache/nginx/etc. to point to the `public` directory, **OR** run `composer start` to launch the built-in web server. @@ -28,6 +28,14 @@ except with information pertinent to your instance and your public key: ### MySQL +To generate MySQL config simply do the following: + +```shell +php bin/install.php --mysql +``` + +The output will be like this: + ```json { "database": { @@ -38,8 +46,35 @@ except with information pertinent to your instance and your public key: "signing-public-key": "gIQOvAxVbF2zLeanIZDQe7S2gBsabfxM3vP8sjBI_08=" } ``` + +There are many available options: + +```shell +php bin/install.php --mysql \ + --host localhost \ + --port 3306 \ + --database chronicle \ + --username mysql_user \ + --password mysql_password +``` + +Short format options: + +```shell +php bin/install.php --mysql -h localhost --port 3306 \ + -d chronicle -u mysql_user -p mysql_password +``` + ### PostgreSQL +To generate PostgreSQL config simply do the following: + +```shell +php bin/install.php --pgsql +``` + +The output will be like this: + ```json { "database": { @@ -51,8 +86,34 @@ except with information pertinent to your instance and your public key: } ``` +There are many available options: + +```shell +php bin/install.php --pgsql \ + --host localhost \ + --port 5432 \ + --database chronicle \ + --username pgsql_user \ + --password pgsql_password +``` + +Short format options: + +```shell +php bin/install.php --pgsql -h localhost --port 5432 \ + -d chronicle -u pgsql_user -p mysql_password +``` + ### SQLite +To generate SQLite config simply do the following: + +```shell +php bin/install.php +``` + +The output will be like this: + ```json { "database": { @@ -61,6 +122,17 @@ except with information pertinent to your instance and your public key: "signing-public-key": "gIQOvAxVbF2zLeanIZDQe7S2gBsabfxM3vP8sjBI_08=" } ``` +There are many available options: + +```shell +php bin/install.php --sqlite --database live --extension db +``` + +Short format options: + +```shell +php bin/install.php --sqlite -d live -e db +``` ## How to add clients to your Chronicle diff --git a/sql/mysql/00-local.sql b/sql/mysql/00-local.sql index 9374fa1..cb24485 100644 --- a/sql/mysql/00-local.sql +++ b/sql/mysql/00-local.sql @@ -1,7 +1,7 @@ CREATE TABLE chronicle_clients ( `id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - `publicid` VARCHAR(128), - `publickey` TEXT, + `publicid` VARCHAR(128) NOT NULL, + `publickey` TEXT NOT NULL, `isAdmin` BOOLEAN NOT NULL DEFAULT FALSE, `comment` TEXT, `created` DATETIME DEFAULT CURRENT_TIMESTAMP, @@ -24,5 +24,6 @@ CREATE TABLE chronicle_chain ( INDEX(`currhash`), INDEX(`summaryhash`), FOREIGN KEY (`prevhash`) REFERENCES chronicle_chain(`currhash`) ON DELETE RESTRICT ON UPDATE RESTRICT, - UNIQUE(`prevhash`) -); + UNIQUE(`prevhash`), + UNIQUE(`currhash`) +); \ No newline at end of file diff --git a/sql/mysql/01-remote.sql b/sql/mysql/01-remote.sql index cfec4bc..0bc44f2 100644 --- a/sql/mysql/01-remote.sql +++ b/sql/mysql/01-remote.sql @@ -1,31 +1,31 @@ CREATE TABLE chronicle_xsign_targets ( `id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - `name` TEXT, - `url` TEXT, - `clientid` TEXT, - `publickey` TEXT, - `policy` TEXT, + `name` TEXT NOT NULL, + `url` TEXT NOT NULL, + `clientid` TEXT NOT NULL, + `publickey` TEXT NOT NULL, + `policy` TEXT NOT NULL, `lastrun` TEXT ); CREATE TABLE chronicle_replication_sources ( `id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - `uniqueid` TEXT, - `name` TEXT, - `url` TEXT, - `publickey` TEXT + `uniqueid` TEXT NOT NULL, + `name` TEXT NOT NULL, + `url` TEXT NOT NULL, + `publickey` TEXT NOT NULL ); CREATE TABLE chronicle_replication_chain ( `id` BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - `source` BIGINT UNSIGNED REFERENCES chronicle_replication_sources(`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, - `data` TEXT, + `source` BIGINT UNSIGNED NOT NULL REFERENCES chronicle_replication_sources(`id`) ON DELETE RESTRICT ON UPDATE RESTRICT, + `data` TEXT NOT NULL, `prevhash` VARCHAR(128) NULL, `currhash` VARCHAR(128) NOT NULL, - `hashstate` TEXT, + `hashstate` TEXT NOT NULL, `summaryhash` VARCHAR(128), - `publickey` TEXT, - `signature` TEXT, + `publickey` TEXT NOT NULL, + `signature` TEXT NOT NULL, `created` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, `replicated` TIMESTAMP NULL, INDEX(`prevhash`), diff --git a/sql/pgsql/00-local.sql b/sql/pgsql/00-local.sql index 573e47d..6aaaab9 100644 --- a/sql/pgsql/00-local.sql +++ b/sql/pgsql/00-local.sql @@ -1,7 +1,7 @@ CREATE TABLE chronicle_clients ( id BIGSERIAL PRIMARY KEY, - publicid TEXT, - publickey TEXT, + publicid TEXT NOT NULL, + publickey TEXT NOT NULL, "isAdmin" BOOLEAN NOT NULL DEFAULT FALSE, comment TEXT, created TIMESTAMP, @@ -12,16 +12,17 @@ CREATE INDEX chronicle_clients_clientid_idx ON chronicle_clients(publicid); CREATE TABLE chronicle_chain ( id BIGSERIAL PRIMARY KEY, - data TEXT, + data TEXT NOT NULL, prevhash TEXT NULL, - currhash TEXT, - hashstate TEXT, - summaryhash TEXT, - publickey TEXT, - signature TEXT, + currhash TEXT NOT NULL, + hashstate TEXT NOT NULL, + summaryhash TEXT NOT NULL, + publickey TEXT NOT NULL, + signature TEXT NOT NULL, created TIMESTAMP, - FOREIGN KEY (currhash) REFERENCES chronicle_chain(prevhash), - UNIQUE(prevhash) + UNIQUE(currhash), + UNIQUE(prevhash), + FOREIGN KEY (prevhash) REFERENCES chronicle_chain(currhash) ); CREATE INDEX chronicle_chain_prevhash_idx ON chronicle_chain(prevhash); diff --git a/sql/pgsql/01-remote.sql b/sql/pgsql/01-remote.sql index f97ef0e..771dc3f 100644 --- a/sql/pgsql/01-remote.sql +++ b/sql/pgsql/01-remote.sql @@ -1,37 +1,38 @@ CREATE TABLE chronicle_xsign_targets ( id BIGSERIAL PRIMARY KEY, - name TEXT, - url TEXT, - clientid TEXT, - publickey TEXT, - policy TEXT, + name TEXT NOT NULL, + url TEXT NOT NULL, + clientid TEXT NOT NULL, + publickey TEXT NOT NULL, + policy TEXT NOT NULL, lastrun TEXT ); CREATE TABLE chronicle_replication_sources ( id BIGSERIAL PRIMARY KEY, - uniqueid TEXT, - name TEXT, - url TEXT, - publickey TEXT + uniqueid TEXT NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL, + publickey TEXT NOT NULL ); CREATE TABLE chronicle_replication_chain ( id BIGSERIAL PRIMARY KEY, - source BIGINT REFERENCES chronicle_replication_sources(id), - data TEXT, + source BIGINT NOT NULL REFERENCES chronicle_replication_sources(id), + data TEXT NOT NULL, prevhash TEXT NULL, - currhash TEXT, - hashstate TEXT, - summaryhash TEXT, - publickey TEXT, - signature TEXT, + currhash TEXT NOT NULL, + hashstate TEXT NOT NULL, + summaryhash TEXT NOT NULL, + publickey TEXT NOT NULL, + signature TEXT NOT NULL, created TIMESTAMP, replicated TIMESTAMP, - FOREIGN KEY (currhash) REFERENCES chronicle_replication_chain(prevhash), + UNIQUE(currhash), + UNIQUE(prevhash), UNIQUE(source, prevhash) ); CREATE INDEX chronicle_replication_chain_prevhash_idx ON chronicle_replication_chain(source, prevhash); CREATE INDEX chronicle_replication_chain_currhash_idx ON chronicle_replication_chain(source, currhash); -CREATE INDEX chronicle_replication_chain_summaryhash_idx ON chronicle_replication_chain(source, summaryhash); +CREATE INDEX chronicle_replication_chain_summaryhash_idx ON chronicle_replication_chain(source, summaryhash); \ No newline at end of file diff --git a/sql/sqlite/00-local.sql b/sql/sqlite/00-local.sql index bfd4ccb..a079e84 100644 --- a/sql/sqlite/00-local.sql +++ b/sql/sqlite/00-local.sql @@ -1,30 +1,33 @@ CREATE TABLE chronicle_clients ( - id INTEGER PRIMARY KEY ASC, - publicid TEXT, - publickey TEXT, + id INTEGER PRIMARY KEY ASC AUTOINCREMENT, + publicid TEXT NOT NULL, + publickey TEXT NOT NULL, isAdmin INTEGER NOT NULL DEFAULT 0, comment TEXT, - created TEXT, - modified TEXT + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX chronicle_clients_clientid_idx ON chronicle_clients(publicid); +CREATE INDEX chronicle_clients_publickey_idx ON chronicle_clients(publickey); CREATE TABLE chronicle_chain ( - id INTEGER PRIMARY KEY ASC, - data TEXT, + id INTEGER PRIMARY KEY ASC AUTOINCREMENT, + data TEXT NOT NULL, prevhash TEXT NULL, - currhash TEXT, - hashstate TEXT, - summaryhash TEXT, - publickey TEXT, - signature TEXT, - created TEXT, + currhash TEXT NOT NULL, + hashstate TEXT NOT NULL, + summaryhash TEXT NOT NULL, + publickey TEXT NOT NULL, + signature TEXT NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (prevhash) REFERENCES chronicle_chain(currhash), + FOREIGN KEY (publickey) REFERENCES chronicle_clients(publickey), + UNIQUE(prevhash), UNIQUE(currhash), - UNIQUE(prevhash) + UNIQUE(signature) ); CREATE INDEX chronicle_chain_prevhash_idx ON chronicle_chain(prevhash); CREATE INDEX chronicle_chain_currhash_idx ON chronicle_chain(currhash); -CREATE INDEX chronicle_chain_summaryhash_idx ON chronicle_chain(summaryhash); +CREATE INDEX chronicle_chain_summaryhash_idx ON chronicle_chain(summaryhash); \ No newline at end of file diff --git a/sql/sqlite/01-remote.sql b/sql/sqlite/01-remote.sql index 65247b5..b8f6728 100644 --- a/sql/sqlite/01-remote.sql +++ b/sql/sqlite/01-remote.sql @@ -1,38 +1,39 @@ CREATE TABLE chronicle_xsign_targets ( - id INTEGER PRIMARY KEY ASC, - name TEXT, - url TEXT, - clientid TEXT, - publickey TEXT, - policy TEXT, - lastrun TEXT + id INTEGER PRIMARY KEY ASC AUTOINCREMENT, + name TEXT NOT NULL, + url TEXT NOT NULL, + clientid TEXT NOT NULL, + publickey TEXT NOT NULL, + policy TEXT NOT NULL, + lastrun TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE chronicle_replication_sources ( - id INTEGER PRIMARY KEY ASC, - uniqueid TEXT, - name TEXT, - url TEXT, - publickey TEXT + id INTEGER PRIMARY KEY ASC AUTOINCREMENT, + uniqueid TEXT NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL, + publickey TEXT NOT NULL ); CREATE TABLE chronicle_replication_chain ( - id INTEGER PRIMARY KEY ASC, - source INTEGER, - data TEXT, + id INTEGER PRIMARY KEY ASC AUTOINCREMENT, + source INTEGER NOT NULL, + data TEXT NOT NULL, prevhash TEXT NULL, - currhash TEXT, - hashstate TEXT, - summaryhash TEXT, - publickey TEXT, - signature TEXT, - created TEXT, - replicated TEXT, - FOREIGN KEY (prevhash) REFERENCES chronicle_replication_chain(currhash), - UNIQUE(source, currhash), - UNIQUE(source, prevhash) + currhash TEXT NOT NULL, + hashstate TEXT NOT NULL, + summaryhash TEXT NOT NULL, + publickey TEXT NOT NULL, + signature TEXT NOT NULL, + created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + replicated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(currhash), + UNIQUE(source, prevhash), + FOREIGN KEY (source) REFERENCES chronicle_replication_sources(id), + FOREIGN KEY (prevhash) REFERENCES chronicle_replication_chain(currhash) ); CREATE INDEX chronicle_replication_chain_prevhash_idx ON chronicle_replication_chain(source, prevhash); CREATE INDEX chronicle_replication_chain_currhash_idx ON chronicle_replication_chain(source, currhash); -CREATE INDEX chronicle_replication_chain_summaryhash_idx ON chronicle_replication_chain(source, summaryhash); +CREATE INDEX chronicle_replication_chain_summaryhash_idx ON chronicle_replication_chain(source, summaryhash); \ No newline at end of file diff --git a/src/Chronicle/Handlers/Lookup.php b/src/Chronicle/Handlers/Lookup.php index d7e7e51..2a70dcb 100644 --- a/src/Chronicle/Handlers/Lookup.php +++ b/src/Chronicle/Handlers/Lookup.php @@ -142,7 +142,7 @@ public function getByHash(array $args = []): ResponseInterface } /** - * List the latest current hash and summary hash + * List the latest current record * * @return ResponseInterface * @@ -150,9 +150,20 @@ public function getByHash(array $args = []): ResponseInterface */ public function getLastHash(): ResponseInterface { - /** @var array $lasthash */ - $lasthash = Chronicle::getDatabase()->row( - 'SELECT currhash, summaryhash FROM chronicle_chain ORDER BY id DESC LIMIT 1' + /** @var array $record */ + $record = Chronicle::getDatabase()->run( + "SELECT + data AS contents, + prevhash, + currhash, + summaryhash, + created, + publickey, + signature + FROM + " . Chronicle::getTableName('chain') . " + ORDER BY id DESC LIMIT 1 + " ); return Chronicle::getSapient()->createSignedJsonResponse( 200, @@ -160,12 +171,7 @@ public function getLastHash(): ResponseInterface 'version' => Chronicle::VERSION, 'datetime' => (new \DateTime())->format(\DateTime::ATOM), 'status' => 'OK', - 'results' => [ - 'current-hash' => - $lasthash['currhash'], - 'summary-hash' => - $lasthash['summaryhash'] - ] + 'results' => $record, ], Chronicle::getSigningKey() );