mojang.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import { LoggerUtil } from '../logging/loggerutil'
  2. import { Agent } from './model/auth/Agent'
  3. import { Status, StatusColor } from './model/internal/Status'
  4. import got, { RequestError, HTTPError, TimeoutError, GotError, ParseError } from 'got'
  5. import { Session } from './model/auth/Session'
  6. import { AuthPayload } from './model/auth/AuthPayload'
  7. import { MojangResponse, MojangResponseCode, deciperResponseCode, isInternalError, MojangErrorBody } from './model/internal/Response'
  8. export class Mojang {
  9. private static readonly logger = LoggerUtil.getLogger('Mojang')
  10. private static readonly TIMEOUT = 2500
  11. public static readonly AUTH_ENDPOINT = 'https://authserver.mojang.com'
  12. public static readonly STATUS_ENDPOINT = 'https://status.mojang.com'
  13. private static authClient = got.extend({
  14. prefixUrl: Mojang.AUTH_ENDPOINT,
  15. responseType: 'json',
  16. retry: 0
  17. })
  18. private static statusClient = got.extend({
  19. prefixUrl: Mojang.STATUS_ENDPOINT,
  20. responseType: 'json',
  21. retry: 0
  22. })
  23. public static readonly MINECRAFT_AGENT: Agent = {
  24. name: 'Minecraft',
  25. version: 1
  26. }
  27. protected static statuses: Status[] = [
  28. {
  29. service: 'sessionserver.mojang.com',
  30. status: StatusColor.GREY,
  31. name: 'Multiplayer Session Service',
  32. essential: true
  33. },
  34. {
  35. service: 'authserver.mojang.com',
  36. status: StatusColor.GREY,
  37. name: 'Authentication Service',
  38. essential: true
  39. },
  40. {
  41. service: 'textures.minecraft.net',
  42. status: StatusColor.GREY,
  43. name: 'Minecraft Skins',
  44. essential: false
  45. },
  46. {
  47. service: 'api.mojang.com',
  48. status: StatusColor.GREY,
  49. name: 'Public API',
  50. essential: false
  51. },
  52. {
  53. service: 'minecraft.net',
  54. status: StatusColor.GREY,
  55. name: 'Minecraft.net',
  56. essential: false
  57. },
  58. {
  59. service: 'account.mojang.com',
  60. status: StatusColor.GREY,
  61. name: 'Mojang Accounts Website',
  62. essential: false
  63. }
  64. ]
  65. /**
  66. * Converts a Mojang status color to a hex value. Valid statuses
  67. * are 'green', 'yellow', 'red', and 'grey'. Grey is a custom status
  68. * to our project which represents an unknown status.
  69. */
  70. public static statusToHex(status: string){
  71. switch(status.toLowerCase()){
  72. case StatusColor.GREEN:
  73. return '#a5c325'
  74. case StatusColor.YELLOW:
  75. return '#eac918'
  76. case StatusColor.RED:
  77. return '#c32625'
  78. case StatusColor.GREY:
  79. default:
  80. return '#848484'
  81. }
  82. }
  83. private static handleGotError<T>(operation: string, error: GotError, dataProvider: () => T): MojangResponse<T> {
  84. const response: MojangResponse<T> = {
  85. data: dataProvider(),
  86. responseCode: MojangResponseCode.ERROR,
  87. error
  88. }
  89. if(error instanceof HTTPError) {
  90. response.responseCode = deciperResponseCode(error.response.body as MojangErrorBody)
  91. Mojang.logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error)
  92. Mojang.logger.debug('Response Details:')
  93. Mojang.logger.debug('Body:', error.response.body)
  94. Mojang.logger.debug('Headers:', error.response.headers)
  95. } else if(error instanceof RequestError) {
  96. Mojang.logger.error(`${operation} request recieved no response (${error.code}).`, error)
  97. } else if(error instanceof TimeoutError) {
  98. Mojang.logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`)
  99. } else if(error instanceof ParseError) {
  100. Mojang.logger.error(`${operation} request recieved unexepected body (Parse Error).`)
  101. } else {
  102. // CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError
  103. Mojang.logger.error(`Error during ${operation} request.`, error)
  104. }
  105. response.isInternalError = isInternalError(response.responseCode)
  106. return response
  107. }
  108. private static expectSpecificSuccess(operation: string, expected: number, actual: number) {
  109. if(actual !== expected) {
  110. Mojang.logger.warn(`${operation} expected ${expected} response, recieved ${actual}.`)
  111. }
  112. }
  113. /**
  114. * Retrieves the status of Mojang's services.
  115. * The response is condensed into a single object. Each service is
  116. * a key, where the value is an object containing a status and name
  117. * property.
  118. *
  119. * @see http://wiki.vg/Mojang_API#API_Status
  120. */
  121. public static async status(): Promise<MojangResponse<Status[]>>{
  122. try {
  123. const res = await Mojang.statusClient.get<{[service: string]: StatusColor}[]>('check')
  124. Mojang.expectSpecificSuccess('Mojang Status', 200, res.statusCode)
  125. res.body.forEach(status => {
  126. const entry = Object.entries(status)[0]
  127. for(let i=0; i<Mojang.statuses.length; i++) {
  128. if(Mojang.statuses[i].service === entry[0]) {
  129. Mojang.statuses[i].status = entry[1]
  130. break
  131. }
  132. }
  133. })
  134. return {
  135. data: Mojang.statuses,
  136. responseCode: MojangResponseCode.SUCCESS
  137. }
  138. } catch(error) {
  139. return Mojang.handleGotError('Mojang Status', error as GotError, () => {
  140. for(let i=0; i<Mojang.statuses.length; i++){
  141. Mojang.statuses[i].status = StatusColor.GREY
  142. }
  143. return Mojang.statuses
  144. })
  145. }
  146. }
  147. /**
  148. * Authenticate a user with their Mojang credentials.
  149. *
  150. * @param {string} username The user's username, this is often an email.
  151. * @param {string} password The user's password.
  152. * @param {string} clientToken The launcher's Client Token.
  153. * @param {boolean} requestUser Optional. Adds user object to the reponse.
  154. * @param {Object} agent Optional. Provided by default. Adds user info to the response.
  155. *
  156. * @see http://wiki.vg/Authentication#Authenticate
  157. */
  158. public static async authenticate(
  159. username: string,
  160. password: string,
  161. clientToken: string | null,
  162. requestUser: boolean = true,
  163. agent: Agent = Mojang.MINECRAFT_AGENT
  164. ): Promise<MojangResponse<Session | null>> {
  165. try {
  166. const json: AuthPayload = {
  167. agent,
  168. username,
  169. password,
  170. requestUser
  171. }
  172. if(clientToken != null){
  173. json.clientToken = clientToken
  174. }
  175. const res = await Mojang.authClient.post<Session>('authenticate', { json })
  176. Mojang.expectSpecificSuccess('Mojang Authenticate', 200, res.statusCode)
  177. return {
  178. data: res.body,
  179. responseCode: MojangResponseCode.SUCCESS
  180. }
  181. } catch(err) {
  182. return Mojang.handleGotError('Mojang Authenticate', err as GotError, () => null)
  183. }
  184. }
  185. /**
  186. * Validate an access token. This should always be done before launching.
  187. * The client token should match the one used to create the access token.
  188. *
  189. * @param {string} accessToken The access token to validate.
  190. * @param {string} clientToken The launcher's client token.
  191. *
  192. * @see http://wiki.vg/Authentication#Validate
  193. */
  194. public static async validate(accessToken: string, clientToken: string): Promise<MojangResponse<boolean>> {
  195. try {
  196. const json = {
  197. accessToken,
  198. clientToken
  199. }
  200. const res = await Mojang.authClient.post('validate', { json })
  201. Mojang.expectSpecificSuccess('Mojang Validate', 204, res.statusCode)
  202. return {
  203. data: res.statusCode === 204,
  204. responseCode: MojangResponseCode.SUCCESS
  205. }
  206. } catch(err) {
  207. if(err instanceof HTTPError && err.response.statusCode === 403) {
  208. return {
  209. data: false,
  210. responseCode: MojangResponseCode.SUCCESS
  211. }
  212. }
  213. return Mojang.handleGotError('Mojang Validate', err as GotError, () => false)
  214. }
  215. }
  216. /**
  217. * Invalidates an access token. The clientToken must match the
  218. * token used to create the provided accessToken.
  219. *
  220. * @param {string} accessToken The access token to invalidate.
  221. * @param {string} clientToken The launcher's client token.
  222. *
  223. * @see http://wiki.vg/Authentication#Invalidate
  224. */
  225. public static async invalidate(accessToken: string, clientToken: string): Promise<MojangResponse<undefined>> {
  226. try {
  227. const json = {
  228. accessToken,
  229. clientToken
  230. }
  231. const res = await Mojang.authClient.post('invalidate', { json })
  232. Mojang.expectSpecificSuccess('Mojang Invalidate', 204, res.statusCode)
  233. return {
  234. data: undefined,
  235. responseCode: MojangResponseCode.SUCCESS
  236. }
  237. } catch(err) {
  238. return Mojang.handleGotError('Mojang Invalidate', err as GotError, () => undefined)
  239. }
  240. }
  241. /**
  242. * Refresh a user's authentication. This should be used to keep a user logged
  243. * in without asking them for their credentials again. A new access token will
  244. * be generated using a recent invalid access token. See Wiki for more info.
  245. *
  246. * @param {string} accessToken The old access token.
  247. * @param {string} clientToken The launcher's client token.
  248. * @param {boolean} requestUser Optional. Adds user object to the reponse.
  249. *
  250. * @see http://wiki.vg/Authentication#Refresh
  251. */
  252. public static async refresh(accessToken: string, clientToken: string, requestUser: boolean = true): Promise<MojangResponse<Session | null>> {
  253. try {
  254. const json = {
  255. accessToken,
  256. clientToken,
  257. requestUser
  258. }
  259. const res = await Mojang.authClient.post<Session>('refresh', { json })
  260. Mojang.expectSpecificSuccess('Mojang Refresh', 200, res.statusCode)
  261. return {
  262. data: res.body,
  263. responseCode: MojangResponseCode.SUCCESS
  264. }
  265. } catch(err) {
  266. return Mojang.handleGotError('Mojang Refresh', err as GotError, () => null)
  267. }
  268. }
  269. }