diff --git a/src/common/mojang/model/auth/Agent.ts b/src/common/mojang/model/auth/Agent.ts deleted file mode 100644 index 0a4ddb7..0000000 --- a/src/common/mojang/model/auth/Agent.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface Agent { - - name: 'Minecraft' - version: number - -} \ No newline at end of file diff --git a/src/common/mojang/model/auth/AuthPayload.ts b/src/common/mojang/model/auth/AuthPayload.ts deleted file mode 100644 index 76b012d..0000000 --- a/src/common/mojang/model/auth/AuthPayload.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Agent } from './Agent' - -export interface AuthPayload { - - agent: Agent - username: string - password: string - clientToken?: string - requestUser?: boolean - -} diff --git a/src/common/mojang/net/Protocol.ts b/src/common/mojang/net/Protocol.ts new file mode 100644 index 0000000..38375a1 --- /dev/null +++ b/src/common/mojang/net/Protocol.ts @@ -0,0 +1,157 @@ +/** + * Utility Class to construct a packet conforming to Minecraft's + * protocol. All data types are BE except VarInt and VarLong. + * + * @see https://wiki.vg/Protocol + */ +export class ServerBoundPacket { + + private buffer: number[] + + protected constructor() { + this.buffer = [] + } + + public static build(): ServerBoundPacket { + return new ServerBoundPacket() + } + + /** + * Packet is prefixed with its data length as a VarInt. + * + * @see https://wiki.vg/Protocol#Packet_format + */ + public toBuffer(): Buffer { + const finalizedPacket = new ServerBoundPacket() + finalizedPacket.writeVarInt(this.buffer.length) + finalizedPacket.writeBytes(...this.buffer) + + return Buffer.from(finalizedPacket.buffer) + } + + public writeBytes(...bytes: number[]): ServerBoundPacket { + this.buffer.push(...bytes) + return this + } + + /** + * @see https://wiki.vg/Protocol#VarInt_and_VarLong + */ + public writeVarInt(value: number): ServerBoundPacket { + do { + let temp = value & 0b01111111 + + value >>>= 7 + + if (value != 0) { + temp |= 0b10000000 + } + + this.writeBytes(temp) + } while (value != 0) + + return this + } + + /** + * Strings are prefixed with their length as a VarInt. + * + * @see https://wiki.vg/Protocol#Data_types + */ + public writeString(string: string): ServerBoundPacket { + this.writeVarInt(string.length) + for (let i=0; i 5) { + throw new Error('VarInt is too big') + } + } while ((read & 0b10000000) != 0) + + return result + } + + public readString(): string { + const length = this.readVarInt() + const data = this.readBytes(length) + + let value = '' + + for (let i=0; i>>= 7 + size++ + } while (value != 0) + + return size + } + +} \ No newline at end of file diff --git a/src/common/util/ServerStatusUtil.ts b/src/common/mojang/net/ServerStatusAPI.ts similarity index 61% rename from src/common/util/ServerStatusUtil.ts rename to src/common/mojang/net/ServerStatusAPI.ts index 526bfdd..063b250 100644 --- a/src/common/util/ServerStatusUtil.ts +++ b/src/common/mojang/net/ServerStatusAPI.ts @@ -1,6 +1,7 @@ /* eslint-disable no-control-regex */ import { connect } from 'net' import { LoggerUtil } from 'common/logging/loggerutil' +import { ServerBoundPacket, ClientBoundPacket, ProtocolUtils } from './Protocol' const logger = LoggerUtil.getLogger('ServerStatusUtil') @@ -30,161 +31,6 @@ export interface ServerStatus { } } -/** - * Utility Class to construct a packet conforming to Minecraft's - * protocol. All data types are BE except VarInt and VarLong. - * - * @see https://wiki.vg/Protocol - */ -class ServerBoundPacket { - - private buffer: number[] - - protected constructor() { - this.buffer = [] - } - - public static build(): ServerBoundPacket { - return new ServerBoundPacket() - } - - /** - * Packet is prefixed with its data length as a VarInt. - * - * @see https://wiki.vg/Protocol#Packet_format - */ - public toBuffer(): Buffer { - const finalizedPacket = new ServerBoundPacket() - finalizedPacket.writeVarInt(this.buffer.length) - finalizedPacket.writeBytes(...this.buffer) - - return Buffer.from(finalizedPacket.buffer) - } - - public writeBytes(...bytes: number[]): ServerBoundPacket { - this.buffer.push(...bytes) - return this - } - - /** - * @see https://wiki.vg/Protocol#VarInt_and_VarLong - */ - public writeVarInt(value: number): ServerBoundPacket { - do { - let temp = value & 0b01111111 - - value >>>= 7 - - if (value != 0) { - temp |= 0b10000000 - } - - this.writeBytes(temp) - } while (value != 0) - - return this - } - - /** - * Strings are prefixed with their length as a VarInt. - * - * @see https://wiki.vg/Protocol#Data_types - */ - public writeString(string: string): ServerBoundPacket { - this.writeVarInt(string.length) - for (let i=0; i 5) { - throw new Error('VarInt is too big') - } - } while ((read & 0b10000000) != 0) - - return result - } - - public readString(): string { - const length = this.readVarInt() - const data = this.readBytes(length) - - let value = '' - - for (let i=0; i>>= 7 - - size++ - } while (value != 0) - - return size -} - /** * Get the handshake packet. * @@ -217,7 +63,15 @@ function getRequestPacket(): Buffer { .toBuffer() } +/** + * Some servers do not return the same status object. Unify + * the response so that the caller need only worry about + * handling a single format. + * + * @param resp The servevr status response. + */ function unifyStatusResponse(resp: ServerStatus): ServerStatus { + // Some servers don't wrap their description in a text object. if(typeof resp.description === 'string') { resp.description = { text: resp.description @@ -261,7 +115,7 @@ export function getServerStatus(protocol: number, address: string, port = 25565) } // Size of packetLength VarInt is not included in the packetLength. - bytesLeft = packetLength + getVarIntSize(packetLength) + bytesLeft = packetLength + ProtocolUtils.getVarIntSize(packetLength) // Listener to keep reading until we have read all the bytes into the buffer. const packetReadListener = (nextData: Buffer, doAppend: boolean) => { diff --git a/src/common/mojang/model/auth/Session.ts b/src/common/mojang/rest/Auth.ts similarity index 55% rename from src/common/mojang/model/auth/Session.ts rename to src/common/mojang/rest/Auth.ts index 9423ea2..1358fe3 100644 --- a/src/common/mojang/model/auth/Session.ts +++ b/src/common/mojang/rest/Auth.ts @@ -1,3 +1,20 @@ +export interface Agent { + + name: 'Minecraft' + version: number + +} + +export interface AuthPayload { + + agent: Agent + username: string + password: string + clientToken?: string + requestUser?: boolean + +} + export interface Session { accessToken: string diff --git a/src/common/mojang/mojang.ts b/src/common/mojang/rest/MojangRestAPI.ts similarity index 71% rename from src/common/mojang/mojang.ts rename to src/common/mojang/rest/MojangRestAPI.ts index bdcb8d9..15d5b79 100644 --- a/src/common/mojang/mojang.ts +++ b/src/common/mojang/rest/MojangRestAPI.ts @@ -1,13 +1,11 @@ -import { LoggerUtil } from '../logging/loggerutil' -import { Agent } from './model/auth/Agent' -import { Status, StatusColor } from './model/internal/Status' +import { LoggerUtil } from '../../logging/loggerutil' +import { MojangStatus, MojangStatusColor } from './internal/MojangStatus' import got, { RequestError, HTTPError } from 'got' -import { Session } from './model/auth/Session' -import { AuthPayload } from './model/auth/AuthPayload' -import { MojangResponse, MojangErrorCode, decipherErrorCode, isInternalError, MojangErrorBody } from './model/internal/MojangResponse' +import { MojangResponse, MojangErrorCode, decipherErrorCode, isInternalError, MojangErrorBody } from './internal/MojangResponse' import { RestResponseStatus, handleGotError } from 'common/got/RestResponse' +import { Agent, AuthPayload, Session } from './Auth' -export class Mojang { +export class MojangRestAPI { private static readonly logger = LoggerUtil.getLogger('Mojang') @@ -17,12 +15,12 @@ export class Mojang { public static readonly STATUS_ENDPOINT = 'https://status.mojang.com' private static authClient = got.extend({ - prefixUrl: Mojang.AUTH_ENDPOINT, + prefixUrl: MojangRestAPI.AUTH_ENDPOINT, responseType: 'json', retry: 0 }) private static statusClient = got.extend({ - prefixUrl: Mojang.STATUS_ENDPOINT, + prefixUrl: MojangRestAPI.STATUS_ENDPOINT, responseType: 'json', retry: 0 }) @@ -32,40 +30,40 @@ export class Mojang { version: 1 } - protected static statuses: Status[] = [ + protected static statuses: MojangStatus[] = [ { service: 'sessionserver.mojang.com', - status: StatusColor.GREY, + status: MojangStatusColor.GREY, name: 'Multiplayer Session Service', essential: true }, { service: 'authserver.mojang.com', - status: StatusColor.GREY, + status: MojangStatusColor.GREY, name: 'Authentication Service', essential: true }, { service: 'textures.minecraft.net', - status: StatusColor.GREY, + status: MojangStatusColor.GREY, name: 'Minecraft Skins', essential: false }, { service: 'api.mojang.com', - status: StatusColor.GREY, + status: MojangStatusColor.GREY, name: 'Public API', essential: false }, { service: 'minecraft.net', - status: StatusColor.GREY, + status: MojangStatusColor.GREY, name: 'Minecraft.net', essential: false }, { service: 'account.mojang.com', - status: StatusColor.GREY, + status: MojangStatusColor.GREY, name: 'Mojang Accounts Website', essential: false } @@ -78,13 +76,13 @@ export class Mojang { */ public static statusToHex(status: string): string { switch(status.toLowerCase()){ - case StatusColor.GREEN: + case MojangStatusColor.GREEN: return '#a5c325' - case StatusColor.YELLOW: + case MojangStatusColor.YELLOW: return '#eac918' - case StatusColor.RED: + case MojangStatusColor.RED: return '#c32625' - case StatusColor.GREY: + case MojangStatusColor.GREY: default: return '#848484' } @@ -92,7 +90,7 @@ export class Mojang { private static handleGotError(operation: string, error: RequestError, dataProvider: () => T): MojangResponse { - const response: MojangResponse = handleGotError(operation, error, Mojang.logger, dataProvider) + const response: MojangResponse = handleGotError(operation, error, MojangRestAPI.logger, dataProvider) if(error instanceof HTTPError) { response.mojangErrorCode = decipherErrorCode(error.response.body as MojangErrorBody) @@ -106,7 +104,7 @@ export class Mojang { private static expectSpecificSuccess(operation: string, expected: number, actual: number) { if(actual !== expected) { - Mojang.logger.warn(`${operation} expected ${expected} response, recieved ${actual}.`) + MojangRestAPI.logger.warn(`${operation} expected ${expected} response, recieved ${actual}.`) } } @@ -118,35 +116,35 @@ export class Mojang { * * @see http://wiki.vg/Mojang_API#API_Status */ - public static async status(): Promise>{ + public static async status(): Promise>{ try { - const res = await Mojang.statusClient.get<{[service: string]: StatusColor}[]>('check') + const res = await MojangRestAPI.statusClient.get<{[service: string]: MojangStatusColor}[]>('check') - Mojang.expectSpecificSuccess('Mojang Status', 200, res.statusCode) + MojangRestAPI.expectSpecificSuccess('Mojang Status', 200, res.statusCode) res.body.forEach(status => { const entry = Object.entries(status)[0] - for(let i=0; i { - for(let i=0; i { + for(let i=0; i> { try { @@ -183,15 +181,15 @@ export class Mojang { json.clientToken = clientToken } - const res = await Mojang.authClient.post('authenticate', { json, responseType: 'json' }) - Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode) + const res = await MojangRestAPI.authClient.post('authenticate', { json, responseType: 'json' }) + MojangRestAPI.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode) return { data: res.body, responseStatus: RestResponseStatus.SUCCESS } } catch(err) { - return Mojang.handleGotError('Mojang Authenticate', err, () => null) + return MojangRestAPI.handleGotError('Mojang Authenticate', err, () => null) } } @@ -214,8 +212,8 @@ export class Mojang { clientToken } - const res = await Mojang.authClient.post('validate', { json }) - Mojang.expectSpecificSuccess('Mojang Validate', 204, res.statusCode) + const res = await MojangRestAPI.authClient.post('validate', { json }) + MojangRestAPI.expectSpecificSuccess('Mojang Validate', 204, res.statusCode) return { data: res.statusCode === 204, @@ -229,7 +227,7 @@ export class Mojang { responseStatus: RestResponseStatus.SUCCESS } } - return Mojang.handleGotError('Mojang Validate', err, () => false) + return MojangRestAPI.handleGotError('Mojang Validate', err, () => false) } } @@ -252,8 +250,8 @@ export class Mojang { clientToken } - const res = await Mojang.authClient.post('invalidate', { json }) - Mojang.expectSpecificSuccess('Mojang Invalidate', 204, res.statusCode) + const res = await MojangRestAPI.authClient.post('invalidate', { json }) + MojangRestAPI.expectSpecificSuccess('Mojang Invalidate', 204, res.statusCode) return { data: undefined, @@ -261,7 +259,7 @@ export class Mojang { } } catch(err) { - return Mojang.handleGotError('Mojang Invalidate', err, () => undefined) + return MojangRestAPI.handleGotError('Mojang Invalidate', err, () => undefined) } } @@ -287,8 +285,8 @@ export class Mojang { requestUser } - const res = await Mojang.authClient.post('refresh', { json, responseType: 'json' }) - Mojang.expectSpecificSuccess('Mojang Refresh', 200, res.statusCode) + const res = await MojangRestAPI.authClient.post('refresh', { json, responseType: 'json' }) + MojangRestAPI.expectSpecificSuccess('Mojang Refresh', 200, res.statusCode) return { data: res.body, @@ -296,7 +294,7 @@ export class Mojang { } } catch(err) { - return Mojang.handleGotError('Mojang Refresh', err, () => null) + return MojangRestAPI.handleGotError('Mojang Refresh', err, () => null) } } diff --git a/src/common/mojang/model/internal/MojangResponse.ts b/src/common/mojang/rest/internal/MojangResponse.ts similarity index 100% rename from src/common/mojang/model/internal/MojangResponse.ts rename to src/common/mojang/rest/internal/MojangResponse.ts diff --git a/src/common/mojang/model/internal/Status.ts b/src/common/mojang/rest/internal/MojangStatus.ts similarity index 60% rename from src/common/mojang/model/internal/Status.ts rename to src/common/mojang/rest/internal/MojangStatus.ts index 9556693..ccbf9c9 100644 --- a/src/common/mojang/model/internal/Status.ts +++ b/src/common/mojang/rest/internal/MojangStatus.ts @@ -1,14 +1,14 @@ -export enum StatusColor { +export enum MojangStatusColor { RED = 'red', YELLOW = 'yellow', GREEN = 'green', GREY = 'grey' } -export interface Status { +export interface MojangStatus { service: string - status: StatusColor + status: MojangStatusColor name: string essential: boolean diff --git a/src/renderer/components/Application.tsx b/src/renderer/components/Application.tsx index 3a21350..09ef877 100644 --- a/src/renderer/components/Application.tsx +++ b/src/renderer/components/Application.tsx @@ -18,7 +18,7 @@ import Overlay from './overlay/Overlay' import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions' import { DistributionAPI } from 'common/distribution/distribution' -import { getServerStatus } from 'common/util/ServerStatusUtil' +import { getServerStatus } from 'common/mojang/net/ServerStatusAPI' import './Application.css' @@ -130,7 +130,7 @@ class Application extends React.Component { @@ -45,7 +45,7 @@ export default class Landing extends React.Component { } private loadMojangStatuses = async (): Promise => { - const response: MojangResponse = await Mojang.status() + const response: MojangResponse = await MojangRestAPI.status() if(response.responseStatus !== RestResponseStatus.SUCCESS) { Landing.logger.warn('Failed to retrieve Mojang Statuses.') @@ -56,7 +56,7 @@ export default class Landing extends React.Component { const statuses = response.data for(const status of statuses) { if(status.service === 'sessionserver.mojang.com' || status.service === 'minecraft.net') { - status.status = StatusColor.GREEN + status.status = MojangStatusColor.GREEN } } @@ -71,27 +71,27 @@ export default class Landing extends React.Component { const essential = this.state.mojangStatuses.filter(s => s.essential) if(this.state.mojangStatuses.length === 0) { - return Mojang.statusToHex(StatusColor.GREY) + return MojangRestAPI.statusToHex(MojangStatusColor.GREY) } // If any essential are red, it's red. - if(essential.filter(s => s.status === StatusColor.RED).length > 0) { - return Mojang.statusToHex(StatusColor.RED) + if(essential.filter(s => s.status === MojangStatusColor.RED).length > 0) { + return MojangRestAPI.statusToHex(MojangStatusColor.RED) } // If any essential are yellow, it's yellow. - if(essential.filter(s => s.status === StatusColor.YELLOW).length > 0) { - return Mojang.statusToHex(StatusColor.YELLOW) + if(essential.filter(s => s.status === MojangStatusColor.YELLOW).length > 0) { + return MojangRestAPI.statusToHex(MojangStatusColor.YELLOW) } // If any non-essential are not green, return yellow. - if(this.state.mojangStatuses.filter(s => s.status !== StatusColor.GREEN && s.status !== StatusColor.GREY).length > 0) { - return Mojang.statusToHex(StatusColor.YELLOW) + if(this.state.mojangStatuses.filter(s => s.status !== MojangStatusColor.GREEN && s.status !== MojangStatusColor.GREY).length > 0) { + return MojangRestAPI.statusToHex(MojangStatusColor.YELLOW) } // if all are grey, return grey. - if(this.state.mojangStatuses.filter(s => s.status === StatusColor.GREY).length === this.state.mojangStatuses.length) { - return Mojang.statusToHex(StatusColor.GREY) + if(this.state.mojangStatuses.filter(s => s.status === MojangStatusColor.GREY).length === this.state.mojangStatuses.length) { + return MojangRestAPI.statusToHex(MojangStatusColor.GREY) } - return Mojang.statusToHex(StatusColor.GREEN) + return MojangRestAPI.statusToHex(MojangStatusColor.GREEN) } private getMojangStatusesAsJSX = (essential: boolean): JSX.Element[] => { @@ -101,7 +101,7 @@ export default class Landing extends React.Component { statuses.push( <>
- + {status.name}
diff --git a/test/mojang/mojangTest.ts b/test/mojang/mojangTest.ts index d0d9875..dc38963 100644 --- a/test/mojang/mojangTest.ts +++ b/test/mojang/mojangTest.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Mojang } from 'common/mojang/mojang' +import { MojangRestAPI } from 'common/mojang/rest/MojangRestAPI' import { expect } from 'chai' import nock from 'nock' -import { Session } from 'common/mojang/model/auth/Session' -import { MojangErrorCode, MojangResponse } from 'common/mojang/model/internal/MojangResponse' +import { Session } from 'common/mojang/rest/Auth' +import { MojangErrorCode, MojangResponse } from 'common/mojang/rest/internal/MojangResponse' import { RestResponseStatus, RestResponse } from 'common/got/RestResponse' function assertResponse(res: RestResponse) { @@ -39,13 +39,13 @@ describe('Mojang Errors', () => { it('Status (Offline)', async () => { - const defStatusHack = Mojang['statuses'] + const defStatusHack = MojangRestAPI['statuses'] - nock(Mojang.STATUS_ENDPOINT) + nock(MojangRestAPI.STATUS_ENDPOINT) .get('/check') .reply(500, 'Service temprarily offline.') - const res = await Mojang.status() + const res = await MojangRestAPI.status() expectFailure(res) expect(res.data).to.be.an('array') expect(res.data).to.deep.equal(defStatusHack) @@ -54,7 +54,7 @@ describe('Mojang Errors', () => { it('Authenticate (Invalid Credentials)', async () => { - nock(Mojang.AUTH_ENDPOINT) + nock(MojangRestAPI.AUTH_ENDPOINT) .post('/authenticate') // eslint-disable-next-line @typescript-eslint/no-unused-vars .reply(403, (uri, requestBody: unknown): { error: string, errorMessage: string } => { @@ -64,7 +64,7 @@ describe('Mojang Errors', () => { } }) - const res = await Mojang.authenticate('user', 'pass', 'xxx', true) + const res = await MojangRestAPI.authenticate('user', 'pass', 'xxx', true) expectMojangResponse(res, MojangErrorCode.ERROR_INVALID_CREDENTIALS) expect(res.data).to.be.a('null') expect(res.error).to.not.be.a('null') @@ -76,13 +76,13 @@ describe('Mojang Status', () => { it('Status (Online)', async () => { - const defStatusHack = Mojang['statuses'] + const defStatusHack = MojangRestAPI['statuses'] - nock(Mojang.STATUS_ENDPOINT) + nock(MojangRestAPI.STATUS_ENDPOINT) .get('/check') .reply(200, defStatusHack) - const res = await Mojang.status() + const res = await MojangRestAPI.status() expectSuccess(res) expect(res.data).to.be.an('array') expect(res.data).to.deep.equal(defStatusHack) @@ -95,7 +95,7 @@ describe('Mojang Auth', () => { it('Authenticate', async () => { - nock(Mojang.AUTH_ENDPOINT) + nock(MojangRestAPI.AUTH_ENDPOINT) .post('/authenticate') .reply(200, (uri, requestBody: any): Session => { const mockResponse: Session = { @@ -117,7 +117,7 @@ describe('Mojang Auth', () => { return mockResponse }) - const res = await Mojang.authenticate('user', 'pass', 'xxx', true) + const res = await MojangRestAPI.authenticate('user', 'pass', 'xxx', true) expectSuccess(res) expect(res.data!.clientToken).to.equal('xxx') expect(res.data).to.have.property('user') @@ -126,7 +126,7 @@ describe('Mojang Auth', () => { it('Validate', async () => { - nock(Mojang.AUTH_ENDPOINT) + nock(MojangRestAPI.AUTH_ENDPOINT) .post('/validate') .times(2) .reply((uri, requestBody: any) => { @@ -135,13 +135,13 @@ describe('Mojang Auth', () => { ] }) - const res = await Mojang.validate('abc', 'def') + const res = await MojangRestAPI.validate('abc', 'def') expectSuccess(res) expect(res.data).to.be.a('boolean') expect(res.data).to.equal(true) - const res2 = await Mojang.validate('def', 'def') + const res2 = await MojangRestAPI.validate('def', 'def') expectSuccess(res2) expect(res2.data).to.be.a('boolean') @@ -151,11 +151,11 @@ describe('Mojang Auth', () => { it('Invalidate', async () => { - nock(Mojang.AUTH_ENDPOINT) + nock(MojangRestAPI.AUTH_ENDPOINT) .post('/invalidate') .reply(204) - const res = await Mojang.invalidate('adc', 'def') + const res = await MojangRestAPI.invalidate('adc', 'def') expectSuccess(res) @@ -163,7 +163,7 @@ describe('Mojang Auth', () => { it('Refresh', async () => { - nock(Mojang.AUTH_ENDPOINT) + nock(MojangRestAPI.AUTH_ENDPOINT) .post('/refresh') .reply(200, (uri, requestBody: any): Session => { const mockResponse: Session = { @@ -185,7 +185,7 @@ describe('Mojang Auth', () => { return mockResponse }) - const res = await Mojang.refresh('gfd', 'xxx', true) + const res = await MojangRestAPI.refresh('gfd', 'xxx', true) expectSuccess(res) expect(res.data!.clientToken).to.equal('xxx') expect(res.data).to.have.property('user')