diff --git a/.gitignore b/.gitignore index 6e95286..0f03dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist /src/config/config.json /.vs/ /.idea/ +/proto_samples/ diff --git a/src/config/example.config.json b/src/config/example.config.json index 09df405..36c5ba5 100644 --- a/src/config/example.config.json +++ b/src/config/example.config.json @@ -2,5 +2,11 @@ "default_port": 8081, "trafficlight_identifier": "AwesomeProtoSender", "redirect_to_golbat_url": null, - "redirect_to_golbat_token": null + "redirect_to_golbat_token": null, + "sample_saving": { + "enabled": true, + "save_directory": "./proto_samples", + "max_samples_per_method": 1, + "endpoints": ["traffic", "golbat"] + } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 708c94c..41764fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import http from "http"; import fs from "fs"; import { WebStreamBuffer, getIPAddress, handleData, moduleConfigIsAvailable, redirect_post_golbat } from "./utils"; import { decodePayload, decodePayloadTraffic } from "./parser/proto-parser"; +import SampleSaver from "./utils/sample-saver"; // try looking if config file exists... let config = require("./config/example.config.json"); @@ -14,6 +15,9 @@ const incomingProtoWebBufferInst = new WebStreamBuffer(); const outgoingProtoWebBufferInst = new WebStreamBuffer(); const portBind = config["default_port"]; +// Initialize sample saver +const sampleSaver = config.sample_saving ? new SampleSaver(config.sample_saving) : null; + // server const httpServer = http.createServer(function (req, res) { let incomingData: Array = []; @@ -44,11 +48,25 @@ const httpServer = http.createServer(function (req, res) { } const identifier = parsedData['username']; for (let i = 0; i < parsedData['contents'].length; i++) { + const rawRequest = parsedData['contents'][i].request || ""; + const rawResponse = parsedData['contents'][i].payload || ""; + const parsedRequestData = decodePayloadTraffic( parsedData['contents'][i].type, - parsedData['contents'][i].request, + rawRequest, "request" ); + const parsedResponseData = decodePayloadTraffic( + parsedData['contents'][i].type, + rawResponse, + "response" + ); + + // Save sample if enabled + if (sampleSaver && parsedRequestData.length > 0 && parsedResponseData.length > 0) { + sampleSaver.savePair(parsedRequestData[0], parsedResponseData[0], rawRequest, rawResponse, "golbat"); + } + if (typeof parsedRequestData === "string") { incomingProtoWebBufferInst.write({ error: parsedRequestData }); } else { @@ -57,11 +75,7 @@ const httpServer = http.createServer(function (req, res) { incomingProtoWebBufferInst.write(parsedObject); } } - const parsedResponseData = decodePayloadTraffic( - parsedData['contents'][i].type, - parsedData['contents'][i].payload, - "response" - ); + if (typeof parsedResponseData === "string") { outgoingProtoWebBufferInst.write({ error: parsedResponseData }); } else { @@ -85,10 +99,10 @@ const httpServer = http.createServer(function (req, res) { res.end(""); if (Array.isArray(parsedData)) { for (let i = 0; i < parsedData.length; i++) { - handleData(incomingProtoWebBufferInst, outgoingProtoWebBufferInst, identifier, parsedData[i]) + handleData(incomingProtoWebBufferInst, outgoingProtoWebBufferInst, identifier, parsedData[i], sampleSaver) } } else { - handleData(incomingProtoWebBufferInst, outgoingProtoWebBufferInst, identifier, parsedData) + handleData(incomingProtoWebBufferInst, outgoingProtoWebBufferInst, identifier, parsedData, sampleSaver) } }); break; diff --git a/src/parser/proto-parser.ts b/src/parser/proto-parser.ts index 700ea27..1512278 100644 --- a/src/parser/proto-parser.ts +++ b/src/parser/proto-parser.ts @@ -2,35 +2,22 @@ import { b64Decode } from "../utils"; import { requestMessagesResponses } from "../constants"; import { DecodedProto } from "../types"; -// For decode dynamics action social. let action_social = 0; -/** - * Callback as used by {@link DecoderInternalPayloadAsResponse}. - * @type {function} - * @param {number|any} - */ -/** - * Returns decoded proto as JSON. Uses Tuples by https://github.com/Furtif/pogo-protos/blob/master/test/test.js, if that implemented. - */ function DecoderInternalPayloadAsResponse(method: number, data: any): any { - // Reset value. action_social = 0; let proto_tuple: any = Object.values(requestMessagesResponses)[method]; let result: any = { Not_Implemented_yet: data }; + + if (!data) { + return {}; + } for (let i = 0; i < Object.keys(requestMessagesResponses).length; i++) { proto_tuple = Object.values(requestMessagesResponses)[i]; const my_req = proto_tuple[0]; if (my_req == method) { - if (proto_tuple[2] != null && b64Decode(data)) { + if (proto_tuple[2] != null && data && b64Decode(data).length > 0) { try { result = proto_tuple[2].decode(b64Decode(data)).toJSON(); - /* - // This not need more because protos as replaced bytes for the proto. - if (method == 10010) { - let profile = POGOProtos.Rpc.PlayerPublicProfileProto.decode(b64Decode(result.friend[0].player.public_data)).toJSON(); - result.friend[0].player.public_data = profile; - } - */ } catch (error) { console.error(`Intenal ProxySocial decoder ${my_req} Error: ${error}`); @@ -85,14 +72,22 @@ export const decodePayload = (contents: any, dataType: string): DecodedProto[] = export const decodeProto = (method: number, data: string, dataType: string): DecodedProto | string => { let returnObject: DecodedProto | string = "Not Found"; + let methodFound = false; + for (let i = 0; i < Object.keys(requestMessagesResponses).length; i++) { let foundMethod: any = Object.values(requestMessagesResponses)[i]; let foundMethodString: string = Object.keys(requestMessagesResponses)[i]; const foundReq = foundMethod[0]; if (foundReq == method) { + methodFound = true; if (foundMethod[1] != null && dataType === "request") { try { - let parsedData = foundMethod[1].decode(b64Decode(data)).toJSON(); + let parsedData; + if (!data || data === "") { + parsedData = {}; + } else { + parsedData = foundMethod[1].decode(b64Decode(data)).toJSON(); + } if (foundMethod[0] === 5012 || foundMethod[0] === 600005) { action_social = parsedData.action; Object.values(requestMessagesResponses).forEach(val => { @@ -109,13 +104,35 @@ export const decodeProto = (method: number, data: string, dataType: string): Dec }; } catch (error) { console.error(`Error parsing request ${foundMethodString} -> ${error}`); + returnObject = { + methodId: foundMethod[0], + methodName: remasterOrCleanMethodString(foundMethodString) + " [PARSE ERROR]", + data: { + error: "Failed to decode proto", + rawBase64: data, + errorMessage: error.toString() + }, + }; } } else if (dataType === "request") { console.warn(`Request ${foundMethod[0]} Not Implemented`) + returnObject = { + methodId: foundMethod[0], + methodName: remasterOrCleanMethodString(foundMethodString) + " [NOT IMPLEMENTED]", + data: { + error: "Proto not implemented", + rawBase64: data + }, + }; } if (foundMethod[2] != null && dataType === "response") { try { - let parsedData = foundMethod[2].decode(b64Decode(data)).toJSON(); + let parsedData; + if (!data || data === "") { + parsedData = {}; + } else { + parsedData = foundMethod[2].decode(b64Decode(data)).toJSON(); + } if (foundMethod[0] === 5012 && action_social > 0 && parsedData.payload) { parsedData.payload = DecoderInternalPayloadAsResponse(action_social, parsedData.payload); } @@ -129,11 +146,40 @@ export const decodeProto = (method: number, data: string, dataType: string): Dec }; } catch (error) { console.error(`Error parsing response ${foundMethodString} method: [${foundReq}] -> ${error}`); + returnObject = { + methodId: foundMethod[0], + methodName: remasterOrCleanMethodString(foundMethodString) + " [PARSE ERROR]", + data: { + error: "Failed to decode proto", + rawBase64: data, + errorMessage: error.toString() + }, + }; } } else if (dataType === "response") { console.warn(`Response ${foundReq} Not Implemented`) + returnObject = { + methodId: foundMethod[0], + methodName: remasterOrCleanMethodString(foundMethodString) + " [NOT IMPLEMENTED]", + data: { + error: "Proto not implemented", + rawBase64: data + }, + }; } } } + + if (!methodFound && returnObject === "Not Found") { + returnObject = { + methodId: method.toString(), + methodName: `Unknown Method ${method} [UNKNOWN]`, + data: { + error: "Unknown method ID", + rawBase64: data + }, + }; + } + return returnObject; }; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index ad35f5c..333c98c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,5 +2,5 @@ export type DecodedProto = { identifier?: string; methodId: string; methodName: string; - data: string; + data: any; }; diff --git a/src/utils/index.ts b/src/utils/index.ts index 6be638d..4c57e70 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,6 +5,9 @@ import { WebStreamBuffer } from "./web-stream-buffer"; import { decodePayloadTraffic } from "../parser/proto-parser"; export const b64Decode = (data: string) => { + if (!data || data === "") { + return Buffer.alloc(0); + } return Buffer.from(data, "base64"); }; @@ -30,13 +33,27 @@ export function getIPAddress() { return '0.0.0.0'; } -export function handleData(incoming: WebStreamBuffer, outgoing: WebStreamBuffer, identifier: any, parsedData: string) { +export function handleData(incoming: WebStreamBuffer, outgoing: WebStreamBuffer, identifier: any, parsedData: string, sampleSaver?: any) { for (let i = 0; i < parsedData['protos'].length; i++) { + const rawRequest = parsedData['protos'][i].request || ""; + const rawResponse = parsedData['protos'][i].response || ""; + const parsedRequestData = decodePayloadTraffic( parsedData['protos'][i].method, - parsedData['protos'][i].request, + rawRequest, "request" ); + const parsedResponseData = decodePayloadTraffic( + parsedData['protos'][i].method, + rawResponse, + "response" + ); + + // Save sample if enabled + if (sampleSaver && parsedRequestData.length > 0 && parsedResponseData.length > 0) { + sampleSaver.savePair(parsedRequestData[0], parsedResponseData[0], rawRequest, rawResponse, "traffic"); + } + if (typeof parsedRequestData === "string") { incoming.write({ error: parsedRequestData }); } else { @@ -45,11 +62,7 @@ export function handleData(incoming: WebStreamBuffer, outgoing: WebStreamBuffer, incoming.write(parsedObject); } } - const parsedResponseData = decodePayloadTraffic( - parsedData['protos'][i].method, - parsedData['protos'][i].response, - "response" - ); + if (typeof parsedResponseData === "string") { outgoing.write({ error: parsedResponseData }); } else { diff --git a/src/utils/sample-saver.ts b/src/utils/sample-saver.ts new file mode 100644 index 0000000..476c39e --- /dev/null +++ b/src/utils/sample-saver.ts @@ -0,0 +1,168 @@ +import fs from "fs"; +import path from "path"; +import { DecodedProto } from "../types"; + +interface SampleConfig { + enabled: boolean; + save_directory: string; + max_samples_per_method: number; + endpoints: string[]; +} + +interface SavedSample { + methodId: string; + methodName: string; + timestamp: string; + request: any; + response: any; + rawRequest: string; + rawResponse: string; +} + +interface SampleFile { + filename: string; + filepath: string; + timestamp: number; + methodId: string; +} + +class SampleSaver { + private config: SampleConfig; + private savedSamples: Map = new Map(); + private saveDirectory: string; + + constructor(config: SampleConfig) { + this.config = config; + this.saveDirectory = path.resolve(config.save_directory); + this.initializeStorage(); + this.loadExistingSamples(); + } + + private initializeStorage(): void { + if (!this.config.enabled) return; + + if (!fs.existsSync(this.saveDirectory)) { + fs.mkdirSync(this.saveDirectory, { recursive: true }); + console.log(`Created sample storage directory: ${this.saveDirectory}`); + } + } + + private loadExistingSamples(): void { + if (!this.config.enabled) return; + + if (fs.existsSync(this.saveDirectory)) { + const files = fs.readdirSync(this.saveDirectory); + + files.forEach(file => { + const match = file.match(/^(\d+)_.*_(\d+)\.json$/); + if (match) { + const methodId = match[1]; + const timestamp = parseInt(match[2]); + const filepath = path.join(this.saveDirectory, file); + + const samples = this.savedSamples.get(methodId) || []; + samples.push({ + filename: file, + filepath: filepath, + timestamp: timestamp, + methodId: methodId + }); + this.savedSamples.set(methodId, samples); + } + }); + + // Sort samples by timestamp for each method + for (const [, samples] of this.savedSamples.entries()) { + samples.sort((a, b) => a.timestamp - b.timestamp); + } + } + } + + private getTimestamp(): string { + return new Date().toISOString(); + } + + private deleteOldestSample(methodId: string): void { + const samples = this.savedSamples.get(methodId); + if (samples && samples.length > 0) { + const oldest = samples.shift(); + if (oldest) { + try { + fs.unlinkSync(oldest.filepath); + console.log(`Deleted oldest sample: ${oldest.filename}`); + } catch (error) { + console.error(`Failed to delete old sample: ${error}`); + } + } + } + } + + private shouldSave(endpoint: string): boolean { + if (!this.config.enabled) return false; + if (!this.config.endpoints.includes(endpoint)) return false; + return true; + } + + public async saveSample( + request: DecodedProto, + response: DecodedProto | null, + rawRequest: string, + rawResponse: string, + endpoint: string + ): Promise { + if (!request || !request.methodId) return; + if (!this.shouldSave(endpoint)) return; + + const methodSamples = this.savedSamples.get(request.methodId) || []; + + // If we've reached the max samples for this method, delete the oldest + if (methodSamples.length >= this.config.max_samples_per_method) { + this.deleteOldestSample(request.methodId); + } + + const sample: SavedSample = { + methodId: request.methodId, + methodName: request.methodName, + timestamp: this.getTimestamp(), + request: request.data, + response: response ? response.data : null, + rawRequest: rawRequest, + rawResponse: rawResponse + }; + + const safeMethodName = request.methodName.replace(/[^a-zA-Z0-9_-]/g, '_'); + const timestamp = Date.now(); + const filename = `${request.methodId}_${safeMethodName}_${timestamp}.json`; + const filepath = path.join(this.saveDirectory, filename); + + try { + fs.writeFileSync(filepath, JSON.stringify(sample, null, 2)); + + // Update our tracking + const samples = this.savedSamples.get(request.methodId) || []; + samples.push({ + filename: filename, + filepath: filepath, + timestamp: timestamp, + methodId: request.methodId + }); + this.savedSamples.set(request.methodId, samples); + + console.log(`Saved sample for method ${request.methodId} (${request.methodName}): ${filename}`); + } catch (error) { + console.error(`Failed to save sample: ${error}`); + } + } + + public async savePair( + request: DecodedProto, + response: DecodedProto, + rawRequest: string, + rawResponse: string, + endpoint: string + ): Promise { + await this.saveSample(request, response, rawRequest, rawResponse, endpoint); + } +} + +export default SampleSaver; \ No newline at end of file diff --git a/src/views/css/style.css b/src/views/css/style.css index c102253..e0100ad 100644 --- a/src/views/css/style.css +++ b/src/views/css/style.css @@ -118,4 +118,116 @@ table tr td.important { text-align: right; border-top: 2px solid rgba(255, 255, 255, 0.5); width: 100%; +} + +/* Mobile styles - hide by default */ +.mobile-only { + display: none !important; +} + +.text-center { + text-align: center; +} + +.clickable-data:hover { + background-color: rgba(0, 123, 255, 0.05) !important; + border: 1px solid rgba(0, 123, 255, 0.2); + transition: all 0.2s ease; +} + +.dark .clickable-data:hover { + background-color: rgba(0, 123, 255, 0.1) !important; + border: 1px solid rgba(0, 123, 255, 0.3); +} + +/* Mobile breakpoint */ +@media (max-width: 768px) { + .mobile-hide { + display: none !important; + } + + .mobile-only { + display: table-cell !important; + } + + /* Optimize table for mobile */ + #data_socket { + font-size: smaller; + } + + #data_socket th, + #data_socket td { + padding: 4px !important; + font-size: 11px; + } + + /* Make method name wrap on mobile */ + #data_socket td:nth-child(3) { + word-break: break-word; + max-width: 120px; + } + + /* Filters optimization for mobile */ + .col-sm-10, .col-sm-2 { + padding: 5px !important; + } + + #filter-mode, #instance-filter-dropdown, #maxlogs { + width: 100% !important; + margin-bottom: 5px; + } + + #method-filter-ids { + width: 100% !important; + margin-top: 5px; + } + + label { + display: block !important; + width: 100% !important; + margin-bottom: 3px; + } + + /* Card header for mobile */ + .card-header .title-proto { + font-size: 16px !important; + } + + .card-header .icons i { + font-size: 12px !important; + margin-right: 8px !important; + } + + /* Footer for mobile */ + .footer { + font-size: 10px; + padding: 5px; + } + + /* Modal optimization for mobile */ + .modal-dialog { + max-width: 95% !important; + margin: 10px auto !important; + } + + .modal-content { + margin: 0 auto; + } + + .modal-body { + padding: 10px !important; + max-height: 70vh; + overflow-y: auto; + } + + .modal-footer { + padding: 10px !important; + flex-wrap: wrap; + } + + .modal-footer button { + margin: 3px !important; + font-size: 12px !important; + padding: 5px 10px !important; + } } \ No newline at end of file diff --git a/src/views/print-protos.html b/src/views/print-protos.html index d5992c0..9f588e4 100644 --- a/src/views/print-protos.html +++ b/src/views/print-protos.html @@ -96,11 +96,14 @@

> - Instance + Instance Method Method Name - Sended Data - Received Data + Sended Data + Received Data + Req + Res + DL @@ -132,10 +135,10 @@