modular_sync #6 F11 나머지: 원격 등록 4벌 · 마스터 문 4벌을 RemoteRegistry · requireSecretGate 하나로
- lib/core/remote_registry.dart: RemoteRegistry<S, C> (configure · servers · server · client · closeAll, onDrop) - 서버마다 다른 것은
비밀 값 목록 · "설정 없음" 오류 · 만들기뿐. DavRegistry · SftpRegistry · SmbRegistry · FtpRegistry 는 같은 이름 그대로 이것을 부름
- 다시 접속하는 바뀜은 예전과 같게 (관리자 조건 ①): 지움 · 비밀이 아닌 칸 (toJson: 주소 · 포트 · 사용자 · 루트 · 보안 · 지문 · 공유 · 도메인 · insecure) ·
비밀 값 (WebDAV · SMB · FTP = 비밀번호, SFTP = 비밀번호 · 개인 키 · 키 암호). 접속에 쓰는 칸은 모두 이 둘 안 - 주소 · 사용자가 바뀌었는데
예전 클라이언트를 쓰는 경우는 예전에도 없음. SMB: 버린 연결의 서명 기록 지우기 · 서명 필수가 바뀌면 모두 닫기 그대로
- 문 (관리자 조건 ②, 보안): SecretGate.pass 를 부르는 곳은 requireSecretGate 하나 - 비밀 값이 있으면 연결 전에 마스터 확인
- 약속 시험 remote_registry_contract (7): 4종 등록 규칙 · SMB 서명 필수 · 4종 문 (거절이면 연결 전 DavLockedException, SFTP 는 키만 있어도) ·
비밀 값이 없으면 묻지 않음. 바꾸기 전 판 (fe8e5b7) 에서도 7개 모두 같게 통과 (동작 그대로임을 확인)
사용자가 보게 될 동작: 바뀌는 것 없음 (구조만)
확인: analyze 0 · 단위 1497 ×2 · 통합 6/6 ×2 · base10 214장 0px · ftp_shot · sshkey_shot · remote_idle_shot ×2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
parent
fe8e5b7bc6
commit
c27eb9b59e
|
|
@ -5,11 +5,11 @@ import 'dart:io';
|
|||
import '../../l10n/tr.dart';
|
||||
import '../dns_resolver.dart';
|
||||
import '../remote_fs.dart';
|
||||
import '../secret_gate.dart';
|
||||
import '../webdav.dart' show DavItem, DavLockedException;
|
||||
import '../remote_idle.dart';
|
||||
import 'ftp_server.dart';
|
||||
import 'ftp_session.dart';
|
||||
import '../remote_registry.dart';
|
||||
|
||||
// 약속 (나누기 ftp): FTP · FTPS 원격 저장소 ([RemoteClient]) 와 서버마다 하나인 클라이언트 목록 ([FtpRegistry]).
|
||||
// 입력: 서버 기준 경로 ("/" 로 시작). 출력: 목록 · 정보 ([DavItem]) · 읽기 흐름 · 쓰기 · 남은 공간 (FTP 는 모름).
|
||||
|
|
@ -29,7 +29,7 @@ class FtpRemote implements RemoteClient {
|
|||
Future<FtpSession> _connect() async {
|
||||
final s = server;
|
||||
if (s.host.trim().isEmpty) throw FtpException(tr('FTP 서버 주소가 없습니다'), FtpFailure.other);
|
||||
if (s.password.isNotEmpty && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage));
|
||||
await requireSecretGate(s.password.isNotEmpty);
|
||||
if (s.user.isNotEmpty && s.password.isEmpty) {
|
||||
throw FtpException(tr('저장된 비밀번호가 없습니다. 서버 설정에서 비밀번호를 넣어 주세요'), FtpFailure.noSecret);
|
||||
}
|
||||
|
|
@ -307,8 +307,13 @@ class FtpRemote implements RemoteClient {
|
|||
|
||||
/// 설정의 FTP 서버 목록과 클라이언트 (서버마다 하나)
|
||||
class FtpRegistry {
|
||||
static final _clients = <String, FtpRemote>{};
|
||||
static final _servers = <String, FtpServer>{};
|
||||
// F11: 몸통은 RemoteRegistry 하나 - 비밀번호는 toJson 에 없으므로 따로 비교
|
||||
static final _r = RemoteRegistry<FtpServer, FtpRemote>(
|
||||
idOf: (s) => s.id,
|
||||
toJson: (s) => s.toJson(),
|
||||
secretsOf: (s) => [s.password],
|
||||
create: FtpRemote.new,
|
||||
missing: (id) => FtpException(trf('FTP 서버 설정이 없습니다 ({0})', [id]), FtpFailure.other));
|
||||
|
||||
/// 이름 풀기 (main 에서 DNS 강제를 따르게 바꾼다). 처음 값: 기기 DNS (null = InternetAddress.lookup)
|
||||
static FtpResolve resolve = (host) async => InternetAddress.tryParse(host);
|
||||
|
|
@ -322,25 +327,8 @@ class FtpRegistry {
|
|||
return addrs.where((a) => a.type == InternetAddressType.IPv4).firstOrNull ?? addrs.firstOrNull;
|
||||
};
|
||||
|
||||
static void configure(List<FtpServer> servers) {
|
||||
final keep = {for (final s in servers) s.id: s};
|
||||
for (final id in _clients.keys.toList()) {
|
||||
final old = _servers[id], now = keep[id];
|
||||
if (now == null || old == null || old.password != now.password || jsonEncode(old.toJson()) != jsonEncode(now.toJson())) {
|
||||
_clients.remove(id)?.close();
|
||||
}
|
||||
}
|
||||
_servers
|
||||
..clear()
|
||||
..addAll(keep);
|
||||
}
|
||||
|
||||
static List<FtpServer> get servers => _servers.values.toList();
|
||||
static FtpServer? server(String id) => _servers[id];
|
||||
|
||||
static FtpRemote client(String id) {
|
||||
final s = _servers[id];
|
||||
if (s == null) throw FtpException(trf('FTP 서버 설정이 없습니다 ({0})', [id]), FtpFailure.other);
|
||||
return _clients.putIfAbsent(id, () => FtpRemote(s));
|
||||
}
|
||||
static void configure(List<FtpServer> servers) => _r.configure(servers);
|
||||
static List<FtpServer> get servers => _r.servers;
|
||||
static FtpServer? server(String id) => _r.server(id);
|
||||
static FtpRemote client(String id) => _r.client(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
/// 약속 (modular_sync #6 · F11): 원격 서버 등록 하나 - 설정의 서버 목록 ([RemoteRegistry.configure]) → 서버 id 마다 클라이언트 하나
|
||||
/// ([RemoteRegistry.client]). WebDAV · SFTP · SMB · FTP 가 같은 몸통을 쓰고, 서버마다 다른 것은 셋뿐: 비밀 값 목록 · "설정 없음" 오류 · 만들기.
|
||||
/// 다시 접속 (예전 클라이언트를 닫고 다음에 새로) 하는 바뀜 - 예전 4벌과 똑같이 (관리자 10/24 조건 ①):
|
||||
/// 서버를 지움 · 새로 들어옴 · toJson 이 다름 (비밀이 아닌 모든 칸: 주소 · 포트 · 사용자 · 루트 · 보안 · 지문 · 공유 · 도메인 · insecure …) ·
|
||||
/// 비밀 값이 다름 (WebDAV · SMB · FTP = password, SFTP = password · privateKey · keyPassphrase)
|
||||
/// 문 ([requireSecretGate]): 비밀 값이 있으면 마스터 확인 (SecretGate) 을 지나야 연다 - 못 지나면 [DavLockedException] (연결 전에)
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
|
||||
import '../l10n/tr.dart';
|
||||
import 'remote_fs.dart';
|
||||
import 'secret_gate.dart';
|
||||
import 'webdav.dart' show DavLockedException;
|
||||
|
||||
class RemoteRegistry<S, C extends RemoteClient> {
|
||||
RemoteRegistry({required this.idOf, required this.toJson, required this.secretsOf, required this.create, required this.missing, this.onDrop});
|
||||
|
||||
final String Function(S server) idOf;
|
||||
final Map<String, Object?> Function(S server) toJson;
|
||||
|
||||
/// 비밀 값들 (toJson 에 없는 것 - 바뀌면 다시 접속)
|
||||
final List<String> Function(S server) secretsOf;
|
||||
final C Function(S server) create;
|
||||
|
||||
/// 등록에 없는 id 로 [client] 를 부를 때의 오류
|
||||
final Exception Function(String id) missing;
|
||||
|
||||
/// 클라이언트를 버릴 때 (다시 접속 · 지움) - SMB 의 지난 연결 서명 기록 지우기 등
|
||||
final void Function(String id)? onDrop;
|
||||
|
||||
final _clients = <String, C>{};
|
||||
final _servers = <String, S>{};
|
||||
|
||||
bool _same(S old, S now) {
|
||||
final a = secretsOf(old), b = secretsOf(now);
|
||||
if (a.length != b.length) return false;
|
||||
for (var i = 0; i < a.length; i++) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return jsonEncode(toJson(old)) == jsonEncode(toJson(now));
|
||||
}
|
||||
|
||||
void configure(List<S> servers) {
|
||||
final keep = {for (final s in servers) idOf(s): s};
|
||||
for (final id in _clients.keys.toList()) {
|
||||
final old = _servers[id], now = keep[id];
|
||||
if (now == null || old == null || !_same(old, now)) {
|
||||
_clients.remove(id)?.close();
|
||||
onDrop?.call(id);
|
||||
}
|
||||
}
|
||||
_servers
|
||||
..clear()
|
||||
..addAll(keep);
|
||||
}
|
||||
|
||||
List<S> get servers => _servers.values.toList();
|
||||
S? server(String id) => _servers[id];
|
||||
|
||||
/// 열린 클라이언트를 모두 닫는다 (다음 [client] 에서 새로 - 연결 규칙이 바뀐 때)
|
||||
void closeAll() {
|
||||
for (final c in _clients.values) {
|
||||
c.close();
|
||||
}
|
||||
_clients.clear();
|
||||
}
|
||||
|
||||
C client(String id) {
|
||||
final s = _servers[id];
|
||||
if (s == null) throw missing(id);
|
||||
return _clients.putIfAbsent(id, () => create(s));
|
||||
}
|
||||
}
|
||||
|
||||
/// 문: 비밀 값이 있으면 마스터 확인을 지나야 한다 (못 지나면 연결 전에 [DavLockedException]). 비밀 값이 없으면 묻지 않는다
|
||||
Future<void> requireSecretGate(bool hasSecret) async {
|
||||
if (hasSecret && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage));
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
|
|
@ -8,10 +7,10 @@ import 'package:dartssh2/dartssh2.dart';
|
|||
import '../../l10n/tr.dart';
|
||||
import '../dns_resolver.dart';
|
||||
import '../remote_fs.dart';
|
||||
import '../secret_gate.dart';
|
||||
import '../webdav.dart' show DavItem, DavLockedException;
|
||||
import '../webdav.dart' show DavItem;
|
||||
import '../remote_idle.dart';
|
||||
import 'sftp_server.dart';
|
||||
import '../remote_registry.dart';
|
||||
|
||||
// 약속 (나누기 sftp): SFTP 원격 저장소 ([RemoteClient], dartssh2) 와 서버마다 하나인 클라이언트 목록 ([SftpRegistry]).
|
||||
// 입력: 서버 기준 경로 ("/" 로 시작). 출력: 목록 · 정보 ([DavItem]) · 부분 읽기 흐름 · 쓰기 · 남은 공간 (모르면 null).
|
||||
|
|
@ -48,7 +47,7 @@ class SftpRemote implements RemoteClient {
|
|||
final s = server;
|
||||
if (s.host.trim().isEmpty) throw SftpException(tr('SFTP 서버 주소가 없습니다'), SftpFailure.other);
|
||||
final hasSecret = s.password.isNotEmpty || s.privateKey.isNotEmpty;
|
||||
if (hasSecret && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage));
|
||||
await requireSecretGate(hasSecret);
|
||||
// 키도 비밀번호도 없으면 (저장하지 않음) 시도하지 않고 정확히 알린다 (40 과 같게)
|
||||
if (!hasSecret) throw SftpException(tr('저장된 비밀번호가 없습니다'), SftpFailure.noSecret);
|
||||
Socket sock;
|
||||
|
|
@ -404,8 +403,6 @@ class _Socket implements SSHSocket {
|
|||
|
||||
/// 설정의 SFTP 서버 목록과 클라이언트 (서버마다 하나)
|
||||
class SftpRegistry {
|
||||
static final _clients = <String, SftpRemote>{};
|
||||
static final _servers = <String, SftpServer>{};
|
||||
|
||||
/// 연결 방법 (main 에서 DNS 강제를 따르게 바꾼다). 처음 값: 기기 DNS
|
||||
static SftpConnect connect = (host, port, timeout) => Socket.connect(host, port, timeout: timeout);
|
||||
|
|
@ -427,31 +424,16 @@ class SftpRegistry {
|
|||
throw last ?? SocketException(trf('{0} 에 연결할 수 없습니다', [host]));
|
||||
};
|
||||
|
||||
static void configure(List<SftpServer> servers) {
|
||||
final keep = {for (final s in servers) s.id: s};
|
||||
for (final id in _clients.keys.toList()) {
|
||||
final old = _servers[id], now = keep[id];
|
||||
// 비밀 값은 toJson 에 없으므로 따로 비교 (바꾸면 바로 새 값으로 접속)
|
||||
if (now == null ||
|
||||
old == null ||
|
||||
old.password != now.password ||
|
||||
old.privateKey != now.privateKey ||
|
||||
old.keyPassphrase != now.keyPassphrase ||
|
||||
jsonEncode(old.toJson()) != jsonEncode(now.toJson())) {
|
||||
_clients.remove(id)?.close();
|
||||
}
|
||||
}
|
||||
_servers
|
||||
..clear()
|
||||
..addAll(keep);
|
||||
}
|
||||
// F11: 몸통은 RemoteRegistry 하나 - 비밀 값 (비밀번호 · 개인 키 · 키 암호) 은 toJson 에 없으므로 따로 비교
|
||||
static final _r = RemoteRegistry<SftpServer, SftpRemote>(
|
||||
idOf: (s) => s.id,
|
||||
toJson: (s) => s.toJson(),
|
||||
secretsOf: (s) => [s.password, s.privateKey, s.keyPassphrase],
|
||||
create: SftpRemote.new,
|
||||
missing: (id) => SftpException(trf('SFTP 서버 설정이 없습니다 ({0})', [id]), SftpFailure.other));
|
||||
|
||||
static List<SftpServer> get servers => _servers.values.toList();
|
||||
static SftpServer? server(String id) => _servers[id];
|
||||
|
||||
static SftpRemote client(String id) {
|
||||
final s = _servers[id];
|
||||
if (s == null) throw SftpException(trf('SFTP 서버 설정이 없습니다 ({0})', [id]), SftpFailure.other);
|
||||
return _clients.putIfAbsent(id, () => SftpRemote(s));
|
||||
}
|
||||
static void configure(List<SftpServer> servers) => _r.configure(servers);
|
||||
static List<SftpServer> get servers => _r.servers;
|
||||
static SftpServer? server(String id) => _r.server(id);
|
||||
static SftpRemote client(String id) => _r.client(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io' show InternetAddress;
|
||||
import 'dart:math' show min;
|
||||
import 'dart:typed_data';
|
||||
|
|
@ -8,9 +7,9 @@ import 'package:flutter/services.dart';
|
|||
|
||||
import '../l10n/tr.dart';
|
||||
import 'remote_fs.dart';
|
||||
import 'secret_gate.dart';
|
||||
import 'webdav.dart' show DavException, DavItem, DavLockedException;
|
||||
import 'webdav.dart' show DavException, DavItem;
|
||||
import 'remote_idle.dart';
|
||||
import 'remote_registry.dart';
|
||||
|
||||
/// 88: SMB 서버 설정 (Android 만 - Windows 는 운영체제가 \\서버\공유 를 연다, 사용자 결정 88-1).
|
||||
/// 비밀번호는 안전 저장소에 (설정 파일 · 보관본에는 넣지 않음).
|
||||
|
|
@ -129,7 +128,7 @@ class SmbRemote implements RemoteClient {
|
|||
if (server.host.trim().isEmpty || server.share.trim().isEmpty) {
|
||||
throw SmbException(tr('SMB 서버 주소 · 공유 이름이 없습니다'), SmbFailure.other);
|
||||
}
|
||||
if (server.password.isNotEmpty && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage));
|
||||
await requireSecretGate(server.password.isNotEmpty);
|
||||
if (server.user.isNotEmpty && server.password.isEmpty) throw SmbException(tr('저장된 비밀번호가 없습니다'), SmbFailure.noPassword);
|
||||
// 보안 검토 5: 이름은 여기서 풀어 주소로 넘긴다 (Android 의 SMB 다리가 시스템 DNS 로 풀어 강제 DNS 를 비켜 가지 않게)
|
||||
String host;
|
||||
|
|
@ -306,8 +305,14 @@ class SmbRemote implements RemoteClient {
|
|||
|
||||
/// 설정의 SMB 서버 목록과 클라이언트 (서버마다 하나)
|
||||
class SmbRegistry {
|
||||
static final _clients = <String, SmbRemote>{};
|
||||
static final _servers = <String, SmbServer>{};
|
||||
// F11: 몸통은 RemoteRegistry 하나 - 비밀번호는 toJson 에 없으므로 따로 비교, 버린 연결의 서명 기록도 지운다
|
||||
static final _r = RemoteRegistry<SmbServer, SmbRemote>(
|
||||
idOf: (s) => s.id,
|
||||
toJson: (s) => s.toJson(),
|
||||
secretsOf: (s) => [s.password],
|
||||
create: SmbRemote.new,
|
||||
missing: (id) => SmbException(trf('SMB 서버 설정이 없습니다 ({0})', [id]), SmbFailure.other),
|
||||
onDrop: (id) => _secure.remove(id));
|
||||
|
||||
/// 보안 검토 6: 서버 id → 지난 연결이 서명이나 암호화되었는지 (모르면 없음)
|
||||
static final _secure = <String, bool>{};
|
||||
|
|
@ -321,33 +326,16 @@ class SmbRegistry {
|
|||
static set requireSecure(bool v) {
|
||||
if (v == _requireSecure) return;
|
||||
_requireSecure = v;
|
||||
for (final c in _clients.values) {
|
||||
c.close();
|
||||
}
|
||||
_clients.clear();
|
||||
_r.closeAll();
|
||||
}
|
||||
|
||||
static void configure(List<SmbServer> servers) {
|
||||
final keep = {for (final s in servers) s.id: s};
|
||||
for (final id in _clients.keys.toList()) {
|
||||
final old = _servers[id], now = keep[id];
|
||||
if (now == null || old == null || old.password != now.password || jsonEncode(old.toJson()) != jsonEncode(now.toJson())) {
|
||||
_clients.remove(id)?.close();
|
||||
_secure.remove(id);
|
||||
}
|
||||
}
|
||||
_secure.removeWhere((id, _) => !keep.containsKey(id));
|
||||
_servers
|
||||
..clear()
|
||||
..addAll(keep);
|
||||
_r.configure(servers);
|
||||
_secure.removeWhere((id, _) => _r.server(id) == null);
|
||||
}
|
||||
|
||||
static List<SmbServer> get servers => _servers.values.toList();
|
||||
static SmbServer? server(String id) => _servers[id];
|
||||
|
||||
static SmbRemote client(String id) {
|
||||
final s = _servers[id];
|
||||
if (s == null) throw SmbException(trf('SMB 서버 설정이 없습니다 ({0})', [id]), SmbFailure.other);
|
||||
return _clients.putIfAbsent(id, () => SmbRemote(s));
|
||||
}
|
||||
static List<SmbServer> get servers => _r.servers;
|
||||
static SmbServer? server(String id) => _r.server(id);
|
||||
static SmbRemote client(String id) => _r.client(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import 'package:xml/xml.dart';
|
|||
|
||||
import '../l10n/tr.dart';
|
||||
import 'remote_fs.dart';
|
||||
import 'secret_gate.dart';
|
||||
import 'remote_idle.dart';
|
||||
import 'remote_registry.dart';
|
||||
|
||||
/// WebDAV 서버 설정 (환경 설정 > 파일 탐색기 > WebDAV). 비밀번호는 안전 저장소에 (설정 파일 · 보관본에는 넣지 않음).
|
||||
class DavServer {
|
||||
|
|
@ -145,7 +145,7 @@ class DavClient implements RemoteClient {
|
|||
|
||||
Future<HttpClientRequest> _open(String method, Uri uri) async {
|
||||
// 124: 저장된 비밀번호로 접속하기 전에 (마스터 비밀번호를 정해 두었으면 묻는다)
|
||||
if (server.password.isNotEmpty && !await SecretGate.pass()) throw DavLockedException(tr(secretGateMessage));
|
||||
await requireSecretGate(server.password.isNotEmpty);
|
||||
final req = await _http.openUrl(method, uri);
|
||||
final a = _auth;
|
||||
if (a != null) req.headers.set(HttpHeaders.authorizationHeader, a);
|
||||
|
|
@ -380,32 +380,16 @@ class DavClient implements RemoteClient {
|
|||
|
||||
/// 설정의 WebDAV 서버들 (id → 클라이언트). 설정이 바뀌면 [configure].
|
||||
class DavRegistry {
|
||||
static final _clients = <String, DavClient>{};
|
||||
static final _servers = <String, DavServer>{};
|
||||
// F11: 몸통은 RemoteRegistry 하나 - 비밀번호는 toJson 에 없으므로 따로 비교 (바꾸면 바로 새 비밀번호로 접속 - 40-2)
|
||||
static final _r = RemoteRegistry<DavServer, DavClient>(
|
||||
idOf: (s) => s.id,
|
||||
toJson: (s) => s.toJson(),
|
||||
secretsOf: (s) => [s.password],
|
||||
create: DavClient.new,
|
||||
missing: (id) => DavException(trf('WebDAV 서버 설정이 없습니다 ({0})', [id])));
|
||||
|
||||
static void configure(List<DavServer> servers) {
|
||||
final keep = {for (final s in servers) s.id: s};
|
||||
for (final id in _clients.keys.toList()) {
|
||||
final old = _servers[id], now = keep[id];
|
||||
// 비밀번호는 toJson 에 없으므로 따로 비교 (바꾸면 바로 새 비밀번호로 접속 - 40-2)
|
||||
if (now == null ||
|
||||
old == null ||
|
||||
old.password != now.password ||
|
||||
jsonEncode(old.toJson()) != jsonEncode(now.toJson())) {
|
||||
_clients.remove(id)?.close();
|
||||
}
|
||||
}
|
||||
_servers
|
||||
..clear()
|
||||
..addAll(keep);
|
||||
}
|
||||
|
||||
static List<DavServer> get servers => _servers.values.toList();
|
||||
static DavServer? server(String id) => _servers[id];
|
||||
|
||||
static DavClient client(String id) {
|
||||
final s = _servers[id];
|
||||
if (s == null) throw DavException(trf('WebDAV 서버 설정이 없습니다 ({0})', [id]));
|
||||
return _clients.putIfAbsent(id, () => DavClient(s));
|
||||
}
|
||||
static void configure(List<DavServer> servers) => _r.configure(servers);
|
||||
static List<DavServer> get servers => _r.servers;
|
||||
static DavServer? server(String id) => _r.server(id);
|
||||
static DavClient client(String id) => _r.client(id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:jj_mkvmaker/core/ftp.dart';
|
||||
import 'package:jj_mkvmaker/core/remote_fs.dart';
|
||||
import 'package:jj_mkvmaker/core/secret_gate.dart';
|
||||
import 'package:jj_mkvmaker/core/sftp.dart';
|
||||
import 'package:jj_mkvmaker/core/smb.dart';
|
||||
import 'package:jj_mkvmaker/core/webdav.dart';
|
||||
|
||||
/// modular_sync #6 F11 약속: 원격 등록 4벌 · 마스터 문 4벌을 한 몸통 (RemoteRegistry · requireSecretGate) 으로.
|
||||
/// ① 다시 접속하는 바뀜은 예전과 같게: 지움 · 비밀이 아닌 칸 (주소 · 사용자 …) · 비밀 값 (SFTP 는 키 · 키 암호까지)
|
||||
/// ② 문 (보안): 비밀 값이 있는데 마스터 확인 없이 열리는 길이 없음 - 거절이면 연결 전에 DavLockedException, 비밀 값이 없으면 묻지 않음
|
||||
void main() {
|
||||
tearDown(() {
|
||||
SecretGate.check = null;
|
||||
DavRegistry.configure(const []);
|
||||
SftpRegistry.configure(const []);
|
||||
SmbRegistry.configure(const []);
|
||||
FtpRegistry.configure(const []);
|
||||
SmbRegistry.requireSecure = false;
|
||||
});
|
||||
|
||||
/// 등록 하나의 규칙: 같으면 같은 클라이언트 · [changes] 마다 새 클라이언트 · 지우면 [missing] 오류
|
||||
void rules<S>(String name, S base, List<S> changes, void Function(List<S>) configure, RemoteClient Function(String) client,
|
||||
Matcher missing) {
|
||||
test('$name: 같으면 같은 클라이언트 · 바뀌면 새로 · 지우면 설정 없음', () {
|
||||
configure([base]);
|
||||
final first = client('x');
|
||||
configure([base]);
|
||||
expect(identical(client('x'), first), isTrue, reason: '그대로면 다시 접속하지 않음');
|
||||
var prev = first;
|
||||
for (final (i, c) in changes.indexed) {
|
||||
configure([base]);
|
||||
prev = client('x');
|
||||
configure([c]);
|
||||
expect(identical(client('x'), prev), isFalse, reason: '바뀜 $i 이면 새 클라이언트');
|
||||
}
|
||||
configure(const []);
|
||||
expect(() => client('x'), throwsA(missing));
|
||||
});
|
||||
}
|
||||
|
||||
const dav = DavServer(id: 'x', name: 'n', url: 'http://127.0.0.1:1/dav', user: 'u', password: 'p');
|
||||
rules<DavServer>('WebDAV', dav, [dav.copyWith(url: 'http://127.0.0.1:2/dav'), dav.copyWith(user: 'v'), dav.copyWith(password: 'q'),
|
||||
dav.copyWith(insecure: true)], DavRegistry.configure, DavRegistry.client, isA<DavException>());
|
||||
|
||||
const sftp = SftpServer(id: 'x', name: 'n', host: '127.0.0.1', port: 1, user: 'u', password: 'p', privateKey: 'k', keyPassphrase: 'kp');
|
||||
rules<SftpServer>('SFTP', sftp, [sftp.copyWith(host: 'h2'), sftp.copyWith(user: 'v'), sftp.copyWith(password: 'q'),
|
||||
sftp.copyWith(privateKey: 'k2'), sftp.copyWith(keyPassphrase: 'kp2')], SftpRegistry.configure, SftpRegistry.client, isA<SftpException>());
|
||||
|
||||
const smb = SmbServer(id: 'x', name: 'n', host: '127.0.0.1', port: 1, share: 's', user: 'u', password: 'p');
|
||||
rules<SmbServer>('SMB', smb, [smb.copyWith(host: 'h2'), smb.copyWith(share: 't'), smb.copyWith(user: 'v'), smb.copyWith(password: 'q')],
|
||||
SmbRegistry.configure, SmbRegistry.client, isA<SmbException>());
|
||||
|
||||
const ftp = FtpServer(id: 'x', name: 'n', host: '127.0.0.1', port: 1, user: 'u', password: 'p', security: 'none');
|
||||
rules<FtpServer>('FTP', ftp, [ftp.copyWith(host: 'h2'), ftp.copyWith(user: 'v'), ftp.copyWith(password: 'q'), ftp.copyWith(security: 'explicit')],
|
||||
FtpRegistry.configure, FtpRegistry.client, isA<FtpException>());
|
||||
|
||||
test('SMB: 서명 · 암호화 필수를 바꾸면 열린 연결을 버림', () {
|
||||
SmbRegistry.configure(const [smb]);
|
||||
final first = SmbRegistry.client('x');
|
||||
SmbRegistry.requireSecure = true;
|
||||
expect(identical(SmbRegistry.client('x'), first), isFalse);
|
||||
});
|
||||
|
||||
test('문: 비밀 값이 있으면 4종 모두 마스터 확인을 지나야 함 - 거절이면 연결 전에 DavLockedException', () async {
|
||||
var asked = 0;
|
||||
SecretGate.check = (_) async {
|
||||
asked++;
|
||||
return false;
|
||||
};
|
||||
final clients = <RemoteClient>[DavClient(dav), SftpRemote(sftp), SmbRemote(smb), FtpRemote(ftp)];
|
||||
for (final c in clients) {
|
||||
await expectLater(c.list('/'), throwsA(isA<DavLockedException>()), reason: '${c.runtimeType}');
|
||||
c.close();
|
||||
}
|
||||
expect(asked, clients.length, reason: '서버마다 물었음');
|
||||
// SFTP 는 개인 키만 있어도 비밀 값
|
||||
final keyOnly = SftpRemote(const SftpServer(id: 'y', name: 'n', host: '127.0.0.1', port: 1, user: 'u', privateKey: 'k'));
|
||||
await expectLater(keyOnly.list('/'), throwsA(isA<DavLockedException>()));
|
||||
keyOnly.close();
|
||||
});
|
||||
|
||||
test('문: 비밀 값이 없으면 묻지 않는다 (막히는 것은 마스터가 아니라 그 서버의 까닭)', () async {
|
||||
var asked = 0;
|
||||
SecretGate.check = (_) async {
|
||||
asked++;
|
||||
return false;
|
||||
};
|
||||
for (final c in <RemoteClient>[
|
||||
DavClient(const DavServer(id: 'z', name: 'n', url: 'http://127.0.0.1:1/dav')),
|
||||
SftpRemote(const SftpServer(id: 'z', name: 'n', host: '127.0.0.1', port: 1, user: 'u')),
|
||||
FtpRemote(const FtpServer(id: 'z', name: 'n', host: '127.0.0.1', port: 1, security: 'none')),
|
||||
]) {
|
||||
await expectLater(c.list('/'), throwsA(isNot(isA<DavLockedException>())), reason: '${c.runtimeType}');
|
||||
c.close();
|
||||
}
|
||||
expect(asked, 0);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue