Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for secretKey and cloud instances + update shared client testcases #14

Merged
merged 3 commits into from
Feb 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,7 @@ Client createClient(
String? database,
String? user,
String? password,
String? secretKey,
Map<String, String>? serverSettings,
String? tlsCA,
String? tlsCAFile,
Expand All @@ -818,6 +819,7 @@ Client createClient(
database: database ?? config?.database,
user: user ?? config?.user,
password: password ?? config?.password,
secretKey: secretKey ?? config?.secretKey,
serverSettings: serverSettings ?? config?.serverSettings,
tlsCA: tlsCA ?? config?.tlsCA,
tlsCAFile: tlsCAFile ?? config?.tlsCAFile,
Expand Down
117 changes: 108 additions & 9 deletions lib/src/connect_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:developer';
import 'dart:io';

import 'package:crypto/crypto.dart';
import 'package:edgedb/src/utils/crc_hqx.dart';
import 'package:path/path.dart';

import 'credentials.dart';
Expand Down Expand Up @@ -47,6 +48,7 @@ class ConnectConfig {
String? database;
String? user;
String? password;
String? secretKey;
Map<String, String>? serverSettings;
String? tlsCA;
String? tlsCAFile;
Expand All @@ -63,6 +65,7 @@ class ConnectConfig {
this.database,
this.user,
this.password,
this.secretKey,
this.serverSettings,
this.tlsCA,
this.tlsCAFile,
Expand All @@ -79,6 +82,7 @@ class ConnectConfig {
database = json['database'],
user = json['user'],
password = json['password'],
secretKey = json['secretKey'],
serverSettings = json['serverSettings'] != null
? Map.castFrom(json['serverSettings'])
: null,
Expand All @@ -104,6 +108,7 @@ class ConnectConfig {
'database': database,
'user': user,
'password': password,
'secretKey': secretKey,
'serverSettings': serverSettings,
'tlsCA': tlsCA,
'tlsCAFile': tlsCAFile,
Expand Down Expand Up @@ -138,6 +143,8 @@ class ResolvedConnectConfig {
SourcedValue<String>? _database;
SourcedValue<String>? _user;
SourcedValue<String>? _password;
SourcedValue<String>? _secretKey;
SourcedValue<String>? _cloudProfile;
SourcedValue<String>? _tlsCAData;
SourcedValue<TLSSecurity>? _tlsSecurity;
SourcedValue<int>? _waitUntilAvailable;
Expand Down Expand Up @@ -180,6 +187,18 @@ class ResolvedConnectConfig {
}
}

void setSecretKey(SourcedValue<String?> secretKey) {
if (_secretKey == null && secretKey.value != null) {
_secretKey = SourcedValue.from(secretKey);
}
}

void setCloudProfile(SourcedValue<String?> cloudProfile) {
if (_cloudProfile == null && cloudProfile.value != null) {
_cloudProfile = SourcedValue.from(cloudProfile);
}
}

void setTlsCAData(SourcedValue<String?> caData) {
if (_tlsCAData == null && caData.value != null) {
_tlsCAData = SourcedValue.from(caData);
Expand Down Expand Up @@ -276,6 +295,14 @@ class ResolvedConnectConfig {
return _password?.value;
}

String? get secretKey {
return _secretKey?.value;
}

String? get cloudProfile {
return _cloudProfile?.value ?? 'default';
}

TLSSecurity get tlsSecurity {
return _tlsSecurity != null &&
_tlsSecurity!.value != TLSSecurity.defaultSecurity
Expand Down Expand Up @@ -372,7 +399,7 @@ class ResolvedConnectConfig {
}
}

Future<String> stashPath(String projectDir) async {
Future<String> getStashPath(String projectDir) async {
var projectPath = await Directory(projectDir).resolveSymbolicLinks();
if (Platform.isWindows && !projectPath.startsWith('\\\\')) {
projectPath = '\\\\?\\$projectPath';
Expand Down Expand Up @@ -479,7 +506,8 @@ Future<ResolvedConnectConfig> parseConnectConfig(ConnectConfig config) async {
var hasCompoundOptions = await resolveConfigOptions(
resolvedConfig,
"Cannot have more than one of the following connection options: "
"'dsnOrInstanceName', 'credentials', 'credentialsFile' or 'host'/'port'",
"'dsn', 'instanceName', 'credentials', 'credentialsFile' or 'host'/'port'",
null,
dsn: sourcedDsn,
instanceName: sourcedInstance,
credentials: SourcedValue(config.credentials, "'credentials' option"),
Expand All @@ -490,6 +518,7 @@ Future<ResolvedConnectConfig> parseConnectConfig(ConnectConfig config) async {
database: SourcedValue(config.database, "'database' option"),
user: SourcedValue(config.user, "'user' option"),
password: SourcedValue(config.password, "'password' option"),
secretKey: SourcedValue(config.secretKey, "'secretKey' option"),
tlsCA: SourcedValue(config.tlsCA, "'tlsCA' option"),
tlsCAFile: SourcedValue(config.tlsCAFile, "'tlsCAFile' option"),
tlsSecurity: SourcedValue(config.tlsSecurity, "'tlsSecurity' option"),
Expand All @@ -515,6 +544,7 @@ Future<ResolvedConnectConfig> parseConnectConfig(ConnectConfig config) async {
"Cannot have more than one of the following connection environment variables: "
"'EDGEDB_DSN', 'EDGEDB_INSTANCE', 'EDGEDB_CREDENTIALS', "
"'EDGEDB_CREDENTIALS_FILE' or 'EDGEDB_HOST'",
null,
dsn: SourcedValue(
getEnvVar('EDGEDB_DSN'), "'EDGEDB_DSN' environment variable"),
instanceName: SourcedValue(getEnvVar('EDGEDB_INSTANCE'),
Expand All @@ -532,6 +562,10 @@ Future<ResolvedConnectConfig> parseConnectConfig(ConnectConfig config) async {
getEnvVar('EDGEDB_USER'), "'EDGEDB_USER' environment variable"),
password: SourcedValue(getEnvVar('EDGEDB_PASSWORD'),
"'EDGEDB_PASSWORD' environment variable"),
secretKey: SourcedValue(getEnvVar('EDGEDB_SECRET_KEY'),
"'EDGEDB_SECRET_KEY' environment variable"),
cloudProfile: SourcedValue(getEnvVar('EDGEDB_CLOUD_PROFILE'),
"'EDGEDB_CLOUD_PROFILE' environment variable"),
tlsCA: SourcedValue(
getEnvVar('EDGEDB_TLS_CA'), "'EDGEDB_TLS_CA' environment variable"),
tlsCAFile: SourcedValue(getEnvVar('EDGEDB_TLS_CA_FILE'),
Expand All @@ -553,14 +587,21 @@ Future<ResolvedConnectConfig> parseConnectConfig(ConnectConfig config) async {
" variables EDGEDB_HOST, EDGEDB_INSTANCE, EDGEDB_DSN, "
"EDGEDB_CREDENTIALS or EDGEDB_CREDENTIALS_FILE");
}
final instancePath = await searchConfigDir(
join(await stashPath(projectDir), 'instance-name'));
final stashPath = await getStashPath(projectDir);
final instancePath =
await searchConfigDir(join(stashPath, 'instance-name'));
final instName = (await readFileOrNull(instancePath))?.trim();

if (instName != null) {
await resolveConfigOptions(resolvedConfig, '',
final cloudProfile = (await readFileOrNull(
await searchConfigDir(join(stashPath, 'cloud-profile'))))
?.trim();

await resolveConfigOptions(resolvedConfig, '', stashPath,
instanceName:
SourcedValue(instName, "project linked instance ('$instName')"));
SourcedValue(instName, "project linked instance ('$instName')"),
cloudProfile: SourcedValue(
cloudProfile, "project defined cloud instance('$cloudProfile')"));
} else {
throw ClientConnectionError(
"Found 'edgedb.toml' but the project is not initialized. "
Expand All @@ -582,8 +623,8 @@ Future<String?> readFileOrNull(String path) async {
}
}

Future<bool> resolveConfigOptions(
ResolvedConnectConfig resolvedConfig, String compoundParamsError,
Future<bool> resolveConfigOptions(ResolvedConnectConfig resolvedConfig,
String compoundParamsError, String? stashPath,
{SourcedValue<String?>? dsn,
SourcedValue<String?>? instanceName,
SourcedValue<String?>? credentials,
Expand All @@ -593,6 +634,8 @@ Future<bool> resolveConfigOptions(
SourcedValue<String?>? database,
SourcedValue<String?>? user,
SourcedValue<String?>? password,
SourcedValue<String?>? secretKey,
SourcedValue<String?>? cloudProfile,
SourcedValue<String?>? tlsCA,
SourcedValue<String?>? tlsCAFile,
SourcedValue<dynamic>? tlsSecurity,
Expand All @@ -606,6 +649,8 @@ Future<bool> resolveConfigOptions(
if (database != null) resolvedConfig.setDatabase(database);
if (user != null) resolvedConfig.setUser(user);
if (password != null) resolvedConfig.setPassword(password);
if (secretKey != null) resolvedConfig.setSecretKey(secretKey);
if (cloudProfile != null) resolvedConfig.setCloudProfile(cloudProfile);
if (tlsCA != null) resolvedConfig.setTlsCAData(tlsCA);
if (tlsCAFile != null) await resolvedConfig.setTlsCAFile(tlsCAFile);
if (tlsSecurity != null) resolvedConfig.setTlsSecurity(tlsSecurity);
Expand Down Expand Up @@ -651,11 +696,16 @@ Future<bool> resolveConfigOptions(
} else {
var credsFile = credentialsFile?.value;
if (credsFile == null) {
if (!RegExp(r'^[A-Za-z_][A-Za-z_0-9]*$')
if (!RegExp(r'^[A-Za-z_]\w*(/[A-Za-z_]\w*)?$')
.hasMatch(instanceName!.value!)) {
throw InterfaceError(
"invalid DSN or instance name: '${instanceName.value}'");
}
if (instanceName.value!.contains('/')) {
await parseCloudInstanceNameIntoConfig(
resolvedConfig, SourcedValue.from(instanceName), stashPath);
return true;
}
credsFile = await getCredentialsPath(instanceName.value!);
source = instanceName.source;
} else {
Expand Down Expand Up @@ -788,6 +838,9 @@ Future<void> parseDSNIntoConfig(
config._password,
(password) => config.setPassword(SourcedValue.from(password)));

await handleDSNPart('secret_key', null, config._secretKey,
(secretKey) => config.setSecretKey(SourcedValue.from(secretKey)));

await handleDSNPart('tls_ca', null, config._tlsCAData,
(caData) => config.setTlsCAData(SourcedValue.from(caData)));

Expand All @@ -799,3 +852,49 @@ Future<void> parseDSNIntoConfig(

config.addServerSettings(searchParams);
}

Future<void> parseCloudInstanceNameIntoConfig(ResolvedConnectConfig config,
SourcedValue<String> cloudInstanceName, String? stashPath) async {
String? secretKey = config.secretKey;
if (secretKey == null) {
try {
final profile = config.cloudProfile;
final profilePath =
await searchConfigDir(join('cloud-credentials', '$profile.json'));
final fileData = await File(profilePath).readAsString();

secretKey = json.decode(fileData)['secret_key']!;

config.setSecretKey(
SourcedValue(secretKey, "cloud-credentials/$profile.json"));
} catch (e) {
throw InterfaceError(
'Cannot connect to cloud instances without a secret key');
}
}

try {
final keyParts = secretKey!.split('.');
if (keyParts.length < 2) {
throw InterfaceError('Invalid secret key: does not contain payload');
}
final dnsZone = _jwtBase64Decode(keyParts[1])["iss"] as String;
final dnsBucket = (crcHqx(utf8.encode(cloudInstanceName.value), 0) % 100)
.toString()
.padLeft(2, '0');
final instanceParts = cloudInstanceName.value.split('/');
final host =
"${instanceParts[1]}--${instanceParts[0]}.c-$dnsBucket.i.$dnsZone";
config.setHost(SourcedValue(
host, "resolved from 'secretKey' and ${cloudInstanceName.source}"));
} on EdgeDBError {
rethrow;
} catch (e) {
throw InterfaceError('Invalid secret key: $e');
}
}

dynamic _jwtBase64Decode(String payload) {
return json.decode(utf8.decode(
base64.decode(payload.padRight((payload.length ~/ 4 + 1) * 4, '='))));
}
26 changes: 19 additions & 7 deletions lib/src/tcp_proto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,27 @@ class TCPProtocol extends BaseProtocol {
Future<void> _connectHandshake(
{required String database,
required String user,
String? password}) async {
String? password,
String? secretKey}) async {
final handshake = WriteMessageBuffer(ClientMessageType.ClientHandshake)
..writeInt16(protoVer.hi)
..writeInt16(protoVer.lo)
..writeInt16(2)
..writeString('user')
..writeString(user)
..writeString('database')
..writeString(database)
..writeInt16(protoVer.lo);

final params = {
'user': user,
'database': database,
};
if (secretKey != null) {
params['token'] = secretKey;
}

handshake.writeInt16(params.length);
for (var param in params.entries) {
handshake
..writeString(param.key)
..writeString(param.value);
}
handshake
..writeInt16(0)
..endMessage();

Expand Down
51 changes: 51 additions & 0 deletions lib/src/utils/crc_hqx.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'dart:typed_data';

// Copied from python binascii.crc_hqx implementation:
// https://github.com/python/cpython/blob/e0b4d966a8d1867c4b535b043e08288ca49b3548/Modules/binascii.c

final crctabHqx = Uint16List.fromList([
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
]);

int crcHqx(List<int> data, int crc) {
crc &= 0xffff;
var len = data.length;

var i = 0;
while (i < len) {
crc = ((crc << 8) & 0xff00) ^ crctabHqx[(crc >> 8) ^ data[i++]];
}

return crc;
}
4 changes: 2 additions & 2 deletions test/_io_mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ class FileMock implements File {
}

@override
Future<File> create({bool recursive = false}) {
Future<File> create({bool exclusive = false, bool recursive = false}) {
throw UnimplementedError();
}

@override
void createSync({bool recursive = false}) {}
void createSync({bool exclusive = false, bool recursive = false}) {}

@override
Future<FileSystemEntity> delete({bool recursive = false}) {
Expand Down
Loading