| … | … | @@ -214,6 +214,7 @@ export class TopicConnector { |
| 214 | 214 | */ |
| 215 | 215 | async connect(serverKey, repoName) { |
| 216 | 216 | const topic = ServerIdentity.deriveTopicFromKey(serverKey, repoName) |
| 217 | const expectedKey = Buffer.isBuffer(serverKey) ? serverKey : Buffer.from(serverKey, 'hex') | |
| 217 | 218 | |
| 218 | 219 | return new Promise((resolve, reject) => { |
| 219 | 220 | const timeout = setTimeout(() => { |
| … | … | @@ -222,13 +223,24 @@ export class TopicConnector { |
| 222 | 223 | |
| 223 | 224 | this.swarm.join(topic, { server: false, client: true }) |
| 224 | 225 | |
| 225 | this.swarm.once('connection', (conn, peerInfo) => { | |
| 226 | const onConnection = (conn, peerInfo) => { | |
| 227 | // Verify we connected to the expected server, not a federation peer | |
| 228 | // or the local machine's own pear-git serving stale data. | |
| 229 | if (!expectedKey.equals(peerInfo.publicKey)) { | |
| 230 | console.error(`[TopicConnector] Wrong peer ${peerInfo.publicKey.toString('hex').slice(0, 8)}... (expected ${expectedKey.toString('hex').slice(0, 8)}...), retrying`) | |
| 231 | conn.destroy() | |
| 232 | return // wait for next connection | |
| 233 | } | |
| 226 | 234 | clearTimeout(timeout) |
| 235 | this.swarm.off('connection', onConnection) | |
| 227 | 236 | resolve(conn) |
| 228 | }) | |
| 237 | } | |
| 238 | ||
| 239 | this.swarm.on('connection', onConnection) | |
| 229 | 240 | |
| 230 | 241 | this.swarm.once('error', (err) => { |
| 231 | 242 | clearTimeout(timeout) |
| 243 | this.swarm.off('connection', onConnection) | |
| 232 | 244 | reject(err) |
| 233 | 245 | }) |
| 234 | 246 | }) |
| … | … | @@ -791,8 +791,15 @@ export class GitP2PHandler { |
| 791 | 791 | gitProcess.stdin.write(chunk) |
| 792 | 792 | }) |
| 793 | 793 | |
| 794 | let firstChunk = true | |
| 794 | 795 | gitProcess.stdout.on('data', (chunk) => { |
| 795 | 796 | console.log(`[GitP2P] git->conn: ${chunk.length} bytes`) |
| 797 | if (firstChunk) { | |
| 798 | // Log first 200 bytes of ref advertisement for debugging stale refs | |
| 799 | const preview = chunk.slice(0, 200).toString('utf-8').replace(/\0/g, '\\0') | |
| 800 | console.log(`[GitP2P] REFS: ${preview}`) | |
| 801 | firstChunk = false | |
| 802 | } | |
| 796 | 803 | conn.write(chunk) |
| 797 | 804 | }) |
| 798 | 805 |
| … | … | @@ -1155,6 +1155,25 @@ class PearGitServer { |
| 1155 | 1155 | console.log(' Keet identity: NOT SET \u2014 run `pgh identity setup`') |
| 1156 | 1156 | } |
| 1157 | 1157 | |
| 1158 | // Recreate git swarm with server identity keypair so clients can verify | |
| 1159 | // conn.remotePublicKey matches the expected server key from punch:// URLs. | |
| 1160 | // Without this, any peer announcing the same topic (including federation | |
| 1161 | // peers with stale data) could serve instead of the origin server. | |
| 1162 | if (this.serverIdentity.publicKey && this.serverIdentity.secretKey) { | |
| 1163 | const oldSwarm = this.swarm | |
| 1164 | const swarmOpts = this._bootstrap ? { bootstrap: this._bootstrap } : {} | |
| 1165 | swarmOpts.keyPair = { | |
| 1166 | publicKey: this.serverIdentity.publicKey, | |
| 1167 | secretKey: this.serverIdentity.secretKey | |
| 1168 | } | |
| 1169 | this.swarm = new Hyperswarm(swarmOpts) | |
| 1170 | this.swarm.on('error', (err) => { | |
| 1171 | console.error('[GitSwarm] Error:', err.message) | |
| 1172 | }) | |
| 1173 | oldSwarm.destroy().catch(() => {}) | |
| 1174 | console.log(`[P2P] Git swarm using server identity key: ${this.serverIdentity.getPublicKeyHex().slice(0, 8)}...`) | |
| 1175 | } | |
| 1176 | ||
| 1158 | 1177 | this.gitP2PHandler = new GitP2PHandler({ |
| 1159 | 1178 | reposPath: this.repoRegistry.gitReposPath, |
| 1160 | 1179 | authManager: this.authManager, |