mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-22 11:42:14 -08:00
Added Axios + Logging & Testing Frameworks.
Rewrote mojang.ts to use axios. This included creating a more robust error handling system and response payload structure. Also included unit tests. Added axios (HTTP Library to replace request) Added winston (Logging Framework) Added mocha (Testing Framework) Added chai (assertion library) Added nock (mock server)
This commit is contained in:
parent
761a46060b
commit
9097bafb5d
856
package-lock.json
generated
856
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -22,7 +22,8 @@
|
|||||||
"dist:linux": "npm run dist -- LINUX",
|
"dist:linux": "npm run dist -- LINUX",
|
||||||
"lint": "eslint --ext=jsx,js,tsx,ts src",
|
"lint": "eslint --ext=jsx,js,tsx,ts src",
|
||||||
"dev": "electron-webpack dev",
|
"dev": "electron-webpack dev",
|
||||||
"compile": "electron-webpack"
|
"compile": "electron-webpack",
|
||||||
|
"test": "cross-env TS_NODE_PROJECT='./tsconfig.test.json' mocha -r ts-node/register test/**/*.ts"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.x.x"
|
"node": "12.x.x"
|
||||||
@ -30,29 +31,37 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.4.14",
|
"adm-zip": "^0.4.14",
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
|
"axios": "^0.19.2",
|
||||||
"discord-rpc": "3.1.0",
|
"discord-rpc": "3.1.0",
|
||||||
"electron-updater": "^4.2.4",
|
"electron-updater": "^4.2.4",
|
||||||
"fs-extra": "^9.0.0",
|
"fs-extra": "^9.0.0",
|
||||||
"github-syntax-dark": "^0.5.0",
|
"github-syntax-dark": "^0.5.0",
|
||||||
"jquery": "^3.4.1",
|
"jquery": "^3.5.0",
|
||||||
|
"moment": "^2.24.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"semver": "^7.1.3",
|
"semver": "^7.2.2",
|
||||||
"tar-fs": "^2.0.0",
|
"tar-fs": "^2.0.0",
|
||||||
"winreg": "^1.2.4"
|
"triple-beam": "^1.3.0",
|
||||||
|
"winreg": "^1.2.4",
|
||||||
|
"winston": "^3.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "^7.9.4",
|
"@babel/preset-react": "^7.9.4",
|
||||||
"@types/adm-zip": "^0.4.32",
|
"@types/adm-zip": "^0.4.33",
|
||||||
"@types/async": "^3.0.8",
|
"@types/async": "^3.0.8",
|
||||||
|
"@types/chai": "^4.2.11",
|
||||||
"@types/discord-rpc": "^3.0.2",
|
"@types/discord-rpc": "^3.0.2",
|
||||||
"@types/fs-extra": "^8.1.0",
|
"@types/fs-extra": "^8.1.0",
|
||||||
"@types/jquery": "^3.3.33",
|
"@types/jquery": "^3.3.33",
|
||||||
|
"@types/mocha": "^7.0.2",
|
||||||
"@types/node": "^12.12.29",
|
"@types/node": "^12.12.29",
|
||||||
"@types/react": "^16.9.23",
|
"@types/react": "^16.9.23",
|
||||||
"@types/react-dom": "^16.9.5",
|
"@types/react-dom": "^16.9.5",
|
||||||
"@types/request": "^2.48.4",
|
"@types/request": "^2.48.4",
|
||||||
"@types/tar-fs": "^1.16.2",
|
"@types/tar-fs": "^1.16.2",
|
||||||
|
"@types/triple-beam": "^1.3.0",
|
||||||
"@types/winreg": "^1.2.30",
|
"@types/winreg": "^1.2.30",
|
||||||
|
"chai": "^4.2.0",
|
||||||
"cross-env": "^7.0.2",
|
"cross-env": "^7.0.2",
|
||||||
"electron": "^8.2.1",
|
"electron": "^8.2.1",
|
||||||
"electron-builder": "^22.4.0",
|
"electron-builder": "^22.4.0",
|
||||||
@ -60,10 +69,13 @@
|
|||||||
"electron-webpack-ts": "^4.0.1",
|
"electron-webpack-ts": "^4.0.1",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
"helios-distribution-types": "1.0.0-pre.1",
|
"helios-distribution-types": "1.0.0-pre.1",
|
||||||
|
"mocha": "^7.1.1",
|
||||||
|
"nock": "^12.0.3",
|
||||||
"react": "^16.13.0",
|
"react": "^16.13.0",
|
||||||
"react-dom": "^16.13.0",
|
"react-dom": "^16.13.0",
|
||||||
"react-hot-loader": "^4.12.19",
|
"react-hot-loader": "^4.12.19",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
|
"ts-node": "^8.8.2",
|
||||||
"typescript": "^3.8.3",
|
"typescript": "^3.8.3",
|
||||||
"webpack": "^4.42.0"
|
"webpack": "^4.42.0"
|
||||||
},
|
},
|
||||||
|
40
src/main/logging/loggerutil.ts
Normal file
40
src/main/logging/loggerutil.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { createLogger, format, transports } from 'winston'
|
||||||
|
import { SPLAT } from 'triple-beam'
|
||||||
|
import moment from 'moment'
|
||||||
|
import { inspect } from 'util'
|
||||||
|
|
||||||
|
export class LoggerUtil {
|
||||||
|
|
||||||
|
public static getLogger(label: string) {
|
||||||
|
return createLogger({
|
||||||
|
format: format.combine(
|
||||||
|
format.label(),
|
||||||
|
format.colorize(),
|
||||||
|
format.label({ label }),
|
||||||
|
format.printf(info => {
|
||||||
|
if(info[SPLAT]) {
|
||||||
|
if(info[SPLAT].length === 1 && info[SPLAT][0] instanceof Error) {
|
||||||
|
const err = info[SPLAT][0] as Error
|
||||||
|
if(info.message.length > err.message.length && info.message.endsWith(err.message)) {
|
||||||
|
info.message = info.message.substring(0, info.message.length-err.message.length)
|
||||||
|
}
|
||||||
|
} else if(info[SPLAT].length > 0) {
|
||||||
|
info.message += ' ' + info[SPLAT].map((it: any) => {
|
||||||
|
if(typeof it === 'object' && it != null) {
|
||||||
|
return inspect(it, false, null, true)
|
||||||
|
}
|
||||||
|
return it
|
||||||
|
}).join(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `[${moment().format('YYYY-MM-DD hh:mm:ss').trim()}] [${info.level}] [${info.label}]: ${info.message}${info.stack ? `\n${info.stack}` : ''}`
|
||||||
|
})
|
||||||
|
),
|
||||||
|
level: 'debug',
|
||||||
|
transports: [
|
||||||
|
new transports.Console()
|
||||||
|
]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,15 +1,20 @@
|
|||||||
import request from 'request'
|
import { LoggerUtil } from '../logging/loggerutil'
|
||||||
import { LoggerUtil } from '../loggerutil'
|
|
||||||
import { Agent } from '../model/mojang/auth/Agent'
|
import { Agent } from '../model/mojang/auth/Agent'
|
||||||
import { AuthPayload } from '../model/mojang/auth/AuthPayload'
|
import { Status, StatusColor } from './type/Status'
|
||||||
|
import axios, { AxiosError } from 'axios'
|
||||||
import { Session } from '../model/mojang/auth/Session'
|
import { Session } from '../model/mojang/auth/Session'
|
||||||
import { Status } from './type/Status'
|
import { AuthPayload } from '../model/mojang/auth/AuthPayload'
|
||||||
|
import { MojangResponse, MojangResponseCode, deciperResponseCode, isInternalError } from './type/Response'
|
||||||
|
|
||||||
export class Mojang {
|
export class Mojang {
|
||||||
|
|
||||||
private static readonly logger = new LoggerUtil('%c[Mojang]', 'color: #a02d2a; font-weight: bold')
|
private static readonly logger = LoggerUtil.getLogger('Mojang')
|
||||||
|
|
||||||
|
private static readonly TIMEOUT = 2500
|
||||||
|
|
||||||
public static readonly AUTH_ENDPOINT = 'https://authserver.mojang.com'
|
public static readonly AUTH_ENDPOINT = 'https://authserver.mojang.com'
|
||||||
|
public static readonly STATUS_ENDPOINT = 'https://status.mojang.com/check'
|
||||||
|
|
||||||
public static readonly MINECRAFT_AGENT: Agent = {
|
public static readonly MINECRAFT_AGENT: Agent = {
|
||||||
name: 'Minecraft',
|
name: 'Minecraft',
|
||||||
version: 1
|
version: 1
|
||||||
@ -18,37 +23,37 @@ export class Mojang {
|
|||||||
protected static statuses: Status[] = [
|
protected static statuses: Status[] = [
|
||||||
{
|
{
|
||||||
service: 'sessionserver.mojang.com',
|
service: 'sessionserver.mojang.com',
|
||||||
status: 'grey',
|
status: StatusColor.GREY,
|
||||||
name: 'Multiplayer Session Service',
|
name: 'Multiplayer Session Service',
|
||||||
essential: true
|
essential: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: 'authserver.mojang.com',
|
service: 'authserver.mojang.com',
|
||||||
status: 'grey',
|
status: StatusColor.GREY,
|
||||||
name: 'Authentication Service',
|
name: 'Authentication Service',
|
||||||
essential: true
|
essential: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: 'textures.minecraft.net',
|
service: 'textures.minecraft.net',
|
||||||
status: 'grey',
|
status: StatusColor.GREY,
|
||||||
name: 'Minecraft Skins',
|
name: 'Minecraft Skins',
|
||||||
essential: false
|
essential: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: 'api.mojang.com',
|
service: 'api.mojang.com',
|
||||||
status: 'grey',
|
status: StatusColor.GREY,
|
||||||
name: 'Public API',
|
name: 'Public API',
|
||||||
essential: false
|
essential: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: 'minecraft.net',
|
service: 'minecraft.net',
|
||||||
status: 'grey',
|
status: StatusColor.GREY,
|
||||||
name: 'Minecraft.net',
|
name: 'Minecraft.net',
|
||||||
essential: false
|
essential: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
service: 'account.mojang.com',
|
service: 'account.mojang.com',
|
||||||
status: 'grey',
|
status: StatusColor.GREY,
|
||||||
name: 'Mojang Accounts Website',
|
name: 'Mojang Accounts Website',
|
||||||
essential: false
|
essential: false
|
||||||
}
|
}
|
||||||
@ -58,24 +63,50 @@ export class Mojang {
|
|||||||
* Converts a Mojang status color to a hex value. Valid statuses
|
* Converts a Mojang status color to a hex value. Valid statuses
|
||||||
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
|
* are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
|
||||||
* to our project which represents an unknown status.
|
* to our project which represents an unknown status.
|
||||||
*
|
|
||||||
* @param {string} status A valid status code.
|
|
||||||
* @returns {string} The hex color of the status code.
|
|
||||||
*/
|
*/
|
||||||
public static statusToHex(status: string){
|
public static statusToHex(status: string){
|
||||||
switch(status.toLowerCase()){
|
switch(status.toLowerCase()){
|
||||||
case 'green':
|
case StatusColor.GREEN:
|
||||||
return '#a5c325'
|
return '#a5c325'
|
||||||
case 'yellow':
|
case StatusColor.YELLOW:
|
||||||
return '#eac918'
|
return '#eac918'
|
||||||
case 'red':
|
case StatusColor.RED:
|
||||||
return '#c32625'
|
return '#c32625'
|
||||||
case 'grey':
|
case StatusColor.GREY:
|
||||||
default:
|
default:
|
||||||
return '#848484'
|
return '#848484'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static handleAxiosError<T>(operation: string, error: AxiosError, dataProvider: () => T): MojangResponse<T> {
|
||||||
|
const response: MojangResponse<T> = {
|
||||||
|
data: dataProvider(),
|
||||||
|
responseCode: MojangResponseCode.ERROR,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
if(error.response) {
|
||||||
|
response.responseCode = deciperResponseCode(error.response.data)
|
||||||
|
Mojang.logger.error(`Error during ${operation} request (HTTP Response ${error.response.status})`, error)
|
||||||
|
Mojang.logger.debug('Response Details:')
|
||||||
|
Mojang.logger.debug('Data:', error.response.data)
|
||||||
|
Mojang.logger.debug('Headers:', error.response.headers)
|
||||||
|
} else if(error.request) {
|
||||||
|
Mojang.logger.error(`${operation} request recieved no response.`, error)
|
||||||
|
} else {
|
||||||
|
Mojang.logger.error(`Error during ${operation} request.`, error)
|
||||||
|
}
|
||||||
|
response.isInternalError = isInternalError(response.responseCode)
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
private static expectSpecificSuccess(operation: string, expected: number, actual: number) {
|
||||||
|
if(actual !== expected) {
|
||||||
|
Mojang.logger.warn(`${operation} expected ${expected} response, recieved ${actual}.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the status of Mojang's services.
|
* Retrieves the status of Mojang's services.
|
||||||
* The response is condensed into a single object. Each service is
|
* The response is condensed into a single object. Each service is
|
||||||
@ -84,38 +115,38 @@ export class Mojang {
|
|||||||
*
|
*
|
||||||
* @see http://wiki.vg/Mojang_API#API_Status
|
* @see http://wiki.vg/Mojang_API#API_Status
|
||||||
*/
|
*/
|
||||||
public static status(): Promise<Status[]>{
|
public static async status(): Promise<MojangResponse<Status[]>>{
|
||||||
return new Promise((resolve, reject) => {
|
try {
|
||||||
request.get('https://status.mojang.com/check',
|
|
||||||
{
|
|
||||||
json: true,
|
|
||||||
timeout: 2500
|
|
||||||
},
|
|
||||||
function(error, response, body: {[service: string]: 'red' | 'yellow' | 'green'}[]){
|
|
||||||
|
|
||||||
if(error || response.statusCode !== 200){
|
const res = await axios.get<{[service: string]: StatusColor}[]>(Mojang.STATUS_ENDPOINT, { timeout: Mojang.TIMEOUT })
|
||||||
Mojang.logger.warn('Unable to retrieve Mojang status.')
|
|
||||||
Mojang.logger.debug('Error while retrieving Mojang statuses:', error)
|
Mojang.expectSpecificSuccess('Mojang Status', 200, res.status)
|
||||||
//reject(error || response.statusCode)
|
|
||||||
|
res.data.forEach(status => {
|
||||||
|
const entry = Object.entries(status)[0]
|
||||||
|
for(let i=0; i<Mojang.statuses.length; i++) {
|
||||||
|
if(Mojang.statuses[i].service === entry[0]) {
|
||||||
|
Mojang.statuses[i].status = entry[1]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: Mojang.statuses,
|
||||||
|
responseCode: MojangResponseCode.SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch(error) {
|
||||||
|
|
||||||
|
return Mojang.handleAxiosError('Mojang Status', error as AxiosError, () => {
|
||||||
for(let i=0; i<Mojang.statuses.length; i++){
|
for(let i=0; i<Mojang.statuses.length; i++){
|
||||||
Mojang.statuses[i].status = 'grey'
|
Mojang.statuses[i].status = StatusColor.GREY
|
||||||
}
|
|
||||||
resolve(Mojang.statuses)
|
|
||||||
} else {
|
|
||||||
for(let i=0; i<body.length; i++){
|
|
||||||
const key = Object.keys(body[i])[0]
|
|
||||||
inner:
|
|
||||||
for(let j=0; j<Mojang.statuses.length; j++){
|
|
||||||
if(Mojang.statuses[j].service === key) {
|
|
||||||
Mojang.statuses[j].status = body[i][key]
|
|
||||||
break inner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(Mojang.statuses)
|
|
||||||
}
|
}
|
||||||
|
return Mojang.statuses
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -129,43 +160,37 @@ export class Mojang {
|
|||||||
*
|
*
|
||||||
* @see http://wiki.vg/Authentication#Authenticate
|
* @see http://wiki.vg/Authentication#Authenticate
|
||||||
*/
|
*/
|
||||||
public static authenticate(
|
public static async authenticate(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
clientToken: string | null,
|
clientToken: string | null,
|
||||||
requestUser: boolean = true,
|
requestUser: boolean = true,
|
||||||
agent: Agent = Mojang.MINECRAFT_AGENT
|
agent: Agent = Mojang.MINECRAFT_AGENT
|
||||||
): Promise<Session> {
|
): Promise<MojangResponse<Session | null>> {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
const body: AuthPayload = {
|
try {
|
||||||
|
|
||||||
|
const data: AuthPayload = {
|
||||||
agent,
|
agent,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
requestUser
|
requestUser
|
||||||
}
|
}
|
||||||
if(clientToken != null){
|
if(clientToken != null){
|
||||||
body.clientToken = clientToken
|
data.clientToken = clientToken
|
||||||
}
|
}
|
||||||
|
|
||||||
request.post(Mojang.AUTH_ENDPOINT + '/authenticate',
|
const res = await axios.post<Session>(`${Mojang.AUTH_ENDPOINT}/authenticate`, data)
|
||||||
{
|
Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.status)
|
||||||
json: true,
|
return {
|
||||||
body
|
data: res.data,
|
||||||
},
|
responseCode: MojangResponseCode.SUCCESS
|
||||||
function(error, response, body){
|
|
||||||
if(error){
|
|
||||||
Mojang.logger.error('Error during authentication.', error)
|
|
||||||
reject(error)
|
|
||||||
} else {
|
|
||||||
if(response.statusCode === 200){
|
|
||||||
resolve(body)
|
|
||||||
} else {
|
|
||||||
reject(body || {code: 'ENOTFOUND'})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
return Mojang.handleAxiosError('Mojang Authenticate', err, () => null)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -177,30 +202,33 @@ export class Mojang {
|
|||||||
*
|
*
|
||||||
* @see http://wiki.vg/Authentication#Validate
|
* @see http://wiki.vg/Authentication#Validate
|
||||||
*/
|
*/
|
||||||
public static validate(accessToken: string, clientToken: string): Promise<boolean> {
|
public static async validate(accessToken: string, clientToken: string): Promise<MojangResponse<boolean>> {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
request.post(Mojang.AUTH_ENDPOINT + '/validate',
|
try {
|
||||||
{
|
|
||||||
json: true,
|
const data = {
|
||||||
body: {
|
|
||||||
accessToken,
|
accessToken,
|
||||||
clientToken
|
clientToken
|
||||||
}
|
}
|
||||||
},
|
|
||||||
function(error, response, body){
|
const res = await axios.post(`${Mojang.AUTH_ENDPOINT}/validate`, data)
|
||||||
if(error){
|
Mojang.expectSpecificSuccess('Mojang Validate', 204, res.status)
|
||||||
Mojang.logger.error('Error during validation.', error)
|
|
||||||
reject(error)
|
return {
|
||||||
} else {
|
data: res.status === 204,
|
||||||
if(response.statusCode === 403){
|
responseCode: MojangResponseCode.SUCCESS
|
||||||
resolve(false)
|
}
|
||||||
} else {
|
|
||||||
// 204 if valid
|
} catch(err) {
|
||||||
resolve(true)
|
if(err.response && err.response.status === 403) {
|
||||||
|
return {
|
||||||
|
data: false,
|
||||||
|
responseCode: MojangResponseCode.SUCCESS
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
return Mojang.handleAxiosError('Mojang Validate', err, () => false)
|
||||||
})
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -212,29 +240,27 @@ export class Mojang {
|
|||||||
*
|
*
|
||||||
* @see http://wiki.vg/Authentication#Invalidate
|
* @see http://wiki.vg/Authentication#Invalidate
|
||||||
*/
|
*/
|
||||||
public static invalidate(accessToken: string, clientToken: string): Promise<void>{
|
public static async invalidate(accessToken: string, clientToken: string): Promise<MojangResponse<undefined>> {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
request.post(Mojang.AUTH_ENDPOINT + '/invalidate',
|
try {
|
||||||
{
|
|
||||||
json: true,
|
const data = {
|
||||||
body: {
|
|
||||||
accessToken,
|
accessToken,
|
||||||
clientToken
|
clientToken
|
||||||
}
|
}
|
||||||
},
|
|
||||||
function(error, response, body){
|
const res = await axios.post(`${Mojang.AUTH_ENDPOINT}/invalidate`, data)
|
||||||
if(error){
|
Mojang.expectSpecificSuccess('Mojang Invalidate', 204, res.status)
|
||||||
Mojang.logger.error('Error during invalidation.', error)
|
|
||||||
reject(error)
|
return {
|
||||||
} else {
|
data: undefined,
|
||||||
if(response.statusCode === 204){
|
responseCode: MojangResponseCode.SUCCESS
|
||||||
resolve()
|
|
||||||
} else {
|
|
||||||
reject(body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
return Mojang.handleAxiosError('Mojang Invalidate', err, () => undefined)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -248,30 +274,28 @@ export class Mojang {
|
|||||||
*
|
*
|
||||||
* @see http://wiki.vg/Authentication#Refresh
|
* @see http://wiki.vg/Authentication#Refresh
|
||||||
*/
|
*/
|
||||||
public static refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<Session> {
|
public static async refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<MojangResponse<Session | null>> {
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
request.post(Mojang.AUTH_ENDPOINT + '/refresh',
|
try {
|
||||||
{
|
|
||||||
json: true,
|
const data = {
|
||||||
body: {
|
|
||||||
accessToken,
|
accessToken,
|
||||||
clientToken,
|
clientToken,
|
||||||
requestUser
|
requestUser
|
||||||
}
|
}
|
||||||
},
|
|
||||||
function(error, response, body){
|
const res = await axios.post<Session>(`${Mojang.AUTH_ENDPOINT}/refresh`, data)
|
||||||
if(error){
|
Mojang.expectSpecificSuccess('Mojang Refresh', 200, res.status)
|
||||||
Mojang.logger.error('Error during refresh.', error)
|
|
||||||
reject(error)
|
return {
|
||||||
} else {
|
data: res.data,
|
||||||
if(response.statusCode === 200){
|
responseCode: MojangResponseCode.SUCCESS
|
||||||
resolve(body)
|
|
||||||
} else {
|
|
||||||
reject(body)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
return Mojang.handleAxiosError('Mojang Refresh', err, () => null)
|
||||||
}
|
}
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
87
src/main/mojang/type/Response.ts
Normal file
87
src/main/mojang/type/Response.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { AxiosError } from "axios"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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?: AxiosError
|
||||||
|
isInternalError?: boolean
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deciperResponseCode(body: { error: string, errorMessage: string, cause?: string }): 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) {
|
||||||
|
// We've sent the wrong method to an endpoint. (ex. GET to POST)
|
||||||
|
case MojangResponseCode.ERROR_METHOD_NOT_ALLOWED:
|
||||||
|
// Indicates endpoint has changed. (404)
|
||||||
|
case MojangResponseCode.ERROR_NOT_FOUND:
|
||||||
|
// Selecting profiles isn't implemented yet. (Shouldnt happen)
|
||||||
|
case MojangResponseCode.ERROR_ACCESS_TOKEN_HAS_PROFILE:
|
||||||
|
// Username/password was not submitted. (UI should forbid this)
|
||||||
|
case MojangResponseCode.ERROR_CREDENTIALS_ARE_NULL:
|
||||||
|
// ??? (Shouldnt happen)
|
||||||
|
case MojangResponseCode.ERROR_INVALID_SALT_VERSION:
|
||||||
|
// Data was not submitted as application/json
|
||||||
|
case MojangResponseCode.ERROR_UNSUPPORTED_MEDIA_TYPE:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,14 @@
|
|||||||
|
export enum StatusColor {
|
||||||
|
RED = 'red',
|
||||||
|
YELLOW = 'yellow',
|
||||||
|
GREEN = 'green',
|
||||||
|
GREY = 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
export interface Status {
|
export interface Status {
|
||||||
|
|
||||||
service: string
|
service: string
|
||||||
status: 'red' | 'yellow' | 'green' | 'grey'
|
status: StatusColor
|
||||||
name: string
|
name: string
|
||||||
essential: boolean
|
essential: boolean
|
||||||
|
|
||||||
|
177
test/mojang/mojangTest.ts
Normal file
177
test/mojang/mojangTest.ts
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import { Mojang } from "../../src/main/mojang/mojang"
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import nock from 'nock'
|
||||||
|
import { URL } from 'url'
|
||||||
|
import { Session } from "../../src/main/model/mojang/auth/Session"
|
||||||
|
import { MojangResponseCode } from "../../src/main/mojang/type/Response"
|
||||||
|
|
||||||
|
function expectMojangResponse(res: any, responseCode: MojangResponseCode, negate = false) {
|
||||||
|
expect(res).to.not.be.an('error')
|
||||||
|
expect(res).to.be.an('object')
|
||||||
|
expect(res).to.have.property('responseCode')
|
||||||
|
if(!negate) {
|
||||||
|
expect(res.responseCode).to.equal(responseCode)
|
||||||
|
} else {
|
||||||
|
expect(res.responseCode).to.not.equal(responseCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Mojang Errors', () => {
|
||||||
|
|
||||||
|
it('Status (Offline)', async () => {
|
||||||
|
|
||||||
|
const defStatusHack = Mojang['statuses']
|
||||||
|
const url = new URL(Mojang.STATUS_ENDPOINT)
|
||||||
|
|
||||||
|
nock(url.origin)
|
||||||
|
.get(url.pathname)
|
||||||
|
.reply(500, 'Service temprarily offline.')
|
||||||
|
|
||||||
|
const res = await Mojang.status();
|
||||||
|
expectMojangResponse(res, MojangResponseCode.SUCCESS, true)
|
||||||
|
expect(res.data).to.be.an('array')
|
||||||
|
expect(res.data).to.deep.equal(defStatusHack)
|
||||||
|
|
||||||
|
}).timeout(2500)
|
||||||
|
|
||||||
|
it('Authenticate (Invalid Credentials)', async () => {
|
||||||
|
|
||||||
|
nock(Mojang.AUTH_ENDPOINT)
|
||||||
|
.post('/authenticate')
|
||||||
|
.reply(403, (uri, requestBody: any): { error: string, errorMessage: string } => {
|
||||||
|
return {
|
||||||
|
error: 'ForbiddenOperationException',
|
||||||
|
errorMessage: 'Invalid credentials. Invalid username or password.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
||||||
|
expectMojangResponse(res, MojangResponseCode.ERROR_INVALID_CREDENTIALS)
|
||||||
|
expect(res.data).to.be.a('null')
|
||||||
|
expect(res.error).to.not.be.a('null')
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Mojang Status', () => {
|
||||||
|
|
||||||
|
it('Status (Online)', async () => {
|
||||||
|
|
||||||
|
const defStatusHack = Mojang['statuses']
|
||||||
|
const url = new URL(Mojang.STATUS_ENDPOINT)
|
||||||
|
|
||||||
|
nock(url.origin)
|
||||||
|
.get(url.pathname)
|
||||||
|
.reply(200, defStatusHack)
|
||||||
|
|
||||||
|
const res = await Mojang.status();
|
||||||
|
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||||
|
expect(res.data).to.be.an('array')
|
||||||
|
expect(res.data).to.deep.equal(defStatusHack)
|
||||||
|
|
||||||
|
}).timeout(2500)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Mojang Auth', () => {
|
||||||
|
|
||||||
|
it('Authenticate', async () => {
|
||||||
|
|
||||||
|
nock(Mojang.AUTH_ENDPOINT)
|
||||||
|
.post('/authenticate')
|
||||||
|
.reply(200, (uri, requestBody: any): Session => {
|
||||||
|
const mockResponse: Session = {
|
||||||
|
accessToken: 'abc',
|
||||||
|
clientToken: requestBody.clientToken,
|
||||||
|
selectedProfile: {
|
||||||
|
id: 'def',
|
||||||
|
name: 'username'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(requestBody.requestUser) {
|
||||||
|
mockResponse.user = {
|
||||||
|
id: 'def',
|
||||||
|
properties: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockResponse
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await Mojang.authenticate('user', 'pass', 'xxx', true)
|
||||||
|
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||||
|
expect(res.data!.clientToken).to.equal('xxx')
|
||||||
|
expect(res.data).to.have.property('user')
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Validate', async () => {
|
||||||
|
|
||||||
|
nock(Mojang.AUTH_ENDPOINT)
|
||||||
|
.post('/validate')
|
||||||
|
.times(2)
|
||||||
|
.reply((uri, requestBody: any) => {
|
||||||
|
return [
|
||||||
|
requestBody.accessToken === 'abc' ? 204 : 403
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await Mojang.validate('abc', 'def')
|
||||||
|
|
||||||
|
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||||
|
expect(res.data).to.be.a('boolean')
|
||||||
|
expect(res.data).to.equal(true)
|
||||||
|
|
||||||
|
const res2 = await Mojang.validate('def', 'def')
|
||||||
|
|
||||||
|
expectMojangResponse(res2, MojangResponseCode.SUCCESS)
|
||||||
|
expect(res2.data).to.be.a('boolean')
|
||||||
|
expect(res2.data).to.equal(false)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Invalidate', async () => {
|
||||||
|
|
||||||
|
nock(Mojang.AUTH_ENDPOINT)
|
||||||
|
.post('/invalidate')
|
||||||
|
.reply(204)
|
||||||
|
|
||||||
|
const res = await Mojang.invalidate('adc', 'def')
|
||||||
|
|
||||||
|
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Refresh', async () => {
|
||||||
|
|
||||||
|
nock(Mojang.AUTH_ENDPOINT)
|
||||||
|
.post('/refresh')
|
||||||
|
.reply(200, (uri, requestBody: any): Session => {
|
||||||
|
const mockResponse: Session = {
|
||||||
|
accessToken: 'abc',
|
||||||
|
clientToken: requestBody.clientToken,
|
||||||
|
selectedProfile: {
|
||||||
|
id: 'def',
|
||||||
|
name: 'username'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(requestBody.requestUser) {
|
||||||
|
mockResponse.user = {
|
||||||
|
id: 'def',
|
||||||
|
properties: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mockResponse
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await Mojang.refresh('gfd', 'xxx', true)
|
||||||
|
expectMojangResponse(res, MojangResponseCode.SUCCESS)
|
||||||
|
expect(res.data!.clientToken).to.equal('xxx')
|
||||||
|
expect(res.data).to.have.property('user')
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
7
tsconfig.test.json
Normal file
7
tsconfig.test.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"extends": "./tsconfig.json"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user