/* * The Peacock Project - a HITMAN server replacement. * Copyright (C) 2021-2022 The Peacock Project Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import axios, { AxiosResponse } from "axios" import type { Request } from "express" import { log, LogLevel } from "./loggingInterop" import { handleAxiosError } from "./utils" import type { GameVersion } from "./types/types" /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Creates the body for the authentication request (urlencoded format). * * @param params The parameters object. * @returns The urlencoded body. */ function createUrlencodedBody(params: Record): string { let out = "" Object.keys(params).forEach((value, index) => { if (index !== 0) { out += "&" } out += `${value}=${params[value]}` }) return out } const requestHeadersH3 = { "User-agent": "G2 Http/1.0 (Windows NT 10.0; DX12/1; d3d12/1)", Version: "8.9.0", } const requestHeadersH2 = { "User-agent": "G2 Http/1.0 (Windows NT 10.0; DX11/1; d3d11/1)", Version: "7.14.0", } const requestHeadersH1 = { "User-agent": "G2 Http/1.0 (Windows NT 10.0; DX11/1; d3d11/1)", Version: "6.74.0", } type AuthResponse = { access_token: string refresh_token: string } /** * Container for official server authentication tokens. */ export class OfficialServerAuth { /** * If this authentication container is ready for use. */ initialized?: boolean protected _usableToken?: string protected _refreshToken?: string protected _gameAuthToken?: string private readonly _headers: { "User-agent": string; Version: string } /** * Kick things off. * * @param gameVersion The game version. * @param gameAuthToken The token for the 3rd party game provider (steam or epic). */ constructor(gameVersion: GameVersion, gameAuthToken: string) { this._gameAuthToken = gameAuthToken this._headers = gameVersion === "h1" ? requestHeadersH1 : gameVersion === "h2" ? requestHeadersH2 : requestHeadersH3 this.initialized = false } /** * Authenticates the client with the official service the first time. * * @param req The initial client request. */ async _initiallyAuthenticate(req: Request): Promise { try { const r = await this._firstTimeObtainData(req) this._usableToken = r.access_token this._refreshToken = r.refresh_token this.initialized = true } catch (e) { handleAxiosError(e) if (PEACOCK_DEV) { log( LogLevel.WARN, `Failed to authenticate with official servers for contracts mode: ${e}`, ) } } } /** * Makes a request with the required context. * * @param url The URL to fetch. * @param get If the request should be a GET (true), or POST (false). * @param body The request's body (defaults to {}). * @param headers The request's extra headers (defaults to {}). * @returns The response data. */ async _useService( url: string, get: boolean, body = {}, headers = {}, ): Promise> { if (get) { return axios(url, { data: body, headers: { ...headers, ...this._headers, Authorization: `bearer ${this._usableToken}`, }, }) } return await axios.post(url, body, { headers: { ...headers, ...this._headers, Authorization: `bearer ${this._usableToken}`, }, }) } /** * Gain a new authentication token. */ async _doRefresh(): Promise { if (PEACOCK_DEV) { log( LogLevel.DEBUG, `Using refresh token (${this._refreshToken}) to reauthenticate.`, ) } const body = `grant_type=refresh_token&refresh_token=${this._refreshToken}&authcode=${this._gameAuthToken}` const res = await axios("https://auth.hitman.io/oauth/token", { headers: { ...this._headers, Authorization: `bearer ${this._usableToken}`, }, data: body, }) const r = res.data as AuthResponse this._usableToken = r.access_token this._refreshToken = r.refresh_token this.initialized = true } /** * Authenticate for the first time. * * @param req The request from the Hitman client connecting to Peacock. * @returns The token data fetched from the official servers. */ private async _firstTimeObtainData(req: Request): Promise { return ( await axios.post( "https://auth.hitman.io/oauth/token", createUrlencodedBody(req.body), { headers: this._headers, }, ) ).data } } export const userAuths = new Map()