Pull out common got error handling for generic use. Initial distribution loading (no application state storage yet).
This commit is contained in:
parent
15fd2c842a
commit
bc43d842e3
654
package-lock.json
generated
654
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@ -31,13 +31,13 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.4.16",
|
"adm-zip": "^0.4.16",
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
"discord-rpc": "^3.1.1",
|
"discord-rpc": "^3.1.3",
|
||||||
"electron-updater": "^4.3.1",
|
"electron-updater": "^4.3.4",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^9.0.1",
|
||||||
"github-syntax-dark": "^0.5.0",
|
"github-syntax-dark": "^0.5.0",
|
||||||
"got": "^11.5.0",
|
"got": "^11.5.2",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.5.1",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.20",
|
||||||
"moment": "^2.27.0",
|
"moment": "^2.27.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
@ -50,46 +50,46 @@
|
|||||||
"@babel/preset-react": "^7.10.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"@types/adm-zip": "^0.4.33",
|
"@types/adm-zip": "^0.4.33",
|
||||||
"@types/async": "^3.2.3",
|
"@types/async": "^3.2.3",
|
||||||
"@types/chai": "^4.2.11",
|
"@types/chai": "^4.2.12",
|
||||||
"@types/discord-rpc": "^3.0.4",
|
"@types/discord-rpc": "^3.0.4",
|
||||||
"@types/fs-extra": "^9.0.1",
|
"@types/fs-extra": "^9.0.1",
|
||||||
"@types/jquery": "^3.5.0",
|
"@types/jquery": "^3.5.1",
|
||||||
"@types/lodash": "^4.14.157",
|
"@types/lodash": "^4.14.160",
|
||||||
"@types/mocha": "^8.0.0",
|
"@types/mocha": "^8.0.3",
|
||||||
"@types/node": "^12.12.50",
|
"@types/node": "^12.12.54",
|
||||||
"@types/react": "^16.9.43",
|
"@types/react": "^16.9.46",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@types/react-redux": "^7.1.9",
|
"@types/react-redux": "^7.1.9",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/request": "^2.48.5",
|
"@types/request": "^2.48.5",
|
||||||
"@types/tar-fs": "^2.0.0",
|
"@types/tar-fs": "^2.0.0",
|
||||||
"@types/triple-beam": "^1.3.1",
|
"@types/triple-beam": "^1.3.2",
|
||||||
"@types/winreg": "^1.2.30",
|
"@types/winreg": "^1.2.30",
|
||||||
"@typescript-eslint/eslint-plugin": "^3.6.1",
|
"@typescript-eslint/eslint-plugin": "^3.10.0",
|
||||||
"@typescript-eslint/parser": "^3.6.1",
|
"@typescript-eslint/parser": "^3.10.0",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"electron": "^9.1.0",
|
"electron": "^9.2.1",
|
||||||
"electron-builder": "^22.7.0",
|
"electron-builder": "^22.8.0",
|
||||||
"electron-devtools-installer": "^3.1.0",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-webpack": "^2.8.2",
|
"electron-webpack": "^2.8.2",
|
||||||
"electron-webpack-ts": "^4.0.1",
|
"electron-webpack-ts": "^4.0.1",
|
||||||
"eslint": "^7.4.0",
|
"eslint": "^7.7.0",
|
||||||
"eslint-plugin-react": "^7.20.3",
|
"eslint-plugin-react": "^7.20.6",
|
||||||
"helios-distribution-types": "1.0.0-pre.1",
|
"helios-distribution-types": "1.0.0-pre.1",
|
||||||
"mocha": "^8.0.1",
|
"mocha": "^8.1.1",
|
||||||
"nock": "^13.0.2",
|
"nock": "^13.0.4",
|
||||||
"react": "^16.13.0",
|
"react": "^16.13.0",
|
||||||
"react-dom": "^16.13.0",
|
"react-dom": "^16.13.0",
|
||||||
"react-hot-loader": "^4.12.21",
|
"react-hot-loader": "^4.12.21",
|
||||||
"react-redux": "^7.2.0",
|
"react-redux": "^7.2.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"tsconfig-paths": "^3.9.0",
|
"tsconfig-paths": "^3.9.0",
|
||||||
"typescript": "^3.9.6",
|
"typescript": "^3.9.7",
|
||||||
"webpack": "^4.43.0"
|
"webpack": "^4.44.1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
106
src/common/distribution/distribution.ts
Normal file
106
src/common/distribution/distribution.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { resolve } from 'path'
|
||||||
|
import { Distribution } from 'helios-distribution-types'
|
||||||
|
import got from 'got'
|
||||||
|
import { LoggerUtil } from 'common/logging/loggerutil'
|
||||||
|
import { RestResponse, handleGotError, RestResponseStatus } from 'common/got/RestResponse'
|
||||||
|
import { pathExists, readFile, writeFile } from 'fs-extra'
|
||||||
|
|
||||||
|
export class DistributionAPI {
|
||||||
|
|
||||||
|
private static readonly logger = LoggerUtil.getLogger('DistributionAPI')
|
||||||
|
|
||||||
|
private readonly REMOTE_URL = 'http://mc.westeroscraft.com/WesterosCraftLauncher/distribution.json'
|
||||||
|
|
||||||
|
private readonly DISTRO_FILE = 'distribution.json'
|
||||||
|
private readonly DISTRO_FILE_DEV = 'distribution_dev.json'
|
||||||
|
|
||||||
|
private readonly DEV_MODE = false // placeholder
|
||||||
|
|
||||||
|
private distroPath: string
|
||||||
|
private distroDevPath: string
|
||||||
|
|
||||||
|
private rawDistribution!: Distribution
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private launcherDirectory: string
|
||||||
|
) {
|
||||||
|
this.distroPath = resolve(launcherDirectory, this.DISTRO_FILE)
|
||||||
|
this.distroDevPath = resolve(launcherDirectory, this.DISTRO_FILE_DEV)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async testLoad(): Promise<Distribution> {
|
||||||
|
await this.loadDistribution()
|
||||||
|
return this.rawDistribution
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async loadDistribution(): Promise<void> {
|
||||||
|
|
||||||
|
let distro
|
||||||
|
|
||||||
|
if(!this.DEV_MODE) {
|
||||||
|
|
||||||
|
distro = (await this.pullRemote()).data
|
||||||
|
if(distro == null) {
|
||||||
|
distro = await this.pullLocal(false)
|
||||||
|
} else {
|
||||||
|
this.writeDistributionToDisk(distro)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
distro = await this.pullLocal(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if(distro == null) {
|
||||||
|
// TODO Bubble this up nicer
|
||||||
|
throw new Error('FATAL: Unable to load distribution from remote server or local disk.')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rawDistribution = distro
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async pullRemote(): Promise<RestResponse<Distribution | null>> {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const res = await got.get<Distribution>(this.REMOTE_URL, { responseType: 'json' })
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: res.body,
|
||||||
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
|
||||||
|
return handleGotError('Pull Remote', error, DistributionAPI.logger, () => null)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async writeDistributionToDisk(distribution: Distribution): Promise<void> {
|
||||||
|
await writeFile(this.distroPath, distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async pullLocal(dev: boolean): Promise<Distribution | null> {
|
||||||
|
return await this.readDistributionFromFile(!dev ? this.distroPath : this.distroDevPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected async readDistributionFromFile(path: string): Promise<Distribution | null> {
|
||||||
|
|
||||||
|
if(await pathExists(path)) {
|
||||||
|
const raw = await readFile(path, 'utf-8')
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw)
|
||||||
|
} catch(error) {
|
||||||
|
DistributionAPI.logger.error(`Malformed distribution file at ${path}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
DistributionAPI.logger.error(`No distribution file found at ${path}!`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
43
src/common/got/RestResponse.ts
Normal file
43
src/common/got/RestResponse.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { RequestError, HTTPError, TimeoutError, ParseError } from 'got'
|
||||||
|
import { Logger } from 'winston'
|
||||||
|
|
||||||
|
export enum RestResponseStatus {
|
||||||
|
|
||||||
|
SUCCESS,
|
||||||
|
ERROR
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RestResponse<T> {
|
||||||
|
|
||||||
|
data: T
|
||||||
|
responseStatus: RestResponseStatus
|
||||||
|
error?: RequestError
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleGotError<T>(operation: string, error: RequestError, logger: Logger, dataProvider: () => T): RestResponse<T> {
|
||||||
|
const response: RestResponse<T> = {
|
||||||
|
data: dataProvider(),
|
||||||
|
responseStatus: RestResponseStatus.ERROR,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
if(error instanceof HTTPError) {
|
||||||
|
logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error)
|
||||||
|
logger.debug('Response Details:')
|
||||||
|
logger.debug('Body:', error.response.body)
|
||||||
|
logger.debug('Headers:', error.response.headers)
|
||||||
|
} else if(Object.getPrototypeOf(error) instanceof RequestError) {
|
||||||
|
logger.error(`${operation} request recieved no response (${error.code}).`, error)
|
||||||
|
} else if(error instanceof TimeoutError) {
|
||||||
|
logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`)
|
||||||
|
} else if(error instanceof ParseError) {
|
||||||
|
logger.error(`${operation} request recieved unexepected body (Parse Error).`)
|
||||||
|
} else {
|
||||||
|
// CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError
|
||||||
|
logger.error(`Error during ${operation} request.`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
@ -1,8 +1,12 @@
|
|||||||
import { createLogger, format, transports, Logger } from 'winston'
|
import { createLogger, format, transports, Logger } from 'winston'
|
||||||
import { SPLAT } from 'triple-beam'
|
import { SPLAT as SPLAT_Symbol } from 'triple-beam'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import { inspect } from 'util'
|
import { inspect } from 'util'
|
||||||
|
|
||||||
|
// Workaround until fixed.
|
||||||
|
// https://github.com/winstonjs/logform/issues/111
|
||||||
|
const SPLAT = SPLAT_Symbol as unknown as string
|
||||||
|
|
||||||
export class LoggerUtil {
|
export class LoggerUtil {
|
||||||
|
|
||||||
public static getLogger(label: string): Logger {
|
public static getLogger(label: string): Logger {
|
||||||
|
87
src/common/mojang/model/internal/MojangResponse.ts
Normal file
87
src/common/mojang/model/internal/MojangResponse.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { RestResponse } from 'common/got/RestResponse'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://wiki.vg/Authentication#Errors
|
||||||
|
*/
|
||||||
|
export enum MojangErrorCode {
|
||||||
|
ERROR_METHOD_NOT_ALLOWED, // INTERNAL
|
||||||
|
ERROR_NOT_FOUND, // INTERNAL
|
||||||
|
ERROR_USER_MIGRATED,
|
||||||
|
ERROR_INVALID_CREDENTIALS,
|
||||||
|
ERROR_RATELIMIT,
|
||||||
|
ERROR_INVALID_TOKEN,
|
||||||
|
ERROR_ACCESS_TOKEN_HAS_PROFILE, // ??
|
||||||
|
ERROR_CREDENTIALS_ARE_NULL, // INTERNAL
|
||||||
|
ERROR_INVALID_SALT_VERSION, // ??
|
||||||
|
ERROR_UNSUPPORTED_MEDIA_TYPE, // INTERNAL
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MojangResponse<T> extends RestResponse<T> {
|
||||||
|
mojangErrorCode?: MojangErrorCode
|
||||||
|
isInternalError?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MojangErrorBody {
|
||||||
|
error: string
|
||||||
|
errorMessage: string
|
||||||
|
cause?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the error response code from the response body.
|
||||||
|
*
|
||||||
|
* @param body The mojang error body response.
|
||||||
|
*/
|
||||||
|
export function decipherErrorCode(body: MojangErrorBody): MojangErrorCode {
|
||||||
|
|
||||||
|
if(body.error === 'Method Not Allowed') {
|
||||||
|
return MojangErrorCode.ERROR_METHOD_NOT_ALLOWED
|
||||||
|
} else if(body.error === 'Not Found') {
|
||||||
|
return MojangErrorCode.ERROR_NOT_FOUND
|
||||||
|
} else if(body.error === 'Unsupported Media Type') {
|
||||||
|
return MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE
|
||||||
|
} else if(body.error === 'ForbiddenOperationException') {
|
||||||
|
|
||||||
|
if(body.cause && body.cause === 'UserMigratedException') {
|
||||||
|
return MojangErrorCode.ERROR_USER_MIGRATED
|
||||||
|
}
|
||||||
|
|
||||||
|
if(body.errorMessage === 'Invalid credentials. Invalid username or password.') {
|
||||||
|
return MojangErrorCode.ERROR_INVALID_CREDENTIALS
|
||||||
|
} else if(body.errorMessage === 'Invalid credentials.') {
|
||||||
|
return MojangErrorCode.ERROR_RATELIMIT
|
||||||
|
} else if(body.errorMessage === 'Invalid token.') {
|
||||||
|
return MojangErrorCode.ERROR_INVALID_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if(body.error === 'IllegalArgumentException') {
|
||||||
|
|
||||||
|
if(body.errorMessage === 'Access token already has a profile assigned.') {
|
||||||
|
return MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE
|
||||||
|
} else if(body.errorMessage === 'credentials is null') {
|
||||||
|
return MojangErrorCode.ERROR_CREDENTIALS_ARE_NULL
|
||||||
|
} else if(body.errorMessage === 'Invalid salt version') {
|
||||||
|
return MojangErrorCode.ERROR_INVALID_SALT_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return MojangErrorCode.UNKNOWN
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// These indicate problems with the code and not the data.
|
||||||
|
export function isInternalError(errorCode: MojangErrorCode): boolean {
|
||||||
|
switch(errorCode) {
|
||||||
|
case MojangErrorCode.ERROR_METHOD_NOT_ALLOWED: // We've sent the wrong method to an endpoint. (ex. GET to POST)
|
||||||
|
case MojangErrorCode.ERROR_NOT_FOUND: // Indicates endpoint has changed. (404)
|
||||||
|
case MojangErrorCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: // Selecting profiles isn't implemented yet. (Shouldnt happen)
|
||||||
|
case MojangErrorCode.ERROR_CREDENTIALS_ARE_NULL: // Username/password was not submitted. (UI should forbid this)
|
||||||
|
case MojangErrorCode.ERROR_INVALID_SALT_VERSION: // ??? (Shouldnt happen)
|
||||||
|
case MojangErrorCode.ERROR_UNSUPPORTED_MEDIA_TYPE: // Data was not submitted as application/json
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -1,87 +0,0 @@
|
|||||||
import { RequestError } from 'got'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @see https://wiki.vg/Authentication#Errors
|
|
||||||
*/
|
|
||||||
export enum MojangResponseCode {
|
|
||||||
SUCCESS,
|
|
||||||
ERROR,
|
|
||||||
ERROR_METHOD_NOT_ALLOWED, // INTERNAL
|
|
||||||
ERROR_NOT_FOUND, // INTERNAL
|
|
||||||
ERROR_USER_MIGRATED,
|
|
||||||
ERROR_INVALID_CREDENTIALS,
|
|
||||||
ERROR_RATELIMIT,
|
|
||||||
ERROR_INVALID_TOKEN,
|
|
||||||
ERROR_ACCESS_TOKEN_HAS_PROFILE, // ??
|
|
||||||
ERROR_CREDENTIALS_ARE_NULL, // INTERNAL
|
|
||||||
ERROR_INVALID_SALT_VERSION, // ??
|
|
||||||
ERROR_UNSUPPORTED_MEDIA_TYPE // INTERNAL
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MojangResponse<T> {
|
|
||||||
|
|
||||||
data: T
|
|
||||||
responseCode: MojangResponseCode
|
|
||||||
error?: RequestError
|
|
||||||
isInternalError?: boolean
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MojangErrorBody {
|
|
||||||
error: string
|
|
||||||
errorMessage: string
|
|
||||||
cause?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deciperResponseCode(body: MojangErrorBody): MojangResponseCode {
|
|
||||||
|
|
||||||
if(body.error === 'Method Not Allowed') {
|
|
||||||
return MojangResponseCode.ERROR_METHOD_NOT_ALLOWED
|
|
||||||
} else if(body.error === 'Not Found') {
|
|
||||||
return MojangResponseCode.ERROR_NOT_FOUND
|
|
||||||
} else if(body.error === 'Unsupported Media Type') {
|
|
||||||
return MojangResponseCode.ERROR_UNSUPPORTED_MEDIA_TYPE
|
|
||||||
} else if(body.error === 'ForbiddenOperationException') {
|
|
||||||
|
|
||||||
if(body.cause && body.cause === 'UserMigratedException') {
|
|
||||||
return MojangResponseCode.ERROR_USER_MIGRATED
|
|
||||||
}
|
|
||||||
|
|
||||||
if(body.errorMessage === 'Invalid credentials. Invalid username or password.') {
|
|
||||||
return MojangResponseCode.ERROR_INVALID_CREDENTIALS
|
|
||||||
} else if(body.errorMessage === 'Invalid credentials.') {
|
|
||||||
return MojangResponseCode.ERROR_RATELIMIT
|
|
||||||
} else if(body.errorMessage === 'Invalid token.') {
|
|
||||||
return MojangResponseCode.ERROR_INVALID_TOKEN
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if(body.error === 'IllegalArgumentException') {
|
|
||||||
|
|
||||||
if(body.errorMessage === 'Access token already has a profile assigned.') {
|
|
||||||
return MojangResponseCode.ERROR_ACCESS_TOKEN_HAS_PROFILE
|
|
||||||
} else if(body.errorMessage === 'credentials is null') {
|
|
||||||
return MojangResponseCode.ERROR_CREDENTIALS_ARE_NULL
|
|
||||||
} else if(body.errorMessage === 'Invalid salt version') {
|
|
||||||
return MojangResponseCode.ERROR_INVALID_SALT_VERSION
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return MojangResponseCode.ERROR
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// These indicate problems with the code and not the data.
|
|
||||||
export function isInternalError(responseCode: MojangResponseCode): boolean {
|
|
||||||
switch(responseCode) {
|
|
||||||
case MojangResponseCode.ERROR_METHOD_NOT_ALLOWED: // We've sent the wrong method to an endpoint. (ex. GET to POST)
|
|
||||||
case MojangResponseCode.ERROR_NOT_FOUND: // Indicates endpoint has changed. (404)
|
|
||||||
case MojangResponseCode.ERROR_ACCESS_TOKEN_HAS_PROFILE: // Selecting profiles isn't implemented yet. (Shouldnt happen)
|
|
||||||
case MojangResponseCode.ERROR_CREDENTIALS_ARE_NULL: // Username/password was not submitted. (UI should forbid this)
|
|
||||||
case MojangResponseCode.ERROR_INVALID_SALT_VERSION: // ??? (Shouldnt happen)
|
|
||||||
case MojangResponseCode.ERROR_UNSUPPORTED_MEDIA_TYPE: // Data was not submitted as application/json
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,10 +1,11 @@
|
|||||||
import { LoggerUtil } from '../logging/loggerutil'
|
import { LoggerUtil } from '../logging/loggerutil'
|
||||||
import { Agent } from './model/auth/Agent'
|
import { Agent } from './model/auth/Agent'
|
||||||
import { Status, StatusColor } from './model/internal/Status'
|
import { Status, StatusColor } from './model/internal/Status'
|
||||||
import got, { RequestError, HTTPError, TimeoutError, ParseError } from 'got'
|
import got, { RequestError, HTTPError } from 'got'
|
||||||
import { Session } from './model/auth/Session'
|
import { Session } from './model/auth/Session'
|
||||||
import { AuthPayload } from './model/auth/AuthPayload'
|
import { AuthPayload } from './model/auth/AuthPayload'
|
||||||
import { MojangResponse, MojangResponseCode, deciperResponseCode, isInternalError, MojangErrorBody } from './model/internal/Response'
|
import { MojangResponse, MojangErrorCode, decipherErrorCode, isInternalError, MojangErrorBody } from './model/internal/MojangResponse'
|
||||||
|
import { RestResponseStatus, handleGotError } from 'common/got/RestResponse'
|
||||||
|
|
||||||
export class Mojang {
|
export class Mojang {
|
||||||
|
|
||||||
@ -90,30 +91,15 @@ export class Mojang {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static handleGotError<T>(operation: string, error: RequestError, dataProvider: () => T): MojangResponse<T> {
|
private static handleGotError<T>(operation: string, error: RequestError, dataProvider: () => T): MojangResponse<T> {
|
||||||
const response: MojangResponse<T> = {
|
|
||||||
data: dataProvider(),
|
const response: MojangResponse<T> = handleGotError(operation, error, Mojang.logger, dataProvider)
|
||||||
responseCode: MojangResponseCode.ERROR,
|
|
||||||
error
|
|
||||||
}
|
|
||||||
|
|
||||||
if(error instanceof HTTPError) {
|
if(error instanceof HTTPError) {
|
||||||
response.responseCode = deciperResponseCode(error.response.body as MojangErrorBody)
|
response.mojangErrorCode = decipherErrorCode(error.response.body as MojangErrorBody)
|
||||||
Mojang.logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error)
|
|
||||||
Mojang.logger.debug('Response Details:')
|
|
||||||
Mojang.logger.debug('Body:', error.response.body)
|
|
||||||
Mojang.logger.debug('Headers:', error.response.headers)
|
|
||||||
} else if(Object.getPrototypeOf(error) instanceof RequestError) {
|
|
||||||
Mojang.logger.error(`${operation} request recieved no response (${error.code}).`, error)
|
|
||||||
} else if(error instanceof TimeoutError) {
|
|
||||||
Mojang.logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`)
|
|
||||||
} else if(error instanceof ParseError) {
|
|
||||||
Mojang.logger.error(`${operation} request recieved unexepected body (Parse Error).`)
|
|
||||||
} else {
|
} else {
|
||||||
// CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError
|
response.mojangErrorCode = MojangErrorCode.UNKNOWN
|
||||||
Mojang.logger.error(`Error during ${operation} request.`, error)
|
|
||||||
}
|
}
|
||||||
|
response.isInternalError = isInternalError(response.mojangErrorCode)
|
||||||
response.isInternalError = isInternalError(response.responseCode)
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
@ -151,7 +137,7 @@ export class Mojang {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: Mojang.statuses,
|
data: Mojang.statuses,
|
||||||
responseCode: MojangResponseCode.SUCCESS
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
@ -201,7 +187,7 @@ export class Mojang {
|
|||||||
Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode)
|
Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode)
|
||||||
return {
|
return {
|
||||||
data: res.body,
|
data: res.body,
|
||||||
responseCode: MojangResponseCode.SUCCESS
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
@ -233,14 +219,14 @@ export class Mojang {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: res.statusCode === 204,
|
data: res.statusCode === 204,
|
||||||
responseCode: MojangResponseCode.SUCCESS
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
if(err instanceof HTTPError && err.response.statusCode === 403) {
|
if(err instanceof HTTPError && err.response.statusCode === 403) {
|
||||||
return {
|
return {
|
||||||
data: false,
|
data: false,
|
||||||
responseCode: MojangResponseCode.SUCCESS
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Mojang.handleGotError('Mojang Validate', err, () => false)
|
return Mojang.handleGotError('Mojang Validate', err, () => false)
|
||||||
@ -271,7 +257,7 @@ export class Mojang {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
responseCode: MojangResponseCode.SUCCESS
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
@ -306,7 +292,7 @@ export class Mojang {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
data: res.body,
|
data: res.body,
|
||||||
responseCode: MojangResponseCode.SUCCESS
|
responseStatus: RestResponseStatus.SUCCESS
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
|
@ -113,7 +113,9 @@ async function createWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '..', 'out', 'preloader.js'),
|
preload: join(__dirname, '..', 'out', 'preloader.js'),
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: false
|
contextIsolation: false,
|
||||||
|
enableRemoteModule: true,
|
||||||
|
worldSafeExecuteJavaScript: true
|
||||||
},
|
},
|
||||||
backgroundColor: '#171614'
|
backgroundColor: '#171614'
|
||||||
})
|
})
|
||||||
|
@ -17,6 +17,8 @@ import { join } from 'path'
|
|||||||
import Overlay from './overlay/Overlay'
|
import Overlay from './overlay/Overlay'
|
||||||
import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions'
|
import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions'
|
||||||
|
|
||||||
|
import { DistributionAPI } from 'common/distribution/distribution'
|
||||||
|
|
||||||
import './Application.css'
|
import './Application.css'
|
||||||
|
|
||||||
declare const __static: string
|
declare const __static: string
|
||||||
@ -120,9 +122,14 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
//this.props.setView(View.WELCOME)
|
//this.props.setView(View.WELCOME)
|
||||||
this.props.pushGenericOverlay({
|
this.props.pushGenericOverlay({
|
||||||
title: 'Test Title',
|
title: 'Load Distribution',
|
||||||
description: 'Test Description',
|
description: 'This is a test. Will load the distribution.',
|
||||||
dismissible: true
|
dismissible: false,
|
||||||
|
acknowledgeCallback: async () => {
|
||||||
|
const distro = new DistributionAPI('C:\\Users\\user\\AppData\\Roaming\\Helios Launcher')
|
||||||
|
const x = await distro.testLoad()
|
||||||
|
console.log(x)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.props.pushGenericOverlay({
|
this.props.pushGenericOverlay({
|
||||||
title: 'Test Title 2',
|
title: 'Test Title 2',
|
||||||
|
@ -10,8 +10,8 @@ export interface GenericOverlayProps {
|
|||||||
acknowledgeText?: string
|
acknowledgeText?: string
|
||||||
dismissText?: string
|
dismissText?: string
|
||||||
dismissible: boolean
|
dismissible: boolean
|
||||||
acknowledgeCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
acknowledgeCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>
|
||||||
dismissCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
dismissCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatch = {
|
const mapDispatch = {
|
||||||
@ -30,16 +30,16 @@ class GenericOverlay extends React.Component<InternalGenericOverlayProps> {
|
|||||||
return this.props.dismissText || 'Dismiss'
|
return this.props.dismissText || 'Dismiss'
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAcknowledgeClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
private onAcknowledgeClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> => {
|
||||||
if(this.props.acknowledgeCallback) {
|
if(this.props.acknowledgeCallback) {
|
||||||
this.props.acknowledgeCallback(event)
|
await this.props.acknowledgeCallback(event)
|
||||||
}
|
}
|
||||||
this.props.popOverlayContent()
|
this.props.popOverlayContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private onDismissClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
|
private onDismissClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> => {
|
||||||
if(this.props.dismissCallback) {
|
if(this.props.dismissCallback) {
|
||||||
this.props.dismissCallback(event)
|
await this.props.dismissCallback(event)
|
||||||
}
|
}
|
||||||
this.props.popOverlayContent()
|
this.props.popOverlayContent()
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,33 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { Mojang } from 'common/mojang/mojang'
|
import { Mojang } from 'common/mojang/mojang'
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import nock from 'nock'
|
import nock from 'nock'
|
||||||
import { Session } from 'common/mojang/model/auth/Session'
|
import { Session } from 'common/mojang/model/auth/Session'
|
||||||
import { MojangResponseCode } from 'common/mojang/model/internal/Response'
|
import { MojangErrorCode, MojangResponse } from 'common/mojang/model/internal/MojangResponse'
|
||||||
|
import { RestResponseStatus, RestResponse } from 'common/got/RestResponse'
|
||||||
|
|
||||||
function expectMojangResponse(res: any, responseCode: MojangResponseCode, negate = false) {
|
function assertResponse(res: RestResponse<unknown>) {
|
||||||
expect(res).to.not.be.an('error')
|
expect(res).to.not.be.an('error')
|
||||||
expect(res).to.be.an('object')
|
expect(res).to.be.an('object')
|
||||||
expect(res).to.have.property('responseCode')
|
}
|
||||||
|
|
||||||
|
function expectSuccess(res: RestResponse<unknown>) {
|
||||||
|
assertResponse(res)
|
||||||
|
expect(res).to.have.property('responseStatus')
|
||||||
|
expect(res.responseStatus).to.equal(RestResponseStatus.SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectFailure(res: RestResponse<unknown>) {
|
||||||
|
expect(res.responseStatus).to.not.equal(RestResponseStatus.SUCCESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectMojangResponse(res: MojangResponse<unknown>, responseCode: MojangErrorCode, negate = false) {
|
||||||
|
assertResponse(res)
|
||||||
|
expect(res).to.have.property('mojangErrorCode')
|
||||||
if(!negate) {
|
if(!negate) {
|
||||||
expect(res.responseCode).to.equal(responseCode)
|
expect(res.mojangErrorCode).to.equal(responseCode)
|
||||||
} else {
|
} else {
|
||||||
expect(res.responseCode).to.not.equal(responseCode)
|
expect(res.mojangErrorCode).to.not.equal(responseCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +46,7 @@ describe('Mojang Errors', () => {
|
|||||||
.reply(500, 'Service temprarily offline.')
|
.reply(500, 'Service temprarily offline.')
|
||||||
|
|
||||||
const res = await Mojang.status()
|
const res = await Mojang.status()
|
||||||
expectMojangResponse(res, MojangResponseCode.SUCCESS, true)
|
expectFailure(res)
|
||||||
expect(res.data).to.be.an('array')
|
expect(res.data).to.be.an('array')
|
||||||
expect(res.data).to.deep.equal(defStatusHack)
|
expect(res.data).to.deep.equal(defStatusHack)
|
||||||
|
|
||||||
@ -40,7 +56,8 @@ describe('Mojang Errors', () => {
|
|||||||
|
|
||||||
nock(Mojang.AUTH_ENDPOINT)
|
nock(Mojang.AUTH_ENDPOINT)
|
||||||
.post('/authenticate')
|
.post('/authenticate')
|
||||||
.reply(403, (uri, requestBody: any): { error: string, errorMessage: string } => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
.reply(403, (uri, requestBody: unknown): { error: string, errorMessage: string } => {
|
||||||
return {
|
return {
|
||||||
error: 'ForbiddenOperationException',
|
error: 'ForbiddenOperationException',
|
||||||
errorMessage: 'Invalid credentials. Invalid username or password.'
|
errorMessage: 'Invalid credentials. Invalid username or password.'
|
||||||
@ -48,7 +65,7 @@ describe('Mojang Errors', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
||||||
expectMojangResponse(res, MojangResponseCode.ERROR_INVALID_CREDENTIALS)
|
expectMojangResponse(res, MojangErrorCode.ERROR_INVALID_CREDENTIALS)
|
||||||
expect(res.data).to.be.a('null')
|
expect(res.data).to.be.a('null')
|
||||||
expect(res.error).to.not.be.a('null')
|
expect(res.error).to.not.be.a('null')
|
||||||
|
|
||||||
@ -66,7 +83,7 @@ describe('Mojang Status', () => {
|
|||||||
.reply(200, defStatusHack)
|
.reply(200, defStatusHack)
|
||||||
|
|
||||||
const res = await Mojang.status()
|
const res = await Mojang.status()
|
||||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
expectSuccess(res)
|
||||||
expect(res.data).to.be.an('array')
|
expect(res.data).to.be.an('array')
|
||||||
expect(res.data).to.deep.equal(defStatusHack)
|
expect(res.data).to.deep.equal(defStatusHack)
|
||||||
|
|
||||||
@ -101,7 +118,7 @@ describe('Mojang Auth', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
||||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
expectSuccess(res)
|
||||||
expect(res.data!.clientToken).to.equal('xxx')
|
expect(res.data!.clientToken).to.equal('xxx')
|
||||||
expect(res.data).to.have.property('user')
|
expect(res.data).to.have.property('user')
|
||||||
|
|
||||||
@ -120,13 +137,13 @@ describe('Mojang Auth', () => {
|
|||||||
|
|
||||||
const res = await Mojang.validate('abc', 'def')
|
const res = await Mojang.validate('abc', 'def')
|
||||||
|
|
||||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
expectSuccess(res)
|
||||||
expect(res.data).to.be.a('boolean')
|
expect(res.data).to.be.a('boolean')
|
||||||
expect(res.data).to.equal(true)
|
expect(res.data).to.equal(true)
|
||||||
|
|
||||||
const res2 = await Mojang.validate('def', 'def')
|
const res2 = await Mojang.validate('def', 'def')
|
||||||
|
|
||||||
expectMojangResponse(res2, MojangResponseCode.SUCCESS)
|
expectSuccess(res2)
|
||||||
expect(res2.data).to.be.a('boolean')
|
expect(res2.data).to.be.a('boolean')
|
||||||
expect(res2.data).to.equal(false)
|
expect(res2.data).to.equal(false)
|
||||||
|
|
||||||
@ -140,7 +157,7 @@ describe('Mojang Auth', () => {
|
|||||||
|
|
||||||
const res = await Mojang.invalidate('adc', 'def')
|
const res = await Mojang.invalidate('adc', 'def')
|
||||||
|
|
||||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
expectSuccess(res)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -169,7 +186,7 @@ describe('Mojang Auth', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const res = await Mojang.refresh('gfd', 'xxx', true)
|
const res = await Mojang.refresh('gfd', 'xxx', true)
|
||||||
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
expectSuccess(res)
|
||||||
expect(res.data!.clientToken).to.equal('xxx')
|
expect(res.data!.clientToken).to.equal('xxx')
|
||||||
expect(res.data).to.have.property('user')
|
expect(res.data).to.have.property('user')
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user