├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── gameServer │ ├── gameServer.ts │ └── gameServerTypes.ts ├── index.ts ├── masterServer │ ├── masterServer.ts │ └── masterServerTypes.ts └── promiseSocket.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | /lib 2 | /node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 GiyoMoon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Steam Server Query 2 | [![npm version](https://img.shields.io/npm/v/steam-server-query.svg)](https://npmjs.com/package/steam-server-query) 3 | [![npm downloads](https://img.shields.io/npm/dm/steam-server-query.svg)](https://npmjs.com/package/steam-server-query) 4 | [![license](https://img.shields.io/npm/l/steam-server-query.svg)](https://github.com/GiyoMoon/steam-server-query/blob/main/LICENSE) 5 | 6 | Module which implements the [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) and [Game Server Queries](https://developer.valvesoftware.com/wiki/Server_queries). It is working with promises. 7 | 8 | ## Install 9 | ```bash 10 | npm install steam-server-query 11 | ``` 12 | 13 | ## API 14 | ### Master Server Query 15 | ```javascript 16 | queryMasterServer(masterServer: string, region: REGIONS, filter?: Filter, timeout?: number, maxHosts?: number): Promise 17 | ``` 18 | Fetch a Steam master server to retrieve a list of game server hosts. 19 | - `masterServer`: Host and port of the master server to call. 20 | - `region`: The region of the world where you wish to find servers in. Use `REGIONS.ALL` for all regions. 21 | - `filter`: Optional. Object which contains filters to be sent with the query. Default is { }. 22 | - `timeout`: Optional. Time in milliseconds after the socket request should fail. Default is 1 second. 23 | - `maxHosts`: Optional. Return a limited amount of hosts. Stops calling the master server after this limit is reached. Can be used to prevent getting rate limited. 24 | - Returns: A promise including an array of game server hosts. 25 | ### Game Server Query 26 | ```javascript 27 | queryGameServerInfo(gameServer: string, attempts?: number, timeout?: number | number[]): Promise 28 | ``` 29 | Send a A2S_INFO request to a game server. Retrieves information like its name, the current map, the number of players and so on. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO). 30 | 31 | --- 32 | ```javascript 33 | queryGameServerPlayer(gameServer: string, attempts?: number, timeout?: number | number[]): Promise 34 | ``` 35 | Send a A2S_PLAYER request to a game server. Retrieves the current playercount and for every player their name, score and duration. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER). 36 | 37 | --- 38 | ```javascript 39 | queryGameServerRules(gameServer: string, attempts?: number, timeout?: number | number[]): Promise 40 | ``` 41 | Send a A2S_RULES request to a game server. Retrieves the rule count and for every rule its name and value. Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_RULES). 42 | 43 | --- 44 | Parameters for every Game Server Query function 45 | - `gameServer`: Host and port of the game server to call. 46 | - `attempts`: Optional. Number of call attempts to make. Default is 1 attempt. 47 | - `timeout`: Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. (Example for 3 attempts: `[1000, 1000, 2000]`) 48 | - Returns: A promise including an object (Either type `InfoResponse`, `PlayerResponse` or `RulesResponse`) 49 | 50 | ## Types 51 | ### Master Server Query 52 | #### `REGIONS` 53 | ```javscript 54 | enum REGIONS { 55 | 'US_EAST_COAST' = 0x00, 56 | 'US_WEST_COAST' = 0x01, 57 | 'SOUTH_AMERICA' = 0x02, 58 | 'EUROPE' = 0x03, 59 | 'ASIA' = 0x04, 60 | 'AUSTRALIA' = 0x05, 61 | 'MIDDLE_EAST' = 0x06, 62 | 'AFRICA' = 0x07, 63 | 'ALL' = 0xFF 64 | } 65 | ``` 66 | #### `Filter` 67 | ```javscript 68 | interface Filter extends BasicFilter { 69 | nor?: BasicFilter; 70 | nand?: BasicFilter; 71 | } 72 | 73 | interface BasicFilter { 74 | dedicated?: 1; 75 | secure?: 1; 76 | gamedir?: string; 77 | map?: string; 78 | linux?: 1; 79 | password?: 0; 80 | empty?: 1; 81 | full?: 1; 82 | proxy?: 1; 83 | appid?: number; 84 | napp?: number; 85 | noplayers?: 1; 86 | white?: 1; 87 | gametype?: string[]; 88 | gamedata?: string[]; 89 | gamedataor?: string[]; 90 | name_match?: string; 91 | version_match?: string; 92 | collapse_addr_hash?: 1; 93 | gameaddr?: string; 94 | } 95 | ``` 96 | ### Game Server Query 97 | #### `InfoResponse` 98 | ```javascript 99 | interface InfoResponse { 100 | protocol: number; 101 | name: string; 102 | map: string; 103 | folder: string; 104 | game: string; 105 | appId: number; 106 | players: number; 107 | maxPlayers: number; 108 | bots: number; 109 | serverType: string; 110 | environment: string; 111 | visibility: number; 112 | vac: number; 113 | version: string; 114 | port?: number; 115 | serverId?: BigInt; 116 | spectatorPort?: number; 117 | spectatorName?: string; 118 | keywords?: string; 119 | gameId?: BigInt; 120 | } 121 | ``` 122 | #### `PlayerResponse` 123 | ```javascript 124 | interface PlayerResponse { 125 | playerCount: number; 126 | players: Player[]; 127 | } 128 | 129 | interface Player { 130 | index: number; 131 | name: string; 132 | score: number; 133 | duration: number; 134 | } 135 | ``` 136 | #### `RulesResponse` 137 | ```javascript 138 | interface RulesResponse { 139 | ruleCount: number; 140 | rules: Rule[]; 141 | } 142 | 143 | interface Rule { 144 | name: string; 145 | value: string; 146 | } 147 | ``` 148 | ## Examples 149 | ### Master Server Protocol 150 | To retrieve all servers from the game [Witch It](https://store.steampowered.com/app/559650/Witch_It/) with players on it: 151 | ```javascript 152 | import { queryMasterServer, REGIONS } from 'steam-server-query'; 153 | 154 | queryMasterServer('hl2master.steampowered.com:27011', REGIONS.ALL, { empty: 1, appid: 559650 }).then(servers => { 155 | console.log(servers); 156 | }).catch((err) => { 157 | console.error(err); 158 | }); 159 | ``` 160 | Response (shortened): 161 | ```json 162 | [ 163 | "176.57.181.178:27003" 164 | "176.57.181.178:27008" 165 | "176.57.171.49:27005" 166 | "176.57.181.178:27005" 167 | ] 168 | ``` 169 | ### Game Server Protocol 170 | #### A2S_INFO 171 | To retrieve information about the game server with the address `176.57.181.178:27003`: 172 | ```javascript 173 | import { queryGameServerInfo } from 'steam-server-query'; 174 | 175 | queryGameServerInfo('176.57.181.178:27003').then(infoResponse => { 176 | console.log(infoResponse); 177 | }).catch((err) => { 178 | console.error(err); 179 | }); 180 | ``` 181 | Response: 182 | ```json 183 | { 184 | "protocol": 17, 185 | "name": "EU04", 186 | "map": "RandomMapCycle", 187 | "folder": "WitchIt", 188 | "game": "Witch Hunt", 189 | "appId": 0, 190 | "players": 10, 191 | "maxPlayers": 16, 192 | "bots": 0, 193 | "serverType": "d", 194 | "environment": "l", 195 | "visibility": 0, 196 | "vac": 1, 197 | "version": "1.0.0.0", 198 | "port": 7780, 199 | "keywords": "BUILDID:0,OWNINGID:90154510593238022,OWNINGNAME:EU04,SESSIONFLAGS:683,GameMode_s:Hide and Seek,PlayerCount_i:10,MatchTime_i:265", 200 | "gameId": 559650 201 | } 202 | ``` 203 | #### A2S_PLAYER 204 | To retrieve players playing on the game server with the address `176.57.181.178:27003`: 205 | ```javascript 206 | import { queryGameServerPlayer } from 'steam-server-query'; 207 | 208 | queryGameServerPlayer('176.57.181.178:27003').then(playerResponse => { 209 | console.log(playerResponse); 210 | }).catch((err) => { 211 | console.error(err); 212 | }); 213 | ``` 214 | Response (shortened): 215 | ```json 216 | { 217 | "playerCount": 10, 218 | "players": [ 219 | { 220 | "index": 0, 221 | "name": "Player_1", 222 | "score": 0, 223 | "duration": 1969.7518310546875 224 | }, 225 | { 226 | "index": 0, 227 | "name": "Player_2", 228 | "score": 0, 229 | "duration": 1958.9234619140625 230 | }, 231 | { 232 | "index": 0, 233 | "name": "Player_3", 234 | "score": 0, 235 | "duration": 1509.9417724609375 236 | } 237 | ] 238 | } 239 | ``` 240 | #### A2S_RULES 241 | To retrieve rules of the game server with the address `176.57.181.178:27003`: 242 | ```javascript 243 | import { queryGameServerRules } from 'steam-server-query'; 244 | 245 | queryGameServerRules('176.57.181.178:27003').then(rulesResponse => { 246 | console.log(rulesResponse); 247 | }).catch((err) => { 248 | console.error(err); 249 | }); 250 | ``` 251 | Response: 252 | ```json 253 | { 254 | "ruleCount": 14, 255 | "rules": [ 256 | { "name": "CONMETHOD", "value": "P2P" }, 257 | { "name": "GameMode_s", "value": "Hide and Seek" }, 258 | { "name": "MatchStarted_b", "value": "true" }, 259 | { "name": "MatchTime_i", "value": "265" }, 260 | { "name": "OWNINGID", "value": "90154510593238022" }, 261 | { "name": "OWNINGNAME", "value": "EU04" }, 262 | { "name": "P2PADDR", "value": "90154510593238022" }, 263 | { "name": "P2PPORT", "value": "7780" }, 264 | { "name": "PlayerCount_i", "value": "10" }, 265 | { "name": "SESSIONFLAGS", "value": "683" }, 266 | { "name": "SessionName_s", "value": "EU04" }, 267 | { "name": "StartTime_s", "value": "2021.12.29-00.47.20" }, 268 | { "name": "Tournament_b", "value": "false" }, 269 | { "name": "VersionNumber_s", "value": "1.2.3" } 270 | ] 271 | } 272 | ``` 273 | ## Notes 274 | - The master servers are rate limited. Requests with large outputs (6000+ servers) will probably reach this limit and a timeout error will be thrown. 275 | 276 | ## Links 277 | - [Master Server Query Protocol](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol) 278 | - [Game Server Queries](https://developer.valvesoftware.com/wiki/Server_queries) 279 | 280 | ## License 281 | This repository and the code inside it is licensed under the MIT License. Read [LICENSE](https://github.com/GiyoMoon/steam-server-query/blob/main/LICENSE) for more information. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steam-server-query", 3 | "version": "1.1.3", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "steam-server-query", 9 | "version": "1.1.3", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/node": "^17.0.17", 13 | "typescript": "^4.5.5" 14 | } 15 | }, 16 | "node_modules/@types/node": { 17 | "version": "17.0.17", 18 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz", 19 | "integrity": "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw==", 20 | "dev": true 21 | }, 22 | "node_modules/typescript": { 23 | "version": "4.5.5", 24 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", 25 | "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", 26 | "dev": true, 27 | "bin": { 28 | "tsc": "bin/tsc", 29 | "tsserver": "bin/tsserver" 30 | }, 31 | "engines": { 32 | "node": ">=4.2.0" 33 | } 34 | } 35 | }, 36 | "dependencies": { 37 | "@types/node": { 38 | "version": "17.0.17", 39 | "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.17.tgz", 40 | "integrity": "sha512-e8PUNQy1HgJGV3iU/Bp2+D/DXh3PYeyli8LgIwsQcs1Ar1LoaWHSIT6Rw+H2rNJmiq6SNWiDytfx8+gYj7wDHw==", 41 | "dev": true 42 | }, 43 | "typescript": { 44 | "version": "4.5.5", 45 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", 46 | "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", 47 | "dev": true 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "steam-server-query", 3 | "version": "1.1.3", 4 | "description": "Module which implements the Master Server Query Protocol and Game Server Queries.", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "lib" 9 | ], 10 | "scripts": { 11 | "build": "tsc" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/GiyoMoon/steam-server-query.git" 16 | }, 17 | "keywords": [ 18 | "steam", 19 | "master server query", 20 | "game server query" 21 | ], 22 | "author": "GiyoMoon", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/GiyoMoon/steam-server-query/issues" 26 | }, 27 | "homepage": "https://github.com/GiyoMoon/steam-server-query#readme", 28 | "devDependencies": { 29 | "@types/node": "^17.0.17", 30 | "typescript": "^4.5.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/gameServer/gameServer.ts: -------------------------------------------------------------------------------- 1 | import { PromiseSocket } from '../promiseSocket'; 2 | import { InfoResponse, Player, PlayerResponse, Rule, RulesResponse } from './gameServerTypes'; 3 | 4 | /** 5 | * Send a A2S_INFO request to a game server. Retrieves information like its name, the current map, the number of players and so on. 6 | * 7 | * Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_INFO). 8 | * @param gameServer Host and port of the game server to call. 9 | * @param attempts Optional. Number of call attempts to make. Default is 1 attempt. 10 | * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. 11 | * @returns A promise including an object of the type `InfoResponse` 12 | */ 13 | export async function queryGameServerInfo(gameServer: string, attempts = 1, timeout: number | number[] = 1000): Promise { 14 | const splitGameServer = gameServer.split(':'); 15 | const host = splitGameServer[0]; 16 | const port = parseInt(splitGameServer[1]); 17 | 18 | const gameServerQuery = new GameServerQuery(host, port, attempts, timeout); 19 | const result = await gameServerQuery.info(); 20 | return result; 21 | } 22 | 23 | /** 24 | * Send a A2S_PLAYER request to a game server. Retrieves the current playercount and for every player their name, score and duration. 25 | * 26 | * Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_PLAYER). 27 | * @param gameServer Host and port of the game server to call. 28 | * @param attempts Optional. Number of call attempts to make. Default is 1 attempt. 29 | * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. 30 | * @returns A promise including an object of the type `PlayerResponse` 31 | */ 32 | export async function queryGameServerPlayer(gameServer: string, attempts = 1, timeout: number | number[] = 1000): Promise { 33 | const splitGameServer = gameServer.split(':'); 34 | const host = splitGameServer[0]; 35 | const port = parseInt(splitGameServer[1]); 36 | 37 | const gameServerQuery = new GameServerQuery(host, port, attempts, timeout); 38 | const result = await gameServerQuery.player(); 39 | return result; 40 | } 41 | 42 | /** 43 | * Send a A2S_RULES request to a game server. Retrieves the rule count and for every rule its name and value. 44 | * 45 | * Read more [here](https://developer.valvesoftware.com/wiki/Server_queries#A2S_RULES). 46 | * @param gameServer Host and port of the game server to call. 47 | * @param attempts Optional. Number of call attempts to make. Default is 1 attempt. 48 | * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1000. Specify an array of timeouts if they should be different for every attempt. 49 | * @returns A promise including an object of the type `RulesResponse` 50 | */ 51 | export async function queryGameServerRules(gameServer: string, attempts = 1, timeout: number | number[] = 1000): Promise { 52 | const splitGameServer = gameServer.split(':'); 53 | const host = splitGameServer[0]; 54 | const port = parseInt(splitGameServer[1]); 55 | 56 | const gameServerQuery = new GameServerQuery(host, port, attempts, timeout); 57 | const result = await gameServerQuery.rules(); 58 | return result; 59 | } 60 | 61 | class GameServerQuery { 62 | private _promiseSocket: PromiseSocket; 63 | 64 | constructor(private _host: string, private _port: number, attempts: number, timeout: number | number[]) { 65 | this._promiseSocket = new PromiseSocket(attempts, timeout); 66 | }; 67 | 68 | public async info(): Promise { 69 | let resultBuffer: Buffer; 70 | try { 71 | resultBuffer = await this._promiseSocket.send(this._buildInfoPacket(), this._host, this._port); 72 | } catch (err: any) { 73 | this._promiseSocket.closeSocket(); 74 | throw new Error(err); 75 | } 76 | 77 | // If the server replied with a challenge, grab challenge number and send request again 78 | if (this._isChallengeResponse(resultBuffer)) { 79 | resultBuffer = resultBuffer.slice(5); 80 | const challenge = resultBuffer; 81 | try { 82 | resultBuffer = await this._promiseSocket.send(this._buildInfoPacket(challenge), this._host, this._port); 83 | } catch (err: any) { 84 | this._promiseSocket.closeSocket(); 85 | throw new Error(err); 86 | } 87 | } 88 | 89 | this._promiseSocket.closeSocket(); 90 | 91 | const parsedInfoBuffer = this._parseInfoBuffer(resultBuffer); 92 | return parsedInfoBuffer as InfoResponse; 93 | } 94 | 95 | public async player(): Promise { 96 | let resultBuffer: Buffer; 97 | let gotPlayerResponse = false; 98 | let challengeTries = 0; 99 | 100 | do { 101 | let challengeResultBuffer: Buffer; 102 | try { 103 | challengeResultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x55])), this._host, this._port); 104 | } catch (err: any) { 105 | this._promiseSocket.closeSocket(); 106 | throw new Error(err); 107 | } 108 | 109 | const challenge = challengeResultBuffer.slice(5); 110 | try { 111 | resultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x55]), challenge), this._host, this._port); 112 | } catch (err: any) { 113 | this._promiseSocket.closeSocket(); 114 | throw new Error(err); 115 | } 116 | 117 | if (!this._isChallengeResponse(resultBuffer)) { 118 | gotPlayerResponse = true; 119 | } 120 | 121 | challengeTries++; 122 | } while (!gotPlayerResponse && challengeTries < 5); 123 | 124 | this._promiseSocket.closeSocket(); 125 | 126 | if (this._isChallengeResponse(resultBuffer)) { 127 | throw new Error('Server kept sending challenge responses.'); 128 | } 129 | 130 | const parsedPlayerBuffer = this._parsePlayerBuffer(resultBuffer); 131 | return parsedPlayerBuffer; 132 | } 133 | 134 | public async rules(): Promise { 135 | let challengeResultBuffer: Buffer; 136 | try { 137 | challengeResultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x56])), this._host, this._port); 138 | } catch (err: any) { 139 | this._promiseSocket.closeSocket(); 140 | throw new Error(err); 141 | } 142 | 143 | const challenge = challengeResultBuffer.slice(5); 144 | 145 | let resultBuffer: Buffer; 146 | try { 147 | resultBuffer = await this._promiseSocket.send(this._buildPacket(Buffer.from([0x56]), challenge), this._host, this._port); 148 | } catch (err: any) { 149 | this._promiseSocket.closeSocket(); 150 | throw new Error(err); 151 | } 152 | 153 | this._promiseSocket.closeSocket(); 154 | 155 | const parsedRulesBuffer = this._parseRulesBuffer(resultBuffer); 156 | return parsedRulesBuffer; 157 | } 158 | 159 | private _buildInfoPacket(challenge?: Buffer) { 160 | let packet = Buffer.concat([ 161 | Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]), 162 | Buffer.from([0x54]), 163 | Buffer.from('Source Engine Query', 'ascii'), 164 | Buffer.from([0x00]) 165 | ]); 166 | if (challenge) { 167 | packet = Buffer.concat([ 168 | packet, 169 | challenge 170 | ]); 171 | } 172 | return packet; 173 | } 174 | 175 | private _buildPacket(header: Buffer, challenge?: Buffer) { 176 | let packet = Buffer.concat([ 177 | Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]), 178 | header 179 | ]); 180 | if (challenge) { 181 | packet = Buffer.concat([ 182 | packet, 183 | challenge 184 | ]); 185 | } else { 186 | packet = Buffer.concat([ 187 | packet, 188 | Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]) 189 | ]); 190 | } 191 | return packet; 192 | } 193 | 194 | private _isChallengeResponse(buffer: Buffer) { 195 | return buffer.compare(Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0x41]), 0, 5, 0, 5) === 0; 196 | } 197 | 198 | private _parseInfoBuffer(buffer: Buffer): InfoResponse { 199 | const infoResponse: Partial = {}; 200 | buffer = buffer.slice(5); 201 | [infoResponse.protocol, buffer] = this._readUInt8(buffer); 202 | [infoResponse.name, buffer] = this._readString(buffer); 203 | [infoResponse.map, buffer] = this._readString(buffer); 204 | [infoResponse.folder, buffer] = this._readString(buffer); 205 | [infoResponse.game, buffer] = this._readString(buffer); 206 | [infoResponse.appId, buffer] = this._readInt16LE(buffer); 207 | [infoResponse.players, buffer] = this._readUInt8(buffer); 208 | [infoResponse.maxPlayers, buffer] = this._readUInt8(buffer); 209 | [infoResponse.bots, buffer] = this._readUInt8(buffer); 210 | 211 | infoResponse.serverType = buffer.subarray(0, 1).toString('utf-8'); 212 | buffer = buffer.slice(1); 213 | 214 | infoResponse.environment = buffer.subarray(0, 1).toString('utf-8'); 215 | buffer = buffer.slice(1); 216 | 217 | [infoResponse.visibility, buffer] = this._readUInt8(buffer); 218 | [infoResponse.vac, buffer] = this._readUInt8(buffer); 219 | [infoResponse.version, buffer] = this._readString(buffer); 220 | 221 | // if the extra data flag (EDF) is present 222 | if (buffer.length > 1) { 223 | let edf: number; 224 | [edf, buffer] = this._readUInt8(buffer); 225 | if (edf & 0x80) { 226 | [infoResponse.port, buffer] = this._readInt16LE(buffer); 227 | } 228 | if (edf & 0x10) { 229 | buffer = buffer.slice(8); 230 | } 231 | if (edf & 0x40) { 232 | [infoResponse.spectatorPort, buffer] = this._readUInt8(buffer); 233 | [infoResponse.spectatorName, buffer] = this._readString(buffer); 234 | } 235 | if (edf & 0x20) { 236 | [infoResponse.keywords, buffer] = this._readString(buffer); 237 | } 238 | if (edf & 0x01) { 239 | infoResponse.gameId = buffer.readBigInt64LE(); 240 | buffer = buffer.slice(8); 241 | } 242 | } 243 | 244 | return infoResponse as InfoResponse; 245 | } 246 | 247 | private _parsePlayerBuffer(buffer: Buffer): PlayerResponse { 248 | const playerResponse: Partial = {}; 249 | buffer = buffer.slice(5); 250 | [playerResponse.playerCount, buffer] = this._readUInt8(buffer); 251 | 252 | playerResponse.players = []; 253 | for (let i = 0; i < playerResponse.playerCount; i++) { 254 | let player: Player; 255 | [player, buffer] = this._readPlayer(buffer); 256 | playerResponse.players.push(player); 257 | } 258 | 259 | return playerResponse as PlayerResponse; 260 | } 261 | 262 | private _parseRulesBuffer(buffer: Buffer): RulesResponse { 263 | const rulesResponse: Partial = {}; 264 | buffer = buffer.slice(5); 265 | [rulesResponse.ruleCount, buffer] = this._readInt16LE(buffer); 266 | 267 | rulesResponse.rules = []; 268 | for (let i = 0; i < rulesResponse.ruleCount; i++) { 269 | let rule: Rule; 270 | [rule, buffer] = this._readRule(buffer); 271 | rulesResponse.rules.push(rule); 272 | } 273 | 274 | return rulesResponse as RulesResponse; 275 | } 276 | 277 | private _readString(buffer: Buffer): [string, Buffer] { 278 | const endOfName = buffer.indexOf(0x00); 279 | const stringBuffer = buffer.subarray(0, endOfName); 280 | const modifiedBuffer = buffer.slice(endOfName + 1); 281 | return [stringBuffer.toString('utf-8'), modifiedBuffer]; 282 | } 283 | 284 | private _readUInt8(buffer: Buffer): [number, Buffer] { 285 | return [buffer.readUInt8(), buffer.slice(1)]; 286 | } 287 | 288 | private _readInt16LE(buffer: Buffer): [number, Buffer] { 289 | return [buffer.readInt16LE(), buffer.slice(2)]; 290 | } 291 | 292 | private _readPlayer(buffer: Buffer): [Player, Buffer] { 293 | let player: Partial = {}; 294 | [player.index, buffer] = this._readUInt8(buffer); 295 | [player.name, buffer] = this._readString(buffer); 296 | player.score = buffer.readInt32LE(); 297 | buffer = buffer.slice(4); 298 | player.duration = buffer.readFloatLE(); 299 | buffer = buffer.slice(4); 300 | 301 | return [player as Player, buffer]; 302 | } 303 | 304 | private _readRule(buffer: Buffer): [Rule, Buffer] { 305 | let rule: Partial = {}; 306 | [rule.name, buffer] = this._readString(buffer); 307 | [rule.value, buffer] = this._readString(buffer); 308 | 309 | return [rule as Rule, buffer]; 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /src/gameServer/gameServerTypes.ts: -------------------------------------------------------------------------------- 1 | export interface InfoResponse { 2 | protocol: number; 3 | name: string; 4 | map: string; 5 | folder: string; 6 | game: string; 7 | appId: number; 8 | players: number; 9 | maxPlayers: number; 10 | bots: number; 11 | serverType: string; 12 | environment: string; 13 | visibility: number; 14 | vac: number; 15 | version: string; 16 | port?: number; 17 | serverId?: BigInt; 18 | spectatorPort?: number; 19 | spectatorName?: string; 20 | keywords?: string; 21 | gameId?: BigInt; 22 | } 23 | 24 | export interface PlayerResponse { 25 | playerCount: number; 26 | players: Player[]; 27 | } 28 | 29 | export interface Player { 30 | index: number; 31 | name: string; 32 | score: number; 33 | duration: number; 34 | } 35 | 36 | export interface RulesResponse { 37 | ruleCount: number; 38 | rules: Rule[]; 39 | } 40 | 41 | export interface Rule { 42 | name: string; 43 | value: string; 44 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { queryMasterServer } from './masterServer/masterServer'; 2 | export { Filter, REGIONS } from './masterServer/masterServerTypes'; 3 | export { queryGameServerInfo, queryGameServerPlayer, queryGameServerRules } from './gameServer/gameServer'; 4 | export { InfoResponse, PlayerResponse, RulesResponse } from './gameServer/gameServerTypes'; 5 | -------------------------------------------------------------------------------- /src/masterServer/masterServer.ts: -------------------------------------------------------------------------------- 1 | import { PromiseSocket } from '../promiseSocket'; 2 | import { REGIONS, Filter } from './masterServerTypes'; 3 | 4 | const ZERO_IP = '0.0.0.0:0'; 5 | const RESPONSE_START = Buffer.from([0xFF, 0xFF, 0xFF, 0xFF, 0x66, 0x0A]); 6 | 7 | /** 8 | * Fetch a Steam master server to retrieve a list of game server hosts. 9 | * 10 | * Read more [here](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol). 11 | * @param masterServer Host and port of the master server to call. 12 | * @param region The region of the world where you wish to find servers in. Use REGIONS.ALL for all regions. 13 | * @param filters Optional. Object which contains filters to be sent with the query. Default is { }. Read more [here](https://developer.valvesoftware.com/wiki/Master_Server_Query_Protocol#Filter). 14 | * @param timeout Optional. Time in milliseconds after the socket request should fail. Default is 1 second. 15 | * @param maxHosts Optional. Return a limited amount of hosts. Stops calling the master server after this limit is reached. Can be used to prevent getting rate limited. 16 | * @returns A promise including an array of game server hosts. 17 | */ 18 | export async function queryMasterServer(masterServer: string, region: REGIONS, filters: Filter = {}, timeout = 1000, maxHosts?: number): Promise { 19 | const splitMasterServer = masterServer.split(':'); 20 | const host = splitMasterServer[0]; 21 | const port = parseInt(splitMasterServer[1]); 22 | 23 | const masterServerQuery = new MasterServerQuery(host, port, region, filters, timeout, maxHosts); 24 | const hosts = await masterServerQuery.fetchServers(); 25 | return hosts; 26 | } 27 | 28 | class MasterServerQuery { 29 | private _seedId = ZERO_IP; 30 | private _promiseSocket: PromiseSocket; 31 | private _hosts: string[] = []; 32 | 33 | constructor(private _host: string, private _port: number, private _region: REGIONS, private _filters: Filter, timeout: number, private _maxHosts?: number) { 34 | this._promiseSocket = new PromiseSocket(1, timeout); 35 | }; 36 | 37 | public async fetchServers() { 38 | do { 39 | let resultBuffer: Buffer; 40 | try { 41 | resultBuffer = await this._promiseSocket.send(this._buildPacket(), this._host, this._port); 42 | // catch promise rejections and throw error 43 | } catch (err: any) { 44 | this._promiseSocket.closeSocket(); 45 | throw new Error(err); 46 | } 47 | 48 | const parsedHosts = this._parseBuffer(resultBuffer); 49 | this._seedId = parsedHosts[parsedHosts.length - 1]; 50 | this._hosts.push(...parsedHosts); 51 | 52 | if ( 53 | this._maxHosts && 54 | this._hosts.length >= this._maxHosts && 55 | this._hosts[this._maxHosts - 1] !== ZERO_IP 56 | ) { 57 | this._promiseSocket.closeSocket(); 58 | return this._hosts.slice(0, this._maxHosts); 59 | } 60 | } while (this._seedId !== ZERO_IP); 61 | 62 | this._promiseSocket.closeSocket(); 63 | 64 | // remove ZERO_IP from end of host list 65 | this._hosts.pop(); 66 | return this._hosts; 67 | } 68 | 69 | private _buildPacket() { 70 | return Buffer.concat([ 71 | Buffer.from([0x31]), 72 | Buffer.from([this._region]), 73 | Buffer.from(this._seedId, 'ascii'), Buffer.from([0x00]), 74 | Buffer.from(this.formatFilters(), 'ascii'), 75 | ]); 76 | } 77 | 78 | private formatFilters() { 79 | let str = ''; 80 | for (const key of Object.keys(this._filters)) { 81 | // @ts-ignore 82 | let val = this._filters[key]; 83 | str += '\\' + key + '\\'; 84 | if (key === 'nor' || key === 'nand') { 85 | str += Object.keys(val).length + this._slashifyObject(val); 86 | } else if (Array.isArray(val)) { 87 | str += val.join(','); 88 | } else { 89 | str += val; 90 | } 91 | } 92 | str += '\x00'; 93 | return str; 94 | } 95 | 96 | private _slashifyObject(object: any) { 97 | let str = ''; 98 | for (const key of Object.keys(object)) { 99 | let val = object[key]; 100 | str += '\\' + key + '\\' + val; 101 | } 102 | return str; 103 | } 104 | 105 | private _parseBuffer(buffer: Buffer) { 106 | const hosts: string[] = []; 107 | if (buffer.compare(RESPONSE_START, 0, 6, 0, 6) === 0) { 108 | buffer = buffer.slice(6); 109 | } 110 | 111 | let i = 0; 112 | while (i < buffer.length) { 113 | const ip = this._numberToIp(buffer.readInt32BE(i)); 114 | const port = buffer[i + 4] << 8 | buffer[i + 5]; 115 | hosts.push(`${ip}:${port}`); 116 | i += 6; 117 | } 118 | return hosts; 119 | } 120 | 121 | private _numberToIp(number: number) { 122 | var nbuffer = new ArrayBuffer(4); 123 | var ndv = new DataView(nbuffer); 124 | ndv.setUint32(0, number); 125 | 126 | var a = new Array(); 127 | for (var i = 0; i < 4; i++) { 128 | a[i] = ndv.getUint8(i); 129 | } 130 | return a.join('.'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/masterServer/masterServerTypes.ts: -------------------------------------------------------------------------------- 1 | export enum REGIONS { 2 | 'US_EAST_COAST' = 0x00, 3 | 'US_WEST_COAST' = 0x01, 4 | 'SOUTH_AMERICA' = 0x02, 5 | 'EUROPE' = 0x03, 6 | 'ASIA' = 0x04, 7 | 'AUSTRALIA' = 0x05, 8 | 'MIDDLE_EAST' = 0x06, 9 | 'AFRICA' = 0x07, 10 | 'ALL' = 0xFF 11 | }; 12 | 13 | export interface Filter extends BasicFilter { 14 | nor?: BasicFilter; 15 | nand?: BasicFilter; 16 | } 17 | 18 | interface BasicFilter { 19 | dedicated?: 1; 20 | secure?: 1; 21 | gamedir?: string; 22 | map?: string; 23 | linux?: 1; 24 | password?: 0; 25 | empty?: 1; 26 | full?: 1; 27 | proxy?: 1; 28 | appid?: number; 29 | napp?: number; 30 | noplayers?: 1; 31 | white?: 1; 32 | gametype?: string[]; 33 | gamedata?: string[]; 34 | gamedataor?: string[]; 35 | name_match?: string; 36 | version_match?: string; 37 | collapse_addr_hash?: 1; 38 | gameaddr?: string; 39 | } 40 | -------------------------------------------------------------------------------- /src/promiseSocket.ts: -------------------------------------------------------------------------------- 1 | import { createSocket, Socket } from 'dgram'; 2 | 3 | export class PromiseSocket { 4 | private _socket: Socket; 5 | 6 | constructor(private _attempts: number, private _timeout: number | number[]) { 7 | if ( 8 | Array.isArray(this._timeout) && 9 | this._attempts !== this._timeout.length 10 | ) { 11 | throw new Error(`Number of attempts (${this._attempts}) does not match the length of the timeout array (${this._timeout.length})`); 12 | } 13 | this._socket = createSocket('udp4'); 14 | } 15 | 16 | public async send(buffer: Buffer, host: string, port: number): Promise { 17 | return new Promise(async (resolve, reject) => { 18 | for (let i = 0; i < this._attempts; i++) { 19 | let timeout: number; 20 | if (Array.isArray(this._timeout)) { 21 | timeout = this._timeout[i]; 22 | } else { 23 | timeout = this._timeout; 24 | } 25 | 26 | try { 27 | const messageBuffer = await this._socketSend(buffer, host, port, timeout); 28 | return resolve(messageBuffer); 29 | } catch (err) { 30 | if (i === this._attempts - 1) { 31 | return reject(err); 32 | } 33 | } 34 | } 35 | }); 36 | } 37 | 38 | public closeSocket() { 39 | this._socket.close(); 40 | } 41 | 42 | private _socketSend(buffer: Buffer, host: string, port: number, timeout: number): Promise { 43 | return new Promise((resolve, reject) => { 44 | this._socket.send(buffer, port, host, (err) => { 45 | if (err) return reject(typeof err == 'string' ? new Error(err) : err); 46 | 47 | const messageListener = (buffer: any) => { 48 | this._socket.removeListener('message', messageListener); 49 | this._socket.removeListener('error', errorListener); 50 | clearTimeout(timeoutFnc); 51 | return resolve(buffer); 52 | }; 53 | 54 | const errorListener = (err: Error) => { 55 | clearTimeout(timeoutFnc); 56 | return reject(err); 57 | }; 58 | 59 | const timeoutFnc = setTimeout(() => { 60 | this._socket.removeListener('message', messageListener); 61 | this._socket.removeListener('error', errorListener); 62 | return reject('Timeout reached. Possible reasons: You are being rate limited; Timeout too short; Wrong server host configured;'); 63 | }, timeout); 64 | 65 | this._socket.on('message', messageListener); 66 | this._socket.on('error', errorListener); 67 | }); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2017", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs", /* Specify what module code is generated. */ 28 | "rootDir": "src", /* Specify the root folder within your source files. */ 29 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | "outDir": "lib", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 75 | 76 | /* Type Checking */ 77 | "strict": true, /* Enable all strict type-checking options. */ 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | }, 101 | "include": [ 102 | "src" 103 | ] 104 | } 105 | --------------------------------------------------------------------------------