mirror of
https://github.com/servalproject/serval-dna.git
synced 2024-12-23 23:12:31 +00:00
1344 lines
50 KiB
C
1344 lines
50 KiB
C
/*
|
|
Serval Rhizome file sharing
|
|
Copyright (C) 2012 The Serval Project
|
|
|
|
This program is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU General Public License
|
|
as published by the Free Software Foundation; either version 2
|
|
of the License, or (at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*/
|
|
|
|
#include "serval.h"
|
|
#include "rhizome.h"
|
|
#include "strbuf.h"
|
|
#include <stdlib.h>
|
|
|
|
long long rhizome_space=0;
|
|
static const char *rhizome_thisdatastore_path = NULL;
|
|
|
|
#define SQLITE_CODE_OK(code) (code==SQLITE_OK || code==SQLITE_DONE)
|
|
|
|
const char *rhizome_datastore_path()
|
|
{
|
|
if (!rhizome_thisdatastore_path)
|
|
rhizome_set_datastore_path(NULL);
|
|
return rhizome_thisdatastore_path;
|
|
}
|
|
|
|
int rhizome_set_datastore_path(const char *path)
|
|
{
|
|
if (!path)
|
|
path = confValueGet("rhizome.datastore_path", NULL);
|
|
if (path) {
|
|
rhizome_thisdatastore_path = strdup(path);
|
|
if (path[0] != '/')
|
|
WARNF("Dangerous rhizome.datastore_path setting: '%s' -- should be absolute", rhizome_thisdatastore_path);
|
|
} else {
|
|
rhizome_thisdatastore_path = serval_instancepath();
|
|
WARNF("Rhizome datastore path not configured -- using instance path '%s'", rhizome_thisdatastore_path);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int form_rhizome_datastore_path(char * buf, size_t bufsiz, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
strbuf b = strbuf_local(buf, bufsiz);
|
|
strbuf_puts(b, rhizome_datastore_path());
|
|
if (fmt) {
|
|
va_start(ap, fmt);
|
|
if (*strbuf_substr(b, -1) != '/')
|
|
strbuf_putc(b, '/');
|
|
strbuf_vsprintf(b, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
if (strbuf_overrun(b)) {
|
|
WHY("Path buffer overrun");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int form_rhizome_import_path(char * buf, size_t bufsiz, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
strbuf b = strbuf_local(buf, bufsiz);
|
|
strbuf_sprintf(b, "%s/import", rhizome_datastore_path());
|
|
if (fmt) {
|
|
va_start(ap, fmt);
|
|
strbuf_putc(b, '/');
|
|
strbuf_vsprintf(b, fmt, ap);
|
|
va_end(ap);
|
|
}
|
|
if (strbuf_overrun(b)) {
|
|
WHY("Path buffer overrun");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
int create_rhizome_datastore_dir()
|
|
{
|
|
if (debug & DEBUG_RHIZOME) DEBUGF("mkdirs(%s, 0700)", rhizome_datastore_path());
|
|
return mkdirs(rhizome_datastore_path(), 0700);
|
|
}
|
|
|
|
int create_rhizome_import_dir()
|
|
{
|
|
char dirname[1024];
|
|
if (!form_rhizome_import_path(dirname, sizeof dirname, NULL))
|
|
return -1;
|
|
if (debug & DEBUG_RHIZOME) DEBUGF("mkdirs(%s, 0700)", dirname);
|
|
return mkdirs(dirname, 0700);
|
|
}
|
|
|
|
sqlite3 *rhizome_db=NULL;
|
|
|
|
/* XXX Requires a messy join that might be slow. */
|
|
int rhizome_manifest_priority(char *id)
|
|
{
|
|
long long result = 0;
|
|
if (sqlite_exec_int64(&result,
|
|
"select max(grouplist.priorty) from grouplist,manifests,groupmemberships"
|
|
" where manifests.id='%s'"
|
|
" and grouplist.id=groupmemberships.groupid"
|
|
" and groupmemberships.manifestid=manifests.id;",
|
|
id
|
|
) == -1
|
|
)
|
|
return -1;
|
|
return (int) result;
|
|
}
|
|
|
|
int rhizome_opendb()
|
|
{
|
|
if (rhizome_db) return 0;
|
|
|
|
IN();
|
|
|
|
if (create_rhizome_datastore_dir() == -1){
|
|
RETURN(WHY("No Directory"));
|
|
}
|
|
char dbpath[1024];
|
|
if (!FORM_RHIZOME_DATASTORE_PATH(dbpath, "rhizome.db")){
|
|
RETURN(WHY("Invalid path"));
|
|
}
|
|
|
|
if (sqlite3_open(dbpath,&rhizome_db)){
|
|
RETURN(WHYF("SQLite could not open database %s: %s", dbpath, sqlite3_errmsg(rhizome_db)));
|
|
}
|
|
int loglevel = (debug & DEBUG_RHIZOME) ? LOG_LEVEL_DEBUG : LOG_LEVEL_SILENT;
|
|
|
|
/* Read Rhizome configuration */
|
|
double rhizome_kb = atof(confValueGet("rhizome_kb", "1024"));
|
|
rhizome_space = 1024LL * rhizome_kb;
|
|
if (debug&DEBUG_RHIZOME) {
|
|
DEBUGF("serval.conf:rhizome_kb=%.f", rhizome_kb);
|
|
DEBUGF("Rhizome will use %lldB of storage for its database.", rhizome_space);
|
|
}
|
|
/* Create tables as required */
|
|
sqlite_exec_void_loglevel(loglevel, "PRAGMA auto_vacuum=2;");
|
|
if ( sqlite_exec_void("CREATE TABLE IF NOT EXISTS GROUPLIST(id text not null primary key, closed integer,ciphered integer,priority integer);") == -1
|
|
|| sqlite_exec_void("CREATE TABLE IF NOT EXISTS MANIFESTS(id text not null primary key, manifest blob, version integer,inserttime integer, bar blob);") == -1
|
|
|| sqlite_exec_void("CREATE TABLE IF NOT EXISTS FILES(id text not null primary key, data blob, length integer, highestpriority integer, datavalid integer);") == -1
|
|
|| sqlite_exec_void("DROP TABLE IF EXISTS FILEMANIFESTS;") == -1
|
|
|| sqlite_exec_void("CREATE TABLE IF NOT EXISTS GROUPMEMBERSHIPS(manifestid text not null, groupid text not null);") == -1
|
|
|| sqlite_exec_void("CREATE TABLE IF NOT EXISTS VERIFICATIONS(sid text not null, did text, name text, starttime integer, endtime integer, signature blob);") == -1
|
|
) {
|
|
RETURN(WHY("Failed to create schema"));
|
|
}
|
|
// No easy way to tell if these columns already exist, should probably create some kind of schema
|
|
// version table. Running these a second time will fail.
|
|
sqlite_exec_void_loglevel(loglevel, "ALTER TABLE MANIFESTS ADD COLUMN filesize text;");
|
|
sqlite_exec_void_loglevel(loglevel, "ALTER TABLE MANIFESTS ADD COLUMN filehash text;");
|
|
sqlite_exec_void_loglevel(loglevel, "ALTER TABLE FILES ADD inserttime integer;");
|
|
/* Clean out database, but if this fails keep going (database may be read-only). */
|
|
sqlite_exec_void_loglevel(LOG_LEVEL_WARN, "CREATE INDEX IF NOT EXISTS IDX_MANIFESTS_HASH ON MANIFESTS(filehash);");
|
|
sqlite_exec_void_loglevel(LOG_LEVEL_WARN, "DELETE FROM MANIFESTS WHERE filehash IS NULL;");
|
|
sqlite_exec_void_loglevel(LOG_LEVEL_WARN, "DELETE FROM FILES WHERE NOT EXISTS( SELECT 1 FROM MANIFESTS WHERE MANIFESTS.filehash = FILES.id);");
|
|
sqlite_exec_void_loglevel(LOG_LEVEL_WARN, "DELETE FROM MANIFESTS WHERE NOT EXISTS( SELECT 1 FROM FILES WHERE MANIFESTS.filehash = FILES.id);");
|
|
RETURN(0);
|
|
}
|
|
|
|
/*
|
|
Convenience wrapper for preparing an SQL command.
|
|
Returns -1 if an error occurs (logged as an error), otherwise zero with the prepared
|
|
statement in *statement.
|
|
*/
|
|
int sqlite_prepare(sqlite3_stmt **statement, const strbuf stmt)
|
|
{
|
|
return sqlite_prepare_loglevel(LOG_LEVEL_ERROR, statement, stmt);
|
|
}
|
|
|
|
int sqlite_prepare_loglevel(int log_level, sqlite3_stmt **statement, const strbuf stmt)
|
|
{
|
|
if (strbuf_overrun(stmt))
|
|
return WHYF("Sql statement overrun: %s", strbuf_str(stmt));
|
|
if (!rhizome_db && rhizome_opendb() == -1)
|
|
return -1;
|
|
switch (sqlite3_prepare_v2(rhizome_db, strbuf_str(stmt), -1, statement, NULL)) {
|
|
case SQLITE_OK: case SQLITE_DONE:
|
|
break;
|
|
default:
|
|
LOGF(log_level, "%s in %s", sqlite3_errmsg(rhizome_db), strbuf_str(stmt));
|
|
sqlite3_finalize(*statement);
|
|
return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
Convenience wrapper for executing an SQL command that returns a no value.
|
|
If an error occurs then logs it at error level and returns -1. Otherwise returns zero.
|
|
*/
|
|
int sqlite_exec_void(const char *sqlformat, ...)
|
|
{
|
|
strbuf stmt = strbuf_alloca(8192);
|
|
strbuf_va_printf(stmt, sqlformat);
|
|
return sqlite_exec_void_strbuf_loglevel(LOG_LEVEL_ERROR, stmt);
|
|
}
|
|
|
|
int sqlite_exec_void_loglevel(int log_level, const char *sqlformat, ...)
|
|
{
|
|
strbuf stmt = strbuf_alloca(8192);
|
|
strbuf_va_printf(stmt, sqlformat);
|
|
return sqlite_exec_void_strbuf_loglevel(log_level, stmt);
|
|
}
|
|
|
|
/*
|
|
Convenience wrapper for executing an SQL command that returns a no value.
|
|
If an error occurs then logs it at the given level and returns -1. Otherwise returns zero.
|
|
*/
|
|
int sqlite_exec_void_strbuf_loglevel(int log_level, const strbuf stmt)
|
|
{
|
|
sqlite3_stmt *statement;
|
|
int ret = sqlite_prepare_loglevel(log_level, &statement, stmt);
|
|
if (ret != -1) {
|
|
int stepcode;
|
|
int rows = 0;
|
|
while ((stepcode = sqlite3_step(statement)) == SQLITE_ROW)
|
|
++rows;
|
|
if (rows) WARNF("void query unexpectedly returned %d row%s", rows, rows == 1 ? "" : "s");
|
|
switch (stepcode) {
|
|
case SQLITE_OK:
|
|
case SQLITE_DONE:
|
|
case SQLITE_ROW:
|
|
ret = 0;
|
|
break;
|
|
default:
|
|
ret = -1;
|
|
LOGF(log_level, "%s in %s", sqlite3_errmsg(rhizome_db), strbuf_str(stmt));
|
|
break;
|
|
}
|
|
sqlite3_finalize(statement);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
Convenience wrapper for executing an SQL command that returns a single int64 value.
|
|
Returns -1 if an error occurs.
|
|
If no row is found, then returns 0 and does not alter *result.
|
|
If exactly one row is found, the assigns its value to *result and returns 1.
|
|
If more than one row is found, then assigns the value of the first row to *result and returns the
|
|
number of rows.
|
|
*/
|
|
int sqlite_exec_int64(long long *result, const char *sqlformat,...)
|
|
{
|
|
strbuf stmt = strbuf_alloca(8192);
|
|
strbuf_va_printf(stmt, sqlformat);
|
|
sqlite3_stmt *statement;
|
|
int ret = sqlite_prepare(&statement, stmt);
|
|
if (ret != -1) {
|
|
int rowcount = 0;
|
|
int stepcode = sqlite3_step(statement);
|
|
if (stepcode == SQLITE_ROW) {
|
|
int n = sqlite3_column_count(statement);
|
|
if (n != 1)
|
|
ret = WHYF("Incorrect column count %d (should be 1): %s", n, strbuf_str(stmt));
|
|
else {
|
|
rowcount = 1;
|
|
*result = sqlite3_column_int64(statement, 0);
|
|
while ((stepcode = sqlite3_step(statement)) == SQLITE_ROW)
|
|
++rowcount;
|
|
}
|
|
}
|
|
if (ret != -1) {
|
|
switch (stepcode) {
|
|
case SQLITE_OK: case SQLITE_DONE:
|
|
ret = rowcount;
|
|
break;
|
|
default:
|
|
ret = WHYF("%s in %s", sqlite3_errmsg(rhizome_db), strbuf_str(stmt));
|
|
break;
|
|
}
|
|
}
|
|
sqlite3_finalize(statement);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
Convenience wrapper for executing an SQL command that returns a single text value.
|
|
Returns -1 if an error occurs, otherwise the number of rows that were found:
|
|
0 means no rows, nothing is appended to the strbuf
|
|
1 means exactly one row, and the its column is appended to the strbuf
|
|
2 more than one row, and the first row's column is appended to the strbuf
|
|
@author Andrew Bettison <andrew@servalproject.com>
|
|
*/
|
|
int sqlite_exec_strbuf(strbuf sb, const char *sqlformat,...)
|
|
{
|
|
strbuf stmt = strbuf_alloca(8192);
|
|
strbuf_va_printf(stmt, sqlformat);
|
|
sqlite3_stmt *statement;
|
|
int ret = sqlite_prepare(&statement, stmt);
|
|
if (ret != -1) {
|
|
int rowcount = 0;
|
|
int stepcode = sqlite3_step(statement);
|
|
if (stepcode == SQLITE_ROW) {
|
|
int n = sqlite3_column_count(statement);
|
|
if (n != 1)
|
|
ret = WHYF("Incorrect column count %d (should be 1): %s", n, strbuf_str(stmt));
|
|
else {
|
|
rowcount = 1;
|
|
strbuf_puts(sb, (const char *)sqlite3_column_text(statement, 0));
|
|
while ((stepcode = sqlite3_step(statement)) == SQLITE_ROW)
|
|
++rowcount;
|
|
}
|
|
}
|
|
if (ret != -1) {
|
|
switch (stepcode) {
|
|
case SQLITE_OK: case SQLITE_DONE:
|
|
ret = rowcount;
|
|
break;
|
|
default:
|
|
ret = -1;
|
|
WHY(strbuf_str(stmt));
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
break;
|
|
}
|
|
}
|
|
sqlite3_finalize(statement);
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
long long rhizome_database_used_bytes()
|
|
{
|
|
long long db_page_size;
|
|
long long db_page_count;
|
|
long long db_free_page_count;
|
|
if ( sqlite_exec_int64(&db_page_size, "PRAGMA page_size;") == -1LL
|
|
|| sqlite_exec_int64(&db_page_count, "PRAGMA page_count;") == -1LL
|
|
|| sqlite_exec_int64(&db_free_page_count, "PRAGMA free_count;") == -1LL
|
|
)
|
|
return WHY("Cannot measure database used bytes");
|
|
return db_page_size * (db_page_count - db_free_page_count);
|
|
}
|
|
|
|
int rhizome_make_space(int group_priority, long long bytes)
|
|
{
|
|
sqlite3_stmt *statement;
|
|
|
|
/* Asked for impossibly large amount */
|
|
if (bytes>=(rhizome_space-65536)) return -1;
|
|
|
|
long long db_used = rhizome_database_used_bytes();
|
|
if (db_used == -1)
|
|
|
|
/* If there is already enough space now, then do nothing more */
|
|
if (db_used<=(rhizome_space-bytes-65536)) return 0;
|
|
|
|
/* Okay, not enough space, so free up some. */
|
|
char sql[1024];
|
|
snprintf(sql,1024,"select id,length from files where highestpriority<%d order by descending length",group_priority);
|
|
if(sqlite3_prepare_v2(rhizome_db,sql, -1, &statement, NULL) != SQLITE_OK )
|
|
{
|
|
WHYF("SQLite error running query '%s': %s",sql,sqlite3_errmsg(rhizome_db));
|
|
sqlite3_finalize(statement);
|
|
sqlite3_close(rhizome_db);
|
|
rhizome_db=NULL;
|
|
exit(-1);
|
|
}
|
|
|
|
while ( bytes>(rhizome_space-65536-rhizome_database_used_bytes()) && sqlite3_step(statement) == SQLITE_ROW)
|
|
{
|
|
/* Make sure we can drop this blob, and if so drop it, and recalculate number of bytes required */
|
|
const unsigned char *id;
|
|
//long long length;
|
|
|
|
/* Get values */
|
|
if (sqlite3_column_type(statement, 0)==SQLITE_TEXT) id=sqlite3_column_text(statement, 0);
|
|
else {
|
|
WARNF("Incorrect type in id column of files table.");
|
|
continue; }
|
|
if (sqlite3_column_type(statement, 1)==SQLITE_INTEGER) ;//length=sqlite3_column_int(statement, 1);
|
|
else {
|
|
WARNF("Incorrect type in length column of files table.");
|
|
continue; }
|
|
|
|
/* Try to drop this file from storage, discarding any references that do not trump the priority of this
|
|
request. The query done earlier should ensure this, but it doesn't hurt to be paranoid, and it also
|
|
protects against inconsistency in the database. */
|
|
rhizome_drop_stored_file((char *)id,group_priority+1);
|
|
}
|
|
sqlite3_finalize(statement);
|
|
|
|
//long long equal_priority_larger_file_space_used = sqlite_exec_int64("SELECT COUNT(length) FROM FILES WHERE highestpriority=%d and length>%lld",group_priority,bytes);
|
|
/* XXX Get rid of any equal priority files that are larger than this one */
|
|
|
|
/* XXX Get rid of any higher priority files that are not relevant in this time or location */
|
|
|
|
/* Couldn't make space */
|
|
return WHY("Incomplete");
|
|
}
|
|
|
|
/* Drop the specified file from storage, and any manifests that reference it,
|
|
provided that none of those manifests are being retained at a higher priority
|
|
than the maximum specified here. */
|
|
int rhizome_drop_stored_file(const char *id,int maximum_priority)
|
|
{
|
|
char sql[1024];
|
|
sqlite3_stmt *statement;
|
|
int cannot_drop=0;
|
|
|
|
if (strlen(id)>(crypto_hash_sha512_BYTES*2+1)) {
|
|
WHY("File ID is wrong length");
|
|
return -1;
|
|
}
|
|
|
|
snprintf(sql,1024,"select id from manifests where filehash='%s'", id);
|
|
if(sqlite3_prepare_v2(rhizome_db,sql, -1, &statement, NULL) != SQLITE_OK )
|
|
{
|
|
WHYF("SQLite error running query '%s': %s",sql,sqlite3_errmsg(rhizome_db));
|
|
sqlite3_finalize(statement);
|
|
sqlite3_close(rhizome_db);
|
|
rhizome_db=NULL;
|
|
return WHY("Could not drop stored file");
|
|
}
|
|
|
|
while ( sqlite3_step(statement) == SQLITE_ROW)
|
|
{
|
|
/* Find manifests for this file */
|
|
const unsigned char *manifestId;
|
|
if (sqlite3_column_type(statement, 0)==SQLITE_TEXT)
|
|
manifestId=sqlite3_column_text(statement, 0);
|
|
else {
|
|
WHYF("Incorrect type in id column of manifests table.");
|
|
continue;
|
|
}
|
|
|
|
/* Check that manifest is not part of a higher priority group.
|
|
If so, we cannot drop the manifest or the file.
|
|
However, we will keep iterating, as we can still drop any other manifests pointing to this file
|
|
that are lower priority, and thus free up a little space. */
|
|
int priority = rhizome_manifest_priority((char *)manifestId);
|
|
if (priority == -1)
|
|
WHYF("Cannot drop due to error, manifestId=%s", manifestId);
|
|
else if (priority > maximum_priority) {
|
|
WHYF("Cannot drop due to manifest %s", manifestId);
|
|
cannot_drop=1;
|
|
} else {
|
|
printf("removing stale manifests, groupmemberships\n");
|
|
sqlite_exec_void("delete from manifests where id='%s';",manifestId);
|
|
sqlite_exec_void("delete from keypairs where public='%s';",manifestId);
|
|
sqlite_exec_void("delete from groupmemberships where manifestid='%s';",manifestId);
|
|
}
|
|
}
|
|
sqlite3_finalize(statement);
|
|
|
|
if (!cannot_drop) {
|
|
sqlite_exec_void("delete from files where id='%s';",id);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
int sqlite3_exec_retry(sqlite3 *db,const char *sql,int (*callback)(void*,int,char**,char**),void *arg,char **errmsg){
|
|
int ret;
|
|
do{
|
|
ret = sqlite3_exec(db, sql, callback, arg, errmsg);
|
|
}while(ret==SQLITE_BUSY || ret==SQLITE_LOCKED);
|
|
if (!SQLITE_CODE_OK(ret))
|
|
WHY(sql);
|
|
return ret;
|
|
}
|
|
|
|
int sqlite3_prepare_v2_retry(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail){
|
|
int ret;
|
|
do{
|
|
ret = sqlite3_prepare_v2(db, zSql, nByte, ppStmt, pzTail);
|
|
}while(ret==SQLITE_BUSY || ret==SQLITE_LOCKED);
|
|
if (!SQLITE_CODE_OK(ret))
|
|
WHY(zSql);
|
|
return ret;
|
|
}
|
|
|
|
int sqlite3_step_retry(sqlite3_stmt *stmt){
|
|
int ret;
|
|
while(1){
|
|
ret = sqlite3_step(stmt);
|
|
if (ret==SQLITE_BUSY || ret==SQLITE_LOCKED){
|
|
WHY("Database locked, retrying");
|
|
sqlite3_reset(stmt);
|
|
}else{
|
|
return ret;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
Store the specified manifest into the sqlite database.
|
|
We assume that sufficient space has been made for us.
|
|
The manifest should be finalised, and so we don't need to
|
|
look at the underlying manifest file, but can just write m->manifest_data
|
|
as a blob.
|
|
|
|
associated_filename needs to be read in and stored as a blob. Hopefully that
|
|
can be done in pieces so that we don't have memory exhaustion issues on small
|
|
architectures. However, we do know it's hash apriori from m, and so we can
|
|
skip loading the file in if it is already stored. mmap() apparently works on
|
|
Linux FAT file systems, and is probably the best choice since it doesn't need
|
|
all pages to be in RAM at the same time.
|
|
|
|
SQLite does allow modifying of blobs once stored in the database.
|
|
The trick is to insert the blob as all zeroes using a special function, and then
|
|
substitute bytes in the blog progressively.
|
|
|
|
We need to also need to create the appropriate row(s) in the MANIFESTS, FILES,
|
|
and GROUPMEMBERSHIPS tables, and possibly GROUPLIST as well.
|
|
*/
|
|
int rhizome_store_bundle(rhizome_manifest *m)
|
|
{
|
|
if (!m->finalised) return WHY("Manifest was not finalised");
|
|
|
|
if (m->haveSecret) {
|
|
/* We used to store the secret in the database, but we don't anymore, as we use
|
|
the BK field in the manifest. So nothing to do here. */
|
|
} else {
|
|
/* We don't have the secret for this manifest, so only allow updates if
|
|
the self-signature is valid */
|
|
if (!m->selfSigned) {
|
|
WHY("*** Insert into manifests failed (-2).");
|
|
return WHY("Manifest is not signed, and I don't have the key. Manifest might be forged or corrupt.");
|
|
}
|
|
}
|
|
|
|
char manifestid[RHIZOME_MANIFEST_ID_STRLEN + 1];
|
|
rhizome_manifest_get(m, "id", manifestid, sizeof manifestid);
|
|
str_toupper_inplace(manifestid);
|
|
|
|
char filehash[RHIZOME_FILEHASH_STRLEN + 1];
|
|
strncpy(filehash, m->fileHexHash, sizeof filehash);
|
|
str_toupper_inplace(filehash);
|
|
|
|
|
|
char *err;
|
|
int sql_ret;
|
|
sqlite3_stmt *stmt;
|
|
|
|
/* Bind BAR to data field */
|
|
unsigned char bar[RHIZOME_BAR_BYTES];
|
|
rhizome_manifest_to_bar(m,bar);
|
|
|
|
// we should add the file in the same transaction, but closing the blob seems to cause some issues.
|
|
/* Store the file */
|
|
// TODO encrypted payloads - pass encryption key here
|
|
if (m->fileLength>0){
|
|
if (rhizome_store_file(m,NULL)){
|
|
WHY("Could not store file");
|
|
sql_ret = 1;
|
|
}
|
|
}
|
|
|
|
if (!rhizome_db) rhizome_opendb();
|
|
sql_ret = sqlite3_exec_retry(rhizome_db, "BEGIN TRANSACTION;", NULL, NULL, &err);
|
|
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_prepare_v2_retry(rhizome_db, "INSERT OR REPLACE INTO MANIFESTS(id,manifest,version,inserttime,bar,filesize,filehash) VALUES(?,?,?,?,?,?,?);", -1, &stmt, NULL);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_text(stmt, 1, manifestid, -1, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_blob(stmt, 2, m->manifestdata, m->manifest_bytes, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int64(stmt, 3, m->version);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int64(stmt, 4, gettime_ms());
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_blob(stmt, 5, bar, RHIZOME_BAR_BYTES, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int64(stmt, 6, m->fileLength);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_text(stmt, 7, filehash, -1, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_step_retry(stmt);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_finalize(stmt);
|
|
|
|
// we might need to leave the old file around for a bit
|
|
// clean out unreferenced files first
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_prepare_v2_retry(rhizome_db, "DELETE FROM FILES WHERE inserttime < ? AND NOT EXISTS( SELECT 1 FROM MANIFESTS WHERE MANIFESTS.filehash = FILES.id);", -1, &stmt, NULL);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int64(stmt, 1, gettime_ms() - 60000);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_step_retry(stmt);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_finalize(stmt);
|
|
|
|
if (rhizome_manifest_get(m,"isagroup",NULL,0)!=NULL) {
|
|
int closed=rhizome_manifest_get_ll(m,"closedgroup");
|
|
if (closed<1) closed=0;
|
|
int ciphered=rhizome_manifest_get_ll(m,"cipheredgroup");
|
|
if (ciphered<1) ciphered=0;
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_prepare_v2_retry(rhizome_db, "INSERT OR REPLACE INTO GROUPLIST(id,closed,ciphered,priority) VALUES (?,?,?,?);", -1, &stmt, NULL);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_text(stmt, 1, manifestid, -1, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int(stmt, 2, closed);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int(stmt, 3, ciphered);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_int(stmt, 4, RHIZOME_PRIORITY_DEFAULT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_step_retry(stmt);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_finalize(stmt);
|
|
}
|
|
|
|
if (m->group_count>0){
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_prepare_v2_retry(rhizome_db, "INSERT OR REPLACE INTO GROUPMEMBERSHIPS(manifestid,groupid) VALUES(?, ?);", -1, &stmt, NULL);
|
|
int i;
|
|
for (i=0;i<m->group_count;i++){
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_text(stmt, 1, manifestid, -1, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_bind_text(stmt, 2, m->groups[i], -1, SQLITE_TRANSIENT);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_step_retry(stmt);
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_reset(stmt);
|
|
}
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_finalize(stmt);
|
|
}
|
|
|
|
if (SQLITE_CODE_OK(sql_ret)) sql_ret = sqlite3_exec_retry(rhizome_db, "COMMIT;", NULL, NULL, &err);
|
|
|
|
if (err!=NULL){
|
|
WHY(err);
|
|
sqlite3_free(err);
|
|
}
|
|
|
|
if (SQLITE_CODE_OK(sql_ret))
|
|
return 0;
|
|
|
|
WHYF("Failed to store bundle %s", sqlite3_errmsg(rhizome_db));
|
|
|
|
sqlite3_exec_retry(rhizome_db, "ROLLBACK;", NULL, NULL, NULL);
|
|
return -1;
|
|
}
|
|
|
|
int rhizome_finish_sqlstatement(sqlite3_stmt *statement)
|
|
{
|
|
/* Do actual insert, and abort if it fails */
|
|
int dud=0;
|
|
int r;
|
|
r=sqlite3_step(statement);
|
|
switch(r) {
|
|
case SQLITE_DONE: case SQLITE_ROW: case SQLITE_OK:
|
|
break;
|
|
default:
|
|
WHY("sqlite3_step() failed.");
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
dud++;
|
|
sqlite3_finalize(statement);
|
|
}
|
|
|
|
if ((!dud)&&((r=sqlite3_finalize(statement))!=SQLITE_OK)) {
|
|
WHY("sqlite3_finalize() failed.");
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
dud++;
|
|
}
|
|
|
|
if (dud) return WHY("SQLite3 could not complete statement.");
|
|
return 0;
|
|
}
|
|
|
|
int rhizome_list_manifests(const char *service, const char *sender_sid, const char *recipient_sid, int limit, int offset)
|
|
{
|
|
IN();
|
|
strbuf b = strbuf_alloca(1024);
|
|
strbuf_sprintf(b, "SELECT id, manifest, version, inserttime FROM manifests ORDER BY inserttime DESC");
|
|
if (limit)
|
|
strbuf_sprintf(b, " LIMIT %u", limit);
|
|
if (offset)
|
|
strbuf_sprintf(b, " OFFSET %u", offset);
|
|
if (strbuf_overrun(b))
|
|
RETURN(WHYF("SQL command too long: ", strbuf_str(b)));
|
|
sqlite3_stmt *statement;
|
|
const char *cmdtail;
|
|
int ret = 0;
|
|
if (sqlite3_prepare_v2(rhizome_db, strbuf_str(b), strbuf_len(b) + 1, &statement, &cmdtail) != SQLITE_OK) {
|
|
sqlite3_finalize(statement);
|
|
ret = WHY(sqlite3_errmsg(rhizome_db));
|
|
} else {
|
|
size_t rows = 0;
|
|
cli_puts("11"); cli_delim("\n"); // number of columns
|
|
cli_puts("service"); cli_delim(":");
|
|
cli_puts("id"); cli_delim(":");
|
|
cli_puts("version"); cli_delim(":");
|
|
cli_puts("date"); cli_delim(":");
|
|
cli_puts(".inserttime"); cli_delim(":");
|
|
cli_puts(".selfsigned"); cli_delim(":");
|
|
cli_puts("filesize"); cli_delim(":");
|
|
cli_puts("filehash"); cli_delim(":");
|
|
cli_puts("sender"); cli_delim(":");
|
|
cli_puts("recipient"); cli_delim(":");
|
|
cli_puts("name"); cli_delim("\n"); // should be last, because name may contain ':'
|
|
while (sqlite3_step(statement) == SQLITE_ROW) {
|
|
++rows;
|
|
if (!( sqlite3_column_count(statement) == 4
|
|
&& sqlite3_column_type(statement, 0) == SQLITE_TEXT
|
|
&& sqlite3_column_type(statement, 1) == SQLITE_BLOB
|
|
&& sqlite3_column_type(statement, 2) == SQLITE_INTEGER
|
|
&& sqlite3_column_type(statement, 3) == SQLITE_INTEGER
|
|
)) {
|
|
ret = WHY("Incorrect statement column");
|
|
break;
|
|
}
|
|
rhizome_manifest *m = rhizome_new_manifest();
|
|
if (m == NULL) {
|
|
ret = WHY("Out of manifests");
|
|
break;
|
|
}
|
|
const char *q_manifestid = (const char *) sqlite3_column_text(statement, 0);
|
|
const char *manifestblob = (char *) sqlite3_column_blob(statement, 1);
|
|
size_t manifestblobsize = sqlite3_column_bytes(statement, 1); // must call after sqlite3_column_blob()
|
|
long long q_version = sqlite3_column_int64(statement, 2);
|
|
long long q_inserttime = sqlite3_column_int64(statement, 3);
|
|
if (rhizome_read_manifest_file(m, manifestblob, manifestblobsize) == -1) {
|
|
WARNF("MANIFESTS row id=%s has invalid manifest blob -- skipped", q_manifestid);
|
|
} else {
|
|
long long blob_version = rhizome_manifest_get_ll(m, "version");
|
|
if (blob_version != q_version)
|
|
WARNF("MANIFESTS row id=%s version=%lld does not match manifest blob.version=%lld", q_manifestid, q_version, blob_version);
|
|
int match = 1;
|
|
const char *blob_service = rhizome_manifest_get(m, "service", NULL, 0);
|
|
if (service[0] && !(blob_service && strcasecmp(service, blob_service) == 0))
|
|
match = 0;
|
|
const char *blob_sender = rhizome_manifest_get(m, "sender", NULL, 0);
|
|
const char *blob_recipient = rhizome_manifest_get(m, "recipient", NULL, 0);
|
|
if (match && sender_sid[0]) {
|
|
if (!(blob_sender && strcasecmp(sender_sid, blob_sender) == 0))
|
|
match = 0;
|
|
}
|
|
if (match && recipient_sid[0]) {
|
|
if (!(blob_recipient && strcasecmp(recipient_sid, blob_recipient) == 0))
|
|
match = 0;
|
|
}
|
|
if (match) {
|
|
const char *blob_name = rhizome_manifest_get(m, "name", NULL, 0);
|
|
long long blob_date = rhizome_manifest_get_ll(m, "date");
|
|
const char *blob_filehash = rhizome_manifest_get(m, "filehash", NULL, 0);
|
|
long long blob_filesize = rhizome_manifest_get_ll(m, "filesize");
|
|
int self_signed = rhizome_is_self_signed(m) ? 0 : 1;
|
|
if (debug & DEBUG_RHIZOME) DEBUGF("manifest payload size = %lld", blob_filesize);
|
|
cli_puts(blob_service ? blob_service : ""); cli_delim(":");
|
|
cli_puts(q_manifestid); cli_delim(":");
|
|
cli_printf("%lld", blob_version); cli_delim(":");
|
|
cli_printf("%lld", blob_date); cli_delim(":");
|
|
cli_printf("%lld", q_inserttime); cli_delim(":");
|
|
cli_printf("%d", self_signed); cli_delim(":");
|
|
cli_printf("%lld", blob_filesize); cli_delim(":");
|
|
cli_puts(blob_filehash ? blob_filehash : ""); cli_delim(":");
|
|
cli_puts(blob_sender ? blob_sender : ""); cli_delim(":");
|
|
cli_puts(blob_recipient ? blob_recipient : ""); cli_delim(":");
|
|
cli_puts(blob_name ? blob_name : ""); cli_delim("\n");
|
|
}
|
|
}
|
|
if (m) rhizome_manifest_free(m);
|
|
}
|
|
}
|
|
sqlite3_finalize(statement);
|
|
RETURN(ret);
|
|
}
|
|
|
|
/* The following function just stores the file (or silently returns if it already exists).
|
|
The relationships of manifests to this file are the responsibility of the caller. */
|
|
int rhizome_store_file(rhizome_manifest *m,const unsigned char *key)
|
|
{
|
|
const char *file=m->dataFileName;
|
|
const char *hash=m->fileHexHash;
|
|
char hash_out[crypto_hash_sha512_BYTES*2+1];
|
|
int priority=m->fileHighestPriority;
|
|
if (m->payloadEncryption)
|
|
return WHY("Writing encrypted payloads not implemented");
|
|
|
|
if (!m->fileHashedP)
|
|
return WHY("Cannot store bundle file until it has been hashed");
|
|
|
|
int fd=open(file,O_RDONLY);
|
|
if (fd == -1) {
|
|
WHY_perror("open");
|
|
return WHY("Could not open associated file");
|
|
}
|
|
|
|
struct stat stat;
|
|
if (fstat(fd,&stat)) {
|
|
WHY_perror("fstat");
|
|
close(fd);
|
|
return WHY("Could not stat() associated file");
|
|
}
|
|
if (stat.st_size<m->fileLength) {
|
|
return WHYF("File has shrunk, so cannot be stored.");
|
|
} else if (stat.st_size>m->fileLength) {
|
|
WARNF("File has grown by %lld bytes. I will just store the original number of bytes so that the hash (hopefully) matches",stat.st_size-m->fileLength);
|
|
}
|
|
|
|
unsigned char *addr = mmap(NULL, m->fileLength, PROT_READ, MAP_FILE|MAP_SHARED, fd, 0);
|
|
if (addr==MAP_FAILED) {
|
|
WHY_perror("mmap");
|
|
close(fd);
|
|
return WHY("mmap() of associated file failed.");
|
|
}
|
|
|
|
/* INSERT INTO FILES(id as text, data blob, length integer, highestpriority integer).
|
|
BUT, we have to do this incrementally so that we can handle blobs larger than available memory.
|
|
This is possible using:
|
|
int sqlite3_bind_zeroblob(sqlite3_stmt*, int, int n);
|
|
That binds an all zeroes blob to a field. We can then populate the data by
|
|
opening a handle to the blob using:
|
|
int sqlite3_blob_write(sqlite3_blob *, const void *z, int n, int iOffset);
|
|
*/
|
|
|
|
char sqlcmd[1024];
|
|
const char *cmdtail;
|
|
|
|
/* See if the file is already stored, and if so, don't bother storing it again */
|
|
long long count = 0;
|
|
if (sqlite_exec_int64(&count, "SELECT COUNT(*) FROM FILES WHERE id='%s' AND datavalid<>0;", hash) < 1) {
|
|
close(fd);
|
|
return WHY("Failed to count stored files");
|
|
}
|
|
if (count >= 1) {
|
|
/* File is already stored, so just update the highestPriority field if required. */
|
|
long long storedPriority = -1;
|
|
if (sqlite_exec_int64(&storedPriority, "SELECT highestPriority FROM FILES WHERE id='%s' AND datavalid!=0", hash) == -1) {
|
|
close(fd);
|
|
return WHY("Failed to select highest priority");
|
|
}
|
|
if (storedPriority<priority) {
|
|
if (sqlite_exec_void("UPDATE FILES SET highestPriority=%d WHERE id='%s';", priority, hash) == -1) {
|
|
close(fd);
|
|
return WHY("SQLite failed to update highestPriority field for stored file.");
|
|
}
|
|
}
|
|
close(fd);
|
|
return 0;
|
|
}
|
|
|
|
/* Okay, so there are no records that match, but we should delete any half-baked record (with datavalid=0) so that the insert below doesn't fail.
|
|
Don't worry about the return result, since it might not delete any records. */
|
|
sqlite3_exec(rhizome_db,"DELETE FROM FILES WHERE datavalid=0;",NULL,NULL,NULL);
|
|
|
|
snprintf(sqlcmd,1024,"INSERT OR REPLACE INTO FILES(id,data,length,highestpriority,datavalid,inserttime) VALUES('%s',?,%lld,%d,0,%lld);",
|
|
hash,(long long)m->fileLength,priority,gettime_ms());
|
|
sqlite3_stmt *statement;
|
|
if (sqlite3_prepare_v2(rhizome_db,sqlcmd,strlen(sqlcmd)+1,&statement,&cmdtail)
|
|
!= SQLITE_OK)
|
|
{
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
sqlite3_finalize(statement);
|
|
close(fd);
|
|
return WHY("sqlite3_prepare_v2() failed");
|
|
}
|
|
|
|
/* Bind appropriate sized zero-filled blob to data field */
|
|
int dud=0;
|
|
int r;
|
|
if ((r=sqlite3_bind_zeroblob(statement,1,m->fileLength))!=SQLITE_OK)
|
|
{
|
|
dud++;
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
WHY("sqlite3_bind_zeroblob() failed");
|
|
}
|
|
|
|
/* Do actual insert, and abort if it fails */
|
|
if (!dud)
|
|
switch(sqlite3_step(statement)) {
|
|
case SQLITE_OK: case SQLITE_ROW: case SQLITE_DONE:
|
|
break;
|
|
default:
|
|
dud++;
|
|
WHY("sqlite3_step() failed");
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
}
|
|
|
|
if (sqlite3_finalize(statement)) dud++;
|
|
if (dud) {
|
|
if (sqlite3_finalize(statement)!=SQLITE_OK)
|
|
{
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
WHY("sqlite3_finalize() failed");
|
|
}
|
|
close(fd);
|
|
return WHY("SQLite3 failed to insert row for file");
|
|
}
|
|
|
|
/* Get rowid for inserted row, so that we can modify the blob */
|
|
int rowid=sqlite3_last_insert_rowid(rhizome_db);
|
|
if (rowid<1) {
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
close(fd);
|
|
return WHY("SQLite3 failed return rowid of inserted row");
|
|
}
|
|
|
|
sqlite3_blob *blob;
|
|
if (sqlite3_blob_open(rhizome_db,"main","FILES","data",rowid,
|
|
1 /* read/write */,
|
|
&blob) != SQLITE_OK)
|
|
{
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
sqlite3_blob_close(blob);
|
|
close(fd);
|
|
return WHY("SQLite3 failed to open file blob for writing");
|
|
}
|
|
|
|
unsigned char nonce[crypto_stream_xsalsa20_NONCEBYTES];
|
|
bzero(nonce,crypto_stream_xsalsa20_NONCEBYTES);
|
|
unsigned char buffer[RHIZOME_CRYPT_PAGE_SIZE];
|
|
|
|
/* Calculate hash of file as we go, so that we can report if
|
|
the contents have changed during import. This is also why we
|
|
use the m->fileLength instead of size returned by stat, in case
|
|
the file has been appended, e.g., if a journal is being appended to
|
|
by a separate process. This has already been shown to happen with
|
|
Serval Maps, and it is also quite possible with MeshMS and other
|
|
services. */
|
|
{
|
|
SHA512_CTX context;
|
|
SHA512_Init(&context);
|
|
|
|
long long i;
|
|
for(i=0;i<m->fileLength;i+=RHIZOME_CRYPT_PAGE_SIZE)
|
|
{
|
|
int n=RHIZOME_CRYPT_PAGE_SIZE;
|
|
if (i+n>m->fileLength) n=m->fileLength-i;
|
|
SHA512_Update(&context, &addr[i], n);
|
|
if (key) {
|
|
/* calculate block nonce */
|
|
int j; for(j=0;j<8;j++) nonce[i]=(i>>(j*8))&0xff;
|
|
crypto_stream_xsalsa20_xor(&addr[i],&buffer[0],n,
|
|
nonce,key);
|
|
if (sqlite3_blob_write(blob,&buffer[0],n,i) !=SQLITE_OK) dud++;
|
|
}
|
|
else
|
|
if (sqlite3_blob_write(blob,&addr[i],n,i) !=SQLITE_OK) dud++;
|
|
}
|
|
SHA512_End(&context, (char *)hash_out);
|
|
str_toupper_inplace(hash_out);
|
|
}
|
|
|
|
if (sqlite3_blob_close(blob)!=SQLITE_OK) dud++;
|
|
|
|
close(fd);
|
|
|
|
if (strcasecmp(hash_out,hash))
|
|
{
|
|
WHYF("Computed hash = %s",hash_out);
|
|
return WHY("File hash does not match -- has file been modified while being stored?");
|
|
}
|
|
|
|
/* Mark file as up-to-date */
|
|
if (sqlite_exec_void("UPDATE FILES SET datavalid=1 WHERE id='%s';", hash))
|
|
return WHY("Failed to set datavalid");
|
|
|
|
if (dud) {
|
|
WHY(sqlite3_errmsg(rhizome_db));
|
|
return WHY("SQLite3 failed write all blob data");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
void rhizome_bytes_to_hex_upper(unsigned const char *in, char *out, int byteCount)
|
|
{
|
|
(void) tohex(out, in, byteCount);
|
|
}
|
|
|
|
int rhizome_update_file_priority(const char *fileid)
|
|
{
|
|
/* work out the highest priority of any referrer */
|
|
long long highestPriority = -1;
|
|
if (sqlite_exec_int64(&highestPriority,
|
|
"SELECT max(grouplist.priority) FROM MANIFESTS,GROUPMEMBERSHIPS,GROUPLIST"
|
|
" where manifests.filehash='%s'"
|
|
" AND groupmemberships.manifestid=manifests.id"
|
|
" AND groupmemberships.groupid=grouplist.id;",
|
|
fileid) == -1)
|
|
return -1;
|
|
if (highestPriority >= 0)
|
|
return sqlite_exec_void("UPDATE files set highestPriority=%lld WHERE id='%s';", highestPriority, fileid);
|
|
return 0;
|
|
}
|
|
|
|
/* Search the database for a manifest having the same name and payload content,
|
|
and if the version is known, having the same version.
|
|
|
|
@author Andrew Bettison <andrew@servalproject.com>
|
|
*/
|
|
int rhizome_find_duplicate(const rhizome_manifest *m, rhizome_manifest **found,
|
|
int checkVersionP)
|
|
{
|
|
if (!m->fileHashedP)
|
|
return WHY("Manifest payload is not hashed");
|
|
const char *service = rhizome_manifest_get(m, "service", NULL, 0);
|
|
const char *name = NULL;
|
|
const char *sender = NULL;
|
|
const char *recipient = NULL;
|
|
if (service == NULL) {
|
|
return WHY("Manifest has no service");
|
|
} else if (strcasecmp(service, RHIZOME_SERVICE_FILE) == 0) {
|
|
name = rhizome_manifest_get(m, "name", NULL, 0);
|
|
if (!name) return WHY("Manifest has no name");
|
|
} else if (strcasecmp(service, RHIZOME_SERVICE_MESHMS) == 0) {
|
|
sender = rhizome_manifest_get(m, "sender", NULL, 0);
|
|
recipient = rhizome_manifest_get(m, "recipient", NULL, 0);
|
|
if (!sender) return WHY("Manifest has no sender");
|
|
if (!recipient) return WHY("Manifest has no recipient");
|
|
} else {
|
|
return WHYF("Unsupported service '%s'", service);
|
|
}
|
|
char sqlcmd[1024];
|
|
char *s = sqlcmd;
|
|
s += snprintf(s, &sqlcmd[sizeof sqlcmd] - s,
|
|
"SELECT id, manifest, version FROM manifests"
|
|
" WHERE filehash = ?"
|
|
);
|
|
if (checkVersionP && s < &sqlcmd[sizeof sqlcmd])
|
|
s += snprintf(s, sqlcmd + sizeof(sqlcmd) - s, " AND version = ?");
|
|
if (s >= &sqlcmd[sizeof sqlcmd])
|
|
return WHY("SQL command too long");
|
|
int ret = 0;
|
|
sqlite3_stmt *statement;
|
|
const char *cmdtail;
|
|
if (debug&DEBUG_RHIZOME) DEBUGF("sql query: %s",sqlcmd);
|
|
if (sqlite3_prepare_v2(rhizome_db, sqlcmd, strlen(sqlcmd) + 1, &statement, &cmdtail) != SQLITE_OK) {
|
|
ret = WHY(sqlite3_errmsg(rhizome_db));
|
|
} else {
|
|
char filehash[RHIZOME_FILEHASH_STRLEN + 1];
|
|
strncpy(filehash, m->fileHexHash, sizeof filehash);
|
|
str_toupper_inplace(filehash);
|
|
if (debug & DEBUG_RHIZOME) DEBUGF("filehash=\"%s\"", filehash);
|
|
sqlite3_bind_text(statement, 1, filehash, -1, SQLITE_STATIC);
|
|
if (checkVersionP)
|
|
sqlite3_bind_int64(statement, 2, m->version);
|
|
size_t rows = 0;
|
|
while (sqlite3_step(statement) == SQLITE_ROW) {
|
|
++rows;
|
|
if (debug & DEBUG_RHIZOME) DEBUGF("Row %d", rows);
|
|
if (!( sqlite3_column_count(statement) == 3
|
|
&& sqlite3_column_type(statement, 0) == SQLITE_TEXT
|
|
&& sqlite3_column_type(statement, 1) == SQLITE_BLOB
|
|
&& sqlite3_column_type(statement, 2) == SQLITE_INTEGER
|
|
)) {
|
|
ret = WHY("Incorrect statement columns");
|
|
break;
|
|
}
|
|
const char *q_manifestid = (const char *) sqlite3_column_text(statement, 0);
|
|
size_t manifestidsize = sqlite3_column_bytes(statement, 0); // must call after sqlite3_column_text()
|
|
unsigned char manifest_id[RHIZOME_MANIFEST_ID_BYTES];
|
|
if ( manifestidsize != crypto_sign_edwards25519sha512batch_PUBLICKEYBYTES * 2
|
|
|| fromhexstr(manifest_id, q_manifestid, RHIZOME_MANIFEST_ID_BYTES) == -1
|
|
) {
|
|
ret = WHYF("Malformed manifest.id from query: %s", q_manifestid);
|
|
break;
|
|
}
|
|
const char *manifestblob = (char *) sqlite3_column_blob(statement, 1);
|
|
size_t manifestblobsize = sqlite3_column_bytes(statement, 1); // must call after sqlite3_column_blob()
|
|
long long q_version = sqlite3_column_int64(statement, 2);
|
|
rhizome_manifest *blob_m = rhizome_new_manifest();
|
|
if (blob_m == NULL) {
|
|
ret = WHY("Out of manifests");
|
|
break;
|
|
}
|
|
if (rhizome_read_manifest_file(blob_m, manifestblob, manifestblobsize) == -1) {
|
|
WARNF("MANIFESTS row id=%s has invalid manifest blob -- skipped", q_manifestid);
|
|
} else if (rhizome_manifest_verify(blob_m)) {
|
|
WARNF("MANIFESTS row id=%s fails verification -- skipped", q_manifestid);
|
|
} else {
|
|
const char *blob_service = rhizome_manifest_get(blob_m, "service", NULL, 0);
|
|
const char *blob_id = rhizome_manifest_get(blob_m, "id", NULL, 0);
|
|
long long blob_version = rhizome_manifest_get_ll(blob_m, "version");
|
|
const char *blob_filehash = rhizome_manifest_get(blob_m, "filehash", NULL, 0);
|
|
long long blob_filesize = rhizome_manifest_get_ll(blob_m, "filesize");
|
|
if (debug & DEBUG_RHIZOME)
|
|
DEBUGF("Consider manifest.service=%s manifest.id=%s manifest.version=%lld", blob_service, q_manifestid, blob_version);
|
|
/* Perform consistency checks, because we're paranoid. */
|
|
int inconsistent = 0;
|
|
if (blob_id && strcasecmp(blob_id, q_manifestid)) {
|
|
WARNF("MANIFESTS row id=%s has inconsistent blob with id=%s -- skipped", q_manifestid, blob_id);
|
|
++inconsistent;
|
|
}
|
|
if (checkVersionP && blob_version != q_version) {
|
|
WARNF("MANIFESTS row id=%s has inconsistent blob: manifests.version=%lld, blob.version=%lld -- skipped",
|
|
q_manifestid, q_version, blob_version);
|
|
++inconsistent;
|
|
}
|
|
if (!blob_filehash && strcasecmp(blob_filehash, m->fileHexHash)) {
|
|
WARNF("MANIFESTS row id=%s joined to FILES row id=%s has inconsistent blob: blob.filehash=%s -- skipped",
|
|
q_manifestid, m->fileHexHash, blob_filehash);
|
|
++inconsistent;
|
|
}
|
|
if (blob_filesize != -1 && blob_filesize != m->fileLength) {
|
|
WARNF("MANIFESTS row id=%s joined to FILES row id=%s has inconsistent blob: known file size %lld, blob.filesize=%lld -- skipped",
|
|
q_manifestid, m->fileHexHash, m->fileLength, blob_filesize);
|
|
++inconsistent;
|
|
}
|
|
if (checkVersionP && q_version != m->version) {
|
|
WARNF("SELECT query with version=%lld returned incorrect row: manifests.version=%lld -- skipped", m->version, q_version);
|
|
++inconsistent;
|
|
}
|
|
if (blob_service == NULL) {
|
|
WARNF("MANIFESTS row id=%s has blob with no 'service' -- skipped", q_manifestid, blob_id);
|
|
++inconsistent;
|
|
}
|
|
if (!inconsistent) {
|
|
strbuf b = strbuf_alloca(1024);
|
|
if (strcasecmp(service, RHIZOME_SERVICE_FILE) == 0) {
|
|
const char *blob_name = rhizome_manifest_get(blob_m, "name", NULL, 0);
|
|
if (blob_name && !strcmp(blob_name, name)) {
|
|
if (debug & DEBUG_RHIZOME)
|
|
strbuf_sprintf(b, " name=\"%s\"", blob_name);
|
|
ret = 1;
|
|
}
|
|
} else if (strcasecmp(service, RHIZOME_SERVICE_FILE) == 0) {
|
|
const char *blob_sender = rhizome_manifest_get(blob_m, "sender", NULL, 0);
|
|
const char *blob_recipient = rhizome_manifest_get(blob_m, "recipient", NULL, 0);
|
|
if (blob_sender && !strcasecmp(blob_sender, sender) && blob_recipient && !strcasecmp(blob_recipient, recipient)) {
|
|
if (debug & DEBUG_RHIZOME)
|
|
strbuf_sprintf(b, " sender=%s recipient=%s", blob_sender, blob_recipient);
|
|
ret = 1;
|
|
}
|
|
}
|
|
if (ret == 1) {
|
|
memcpy(blob_m->cryptoSignPublic, manifest_id, RHIZOME_MANIFEST_ID_BYTES);
|
|
memcpy(blob_m->fileHexHash, m->fileHexHash, RHIZOME_FILEHASH_STRLEN + 1);
|
|
blob_m->fileHashedP = 1;
|
|
blob_m->fileLength = m->fileLength;
|
|
blob_m->version = q_version;
|
|
*found = blob_m;
|
|
DEBUGF("Found duplicate payload: service=%s%s version=%llu hexhash=%s",
|
|
blob_service, strbuf_str(b), blob_m->version, blob_m->fileHexHash
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (blob_m) rhizome_manifest_free(blob_m);
|
|
}
|
|
}
|
|
sqlite3_finalize(statement);
|
|
return ret;
|
|
}
|
|
|
|
/* Retrieve a manifest from the database, given its manifest ID.
|
|
*
|
|
* Returns 1 if manifest is found (if mp != NULL then a new manifest struct is allocated, made
|
|
* finalisable and * assigned to *mp, caller is responsible for freeing).
|
|
* Returns 0 if manifest is not found (*mp is unchanged).
|
|
* Returns -1 on error (*mp is unchanged).
|
|
*/
|
|
int rhizome_retrieve_manifest(const char *manifestid, rhizome_manifest **mp)
|
|
{
|
|
unsigned char manifest_id[RHIZOME_MANIFEST_ID_BYTES];
|
|
if (fromhexstr(manifest_id, manifestid, RHIZOME_MANIFEST_ID_BYTES) == -1)
|
|
return WHY("Invalid manifest ID");
|
|
char sqlcmd[1024];
|
|
int n = snprintf(sqlcmd, sizeof(sqlcmd), "SELECT id, manifest, version, inserttime FROM manifests WHERE id = ?");
|
|
if (n >= sizeof(sqlcmd))
|
|
return WHY("SQL command too long");
|
|
sqlite3_stmt *statement;
|
|
const char *cmdtail;
|
|
int ret = 0;
|
|
rhizome_manifest *m = NULL;
|
|
if (sqlite3_prepare_v2(rhizome_db, sqlcmd, strlen(sqlcmd) + 1, &statement, &cmdtail) != SQLITE_OK) {
|
|
sqlite3_finalize(statement);
|
|
ret = WHY(sqlite3_errmsg(rhizome_db));
|
|
} else {
|
|
char manifestIdUpper[RHIZOME_MANIFEST_ID_STRLEN + 1];
|
|
tohex(manifestIdUpper, manifest_id, RHIZOME_MANIFEST_ID_BYTES);
|
|
sqlite3_bind_text(statement, 1, manifestIdUpper, -1, SQLITE_STATIC);
|
|
while (sqlite3_step(statement) == SQLITE_ROW) {
|
|
if (!( sqlite3_column_count(statement) == 4
|
|
&& sqlite3_column_type(statement, 0) == SQLITE_TEXT
|
|
&& sqlite3_column_type(statement, 1) == SQLITE_BLOB
|
|
&& sqlite3_column_type(statement, 2) == SQLITE_INTEGER
|
|
&& sqlite3_column_type(statement, 3) == SQLITE_INTEGER
|
|
)) {
|
|
ret = WHY("Incorrect statement column");
|
|
break;
|
|
}
|
|
const char *q_manifestid = (const char *) sqlite3_column_text(statement, 0);
|
|
const char *manifestblob = (char *) sqlite3_column_blob(statement, 1);
|
|
size_t manifestblobsize = sqlite3_column_bytes(statement, 1); // must call after sqlite3_column_blob()
|
|
if (mp) {
|
|
m = rhizome_new_manifest();
|
|
if (m == NULL) {
|
|
WARNF("MANIFESTS row id=%s has invalid manifest blob -- skipped", q_manifestid);
|
|
ret = WHY("Out of manifests");
|
|
} else if (rhizome_read_manifest_file(m, manifestblob, manifestblobsize) == -1) {
|
|
WARNF("MANIFESTS row id=%s has invalid manifest blob -- skipped", q_manifestid);
|
|
ret = WHY("Invalid manifest blob from database");
|
|
} else {
|
|
ret = 1;
|
|
memcpy(m->cryptoSignPublic, manifest_id, RHIZOME_MANIFEST_ID_BYTES);
|
|
const char *blob_service = rhizome_manifest_get(m, "service", NULL, 0);
|
|
if (blob_service == NULL)
|
|
ret = WHY("Manifest is missing 'service' field");
|
|
const char *blob_filehash = rhizome_manifest_get(m, "filehash", NULL, 0);
|
|
if (blob_filehash == NULL)
|
|
ret = WHY("Manifest is missing 'filehash' field");
|
|
else {
|
|
memcpy(m->fileHexHash, blob_filehash, RHIZOME_FILEHASH_STRLEN + 1);
|
|
m->fileHashedP = 1;
|
|
}
|
|
long long blob_version = rhizome_manifest_get_ll(m, "version");
|
|
if (blob_version == -1)
|
|
ret = WHY("Manifest is missing 'version' field");
|
|
else
|
|
m->version = blob_version;
|
|
long long filesizeq = rhizome_manifest_get_ll(m, "filesize");
|
|
if (filesizeq == -1)
|
|
ret = WHY("Manifest is missing 'filesize' field");
|
|
else
|
|
m->fileLength = filesizeq;
|
|
if (ret == 1) {
|
|
cli_puts("service"); cli_delim(":");
|
|
cli_puts(blob_service); cli_delim("\n");
|
|
cli_puts("manifestid"); cli_delim(":");
|
|
cli_puts(q_manifestid); cli_delim("\n");
|
|
cli_puts("version"); cli_delim(":");
|
|
cli_printf("%lld", (long long) sqlite3_column_int64(statement, 2)); cli_delim("\n");
|
|
cli_puts("inserttime"); cli_delim(":");
|
|
cli_printf("%lld", (long long) sqlite3_column_int64(statement, 3)); cli_delim("\n");
|
|
cli_puts("filehash"); cli_delim(":");
|
|
cli_puts(m->fileHexHash); cli_delim("\n");
|
|
cli_puts("filesize"); cli_delim(":");
|
|
cli_printf("%lld", (long long) m->fileLength); cli_delim("\n");
|
|
// Could write the manifest blob to the CLI output here, but that would require the output to
|
|
// support byte[] fields as well as String fields.
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
sqlite3_finalize(statement);
|
|
if (mp && ret == 1)
|
|
*mp = m;
|
|
return ret;
|
|
}
|
|
|
|
/* Retrieve a file from the database, given its file hash.
|
|
*
|
|
* Returns 1 if file is found (contents are written to filepath if given).
|
|
* Returns 0 if file is not found.
|
|
* Returns -1 on error.
|
|
*/
|
|
int rhizome_retrieve_file(const char *fileid, const char *filepath,
|
|
const unsigned char *key)
|
|
{
|
|
sqlite3_blob *blob=NULL;
|
|
if (rhizome_update_file_priority(fileid) == -1) {
|
|
WHY("Failed to update file priority");
|
|
return 0;
|
|
}
|
|
char sqlcmd[1024];
|
|
int n = snprintf(sqlcmd, sizeof(sqlcmd), "SELECT id, rowid, length FROM files WHERE id = ? AND datavalid != 0");
|
|
if (n >= sizeof(sqlcmd))
|
|
{ WHY("SQL command too long"); return 0; }
|
|
sqlite3_stmt *statement;
|
|
const char *cmdtail;
|
|
int ret = 0;
|
|
if (sqlite3_prepare_v2(rhizome_db, sqlcmd, strlen(sqlcmd) + 1, &statement, &cmdtail) != SQLITE_OK) {
|
|
ret = WHY(sqlite3_errmsg(rhizome_db));
|
|
} else {
|
|
char fileIdUpper[RHIZOME_FILEHASH_STRLEN + 1];
|
|
strncpy(fileIdUpper, fileid, sizeof fileIdUpper);
|
|
fileIdUpper[RHIZOME_FILEHASH_STRLEN] = '\0';
|
|
str_toupper_inplace(fileIdUpper);
|
|
sqlite3_bind_text(statement, 1, fileIdUpper, -1, SQLITE_STATIC);
|
|
int stepcode = sqlite3_step(statement);
|
|
if (stepcode != SQLITE_ROW) {
|
|
WHY("File not found");
|
|
ret = 0; /* no files returned */
|
|
} else if (!( sqlite3_column_count(statement) == 3
|
|
&& sqlite3_column_type(statement, 0) == SQLITE_TEXT
|
|
&& sqlite3_column_type(statement, 1) == SQLITE_INTEGER
|
|
&& sqlite3_column_type(statement, 2) == SQLITE_INTEGER
|
|
)) {
|
|
WHY("Incorrect statement column");
|
|
ret = 0; /* no files returned */
|
|
} else {
|
|
long long length = sqlite3_column_int64(statement, 2);
|
|
long long rowid = sqlite3_column_int64(statement, 1);
|
|
if (sqlite3_blob_open(rhizome_db,"main","files","data",rowid,
|
|
0 /* read only */,&blob)!=SQLITE_OK) {
|
|
ret =0;
|
|
WHY("Could not open blob for reading");
|
|
}
|
|
|
|
cli_puts("filehash"); cli_delim(":");
|
|
cli_puts((const char *)sqlite3_column_text(statement, 0)); cli_delim("\n");
|
|
cli_puts("filesize"); cli_delim(":");
|
|
cli_printf("%lld", length); cli_delim("\n");
|
|
ret = 1;
|
|
if (filepath&&filepath[0]) {
|
|
int fd = open(filepath, O_WRONLY | O_CREAT | O_TRUNC, 0775);
|
|
if (fd == -1) {
|
|
WHY_perror("open");
|
|
ret = WHYF("Cannot open %s for write/create", filepath);
|
|
} else {
|
|
/* read from blob and write to disk, decrypting if necessary as
|
|
we go.
|
|
Each 4KB block of data has a nonce which is fed with the key
|
|
into crypto_stream_xsalsa20(). The nonce is the file address
|
|
divided by 4KB. This approach is used as it allows us to append
|
|
to files easily, without having to get the XOR stream for the whole
|
|
file, and without the cipher on existing bytes having to change.
|
|
Both of these are important properties for journal bundles, such as
|
|
will be used by MeshMS. For non-journal bundles where it is important
|
|
that changing the payload changes the encryption key (so that the XOR
|
|
between any two versions of the payload cannot be easily obtained).
|
|
We will do this by having journal manifests identified, causing the
|
|
key to be locked, rather than based on the version number.
|
|
But anyway, we are supplied with the key here, so all we need to do
|
|
is do the block counting and call crypto_stream_xsalsa20().
|
|
*/
|
|
long long offset;
|
|
unsigned char nonce[crypto_stream_xsalsa20_NONCEBYTES];
|
|
bzero(nonce,crypto_stream_xsalsa20_NONCEBYTES);
|
|
unsigned char buffer[RHIZOME_CRYPT_PAGE_SIZE];
|
|
for(offset=0;offset<length;offset+=RHIZOME_CRYPT_PAGE_SIZE)
|
|
{
|
|
long long count=length-offset;
|
|
if (count>RHIZOME_CRYPT_PAGE_SIZE) count=RHIZOME_CRYPT_PAGE_SIZE;
|
|
if(sqlite3_blob_read(blob,&buffer[0],count,offset)!=SQLITE_OK) {
|
|
ret =0;
|
|
WHYF("Error reading %lld bytes of data from blob at offset 0x%llx",
|
|
count, offset);
|
|
WHYF("sqlite says: %s",sqlite3_errmsg(rhizome_db));
|
|
}
|
|
if (key) {
|
|
/* calculate block nonce */
|
|
int i; for(i=0;i<8;i++) nonce[i]=(offset>>(i*8))&0xff;
|
|
crypto_stream_xsalsa20_xor(&buffer[0],&buffer[0],count,
|
|
nonce,key);
|
|
}
|
|
if (write(fd,buffer,count)!=count) {
|
|
ret =0;
|
|
WHY("Failed to write data to file");
|
|
}
|
|
}
|
|
sqlite3_blob_close(blob); blob=NULL;
|
|
}
|
|
if (fd != -1 && close(fd) == -1) {
|
|
WHY_perror("close");
|
|
ret = 0; WHYF("Error flushing to %s ", filepath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (blob) sqlite3_blob_close(blob);
|
|
sqlite3_finalize(statement);
|
|
return ret;
|
|
}
|
|
|