Skip to content
This repository was archived by the owner on Jul 12, 2025. It is now read-only.

Commit 725ff00

Browse files
committed
feat: implement bloop version 4 protocol
BREAKING CHANGE: This will require a client implementing version 4 of the bloop protocol.
1 parent e902a4b commit 725ff00

18 files changed

+2653
-7467
lines changed

.eslintrc.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"extends": "eslint-config-dasprid",
33
"parserOptions": {
4-
"project": "tsconfig.eslint.json"
4+
"project": "tsconfig.json"
55
}
66
}

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright (c) 2022, Bloop Box
1+
Copyright (c) 2023, Bloop Box
22
All rights reserved.
33

44
Redistribution and use in source and binary forms, with or without

package-lock.json

Lines changed: 2433 additions & 7402 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,20 +26,21 @@
2626
"Typescript"
2727
],
2828
"devDependencies": {
29-
"@commitlint/cli": "^17.3.0",
30-
"@commitlint/config-conventional": "^17.3.0",
31-
"@rollup/plugin-typescript": "^10.0.1",
32-
"@tsconfig/node16": "^1.0.3",
29+
"@commitlint/cli": "^18.0.0",
30+
"@commitlint/config-conventional": "^18.0.0",
31+
"@rollup/plugin-typescript": "^11.1.5",
32+
"@tsconfig/node18": "^18.2.2",
3333
"@types/jest": "^29.2.4",
34-
"@types/node": "^16.18.4",
34+
"@types/node": "^20.8.8",
3535
"eslint": "^8.29.0",
36-
"eslint-config-dasprid": "^0.1.13",
36+
"eslint-config-dasprid": "^0.3.1",
3737
"husky": "^8.0.2",
3838
"jest": "^29.3.1",
39-
"lint-staged": "^13.1.0",
40-
"rollup": "^3.6.0",
39+
"lint-staged": "^15.0.2",
40+
"rollup": "^4.1.4",
4141
"ts-jest": "^29.0.3",
42-
"typescript": "^4.9.3",
42+
"tslib": "^2.6.2",
43+
"typescript": "^5.2.2",
4344
"winston": "^3.8.2"
4445
},
4546
"lint-staged": {

rollup.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default {
88
{file: pkg.module, format: 'es', sourcemap: true},
99
],
1010
plugins: [
11-
typescript({tsconfig: './tsconfig.json'}),
11+
typescript({tsconfig: './tsconfig.build.json'}),
1212
],
1313
external: ['tls'],
1414
};

src/BufferedStream.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class BufferedStream<T extends Socket> implements Stream {
100100
this.queue.shift();
101101
} else {
102102
this.queue[0].copy(result, offset, 0, length);
103-
this.queue[0] = this.queue[0].slice(length);
103+
this.queue[0] = this.queue[0].subarray(length);
104104
}
105105

106106
length -= bufferLength;

src/Processor.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type {IpAddr} from './ip-addr';
2+
13
export class UnknownUidResult {}
24
export class ThrottledUidResult {}
35
export class ValidUidResult {
@@ -25,7 +27,7 @@ export class AudioFoundResult {
2527
export type GetAudioResult = AudioNotFoundResult | AudioFoundResult;
2628

2729
type Processor = {
28-
authenticate : (clientId : string, secret : string) => Promise<boolean> | boolean;
30+
authenticate : (clientId : string, secret : string, localIp : IpAddr) => Promise<boolean> | boolean;
2931
checkUid : (clientId : string, uid : Buffer) => Promise<CheckUidResult>;
3032
getAudio : (id : Buffer) => Promise<GetAudioResult>;
3133
};

src/client.ts

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type {Logger} from 'winston';
2+
import {IpV4Addr, IpV6Addr} from './ip-addr';
3+
import type {IpAddr} from './ip-addr';
24
import type Processor from './Processor';
35
import {AudioNotFoundResult, ThrottledUidResult, UnknownUidResult} from './Processor';
46
import type Stream from './Stream';
@@ -7,16 +9,26 @@ export enum Command {
79
checkUid = 0,
810
getAudio = 1,
911
ping = 2,
12+
quit = 3,
1013
}
1114

12-
export enum Response {
15+
export enum AuthResponse {
1316
invalidAuth = 0,
1417
validAuth = 1,
18+
}
19+
20+
export enum CheckUidResponse {
1521
unknownUid = 0,
1622
throttledUid = 2,
1723
validUid = 1,
24+
}
25+
26+
export enum GetAudioResponse {
1827
audioNotFound = 0,
1928
audioFound = 1,
29+
}
30+
31+
export enum PingResponse {
2032
pong = 0,
2133
}
2234

@@ -39,25 +51,39 @@ const handleClient = async (
3951
}, authTimeout);
4052
}),
4153
(async () => {
42-
const credentialsLength = await stream.readUint8();
43-
const credentials = (await stream.readExact(credentialsLength)).toString('ascii');
54+
const clientIdLength = await stream.readUint8();
55+
const clientId = (await stream.readExact(clientIdLength)).toString('utf-8');
4456

45-
if (!credentials.includes(':')) {
46-
logger?.info('Malformed credentials');
47-
stream.writeUint8(Response.invalidAuth);
48-
return null;
57+
const secretLength = await stream.readUint8();
58+
const secret = (await stream.readExact(secretLength)).toString('utf-8');
59+
60+
const inetVersion = await stream.readUint8();
61+
let localIp : IpAddr;
62+
63+
switch (inetVersion) {
64+
case 4:
65+
localIp = new IpV4Addr(await stream.readExact(4));
66+
break;
67+
68+
case 6:
69+
localIp = new IpV6Addr(await stream.readExact(16));
70+
break;
71+
72+
default:
73+
logger?.info(`Invalid INET version "${inetVersion}"`);
74+
stream.writeUint8(AuthResponse.invalidAuth);
75+
return null;
4976
}
5077

51-
const [clientId, secret] = credentials.split(':', 2);
52-
const authResult = await processor.authenticate(clientId, secret);
78+
const authResult = await processor.authenticate(clientId, secret, localIp);
5379

5480
if (!authResult) {
5581
logger?.info(`Unknown client ID "${clientId}" or invalid secret`);
56-
stream.writeUint8(Response.invalidAuth);
82+
stream.writeUint8(AuthResponse.invalidAuth);
5783
return null;
5884
}
5985

60-
stream.writeUint8(Response.validAuth);
86+
stream.writeUint8(AuthResponse.validAuth);
6187
return clientId;
6288
})(),
6389
]);
@@ -83,16 +109,16 @@ const handleClient = async (
83109
const response = await processor.checkUid(clientId, uid);
84110

85111
if (response instanceof UnknownUidResult) {
86-
stream.writeUint8(Response.unknownUid);
112+
stream.writeUint8(CheckUidResponse.unknownUid);
87113
break;
88114
}
89115

90116
if (response instanceof ThrottledUidResult) {
91-
stream.writeUint8(Response.throttledUid);
117+
stream.writeUint8(CheckUidResponse.throttledUid);
92118
break;
93119
}
94120

95-
stream.writeUint8(Response.validUid);
121+
stream.writeUint8(CheckUidResponse.validUid);
96122
stream.writeUint8(response.achievementIds.length);
97123

98124
for (const achievementId of response.achievementIds) {
@@ -107,20 +133,23 @@ const handleClient = async (
107133
const result = await processor.getAudio(id);
108134

109135
if (result instanceof AudioNotFoundResult) {
110-
stream.writeUint8(Response.audioNotFound);
136+
stream.writeUint8(GetAudioResponse.audioNotFound);
111137
break;
112138
}
113139

114-
stream.writeUint8(Response.audioFound);
140+
stream.writeUint8(GetAudioResponse.audioFound);
115141
stream.writeUInt32LE(result.data.length);
116142
stream.writeAll(result.data);
117143
break;
118144
}
119145

120146
case Command.ping:
121-
stream.writeUint8(Response.pong);
147+
stream.writeUint8(PingResponse.pong);
122148
break;
123149

150+
case Command.quit:
151+
return;
152+
124153
default:
125154
logger?.info(`Unknown command: ${commandCode}`);
126155
return;

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export {startServer} from './server';
33

44
export type {default as Processor, CheckUidResult, GetAudioResult} from './Processor';
55
export {AudioFoundResult, AudioNotFoundResult, ThrottledUidResult, ValidUidResult, UnknownUidResult} from './Processor';
6+
7+
export type {IpAddr} from './ip-addr';
8+
export {IpV4Addr, IpV6Addr} from './ip-addr';

src/ip-addr.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type IpAddr = {
2+
toString : () => string;
3+
};
4+
5+
export class IpV4Addr implements IpAddr {
6+
private readonly bytes : Buffer;
7+
8+
public constructor(bytes : Buffer) {
9+
if (bytes.length !== 4) {
10+
throw new Error('Bytes must be exactly 4 in length');
11+
}
12+
13+
this.bytes = bytes;
14+
}
15+
16+
public toString() : string {
17+
return [...this.bytes].join('.');
18+
}
19+
}
20+
21+
export class IpV6Addr implements IpAddr {
22+
private readonly bytes : Buffer;
23+
24+
public constructor(bytes : Buffer) {
25+
if (bytes.length !== 16) {
26+
throw new Error('Bytes must be exactly 16 in length');
27+
}
28+
29+
this.bytes = bytes;
30+
}
31+
32+
public toString() : string {
33+
return (
34+
this.bytes
35+
.toString('hex')
36+
.match(/.{4}/g) as string[]
37+
)
38+
.map(value => value.replace(/^0+/, ''))
39+
.join(':')
40+
.replace(/0000:/g, ':')
41+
.replace(/:{2,}/g, '::');
42+
}
43+
}

0 commit comments

Comments
 (0)