feat: add Gemini and Spartan protocol support
This commit is contained in:
parent
a4a49f41d7
commit
fa7aa5557c
48 changed files with 4992 additions and 161 deletions
|
|
@ -6,6 +6,7 @@ import 'package:path/path.dart' as p;
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../mews/document.dart' show MewsEntry;
|
||||
import '../protocols/tofu.dart' show TofuPin;
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
|
|
@ -101,6 +102,42 @@ class CachedPages extends Table {
|
|||
Set<Column> get primaryKey => {url};
|
||||
}
|
||||
|
||||
/// A Gemini host the reader's first contact with it pinned, together with the
|
||||
/// certificate that was accepted (see lib/protocols/tofu.dart). The pin is
|
||||
/// the trust: a later visit compares against it, and a change is accepted
|
||||
/// only when the pin has expired or the reader chose to trust the new one.
|
||||
class KnownHosts extends Table {
|
||||
TextColumn get host => text()();
|
||||
IntColumn get port => integer()();
|
||||
TextColumn get fingerprint => text()();
|
||||
DateTimeColumn get expires => dateTime()();
|
||||
DateTimeColumn get pinnedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {host, port};
|
||||
}
|
||||
|
||||
/// A client certificate the reader holds, for wearing as an identity on sites
|
||||
/// that ask for one. The certificate is public record; its private key lives
|
||||
/// in a file of the app's own storage, never in this database.
|
||||
class ClientCertificates extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text()();
|
||||
TextColumn get certPem => text()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Which certificate the reader presents to one host: a binding the reader
|
||||
/// made on a certificate request, worn only there.
|
||||
class SiteCertificates extends Table {
|
||||
TextColumn get host => text()();
|
||||
IntColumn get port => integer()();
|
||||
IntColumn get certificateId => integer()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {host, port};
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [
|
||||
HistoryItems,
|
||||
Bookmarks,
|
||||
|
|
@ -109,6 +146,9 @@ class CachedPages extends Table {
|
|||
ItemTags,
|
||||
ReaderSettings,
|
||||
CachedPages,
|
||||
KnownHosts,
|
||||
ClientCertificates,
|
||||
SiteCertificates,
|
||||
])
|
||||
class MewsDatabase extends _$MewsDatabase {
|
||||
MewsDatabase() : super(_open());
|
||||
|
|
@ -117,7 +157,7 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
MewsDatabase.test(super.executor);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 5;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
|
|
@ -136,6 +176,13 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
if (from < 3) {
|
||||
await m.addColumn(readerSettings, readerSettings.language);
|
||||
}
|
||||
if (from < 4) {
|
||||
await m.createTable(knownHosts);
|
||||
}
|
||||
if (from < 5) {
|
||||
await m.createTable(clientCertificates);
|
||||
await m.createTable(siteCertificates);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -557,6 +604,92 @@ class MewsDatabase extends _$MewsDatabase {
|
|||
|
||||
Future<void> saveCachedPage(CachedPagesCompanion entry) =>
|
||||
into(cachedPages).insert(entry, mode: InsertMode.insertOrReplace);
|
||||
|
||||
// Known hosts ---------------------------------------------------------------
|
||||
|
||||
/// Every pinned host, keyed `host:port`, for the fetch that checks each of
|
||||
/// them, redirect targets included. The table holds only hosts the reader
|
||||
/// has actually visited, so it is small enough to hand over whole.
|
||||
Future<Map<String, TofuPin>> allPins() async {
|
||||
final rows = await select(knownHosts).get();
|
||||
return {
|
||||
for (final row in rows)
|
||||
'${row.host}:${row.port}':
|
||||
TofuPin(fingerprint: row.fingerprint, expires: row.expires),
|
||||
};
|
||||
}
|
||||
|
||||
/// Pins a host's certificate: the first time the host was seen, a renewal
|
||||
/// the old pin had expired for, or the reader choosing to trust a change.
|
||||
Future<void> savePin(String host, int port, TofuPin pin) =>
|
||||
into(knownHosts).insert(
|
||||
KnownHostsCompanion.insert(
|
||||
host: host,
|
||||
port: port,
|
||||
fingerprint: pin.fingerprint,
|
||||
expires: pin.expires,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace);
|
||||
|
||||
// Identities ---------------------------------------------------------------
|
||||
|
||||
/// Every certificate the reader holds, oldest first.
|
||||
Future<List<ClientCertificate>> allClientCertificates() =>
|
||||
(select(clientCertificates)..orderBy([
|
||||
(r) => OrderingTerm.asc(r.createdAt),
|
||||
(r) => OrderingTerm.asc(r.id),
|
||||
]))
|
||||
.get();
|
||||
|
||||
Future<ClientCertificate?> clientCertificate(int id) =>
|
||||
(select(clientCertificates)..where((r) => r.id.equals(id))).getSingleOrNull();
|
||||
|
||||
Future<int> addClientCertificate(String name, String certPem) =>
|
||||
into(clientCertificates)
|
||||
.insert(ClientCertificatesCompanion.insert(name: name, certPem: certPem));
|
||||
|
||||
Future<void> renameClientCertificate(int id, String name) =>
|
||||
(update(clientCertificates)..where((r) => r.id.equals(id)))
|
||||
.write(ClientCertificatesCompanion(name: Value(name)));
|
||||
|
||||
/// Removes a certificate together with the bindings that wear it, so no
|
||||
/// site is left asking for a certificate that no longer exists.
|
||||
Future<void> deleteClientCertificate(int id) => transaction(() async {
|
||||
await (delete(siteCertificates)..where((r) => r.certificateId.equals(id))).go();
|
||||
await (delete(clientCertificates)..where((r) => r.id.equals(id))).go();
|
||||
});
|
||||
|
||||
/// The certificate bound to one host, or null when the reader has not
|
||||
/// chosen one for it.
|
||||
Future<int?> bindingFor(String host, int port) async {
|
||||
final row = await (select(siteCertificates)
|
||||
..where((r) => r.host.equals(host) & r.port.equals(port)))
|
||||
.getSingleOrNull();
|
||||
return row?.certificateId;
|
||||
}
|
||||
|
||||
/// The hosts a certificate is presented to, so the page can say where an
|
||||
/// identity is worn.
|
||||
Future<List<String>> bindingsOf(int certificateId) async {
|
||||
final rows = await (select(siteCertificates)
|
||||
..where((r) => r.certificateId.equals(certificateId))
|
||||
..orderBy([
|
||||
(r) => OrderingTerm.asc(r.host),
|
||||
(r) => OrderingTerm.asc(r.port),
|
||||
]))
|
||||
.get();
|
||||
return [for (final row in rows) '${row.host}:${row.port}'];
|
||||
}
|
||||
|
||||
/// Binds a certificate to a host, replacing whatever was bound before.
|
||||
Future<void> bindCertificate(String host, int port, int certificateId) =>
|
||||
into(siteCertificates).insert(
|
||||
SiteCertificatesCompanion.insert(
|
||||
host: host,
|
||||
port: port,
|
||||
certificateId: certificateId,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
/// How the posts page is ordered.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue