Skip to content

Commit

Permalink
Credential bot import
Browse files Browse the repository at this point in the history
  • Loading branch information
rtm516 committed Jun 29, 2024
1 parent 9a5a299 commit 9ae22cf
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package com.rtm516.mcxboxbroadcast.manager.controllers;

import com.rtm516.mcxboxbroadcast.core.AuthManager;
import com.rtm516.mcxboxbroadcast.core.Constants;
import com.rtm516.mcxboxbroadcast.core.models.auth.XstsAuthData;
import com.rtm516.mcxboxbroadcast.manager.BackendManager;
import com.rtm516.mcxboxbroadcast.manager.BotManager;
import com.rtm516.mcxboxbroadcast.manager.models.BotContainer;
import com.rtm516.mcxboxbroadcast.manager.models.response.ErrorResponse;
import jakarta.servlet.http.HttpServletResponse;
import net.raphimc.minecraftauth.MinecraftAuth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand Down Expand Up @@ -35,8 +39,8 @@ public BotsImportController(BotManager botManager, BackendManager backendManager
this.backendManager = backendManager;
}

@PostMapping("")
public ErrorResponse bots(HttpServletResponse response, @RequestParam("file") MultipartFile file) {
@PostMapping("/legacy")
public ErrorResponse importLegacy(HttpServletResponse response, @RequestParam("file") MultipartFile file) {
// Check file format
if (!file.getOriginalFilename().endsWith(".zip")) {
response.setStatus(400);
Expand Down Expand Up @@ -97,6 +101,55 @@ public ErrorResponse bots(HttpServletResponse response, @RequestParam("file") Mu
return null;
}

@PostMapping("/credentials")
public ErrorResponse importCredentials(HttpServletResponse response, @RequestBody String credentialsData) {
if (credentialsData.isBlank()) {
response.setStatus(400);
return new ErrorResponse("No credentials provided");
}

List<BotContainer> importedBots = new ArrayList<>();

// Parse the credentials data email:password
String[] lines = credentialsData.split("\n");
for (String credential : lines) {
String[] parts = credential.split(":");
if (parts.length != 2) {
// TODO Count these and let the user know
// response.setStatus(400);
// return new ErrorResponse("Invalid credentials format");
continue;
}

// Process the credentials
String cacheData = "";
try {
// TODO Use a different logger
XstsAuthData xstsAuthData = AuthManager.fromCredentials(parts[0], parts[1], MinecraftAuth.LOGGER);
cacheData = Constants.GSON.toJson(xstsAuthData.xstsAuth().toJson(xstsAuthData.xstsToken()));
} catch (Exception e) {
// TODO Catch the exception and let the user know
e.printStackTrace();
}

// Skip if the cache data is blank
if (cacheData.isBlank()) {
continue;
}

// Create a bot for each credential
BotContainer bot = botManager.addBot();
bot.cache(cacheData);
importedBots.add(bot);
}

// Start all the imported bots
backendManager.scheduledThreadPool().execute(() -> importedBots.forEach(BotContainer::start));

response.setStatus(200);
return null;
}

/**
* Get the string contents of a zip entry
*
Expand Down
59 changes: 59 additions & 0 deletions bootstrap/manager/src/ui/src/components/modals/TextInputModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useEffect, useState } from 'react'

import Modal from './Modal'

function TextInputModal ({ title, message, confirmText = 'Submit', rows = 4, cols = 40, open = false, onClose }) {
const [textData, setTextData] = useState('')

useEffect(() => {
if (open) {
setTextData('')
}
}, [open])

const handleSubmit = (success) => {
if (!success) return onClose(success)

onClose(true, textData)
}

return (
<Modal
title={title}
confirmText={confirmText}
color='green'
open={open}
onClose={handleSubmit}
content={
<>
{message &&
<p className='text-sm text-gray-500 pb-2'>
{message}
</p>}
<form className='flex flex-col gap-4 mt-2'>
<textarea
id='text'
name='text'
value={textData}
onChange={(e) => setTextData(e.target.value)}
required
rows={rows}
cols={cols}
className='ring-1 ring-inset ring-gray-300 rounded p-2'
/>
<input
type='submit'
className='hidden'
onClick={(e) => {
e.preventDefault()
handleSubmit(true)
}}
/>
</form>
</>
}
/>
)
}

export default TextInputModal
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ import Banner from '../Banner'

function UploadFileModal ({ title, message, accept = '', open = false, onClose }) {
const [fileData, setFileData] = useState(null)
const [error, setError] = useState('')

useEffect(() => {
if (open) {
setError('')
setFileData(null)
}
}, [open])
Expand All @@ -29,9 +27,6 @@ function UploadFileModal ({ title, message, accept = '', open = false, onClose }
onClose={handleSubmit}
content={
<>
<div className='flex flex-col mt-2'>
{error !== '' && <Banner className='pb-2' color='red' width='full'>{error}</Banner>}
</div>
{message &&
<p className='text-sm text-gray-500 pb-2'>
{message}
Expand Down
24 changes: 22 additions & 2 deletions bootstrap/manager/src/ui/src/pages/Bots.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import Banner from '../components/Banner'
import Button from '../components/Button'
import Dropdown from '../components/Dropdown'
import UploadFileModal from '../components/modals/UploadFileModal'
import TextInputModal from '../components/modals/TextInputModal'

function Bots () {
const navigate = useNavigate()
const { state } = useLocation()

const [bots, setBots] = useState([])
const [importLegacyOpen, setImportLegacyOpen] = useState(false)
const [importCredentialsOpen, setImportCredentialsOpen] = useState(false)

const updateBots = () => {
fetch('/api/bots').then((res) => res.json()).then((data) => {
Expand Down Expand Up @@ -44,7 +46,14 @@ function Bots () {
const formData = new FormData()
formData.append('file', file)

fetch('/api/bots/import', { method: 'POST', body: formData }).then((res) => res.json()).then((data) => {
fetch('/api/bots/import/legacy', { method: 'POST', body: formData }).then((res) => res.json()).then((data) => {
console.log(data)
updateBots()
})
}

const importCredentials = (data) => {
fetch('/api/bots/import/credentials', { method: 'POST', body: data }).then((res) => res.json()).then((data) => {
console.log(data)
updateBots()
})
Expand All @@ -64,6 +73,17 @@ function Bots () {
importLegacy(file)
}}
/>
<TextInputModal
title='Import bots from credentials'
message='Enter the credentials for the bots to import. Must be in the format: email:password'
open={importCredentialsOpen}
onClose={(success, data) => {
setImportCredentialsOpen(false)
if (!success) return
console.log(data)
importCredentials(data)
}}
/>
<div className='px-8 pb-6 flex justify-center'>
<div className='max-w-2xl w-full flex flex-row justify-end gap-1'>
<Dropdown
Expand All @@ -75,7 +95,7 @@ function Bots () {
setImportLegacyOpen(true)
},
'From credentials': () => {
console.log('Importing from credentials')
setImportCredentialsOpen(true)
}
}}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.rtm516.mcxboxbroadcast.core;

import com.google.gson.JsonObject;
import com.rtm516.mcxboxbroadcast.core.models.auth.XstsAuthData;
import com.rtm516.mcxboxbroadcast.core.models.auth.XboxTokenInfo;
import net.lenni0451.commons.httpclient.HttpClient;
import net.raphimc.minecraftauth.MinecraftAuth;
import net.raphimc.minecraftauth.step.AbstractStep;
import net.raphimc.minecraftauth.step.msa.MsaCodeStep;
import net.raphimc.minecraftauth.step.msa.StepCredentialsMsaCode;
import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCode;
import net.raphimc.minecraftauth.step.msa.StepMsaDeviceCodeMsaCode;
import net.raphimc.minecraftauth.step.msa.StepMsaToken;
Expand All @@ -15,6 +18,7 @@
import net.raphimc.minecraftauth.util.JsonUtil;
import net.raphimc.minecraftauth.util.MicrosoftConstants;
import net.raphimc.minecraftauth.util.OAuthEnvironment;
import net.raphimc.minecraftauth.util.logging.ILogger;

import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -44,16 +48,32 @@ public AuthManager(String cache, Logger logger) {

this.xstsToken = null;
}

private static StepXblSisuAuthentication sisuAuthentication(AbstractStep<?, MsaCodeStep.MsaCode> msaInput) {
StepMsaToken initialAuth = new StepMsaToken(msaInput);
StepInitialXblSession xblAuth = new StepInitialXblSession(initialAuth, new StepXblDeviceToken("Android"));
return new StepXblSisuAuthentication(xblAuth, MicrosoftConstants.XBL_XSTS_RELYING_PARTY);
}

private static MsaCodeStep.ApplicationDetails appDetails() {
return new MsaCodeStep.ApplicationDetails(MicrosoftConstants.BEDROCK_ANDROID_TITLE_ID, MicrosoftConstants.SCOPE_TITLE_AUTH, null, OAuthEnvironment.LIVE.getNativeClientUrl(), OAuthEnvironment.LIVE);
}

public static XstsAuthData fromCredentials(String email, String password, ILogger logger) throws Exception {
StepXblSisuAuthentication xstsAuth = sisuAuthentication(new StepCredentialsMsaCode(appDetails()));

HttpClient httpClient = MinecraftAuth.createHttpClient();
return new XstsAuthData(xstsAuth.getFromInput(logger, httpClient, new StepCredentialsMsaCode.MsaCredentials(email, password)), xstsAuth);
}

/**
* Follow the auth flow to get the Xbox token and store it
*/
private void initialise() {
// Setup the authentication steps
HttpClient httpClient = MinecraftAuth.createHttpClient();
MsaCodeStep.ApplicationDetails appDetails = new MsaCodeStep.ApplicationDetails(MicrosoftConstants.BEDROCK_ANDROID_TITLE_ID, MicrosoftConstants.SCOPE_TITLE_AUTH, null, null, OAuthEnvironment.LIVE);
StepMsaToken initialAuth = new StepMsaToken(new StepMsaDeviceCodeMsaCode(new StepMsaDeviceCode(appDetails), 120 * 1000));
StepInitialXblSession xblAuth = new StepInitialXblSession(initialAuth, new StepXblDeviceToken("Android"));
StepXblSisuAuthentication xstsAuth = new StepXblSisuAuthentication(xblAuth, MicrosoftConstants.XBL_XSTS_RELYING_PARTY);
MsaCodeStep.ApplicationDetails appDetails = appDetails();
StepXblSisuAuthentication xstsAuth = sisuAuthentication(new StepMsaDeviceCodeMsaCode(new StepMsaDeviceCode(appDetails), 120 * 1000));

// Check if we have an old live_token.json file and try to import the refresh token from it
if (Files.exists(oldLiveAuth)) {
Expand All @@ -62,9 +82,7 @@ private void initialise() {
JsonObject liveToken = JsonUtil.parseString(Files.readString(oldLiveAuth)).getAsJsonObject();
JsonObject tokenData = liveToken.getAsJsonObject("token");

StepMsaToken convertInitialAuth = new StepMsaToken(new StepRefreshTokenMsaCode(appDetails));
StepInitialXblSession convertXblAuth = new StepInitialXblSession(convertInitialAuth, new StepXblDeviceToken("Android"));
StepXblSisuAuthentication convertXstsAuth = new StepXblSisuAuthentication(convertXblAuth, MicrosoftConstants.XBL_XSTS_RELYING_PARTY);
StepXblSisuAuthentication convertXstsAuth = sisuAuthentication(new StepRefreshTokenMsaCode(appDetails));

xstsToken = convertXstsAuth.getFromInput(logger, httpClient, new StepRefreshTokenMsaCode.RefreshToken(tokenData.get("refresh_token").getAsString()));

Expand Down Expand Up @@ -93,7 +111,7 @@ private void initialise() {
}

// Save to cache.json
Files.writeString(cache, JsonUtil.GSON.toJson(xstsAuth.toJson(xstsToken)));
Files.writeString(cache, Constants.GSON.toJson(xstsAuth.toJson(xstsToken)));

// Construct and store the Xbox token info
xboxTokenInfo = new XboxTokenInfo(xstsToken.getDisplayClaims().get("xid"), xstsToken.getUserHash(), xstsToken.getDisplayClaims().get("gtg"), xstsToken.getToken(), String.valueOf(xstsToken.getExpireTimeMs()));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.rtm516.mcxboxbroadcast.core.models.auth;

import net.raphimc.minecraftauth.step.xbl.StepXblSisuAuthentication;

public record XstsAuthData(StepXblSisuAuthentication.XblSisuTokens xstsToken, StepXblSisuAuthentication xstsAuth) {
}

0 comments on commit 9ae22cf

Please sign in to comment.