Добавить ESP pairing через доверенные сессии

This commit is contained in:
AidarKC
2026-06-14 18:21:23 +04:00
parent 5d13112b00
commit 3e04727022
36 changed files with 2500 additions and 6 deletions
@@ -190,6 +190,47 @@ public final class DatabaseInitializer {
ON active_sessions (login);
""");
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS esp_pairing_settings (
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
enabled INTEGER NOT NULL DEFAULT 0,
password_hash TEXT NOT NULL DEFAULT '',
ttl_seconds INTEGER NOT NULL DEFAULT 300,
failed_attempts INTEGER NOT NULL DEFAULT 0,
first_failed_at_ms INTEGER NOT NULL DEFAULT 0,
blocked_until_ms INTEGER NOT NULL DEFAULT 0,
updated_at_ms INTEGER NOT NULL,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS esp_pairing_requests (
pairing_id TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL,
requester_session_key TEXT NOT NULL,
requester_session_type INTEGER NOT NULL DEFAULT 1,
requester_client_platform TEXT NOT NULL DEFAULT '',
payload_type INTEGER NOT NULL,
status TEXT NOT NULL,
short_code TEXT NOT NULL,
fingerprint_b58 TEXT NOT NULL,
encrypted_payload TEXT,
reject_reason TEXT,
approved_by_session_id TEXT,
created_at_ms INTEGER NOT NULL,
expires_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
delivered_to_homeserver INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_esp_pairing_requests_login_status
ON esp_pairing_requests (login, status, expires_at_ms);
""");
// 3. users_params
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS users_params (
@@ -14,7 +14,7 @@ import java.sql.Statement;
public final class SqliteDbController {
private static volatile SqliteDbController instance;
private static final int LATEST_SCHEMA_VERSION = 4;
private static final int LATEST_SCHEMA_VERSION = 5;
private final String jdbcUrl;
@@ -87,6 +87,7 @@ public final class SqliteDbController {
case 2 -> migrateToV2();
case 3 -> migrateToV3();
case 4 -> migrateToV4();
case 5 -> migrateToV5();
default -> throw new RuntimeException("Unknown DB migration target version: " + targetVersion);
}
}
@@ -189,6 +190,25 @@ public final class SqliteDbController {
}
}
private void migrateToV5() {
try (Connection c = DriverManager.getConnection(jdbcUrl);
Statement st = c.createStatement()) {
c.setAutoCommit(false);
try {
ensureEspPairingTables(st);
setSchemaVersion(c, 5);
c.commit();
} catch (Exception e) {
try { c.rollback(); } catch (Exception ignored) {}
throw new RuntimeException("DB migration to v5 failed", e);
} finally {
try { c.setAutoCommit(true); } catch (Exception ignored) {}
}
} catch (SQLException e) {
throw new RuntimeException("DB migration to v5 failed", e);
}
}
private static void ensureChat200StateTables(Statement st) throws SQLException {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS chat200_state (
@@ -266,6 +286,49 @@ public final class SqliteDbController {
st.executeUpdate("ALTER TABLE active_sessions ADD COLUMN client_platform TEXT NOT NULL DEFAULT ''");
}
private static void ensureEspPairingTables(Statement st) throws SQLException {
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS esp_pairing_settings (
login TEXT NOT NULL PRIMARY KEY COLLATE NOCASE,
enabled INTEGER NOT NULL DEFAULT 0,
password_hash TEXT NOT NULL DEFAULT '',
ttl_seconds INTEGER NOT NULL DEFAULT 300,
failed_attempts INTEGER NOT NULL DEFAULT 0,
first_failed_at_ms INTEGER NOT NULL DEFAULT 0,
blocked_until_ms INTEGER NOT NULL DEFAULT 0,
updated_at_ms INTEGER NOT NULL,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
st.executeUpdate("""
CREATE TABLE IF NOT EXISTS esp_pairing_requests (
pairing_id TEXT NOT NULL PRIMARY KEY,
login TEXT NOT NULL,
requester_session_key TEXT NOT NULL,
requester_session_type INTEGER NOT NULL DEFAULT 1,
requester_client_platform TEXT NOT NULL DEFAULT '',
payload_type INTEGER NOT NULL,
status TEXT NOT NULL,
short_code TEXT NOT NULL,
fingerprint_b58 TEXT NOT NULL,
encrypted_payload TEXT,
reject_reason TEXT,
approved_by_session_id TEXT,
created_at_ms INTEGER NOT NULL,
expires_at_ms INTEGER NOT NULL,
updated_at_ms INTEGER NOT NULL,
delivered_to_homeserver INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (login) REFERENCES solana_users(login)
);
""");
st.executeUpdate("""
CREATE INDEX IF NOT EXISTS idx_esp_pairing_requests_login_status
ON esp_pairing_requests (login, status, expires_at_ms);
""");
}
private static boolean columnExists(Connection c, String tableName, String columnName) throws SQLException {
try (Statement probe = c.createStatement();
ResultSet rs = probe.executeQuery("PRAGMA table_info(" + tableName + ")")) {
@@ -0,0 +1,249 @@
package shine.db.dao;
import shine.db.SqliteDbController;
import shine.db.entities.EspPairingRequestEntry;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
public final class EspPairingRequestsDAO {
private static volatile EspPairingRequestsDAO instance;
private final SqliteDbController db = SqliteDbController.getInstance();
private EspPairingRequestsDAO() { }
public static EspPairingRequestsDAO getInstance() {
if (instance == null) {
synchronized (EspPairingRequestsDAO.class) {
if (instance == null) instance = new EspPairingRequestsDAO();
}
}
return instance;
}
public void insert(EspPairingRequestEntry entry) throws SQLException {
try (Connection c = db.getConnection()) {
insert(c, entry);
}
}
public void insert(Connection c, EspPairingRequestEntry entry) throws SQLException {
String sql = """
INSERT INTO esp_pairing_requests (
pairing_id,
login,
requester_session_key,
requester_session_type,
requester_client_platform,
payload_type,
status,
short_code,
fingerprint_b58,
encrypted_payload,
reject_reason,
approved_by_session_id,
created_at_ms,
expires_at_ms,
updated_at_ms,
delivered_to_homeserver
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, entry.getPairingId());
ps.setString(2, entry.getLogin());
ps.setString(3, entry.getRequesterSessionKey());
ps.setInt(4, entry.getRequesterSessionType());
ps.setString(5, entry.getRequesterClientPlatform());
ps.setInt(6, entry.getPayloadType());
ps.setString(7, entry.getStatus());
ps.setString(8, entry.getShortCode());
ps.setString(9, entry.getFingerprintB58());
ps.setString(10, entry.getEncryptedPayload());
ps.setString(11, entry.getRejectReason());
ps.setString(12, entry.getApprovedBySessionId());
ps.setLong(13, entry.getCreatedAtMs());
ps.setLong(14, entry.getExpiresAtMs());
ps.setLong(15, entry.getUpdatedAtMs());
ps.setInt(16, entry.isDeliveredToHomeserver() ? 1 : 0);
ps.executeUpdate();
}
}
public EspPairingRequestEntry getByPairingId(String pairingId) throws SQLException {
try (Connection c = db.getConnection()) {
return getByPairingId(c, pairingId);
}
}
public EspPairingRequestEntry getByPairingId(Connection c, String pairingId) throws SQLException {
String sql = """
SELECT pairing_id, login, requester_session_key, requester_session_type, requester_client_platform,
payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
FROM esp_pairing_requests
WHERE pairing_id = ?
LIMIT 1
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, pairingId);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
return mapRow(rs);
}
}
}
public List<EspPairingRequestEntry> listActiveByLogin(String login, long nowMs) throws SQLException {
try (Connection c = db.getConnection()) {
return listActiveByLogin(c, login, nowMs);
}
}
public List<EspPairingRequestEntry> listActiveByLogin(Connection c, String login, long nowMs) throws SQLException {
String sql = """
SELECT pairing_id, login, requester_session_key, requester_session_type, requester_client_platform,
payload_type, status, short_code, fingerprint_b58, encrypted_payload, reject_reason,
approved_by_session_id, created_at_ms, expires_at_ms, updated_at_ms, delivered_to_homeserver
FROM esp_pairing_requests
WHERE login = ? COLLATE NOCASE
AND expires_at_ms > ?
AND status IN ('created', 'approved', 'rejected')
ORDER BY created_at_ms DESC
""";
List<EspPairingRequestEntry> list = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, login);
ps.setLong(2, nowMs);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) list.add(mapRow(rs));
}
}
return list;
}
public int countRecentByLoginAndStatuses(String login, long sinceMs, String... statuses) throws SQLException {
try (Connection c = db.getConnection()) {
StringBuilder sql = new StringBuilder("""
SELECT COUNT(*)
FROM esp_pairing_requests
WHERE login = ? COLLATE NOCASE
AND created_at_ms >= ?
AND status IN (
""");
for (int i = 0; i < statuses.length; i++) {
if (i > 0) sql.append(", ");
sql.append("?");
}
sql.append(")");
try (PreparedStatement ps = c.prepareStatement(sql.toString())) {
ps.setString(1, login);
ps.setLong(2, sinceMs);
for (int i = 0; i < statuses.length; i++) {
ps.setString(3 + i, statuses[i]);
}
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getInt(1) : 0;
}
}
}
}
public void updateDeliveryFlag(String pairingId, boolean delivered, long updatedAtMs) throws SQLException {
updateSimple(pairingId, """
UPDATE esp_pairing_requests
SET delivered_to_homeserver = ?, updated_at_ms = ?
WHERE pairing_id = ?
""", ps -> {
ps.setInt(1, delivered ? 1 : 0);
ps.setLong(2, updatedAtMs);
ps.setString(3, pairingId);
});
}
public void markApproved(String pairingId, String encryptedPayload, String approvedBySessionId, long updatedAtMs) throws SQLException {
updateSimple(pairingId, """
UPDATE esp_pairing_requests
SET status = 'approved',
encrypted_payload = ?,
approved_by_session_id = ?,
reject_reason = NULL,
updated_at_ms = ?
WHERE pairing_id = ?
""", ps -> {
ps.setString(1, encryptedPayload);
ps.setString(2, approvedBySessionId);
ps.setLong(3, updatedAtMs);
ps.setString(4, pairingId);
});
}
public void markRejected(String pairingId, String rejectReason, String approvedBySessionId, long updatedAtMs) throws SQLException {
updateSimple(pairingId, """
UPDATE esp_pairing_requests
SET status = 'rejected',
reject_reason = ?,
approved_by_session_id = ?,
encrypted_payload = NULL,
updated_at_ms = ?
WHERE pairing_id = ?
""", ps -> {
ps.setString(1, rejectReason);
ps.setString(2, approvedBySessionId);
ps.setLong(3, updatedAtMs);
ps.setString(4, pairingId);
});
}
public int expirePending(long nowMs) throws SQLException {
try (Connection c = db.getConnection();
PreparedStatement ps = c.prepareStatement("""
UPDATE esp_pairing_requests
SET status = 'expired',
updated_at_ms = ?
WHERE status = 'created'
AND expires_at_ms <= ?
""")) {
ps.setLong(1, nowMs);
ps.setLong(2, nowMs);
return ps.executeUpdate();
}
}
private interface PreparedStatementSetter {
void accept(PreparedStatement ps) throws SQLException;
}
private void updateSimple(String pairingId, String sql, PreparedStatementSetter setter) throws SQLException {
try (Connection c = db.getConnection();
PreparedStatement ps = c.prepareStatement(sql)) {
setter.accept(ps);
ps.executeUpdate();
}
}
private static EspPairingRequestEntry mapRow(ResultSet rs) throws SQLException {
EspPairingRequestEntry entry = new EspPairingRequestEntry();
entry.setPairingId(rs.getString("pairing_id"));
entry.setLogin(rs.getString("login"));
entry.setRequesterSessionKey(rs.getString("requester_session_key"));
entry.setRequesterSessionType(rs.getInt("requester_session_type"));
entry.setRequesterClientPlatform(rs.getString("requester_client_platform"));
entry.setPayloadType(rs.getInt("payload_type"));
entry.setStatus(rs.getString("status"));
entry.setShortCode(rs.getString("short_code"));
entry.setFingerprintB58(rs.getString("fingerprint_b58"));
entry.setEncryptedPayload(rs.getString("encrypted_payload"));
entry.setRejectReason(rs.getString("reject_reason"));
entry.setApprovedBySessionId(rs.getString("approved_by_session_id"));
entry.setCreatedAtMs(rs.getLong("created_at_ms"));
entry.setExpiresAtMs(rs.getLong("expires_at_ms"));
entry.setUpdatedAtMs(rs.getLong("updated_at_ms"));
entry.setDeliveredToHomeserver(rs.getInt("delivered_to_homeserver") != 0);
return entry;
}
}
@@ -0,0 +1,101 @@
package shine.db.dao;
import shine.db.SqliteDbController;
import shine.db.entities.EspPairingSettingsEntry;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public final class EspPairingSettingsDAO {
private static volatile EspPairingSettingsDAO instance;
private final SqliteDbController db = SqliteDbController.getInstance();
private EspPairingSettingsDAO() { }
public static EspPairingSettingsDAO getInstance() {
if (instance == null) {
synchronized (EspPairingSettingsDAO.class) {
if (instance == null) instance = new EspPairingSettingsDAO();
}
}
return instance;
}
public void upsert(EspPairingSettingsEntry entry) throws SQLException {
try (Connection c = db.getConnection()) {
upsert(c, entry);
}
}
public void upsert(Connection c, EspPairingSettingsEntry entry) throws SQLException {
String sql = """
INSERT INTO esp_pairing_settings (
login,
enabled,
password_hash,
ttl_seconds,
failed_attempts,
first_failed_at_ms,
blocked_until_ms,
updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(login) DO UPDATE SET
enabled = excluded.enabled,
password_hash = excluded.password_hash,
ttl_seconds = excluded.ttl_seconds,
failed_attempts = excluded.failed_attempts,
first_failed_at_ms = excluded.first_failed_at_ms,
blocked_until_ms = excluded.blocked_until_ms,
updated_at_ms = excluded.updated_at_ms
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, entry.getLogin());
ps.setInt(2, entry.isEnabled() ? 1 : 0);
ps.setString(3, entry.getPasswordHash());
ps.setInt(4, entry.getTtlSeconds());
ps.setInt(5, entry.getFailedAttempts());
ps.setLong(6, entry.getFirstFailedAtMs());
ps.setLong(7, entry.getBlockedUntilMs());
ps.setLong(8, entry.getUpdatedAtMs());
ps.executeUpdate();
}
}
public EspPairingSettingsEntry getByLogin(String login) throws SQLException {
try (Connection c = db.getConnection()) {
return getByLogin(c, login);
}
}
public EspPairingSettingsEntry getByLogin(Connection c, String login) throws SQLException {
String sql = """
SELECT login, enabled, password_hash, ttl_seconds, failed_attempts, first_failed_at_ms, blocked_until_ms, updated_at_ms
FROM esp_pairing_settings
WHERE login = ? COLLATE NOCASE
LIMIT 1
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, login);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return null;
return mapRow(rs);
}
}
}
private static EspPairingSettingsEntry mapRow(ResultSet rs) throws SQLException {
EspPairingSettingsEntry entry = new EspPairingSettingsEntry();
entry.setLogin(rs.getString("login"));
entry.setEnabled(rs.getInt("enabled") != 0);
entry.setPasswordHash(rs.getString("password_hash"));
entry.setTtlSeconds(rs.getInt("ttl_seconds"));
entry.setFailedAttempts(rs.getInt("failed_attempts"));
entry.setFirstFailedAtMs(rs.getLong("first_failed_at_ms"));
entry.setBlockedUntilMs(rs.getLong("blocked_until_ms"));
entry.setUpdatedAtMs(rs.getLong("updated_at_ms"));
return entry;
}
}
@@ -0,0 +1,149 @@
package shine.db.entities;
public class EspPairingRequestEntry {
private String pairingId;
private String login;
private String requesterSessionKey;
private int requesterSessionType;
private String requesterClientPlatform;
private int payloadType;
private String status;
private String shortCode;
private String fingerprintB58;
private String encryptedPayload;
private String rejectReason;
private String approvedBySessionId;
private long createdAtMs;
private long expiresAtMs;
private long updatedAtMs;
private boolean deliveredToHomeserver;
public String getPairingId() {
return pairingId;
}
public void setPairingId(String pairingId) {
this.pairingId = pairingId;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getRequesterSessionKey() {
return requesterSessionKey;
}
public void setRequesterSessionKey(String requesterSessionKey) {
this.requesterSessionKey = requesterSessionKey;
}
public int getRequesterSessionType() {
return requesterSessionType;
}
public void setRequesterSessionType(int requesterSessionType) {
this.requesterSessionType = requesterSessionType;
}
public String getRequesterClientPlatform() {
return requesterClientPlatform;
}
public void setRequesterClientPlatform(String requesterClientPlatform) {
this.requesterClientPlatform = requesterClientPlatform;
}
public int getPayloadType() {
return payloadType;
}
public void setPayloadType(int payloadType) {
this.payloadType = payloadType;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getShortCode() {
return shortCode;
}
public void setShortCode(String shortCode) {
this.shortCode = shortCode;
}
public String getFingerprintB58() {
return fingerprintB58;
}
public void setFingerprintB58(String fingerprintB58) {
this.fingerprintB58 = fingerprintB58;
}
public String getEncryptedPayload() {
return encryptedPayload;
}
public void setEncryptedPayload(String encryptedPayload) {
this.encryptedPayload = encryptedPayload;
}
public String getRejectReason() {
return rejectReason;
}
public void setRejectReason(String rejectReason) {
this.rejectReason = rejectReason;
}
public String getApprovedBySessionId() {
return approvedBySessionId;
}
public void setApprovedBySessionId(String approvedBySessionId) {
this.approvedBySessionId = approvedBySessionId;
}
public long getCreatedAtMs() {
return createdAtMs;
}
public void setCreatedAtMs(long createdAtMs) {
this.createdAtMs = createdAtMs;
}
public long getExpiresAtMs() {
return expiresAtMs;
}
public void setExpiresAtMs(long expiresAtMs) {
this.expiresAtMs = expiresAtMs;
}
public long getUpdatedAtMs() {
return updatedAtMs;
}
public void setUpdatedAtMs(long updatedAtMs) {
this.updatedAtMs = updatedAtMs;
}
public boolean isDeliveredToHomeserver() {
return deliveredToHomeserver;
}
public void setDeliveredToHomeserver(boolean deliveredToHomeserver) {
this.deliveredToHomeserver = deliveredToHomeserver;
}
}
@@ -0,0 +1,77 @@
package shine.db.entities;
public class EspPairingSettingsEntry {
private String login;
private boolean enabled;
private String passwordHash;
private int ttlSeconds;
private int failedAttempts;
private long firstFailedAtMs;
private long blockedUntilMs;
private long updatedAtMs;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getPasswordHash() {
return passwordHash;
}
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
}
public int getTtlSeconds() {
return ttlSeconds;
}
public void setTtlSeconds(int ttlSeconds) {
this.ttlSeconds = ttlSeconds;
}
public int getFailedAttempts() {
return failedAttempts;
}
public void setFailedAttempts(int failedAttempts) {
this.failedAttempts = failedAttempts;
}
public long getFirstFailedAtMs() {
return firstFailedAtMs;
}
public void setFirstFailedAtMs(long firstFailedAtMs) {
this.firstFailedAtMs = firstFailedAtMs;
}
public long getBlockedUntilMs() {
return blockedUntilMs;
}
public void setBlockedUntilMs(long blockedUntilMs) {
this.blockedUntilMs = blockedUntilMs;
}
public long getUpdatedAtMs() {
return updatedAtMs;
}
public void setUpdatedAtMs(long updatedAtMs) {
this.updatedAtMs = updatedAtMs;
}
}