Skip to content

Commit 2ed81f5

Browse files
Copilotcameri
andauthored
fix: address inline review comments — multi-line Tor parsing, bech32 validation, type deps, engines, await closeTorClient, TorClient tests
Agent-Logs-Url: https://github.com/cameri/nostream/sessions/f56791f2-7216-4d11-b88a-63b5d2c432e5 Co-authored-by: cameri <378886+cameri@users.noreply.github.com>
1 parent 353caca commit 2ed81f5

6 files changed

Lines changed: 310 additions & 18 deletions

File tree

package-lock.json

Lines changed: 7 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@semantic-release/github": "8.1.0",
9191
"@semantic-release/npm": "13.1.5",
9292
"@semantic-release/release-notes-generator": "10.0.3",
93+
"@types/accepts": "^1.3.7",
9394
"@types/chai": "^4.3.1",
9495
"@types/chai-as-promised": "^7.1.5",
9596
"@types/debug": "4.1.7",
@@ -123,9 +124,11 @@
123124
"typescript": "~5.7.3",
124125
"uuid": "^8.3.2"
125126
},
127+
"engines": {
128+
"node": ">=22.9"
129+
},
126130
"dependencies": {
127131
"@noble/secp256k1": "1.7.1",
128-
"@types/accepts": "^1.3.7",
129132
"accepts": "^1.3.8",
130133
"axios": "^1.15.0",
131134
"debug": "4.3.4",

src/tor/client.ts

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,89 @@ export class TorClient {
4646
})
4747
}
4848

49+
private isCompleteTorReply(buffer: string): boolean {
50+
if (!buffer.endsWith('\r\n')) {
51+
return false
52+
}
53+
54+
const lines = buffer.split('\r\n')
55+
if (lines[lines.length - 1] === '') {
56+
lines.pop()
57+
}
58+
59+
if (lines.length === 0) {
60+
return false
61+
}
62+
63+
const firstLine = lines[0].match(/^(\d{3})([\s\-+])/)
64+
if (!firstLine) {
65+
return false
66+
}
67+
68+
const statusCode = firstLine[1]
69+
let inDataBlock = false
70+
71+
for (let i = 0; i < lines.length; i++) {
72+
const line = lines[i]
73+
74+
if (inDataBlock) {
75+
if (line === '.') {
76+
inDataBlock = false
77+
}
78+
continue
79+
}
80+
81+
const match = line.match(/^(\d{3})([\s\-+])/)
82+
if (!match || match[1] !== statusCode) {
83+
return false
84+
}
85+
86+
if (match[2] === ' ') {
87+
return i === lines.length - 1
88+
}
89+
90+
if (match[2] === '+') {
91+
inDataBlock = true
92+
}
93+
}
94+
95+
return false
96+
}
97+
4998
private sendCommand(command: string): Promise<string> {
5099
return new Promise((resolve, reject) => {
51100
if (!this.socket) {
52101
reject(new Error('Not connected to Tor control port'))
53102
return
54103
}
104+
105+
const socket = this.socket
55106
let buf = ''
107+
108+
const cleanup = () => {
109+
socket.off('data', onData)
110+
socket.off('error', onError)
111+
}
112+
113+
const onError = (error: Error) => {
114+
cleanup()
115+
reject(error)
116+
}
117+
56118
const onData = (data: Buffer) => {
57119
buf += data.toString()
58-
if (buf.endsWith('\r\n')) {
59-
this.socket!.off('data', onData)
60-
if (/^250/.test(buf)) { resolve(buf) }
61-
else { reject(new Error(buf.trim())) }
120+
if (!this.isCompleteTorReply(buf)) {
121+
return
62122
}
123+
124+
cleanup()
125+
if (/^250/.test(buf)) { resolve(buf) }
126+
else { reject(new Error(buf.trim())) }
63127
}
64-
this.socket.on('data', onData)
65-
this.socket.write(`${command}\r\n`)
128+
129+
socket.on('data', onData)
130+
socket.on('error', onError)
131+
socket.write(`${command}\r\n`)
66132
})
67133
}
68134

src/utils/transform.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,25 @@ function bech32PrefixChk(prefix: string): number {
4848
function bech32Convert(data: number[], inBits: number, outBits: number, pad: boolean): number[] {
4949
let value = 0, bits = 0
5050
const maxV = (1 << outBits) - 1
51+
const maxAcc = (1 << (inBits + outBits - 1)) - 1
52+
const maxInput = (1 << inBits) - 1
5153
const result: number[] = []
5254
for (const byte of data) {
53-
value = (value << inBits) | byte
55+
if (!Number.isInteger(byte) || byte < 0 || byte > maxInput) {
56+
throw new Error(`Invalid value for ${inBits}-bit input: ${byte}`)
57+
}
58+
value = ((value << inBits) | byte) & maxAcc
5459
bits += inBits
5560
while (bits >= outBits) {
5661
bits -= outBits
5762
result.push((value >> bits) & maxV)
5863
}
5964
}
60-
if (pad && bits > 0) { result.push((value << (outBits - bits)) & maxV) }
65+
if (pad) {
66+
if (bits > 0) { result.push((value << (outBits - bits)) & maxV) }
67+
} else if (bits >= inBits || ((value << (outBits - bits)) & maxV) !== 0) {
68+
throw new Error('Invalid bech32 padding')
69+
}
6170
return result
6271
}
6372

@@ -135,8 +144,14 @@ export const fromDBUser = applySpec<User>({
135144
})
136145

137146
export const fromBech32 = (input: string) => {
147+
const normalizedInput = input.toLowerCase()
148+
149+
if (input !== normalizedInput && input !== input.toUpperCase()) {
150+
throw new Error('Bech32 mixed-case input is invalid')
151+
}
152+
138153
const { prefix, words } = bech32Decode(input)
139-
if (!input.startsWith(prefix)) {
154+
if (!normalizedInput.startsWith(prefix)) {
140155
throw new Error(`Bech32 invalid prefix: ${prefix}`)
141156
}
142157

0 commit comments

Comments
 (0)