fix: protocol compliance — DTLS binding, sha256 integrity, derived signaling ID
Go (transport.go, crypto.go): - DeriveForNetwork: HKDF per-room Ed25519 identity to avoid anchor peer-ID collision when daemon holds multiple simultaneous pair-room connections (§ waste-go pattern) - sendHello: sign prefix||local_fp||remote_fp per yaw/2 §6 DTLS fingerprint binding - onControl hello: verify fingerprint binding + sig before setting verified=true - FileOffer.SHA256: carry file hash in offer message per yaw/2 §9 - SendFile: sha256 before offer, sends file-done with hash after streaming - wireFileRecv: pion Detach API + sha256 during recv + wait for file-done + verify - pendingDones map: goroutine-safe channel for file-done routing - signaling keepalive: ping with 10 s timeout, 3-min read deadline on anchor loop PWA (flit.ts): - dtlsFP/sha256hex helpers - Split PeerConn.signalingId (routing) from .peerId (cryptoId, for crypto/hello) - _sendHello: include DTLS fingerprints in signature per §6 - _onControl hello: verify fingerprint binding - sendFile: async, sha256 before offer - _stream: send file-done after DC close - _wire: set dataComplete=true on DC close, defer emit until file-done arrives - _onControl file-done: set expectedSHA256, conditionally call _finalizeRecv - _finalizeRecv: concatenate bufs, verify sha256, create URL, emit fileRecv Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -158,6 +158,7 @@ type FileOffer struct {
|
|||||||
XID string
|
XID string
|
||||||
Name string
|
Name string
|
||||||
Size int64
|
Size int64
|
||||||
|
SHA256 string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session is a 1:1 ephemeral pairing: join a room, connect to exactly one
|
// Session is a 1:1 ephemeral pairing: join a room, connect to exactly one
|
||||||
@@ -180,6 +181,7 @@ type Session struct {
|
|||||||
sig *signaling
|
sig *signaling
|
||||||
pendingRecvs map[string]*recvState
|
pendingRecvs map[string]*recvState
|
||||||
pendingAccepts map[string]chan int64
|
pendingAccepts map[string]chan int64
|
||||||
|
pendingDones map[string]chan string // xid → channel receiving expected sha256 from file-done
|
||||||
|
|
||||||
OnConnected func(verified bool)
|
OnConnected func(verified bool)
|
||||||
OnDisconnected func()
|
OnDisconnected func()
|
||||||
@@ -565,9 +567,25 @@ func (s *Session) wireDC(dc *webrtc.DataChannel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Session) sendHello() {
|
func (s *Session) sendHello() {
|
||||||
|
s.mu.Lock()
|
||||||
|
pc := s.pc
|
||||||
|
s.mu.Unlock()
|
||||||
|
if pc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
lfp, err1 := dtlsFP(pc.LocalDescription().SDP)
|
||||||
|
rfp, err2 := dtlsFP(pc.RemoteDescription().SDP)
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
log.Printf("flit: sendHello: fingerprint error: %v / %v", err1, err2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// §6: sender signs prefix || local_fp || remote_fp
|
||||||
|
bindMsg := append([]byte(bindPrefix), lfp...)
|
||||||
|
bindMsg = append(bindMsg, rfp...)
|
||||||
s.controlSend(map[string]string{
|
s.controlSend(map[string]string{
|
||||||
"type": "hello", "id": s.identity.PeerID(),
|
"type": "hello",
|
||||||
"sig": s.identity.Sign([]byte(bindPrefix)),
|
"id": s.identity.PeerID(),
|
||||||
|
"sig": s.identity.Sign(bindMsg),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,16 +596,41 @@ func (s *Session) onControl(data []byte) {
|
|||||||
}
|
}
|
||||||
switch m["type"] {
|
switch m["type"] {
|
||||||
case "hello":
|
case "hello":
|
||||||
|
helloID, _ := m["id"].(string)
|
||||||
|
sigHex, _ := m["sig"].(string)
|
||||||
|
verified := false
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.verified = m["id"] == s.peerID
|
pc := s.pc
|
||||||
|
s.mu.Unlock()
|
||||||
|
if pc != nil && pc.RemoteDescription() != nil && pc.LocalDescription() != nil {
|
||||||
|
// §6: verifier reconstructs prefix || remote_fp || local_fp
|
||||||
|
// (sender's local = our remote; sender's remote = our local)
|
||||||
|
rfp, err1 := dtlsFP(pc.RemoteDescription().SDP)
|
||||||
|
lfp, err2 := dtlsFP(pc.LocalDescription().SDP)
|
||||||
|
if err1 == nil && err2 == nil {
|
||||||
|
bindMsg := append([]byte(bindPrefix), rfp...)
|
||||||
|
bindMsg = append(bindMsg, lfp...)
|
||||||
|
verified = helloID == s.peerID && crypto.Verify(helloID, bindMsg, sigHex) == nil
|
||||||
|
} else {
|
||||||
|
log.Printf("flit: hello verify: fingerprint error: %v / %v", err1, err2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
s.verified = verified
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
if s.OnConnected != nil {
|
if s.OnConnected != nil {
|
||||||
s.OnConnected(s.verified)
|
s.OnConnected(verified)
|
||||||
}
|
}
|
||||||
case "file-offer":
|
case "file-offer":
|
||||||
if s.OnFileOffer != nil {
|
if s.OnFileOffer != nil {
|
||||||
size, _ := m["size"].(float64)
|
size, _ := m["size"].(float64)
|
||||||
s.OnFileOffer(FileOffer{XID: m["xid"].(string), Name: m["name"].(string), Size: int64(size)})
|
sha256, _ := m["sha256"].(string)
|
||||||
|
s.OnFileOffer(FileOffer{
|
||||||
|
XID: m["xid"].(string),
|
||||||
|
Name: m["name"].(string),
|
||||||
|
Size: int64(size),
|
||||||
|
SHA256: sha256,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
case "file-accept":
|
case "file-accept":
|
||||||
xid, _ := m["xid"].(string)
|
xid, _ := m["xid"].(string)
|
||||||
@@ -602,6 +645,16 @@ func (s *Session) onControl(data []byte) {
|
|||||||
if ch != nil {
|
if ch != nil {
|
||||||
ch <- offset
|
ch <- offset
|
||||||
}
|
}
|
||||||
|
case "file-done":
|
||||||
|
xid, _ := m["xid"].(string)
|
||||||
|
sha256hex, _ := m["sha256"].(string)
|
||||||
|
s.mu.Lock()
|
||||||
|
ch := s.pendingDones[xid]
|
||||||
|
delete(s.pendingDones, xid)
|
||||||
|
s.mu.Unlock()
|
||||||
|
if ch != nil {
|
||||||
|
ch <- sha256hex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,6 +685,16 @@ func (s *Session) SendFile(path string) error {
|
|||||||
xid := fmt.Sprintf("push-%d", time.Now().UnixNano())
|
xid := fmt.Sprintf("push-%d", time.Now().UnixNano())
|
||||||
name := st.Name()
|
name := st.Name()
|
||||||
|
|
||||||
|
// Compute sha256 over the whole file before offering (§9).
|
||||||
|
h := sha256.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return fmt.Errorf("hashing file: %w", err)
|
||||||
|
}
|
||||||
|
sha256hex := hex.EncodeToString(h.Sum(nil))
|
||||||
|
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return fmt.Errorf("seek after hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
acceptCh := make(chan int64, 1)
|
acceptCh := make(chan int64, 1)
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if s.pendingAccepts == nil {
|
if s.pendingAccepts == nil {
|
||||||
@@ -640,7 +703,13 @@ func (s *Session) SendFile(path string) error {
|
|||||||
s.pendingAccepts[xid] = acceptCh
|
s.pendingAccepts[xid] = acceptCh
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
s.controlSend(map[string]string{"type": "file-offer", "name": name, "size": fmt.Sprint(st.Size()), "xid": xid})
|
s.controlSend(map[string]string{
|
||||||
|
"type": "file-offer",
|
||||||
|
"name": name,
|
||||||
|
"size": fmt.Sprint(st.Size()),
|
||||||
|
"xid": xid,
|
||||||
|
"sha256": sha256hex,
|
||||||
|
})
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
pc := s.pc
|
pc := s.pc
|
||||||
@@ -689,7 +758,12 @@ func (s *Session) SendFile(path string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return dc.Close()
|
if err := dc.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// §9: send file-done with the pre-computed hash so the receiver can verify.
|
||||||
|
s.controlSend(map[string]string{"type": "file-done", "xid": xid, "sha256": sha256hex})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcceptOffer accepts an incoming file-offer and writes the transfer to
|
// AcceptOffer accepts an incoming file-offer and writes the transfer to
|
||||||
@@ -768,6 +842,17 @@ func (s *Session) wireFileRecv(dc *webrtc.DataChannel) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hash the already-received bytes first when resuming, so our
|
||||||
|
// running sha256 covers the whole file from byte 0.
|
||||||
|
h := sha256.New()
|
||||||
|
if resuming {
|
||||||
|
partPath := finalPath + ".part"
|
||||||
|
if pf, perr := os.Open(partPath); perr == nil {
|
||||||
|
_, _ = io.Copy(h, pf)
|
||||||
|
pf.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buf := make([]byte, chunkSize)
|
buf := make([]byte, chunkSize)
|
||||||
for {
|
for {
|
||||||
n, readErr := raw.Read(buf)
|
n, readErr := raw.Read(buf)
|
||||||
@@ -776,6 +861,7 @@ func (s *Session) wireFileRecv(dc *webrtc.DataChannel) {
|
|||||||
log.Printf("flit: write %s: %v", rs.offer.Name, werr)
|
log.Printf("flit: write %s: %v", rs.offer.Name, werr)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
h.Write(buf[:n])
|
||||||
rs.have += int64(n)
|
rs.have += int64(n)
|
||||||
if s.OnFileProgress != nil {
|
if s.OnFileProgress != nil {
|
||||||
s.OnFileProgress(xid, rs.have, rs.offer.Size)
|
s.OnFileProgress(xid, rs.have, rs.offer.Size)
|
||||||
@@ -786,6 +872,7 @@ func (s *Session) wireFileRecv(dc *webrtc.DataChannel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
f.Close()
|
f.Close()
|
||||||
|
actualSHA256 := hex.EncodeToString(h.Sum(nil))
|
||||||
log.Printf("flit: recv done: %s wrote=%d of %d", rs.offer.Name, rs.have, rs.offer.Size)
|
log.Printf("flit: recv done: %s wrote=%d of %d", rs.offer.Name, rs.have, rs.offer.Size)
|
||||||
|
|
||||||
if resuming {
|
if resuming {
|
||||||
@@ -795,6 +882,27 @@ func (s *Session) wireFileRecv(dc *webrtc.DataChannel) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// §9: wait for file-done and verify sha256 before reporting success.
|
||||||
|
doneCh := make(chan string, 1)
|
||||||
|
s.mu.Lock()
|
||||||
|
if s.pendingDones == nil {
|
||||||
|
s.pendingDones = make(map[string]chan string)
|
||||||
|
}
|
||||||
|
s.pendingDones[xid] = doneCh
|
||||||
|
s.mu.Unlock()
|
||||||
|
var expectedSHA256 string
|
||||||
|
select {
|
||||||
|
case expectedSHA256 = <-doneCh:
|
||||||
|
case <-time.After(15 * time.Second):
|
||||||
|
log.Printf("flit: file-done timeout for %s", rs.offer.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if actualSHA256 != expectedSHA256 {
|
||||||
|
log.Printf("flit: sha256 mismatch for %s: got %s want %s", rs.offer.Name, actualSHA256, expectedSHA256)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if s.OnFileDone != nil {
|
if s.OnFileDone != nil {
|
||||||
s.OnFileDone(rs.offer.Name, finalPath)
|
s.OnFileDone(rs.offer.Name, finalPath)
|
||||||
}
|
}
|
||||||
@@ -808,6 +916,24 @@ func (s *Session) wireFileRecv(dc *webrtc.DataChannel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dtlsFP extracts and hex-decodes the sha-256 DTLS fingerprint from an SDP blob.
|
||||||
|
// The spec (§6) requires this for the hello binding signature.
|
||||||
|
func dtlsFP(sdp string) ([]byte, error) {
|
||||||
|
const marker = "a=fingerprint:sha-256 "
|
||||||
|
for _, line := range strings.Split(sdp, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(line, marker) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fp, err := hex.DecodeString(strings.ReplaceAll(strings.TrimPrefix(line, marker), ":", ""))
|
||||||
|
if err != nil || len(fp) != 32 {
|
||||||
|
return nil, fmt.Errorf("bad DTLS fingerprint in SDP")
|
||||||
|
}
|
||||||
|
return fp, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no sha-256 DTLS fingerprint in SDP")
|
||||||
|
}
|
||||||
|
|
||||||
func mustHex(s string) []byte {
|
func mustHex(s string) []byte {
|
||||||
b, _ := hex.DecodeString(s)
|
b, _ := hex.DecodeString(s)
|
||||||
return b
|
return b
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ async function iceServers(cfg: FlitConfig): Promise<RTCIceServer[]> {
|
|||||||
|
|
||||||
const enc = (s: string) => new TextEncoder().encode(s)
|
const enc = (s: string) => new TextEncoder().encode(s)
|
||||||
|
|
||||||
|
// dtlsFP extracts and hex-decodes the sha-256 DTLS fingerprint from an SDP string.
|
||||||
|
function dtlsFP(sdp: string): Uint8Array {
|
||||||
|
const m = sdp.match(/a=fingerprint:sha-256 ([\dA-Fa-f:]+)/i)
|
||||||
|
if (!m) throw new Error('no sha-256 DTLS fingerprint in SDP')
|
||||||
|
return sodium.from_hex(m[1].replace(/:/g, ''))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sha256hex(buf: ArrayBuffer): Promise<string> {
|
||||||
|
const digest = await globalThis.crypto.subtle.digest('SHA-256', buf)
|
||||||
|
return toHex(new Uint8Array(digest))
|
||||||
|
}
|
||||||
|
|
||||||
function concat(...arrs: Uint8Array[]): Uint8Array {
|
function concat(...arrs: Uint8Array[]): Uint8Array {
|
||||||
const n = arrs.reduce((a, b) => a + b.length, 0)
|
const n = arrs.reduce((a, b) => a + b.length, 0)
|
||||||
const out = new Uint8Array(n); let o = 0
|
const out = new Uint8Array(n); let o = 0
|
||||||
@@ -250,9 +262,9 @@ export class PeerConn {
|
|||||||
private _offerPending = false
|
private _offerPending = false
|
||||||
private _offered = false
|
private _offered = false
|
||||||
|
|
||||||
private _recv: Record<string, { name: string; size: number; buf: ArrayBuffer[]; have: number; cancelled?: boolean }> = {}
|
private _recv: Record<string, { name: string; size: number; buf: ArrayBuffer[]; have: number; cancelled?: boolean; dataComplete?: boolean; expectedSHA256?: string }> = {}
|
||||||
private _recvChannels: Record<string, RTCDataChannel> = {}
|
private _recvChannels: Record<string, RTCDataChannel> = {}
|
||||||
private _pushQueue: Map<string, File> = new Map()
|
private _pushQueue: Map<string, { file: File; sha256: string; buf: ArrayBuffer }> = new Map()
|
||||||
|
|
||||||
private identity: Identity
|
private identity: Identity
|
||||||
private sig: Signaling
|
private sig: Signaling
|
||||||
@@ -382,17 +394,24 @@ export class PeerConn {
|
|||||||
}
|
}
|
||||||
channel.onclose = () => {
|
channel.onclose = () => {
|
||||||
if (rx.cancelled) { delete this._recv[xid]; return }
|
if (rx.cancelled) { delete this._recv[xid]; return }
|
||||||
const blob = new Blob(rx.buf)
|
rx.dataComplete = true
|
||||||
const url = URL.createObjectURL(blob)
|
// If file-done already arrived before DC closed, finalize now.
|
||||||
this.events.fileRecv?.({ peer: this.peerId, name: rx.name, size: rx.size, url })
|
if (rx.expectedSHA256 !== undefined) void this._finalizeRecv(xid)
|
||||||
delete this._recv[xid]
|
|
||||||
}
|
}
|
||||||
this._recvChannels[xid] = channel
|
this._recvChannels[xid] = channel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sendHello() {
|
private _sendHello() {
|
||||||
this._dc({ type: 'hello', id: this.identity.id, sig: sodium.to_hex(this.identity.sign(enc(BIND_PREFIX))) })
|
try {
|
||||||
|
const lfp = dtlsFP(this.pc.localDescription!.sdp)
|
||||||
|
const rfp = dtlsFP(this.pc.remoteDescription!.sdp)
|
||||||
|
// §6: sender signs prefix || local_fp || remote_fp
|
||||||
|
const bindMsg = concat(enc(BIND_PREFIX), lfp, rfp)
|
||||||
|
this._dc({ type: 'hello', id: this.identity.id, sig: sodium.to_hex(this.identity.sign(bindMsg)) })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('flit: sendHello fingerprint error', e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onControl(data: string) {
|
private _onControl(data: string) {
|
||||||
@@ -402,8 +421,17 @@ export class PeerConn {
|
|||||||
if (m['type'] === 'hello') {
|
if (m['type'] === 'hello') {
|
||||||
const helloId = m['id'] as string
|
const helloId = m['id'] as string
|
||||||
const helloSig = sodium.from_hex(m['sig'] as string)
|
const helloSig = sodium.from_hex(m['sig'] as string)
|
||||||
const sigOk = Identity.verify(helloId, enc(BIND_PREFIX), helloSig)
|
try {
|
||||||
|
// §6: verifier reconstructs prefix || remote_fp || local_fp
|
||||||
|
// (sender's local = our remote; sender's remote = our local)
|
||||||
|
const rfp = dtlsFP(this.pc.remoteDescription!.sdp)
|
||||||
|
const lfp = dtlsFP(this.pc.localDescription!.sdp)
|
||||||
|
const bindMsg = concat(enc(BIND_PREFIX), rfp, lfp)
|
||||||
|
const sigOk = Identity.verify(helloId, bindMsg, helloSig)
|
||||||
this.verified = helloId === this.peerId && sigOk && this.peerAuthed
|
this.verified = helloId === this.peerId && sigOk && this.peerAuthed
|
||||||
|
} catch {
|
||||||
|
this.verified = false
|
||||||
|
}
|
||||||
this.events.connected?.(this.verified, this.peerId)
|
this.events.connected?.(this.verified, this.peerId)
|
||||||
} else if (m['type'] === 'file-offer') {
|
} else if (m['type'] === 'file-offer') {
|
||||||
this.events.fileOffer?.({ peer: this.peerId, xid: m['xid'] as string, name: m['name'] as string, size: m['size'] as number })
|
this.events.fileOffer?.({ peer: this.peerId, xid: m['xid'] as string, name: m['name'] as string, size: m['size'] as number })
|
||||||
@@ -419,8 +447,34 @@ export class PeerConn {
|
|||||||
delete this._recvChannels[xid]
|
delete this._recvChannels[xid]
|
||||||
this.events.fileCancelled?.(xid)
|
this.events.fileCancelled?.(xid)
|
||||||
}
|
}
|
||||||
|
} else if (m['type'] === 'file-done') {
|
||||||
|
const xid = m['xid'] as string
|
||||||
|
const expectedSHA256 = m['sha256'] as string
|
||||||
|
const rx = this._recv[xid]
|
||||||
|
if (rx) {
|
||||||
|
rx.expectedSHA256 = expectedSHA256
|
||||||
|
// If DC already closed and all data is in, finalize now.
|
||||||
|
if (rx.dataComplete) void this._finalizeRecv(xid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _finalizeRecv(xid: string) {
|
||||||
|
const rx = this._recv[xid]
|
||||||
|
if (!rx) return
|
||||||
|
delete this._recv[xid]
|
||||||
|
delete this._recvChannels[xid]
|
||||||
|
const combined = new Uint8Array(rx.buf.reduce((acc, b) => acc + b.byteLength, 0))
|
||||||
|
let offset = 0
|
||||||
|
for (const b of rx.buf) { combined.set(new Uint8Array(b), offset); offset += b.byteLength }
|
||||||
|
const actualSHA256 = await sha256hex(combined.buffer)
|
||||||
|
if (actualSHA256 !== rx.expectedSHA256) {
|
||||||
|
console.error(`flit: sha256 mismatch for ${rx.name}: got ${actualSHA256} want ${rx.expectedSHA256}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const url = URL.createObjectURL(new Blob([combined]))
|
||||||
|
this.events.fileRecv?.({ peer: this.peerId, name: rx.name, size: rx.size, url })
|
||||||
|
}
|
||||||
|
|
||||||
acceptOffer(xid: string, name: string, size: number) {
|
acceptOffer(xid: string, name: string, size: number) {
|
||||||
this._recv[xid] = { name, size, buf: [], have: 0 }
|
this._recv[xid] = { name, size, buf: [], have: 0 }
|
||||||
@@ -431,22 +485,23 @@ export class PeerConn {
|
|||||||
this._dc({ type: 'file-cancel', xid })
|
this._dc({ type: 'file-cancel', xid })
|
||||||
}
|
}
|
||||||
|
|
||||||
sendFile(file: File) {
|
async sendFile(file: File) {
|
||||||
const xid = `push-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
const xid = `push-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`
|
||||||
this._pushQueue.set(xid, file)
|
const buf = await file.arrayBuffer()
|
||||||
this._dc({ type: 'file-offer', name: file.name, size: file.size, xid })
|
const hash = await sha256hex(buf)
|
||||||
|
this._pushQueue.set(xid, { file, sha256: hash, buf })
|
||||||
|
this._dc({ type: 'file-offer', name: file.name, size: file.size, xid, sha256: hash })
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _stream(xid: string, resumeOffset = 0) {
|
private async _stream(xid: string, resumeOffset = 0) {
|
||||||
const file = this._pushQueue.get(xid)
|
const entry = this._pushQueue.get(xid)
|
||||||
if (!file) return
|
if (!entry) return
|
||||||
this._pushQueue.delete(xid)
|
this._pushQueue.delete(xid)
|
||||||
|
const { file, sha256: hash, buf: fullBuf } = entry
|
||||||
const dc = this.pc.createDataChannel(`f:${xid}`)
|
const dc = this.pc.createDataChannel(`f:${xid}`)
|
||||||
dc.binaryType = 'arraybuffer'
|
dc.binaryType = 'arraybuffer'
|
||||||
await new Promise<void>(res => { dc.onopen = () => res() })
|
await new Promise<void>(res => { dc.onopen = () => res() })
|
||||||
// Slice from resume offset so we only transfer the remaining bytes.
|
const buf = resumeOffset > 0 ? fullBuf.slice(resumeOffset) : fullBuf
|
||||||
const slice = resumeOffset > 0 ? file.slice(resumeOffset) : file
|
|
||||||
const buf = await slice.arrayBuffer()
|
|
||||||
let localOffset = 0
|
let localOffset = 0
|
||||||
while (localOffset < buf.byteLength) {
|
while (localOffset < buf.byteLength) {
|
||||||
while (dc.bufferedAmount > 1024 * 1024) await new Promise(r => setTimeout(r, 10))
|
while (dc.bufferedAmount > 1024 * 1024) await new Promise(r => setTimeout(r, 10))
|
||||||
@@ -456,6 +511,8 @@ export class PeerConn {
|
|||||||
await new Promise(r => setTimeout(r, 0))
|
await new Promise(r => setTimeout(r, 0))
|
||||||
}
|
}
|
||||||
dc.close()
|
dc.close()
|
||||||
|
// §9: send file-done with sha256 so receiver can verify integrity.
|
||||||
|
this._dc({ type: 'file-done', xid, sha256: hash })
|
||||||
this.events.fileSent?.({ peer: this.peerId, xid, name: file.name })
|
this.events.fileSent?.({ peer: this.peerId, xid, name: file.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user