added standard auth section
This commit is contained in:
parent
0ad1a86a98
commit
a802a5bffe
@ -1,3 +1,4 @@
|
||||
# CyberChef Cloud Operations Tests
|
||||
# Copy this file to `.env` and fill in your valid GCP API Key if you want to run the Nightwatch browser tests against live Cloud APIs.
|
||||
CYBERCHEF_GCP_TEST_KEY="YOUR_API_KEY_HERE"
|
||||
CLIENT_ID="YOUR_CLIENT_ID_HERE"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,3 +14,4 @@ src/node/index.mjs
|
||||
tests/browser/output/*
|
||||
.node-version
|
||||
.env
|
||||
.antigravity
|
||||
|
||||
122
docs/AddingCloudOperations.md
Normal file
122
docs/AddingCloudOperations.md
Normal file
@ -0,0 +1,122 @@
|
||||
# Guide: Adding New Google Cloud Operations to CyberChef
|
||||
|
||||
With the introduction of the Google Identity Services (GIS) Web Application PKCE flow and strict Content Security Policies (CSP), adding new Google Cloud operations requires a few extra steps compared to standard CyberChef operations.
|
||||
|
||||
This guide outlines the exact process for creating, securing, and testing a new Google Cloud integration.
|
||||
|
||||
## 1. Create the Operation File
|
||||
Create your new operation in `src/core/operations/` (e.g., `GCloudVisionAnalyze.mjs`).
|
||||
Include the standard CyberChef operation scaffolding and ensure you import the authentication utilities from `GoogleCloud.mjs`:
|
||||
|
||||
```javascript
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { applyGCPAuth } from "../lib/GoogleCloud.mjs";
|
||||
|
||||
class GCloudVisionAnalyze extends Operation {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = "GCloud Vision Analyze";
|
||||
this.module = "Cloud";
|
||||
this.description = "Analyzes an image using Google Cloud Vision.";
|
||||
this.infoURL = "https://cloud.google.com/vision/docs";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "JSON";
|
||||
this.args = [
|
||||
/* Your args */
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Leverage `applyGCPAuth`
|
||||
**Do not** add authentication fields (like API keys or tokens) directly into your new operation's `args`.
|
||||
Instead, instruct the user (via the `description`) to place the `Authenticate Google Cloud` operation at the top of their recipe.
|
||||
|
||||
Inside your `run` method, use `applyGCPAuth` to seamlessly inject the credentials. This handles API Keys, Personal Access Tokens (PATs), and the GIS OAuth Tokens interchangeably:
|
||||
|
||||
```javascript
|
||||
async run(input, args) {
|
||||
let url = "https://vision.googleapis.com/v1/images:annotate";
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
|
||||
// 1. Inject Auth into URL or Headers
|
||||
try {
|
||||
[url, headers] = applyGCPAuth(url, headers);
|
||||
} catch (e) {
|
||||
throw new OperationError(e.message);
|
||||
}
|
||||
|
||||
// 2. Perform Fetch
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: headers,
|
||||
body: JSON.stringify({ /* payload */ })
|
||||
});
|
||||
|
||||
// 3. Handle response...
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Update Content Security Policy (CSP)
|
||||
CyberChef utilizes strict CSP headers to prevent XSS attacks from exfiltrating the OAuth tokens stored in `sessionStorage`.
|
||||
If your new operation contacts a new Google API endpoint, **the request will be blocked by the browser** unless you whitelist it.
|
||||
|
||||
1. Open `src/web/html/index.html`.
|
||||
2. Locate the `<meta http-equiv="Content-Security-Policy">` tag in the `<head>`.
|
||||
3. Find the `connect-src` directive.
|
||||
4. Append your new API endpoint (e.g., `https://vision.googleapis.com`).
|
||||
|
||||
Example:
|
||||
```html
|
||||
<meta http-equiv="Content-Security-Policy" content="... connect-src 'self' https://accounts.google.com https://oauth2.googleapis.com https://www.googleapis.com https://storage.googleapis.com https://speech.googleapis.com https://translation.googleapis.com https://vision.googleapis.com;" />
|
||||
```
|
||||
|
||||
## 4. Testing End-to-End (Nightwatch)
|
||||
|
||||
Because the PKCE OAuth flow relies on a popup protected by anti-bot measures (reCAPTCHA) from Google Identity Services, **it cannot be fully automated** with headless browsers like Nightwatch.
|
||||
|
||||
For E2E tests, you **must use** either an API Key or a PAT (`CYBERCHEF_GCP_TEST_KEY` or `CYBERCHEF_GCP_TEST_TOKEN`).
|
||||
|
||||
1. Open `tests/browser/03_cloud_ops.js`.
|
||||
2. Create your test suite.
|
||||
3. Use the PAT or API Key fallback logic present in other tests to inject credentials into your dummy recipe:
|
||||
|
||||
```javascript
|
||||
"GCloud Vision Analyze: Translates image to text": function (browser) {
|
||||
let testToken = process.env.CYBERCHEF_GCP_TEST_TOKEN;
|
||||
|
||||
// Fallback to gcloud SDK if CYBERCHEF_GCP_TEST_TOKEN isn't set manually
|
||||
if (!testToken || testToken === "YOUR_OAUTH_TOKEN_HERE") {
|
||||
try {
|
||||
testToken = require("child_process").execSync("gcloud auth print-access-token", { stdio: "pipe", encoding: "utf-8" }).trim();
|
||||
} catch (e) {
|
||||
console.log("No valid token found. Skipping live Vision API test.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"Personal Access Token (PAT)",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "GCloud Vision Analyze",
|
||||
args: []
|
||||
}
|
||||
], "Dummy Input data...");
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
browser.pause(5000);
|
||||
|
||||
// Assert output
|
||||
}
|
||||
```
|
||||
|
||||
Run tests with `npm run test:browser` or `npx nightwatch tests/browser/03_cloud_ops.js` (while `npm run start` is running locally).
|
||||
@ -1,37 +1,58 @@
|
||||
# Google Cloud Setup Guide for CyberChef
|
||||
|
||||
To use Google Cloud capabilities (like Google Translate) within CyberChef, you need to configure a Google Cloud Project and obtain an authentication string (either an API Key or an OAuth Token).
|
||||
To use Google Cloud capabilities (like Google Translate, Speech-to-Text, or Cloud Storage) within CyberChef, you need to configure a Google Cloud Project and obtain an authentication string. We strongly recommend using **OAuth 2.0 (Web Application: PKCE)** as it is the most secure method for browser-based applications like CyberChef.
|
||||
|
||||
## Method 1: API Key (Recommended for simplicity)
|
||||
By setting up a personal Cloud Project, your CyberChef instance communicates directly with your Google Cloud without a middleman.
|
||||
|
||||
1. **Create a Project:**
|
||||
- Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
||||
- Click on the project dropdown at the top and select **New Project**.
|
||||
- Name your project (e.g., `cyberchefcloud`) and click **Create**.
|
||||
## Method 1: OAuth 2.0 (Web Application: PKCE) - Highly Recommended
|
||||
|
||||
2. **Enable the API:**
|
||||
- In the Cloud Console search bar, type **"Cloud Translation API"** and select it.
|
||||
- Click **Enable**.
|
||||
- *(Note: You will need to have billing enabled on your Google Cloud account for the Translation API, even for the free tier).*
|
||||
This method uses your Google account and Google Identity Services to grant CyberChef temporary, secure access to your Google Cloud Project. Because CyberChef is a client-side architecture web app without a backend server, we use the Authorization Code Flow with Proof Key for Code Exchange (PKCE).
|
||||
|
||||
3. **Create the API Key:**
|
||||
- Navigate to **APIs & Services > Credentials** in the left sidebar.
|
||||
- Click **+ CREATE CREDENTIALS** at the top and select **API Key**.
|
||||
- Your API Key will be generated. Copy this key; you will need it for the CyberChef "GCP Auth String" input.
|
||||
### Step 1: Create a Project
|
||||
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
|
||||
2. Click on the project dropdown at the top and select **New Project**.
|
||||
3. Name your project (e.g., `cyberchef-personal`) and click **Create**.
|
||||
|
||||
4. **Secure the API Key (CRITICAL):**
|
||||
- Since CyberChef runs entirely in your browser, your API Key will be visible to anyone you share your CyberChef recipe with or who inspects the network traffic. you **MUST** restrict it.
|
||||
- Click on the newly created API Key to edit its settings.
|
||||
- Under **Application restrictions**, select **Websites**.
|
||||
- Under **Website restrictions**, click **ADD**.
|
||||
- Enter the URLs where your CyberChef instance is hosted (e.g., `https://gchq.github.io/CyberChef/*` or `http://localhost:8080/*` for local testing).
|
||||
- Under **API restrictions**, select **Restrict key**.
|
||||
- Check the box for **Cloud Translation API**.
|
||||
- Click **SAVE**.
|
||||
### Step 2: Enable Required APIs
|
||||
You must enable the APIs for any operation you intend to use.
|
||||
1. In the Cloud Console search bar, type the name of the API and select it, then click **Enable**.
|
||||
- For **Google Translate**: Enable "Cloud Translation API" (requires billing enabled).
|
||||
- For **Speech-to-Text**: Enable "Cloud Speech-to-Text API".
|
||||
- For **Read/List Bucket**: Enable "Cloud Storage JSON API" (usually enabled by default).
|
||||
|
||||
## Method 2: Temporary OAuth Token (Recommended for Security)
|
||||
### Step 3: Configure the OAuth Consent Screen
|
||||
1. Navigate to **APIs & Services > OAuth consent screen** in the left sidebar.
|
||||
2. Select **External** user type and click **Create**.
|
||||
3. Fill in the required fields (App Name, User Support Email, Developer Contact Info). You can use your own email for all of these. Click **Save and Continue**.
|
||||
4. Skip the **Scopes** section by clicking **Save and Continue**.
|
||||
5. **CRITICAL STEP: Configure Test Users**.
|
||||
- Navigate to **APIs & Services > OAuth consent screen** > **Audience** > **Test Users** > **Add Users**
|
||||
- Type your own Google email address here (the one you use to login to the Cloud Console).
|
||||
- *(Since we will leave the app in "Testing" state forever, only users listed here can authenticate).*
|
||||
- **WARNING**: Do **NOT** click "Publish App". Leave the publishing status as "Testing". If you publish it, Google will require your app to undergo a verification process.
|
||||
|
||||
If you have the Google Cloud SDK (`gcloud`) installed locally and you are authorized in your project, you can generate a short-lived token to use instead of an API Key. This is much more secure because the token expires automatically.
|
||||
### Step 4: Create OAuth Client ID
|
||||
1. Navigate to **APIs & Services > Credentials** in the left sidebar.
|
||||
2. Click **+ CREATE CREDENTIALS** at the top and select **OAuth client ID**.
|
||||
3. For **Application type**, select **Web application**.
|
||||
4. Name it something like "CyberChef PKCE Client".
|
||||
5. Under **Authorized JavaScript origins**, click **ADD URI**.
|
||||
- For local testing, add: `http://localhost:8080`
|
||||
- For production, add your deployed CyberChef URL (e.g., `https://gchq.github.io`).
|
||||
6. Click **Create**.
|
||||
7. A box will appear with your **Client ID**. Copy this string. *(Note: You do not need the Client Secret).*
|
||||
|
||||
### Step 5: Authenticate in CyberChef
|
||||
1. In CyberChef, add the **Authenticate Google Cloud** operation to the TOP of your recipe.
|
||||
2. Set "Auth Type" to **OAuth 2.0 (Web Application: PKCE)**.
|
||||
3. Paste your Client ID into the "Credentials" box.
|
||||
4. Click **Bake**. CyberChef will popup a secure Google Login window. Your session token is now securely stored in your browser's Session Storage for the duration of the tab being open.
|
||||
|
||||
---
|
||||
|
||||
## Method 2: Temporary Personal Access Token (PAT)
|
||||
|
||||
If you have the Google Cloud SDK (`gcloud`) installed locally and you are authorized in your project, you can generate a short-lived token to use. This is secure because the token expires automatically.
|
||||
|
||||
1. Ensure you are logged into your `gcloud` CLI:
|
||||
```bash
|
||||
@ -41,4 +62,26 @@ If you have the Google Cloud SDK (`gcloud`) installed locally and you are author
|
||||
```bash
|
||||
gcloud auth print-access-token
|
||||
```
|
||||
3. Copy the output token and paste it into the CyberChef "GCP Auth String" input, making sure to change the "Auth Type" dropdown to **OAuth Token**.
|
||||
3. In CyberChef's **Authenticate Google Cloud** operation, set "Auth Type" to **Personal Access Token (PAT)** and paste the token output into the Credentials box.
|
||||
|
||||
---
|
||||
|
||||
## Method 3: API Key (For Simple APIs only)
|
||||
|
||||
*Warning: API Keys are not recommended for operations that access private data like Cloud Storage. They are best suited for simple APIs like Translation.*
|
||||
|
||||
1. **Create the API Key:**
|
||||
- Navigate to **APIs & Services > Credentials** in the left sidebar.
|
||||
- Click **+ CREATE CREDENTIALS** at the top and select **API Key**.
|
||||
- Your API Key will be generated. Copy this key.
|
||||
|
||||
2. **Secure the API Key (CRITICAL):**
|
||||
- Since CyberChef runs entirely in your browser, your API Key will be visible to anyone you share your CyberChef recipe with or who inspects the network traffic. you **MUST** restrict it.
|
||||
- Click on the newly created API Key to edit its settings.
|
||||
- Under **Application restrictions**, select **Websites**.
|
||||
- Under **Website restrictions**, click **ADD**.
|
||||
- Enter the URLs where your CyberChef instance is hosted (e.g., `https://gchq.github.io/CyberChef/*` or `http://localhost:8080/*` for local testing).
|
||||
- Under **API restrictions**, select **Restrict key** and check the relevant API (e.g., **Cloud Translation API**).
|
||||
- Click **SAVE**.
|
||||
|
||||
3. In CyberChef's **Authenticate Google Cloud** operation, set "Auth Type" to **API Key** and paste the key into the Credentials box.
|
||||
|
||||
@ -58,7 +58,9 @@ npm run test
|
||||
|
||||
CyberChef is equipped with Nightwatch for end-to-end testing of features running in a real Chromium browser. This is extremely useful for Cloud Operations that cannot be easily tested within the headless NodeJS environment.
|
||||
|
||||
### Setting up API Keys
|
||||
**Important Note on Authentication:** Google Identity Services (GIS), the library used for the secure **OAuth 2.0 (Web Application: PKCE)** flow, contains strict anti-automation reCAPTCHA logic. Because of this, it is nearly impossible to automate the Google Login popup using standard E2E frameworks like Nightwatch. Therefore, all automated Nightwatch tests utilize either **API Keys** or **Personal Access Tokens (PATs)** to authenticate. Manual testing is still required to verify the PKCE Popup flow works as expected for human users.
|
||||
|
||||
### Setting up Credentials
|
||||
|
||||
To prevent live API keys from being leaked in CI/CD, the Cloud API browser tests rely on local environment variables:
|
||||
1. Copy the `.env.template` file to `.env`:
|
||||
@ -66,6 +68,7 @@ To prevent live API keys from being leaked in CI/CD, the Cloud API browser tests
|
||||
cp .env.template .env
|
||||
```
|
||||
2. Open `.env` and replace `YOUR_API_KEY_HERE` with your actual Google Cloud API key for the `CYBERCHEF_GCP_TEST_KEY` variable. Ensure the API Key restrictions at console.cloud.google.com are set up to accept requests from `http://localhost:8080/*`.
|
||||
3. If tests require a PAT, you must update `CYBERCHEF_GCP_TEST_TOKEN` with a fresh token obtained via `gcloud auth print-access-token` before running the tests.
|
||||
|
||||
### Running Nightwatch Tests
|
||||
|
||||
|
||||
36
docs/Troubleshooting.md
Normal file
36
docs/Troubleshooting.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Google Cloud Troubleshooting Guide
|
||||
|
||||
## Error: "401 Anonymous caller" or "403 Permission 'storage.objects.list' denied"
|
||||
|
||||
If you successfully authenticated with the **Authenticate Google Cloud** operation but received a `401` or `403` error when trying to run an operation like **GCloud List Bucket** or **GCloud Read File**, the issue is **Identity and Access Management (IAM)**.
|
||||
|
||||
Even though your token is valid (Authentication), your Google Account does not have the necessary permissions (Authorization) to perform that specific action on that specific resource.
|
||||
|
||||
### How to Fix IAM Permissions
|
||||
|
||||
1. **Verify Your Google Login Account**
|
||||
Make sure the email address you selected in the Google Login popup is the *exact same* email address that holds permissions in your Google Cloud Project.
|
||||
|
||||
2. **Grant Storage Roles**
|
||||
To read from Cloud Storage, your Google Account must be granted a role that contains the required permissions (like `storage.objects.list` and `storage.objects.get`).
|
||||
|
||||
- Go to the [Google Cloud Console](https://console.cloud.google.com).
|
||||
- Navigate to **IAM & Admin > IAM**.
|
||||
- Find your email address in the list of principals.
|
||||
- Click the pencil icon (Edit principal) next to your name.
|
||||
- Click **Add Another Role**.
|
||||
- To fully utilize the CyberChef Cloud Storage operations, add the **Storage Object Viewer** role (or **Storage Object Admin** if you plan to write files later).
|
||||
- Click **Save**.
|
||||
|
||||
*Note: IAM changes can take anywhere from 60 seconds to a few minutes to propagate across Google's infrastructure.*
|
||||
|
||||
3. **Check Bucket-Level Permissions**
|
||||
If the bucket belongs to a different project than your OAuth Client ID, or has "fine-grained" access control instead of "Uniform" access control, you may need to grant yourself permission directly on the bucket itself.
|
||||
- Navigate to **Cloud Storage > Buckets**.
|
||||
- Click on the bucket you are trying to read.
|
||||
- Go to the **Permissions** tab.
|
||||
- Click **Grant Access**.
|
||||
- Add your Google email address and assign the **Storage Object Viewer** role.
|
||||
|
||||
4. **Retry the Operation**
|
||||
After waiting two minutes for IAM propagation, refresh CyberChef (this forces a new session and drops the old token). Re-authenticate with **Authenticate Google Cloud**, then bake the recipe again.
|
||||
@ -6,17 +6,38 @@
|
||||
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
|
||||
/**
|
||||
* Global store for GCP credentials in this web worker session.
|
||||
*/
|
||||
globalThis.__gcpAuthStore = globalThis.__gcpAuthStore || null;
|
||||
|
||||
/**
|
||||
* Retrieves the currently active GCP credentials.
|
||||
* @returns {Object|null} { authType, authString, quotaProject }
|
||||
*/
|
||||
export function get_gcp_credentials() {
|
||||
if (globalThis.__gcpAuthStore) {
|
||||
return globalThis.__gcpAuthStore;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active GCP credentials for the web worker session.
|
||||
* @param {Object} credObj { authType, authString, quotaProject }
|
||||
*/
|
||||
export function set_gcp_credentials(credObj) {
|
||||
globalThis.__gcpAuthStore = credObj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists objects in a GCS bucket under a given prefix.
|
||||
*
|
||||
* @param {string} bucket - The GCS bucket name (without gs://).
|
||||
* @param {string} prefix - The folder prefix to filter by (e.g. "audio/").
|
||||
* @param {string} authType - "API Key" or "OAuth Token".
|
||||
* @param {Object|string} authStringObj - The auth credential.
|
||||
* @param {string} quotaProject - Optional quota project for ADC OAuth tokens.
|
||||
* @returns {Promise<Array>} Array of GCS object metadata { name, gs_uri, size, contentType }.
|
||||
*/
|
||||
export async function listGCSBucket(bucket, prefix, authType, authStringObj, quotaProject) {
|
||||
export async function listGCSBucket(bucket, prefix) {
|
||||
let url = `https://storage.googleapis.com/storage/v1/b/${encodeURIComponent(bucket)}/o`;
|
||||
const params = new URLSearchParams();
|
||||
if (prefix) params.set("prefix", prefix);
|
||||
@ -25,7 +46,7 @@ export async function listGCSBucket(bucket, prefix, authType, authStringObj, quo
|
||||
if (paramStr) url += `?${paramStr}`;
|
||||
|
||||
const headers = new Headers();
|
||||
const authed = applyGCPAuth(url, headers, authType, authStringObj, quotaProject);
|
||||
const authed = applyGCPAuth(url, headers);
|
||||
|
||||
const response = await fetch(authed.url, { method: "GET", headers: authed.headers, mode: "cors", cache: "no-cache" });
|
||||
let data;
|
||||
@ -54,12 +75,9 @@ export async function listGCSBucket(bucket, prefix, authType, authStringObj, quo
|
||||
* Downloads a file from GCS and returns its raw bytes.
|
||||
*
|
||||
* @param {string} gcsUri - Full gs:// URI of the file.
|
||||
* @param {string} authType - "API Key" or "OAuth Token".
|
||||
* @param {Object|string} authStringObj - The auth credential.
|
||||
* @param {string} quotaProject - Optional quota project.
|
||||
* @returns {Promise<ArrayBuffer>} Raw file bytes.
|
||||
*/
|
||||
export async function readGCSFile(gcsUri, authType, authStringObj, quotaProject) {
|
||||
export async function readGCSFile(gcsUri) {
|
||||
const match = gcsUri.match(/^gs:\/\/([^/]+)\/(.+)$/);
|
||||
if (!match) throw new OperationError(`GCloud Read File: Invalid GCS URI: ${gcsUri}`);
|
||||
const [, bucket, object] = match;
|
||||
@ -67,7 +85,7 @@ export async function readGCSFile(gcsUri, authType, authStringObj, quotaProject)
|
||||
let url = `https://storage.googleapis.com/storage/v1/b/${encodeURIComponent(bucket)}/o/${encodedObject}?alt=media`;
|
||||
|
||||
const headers = new Headers();
|
||||
const authed = applyGCPAuth(url, headers, authType, authStringObj, quotaProject);
|
||||
const authed = applyGCPAuth(url, headers);
|
||||
|
||||
const response = await fetch(authed.url, { method: "GET", headers: authed.headers, mode: "cors", cache: "no-cache" });
|
||||
if (!response.ok) {
|
||||
@ -84,18 +102,15 @@ export async function readGCSFile(gcsUri, authType, authStringObj, quotaProject)
|
||||
* @param {string} bucket - Destination bucket name (without gs://).
|
||||
* @param {string} objectPath - Destination object path within the bucket.
|
||||
* @param {string} content - Text content to write.
|
||||
* @param {string} authType - "API Key" or "OAuth Token".
|
||||
* @param {Object|string} authStringObj - The auth credential.
|
||||
* @param {string} quotaProject - Optional quota project.
|
||||
* @returns {Promise<string>} The gs:// URI of the written file.
|
||||
*/
|
||||
export async function writeGCSFile(bucket, objectPath, content, authType, authStringObj, quotaProject) {
|
||||
export async function writeGCSFile(bucket, objectPath, content) {
|
||||
const encodedObject = encodeURIComponent(objectPath).replace(/%2F/g, "%2F");
|
||||
let url = `https://storage.googleapis.com/upload/storage/v1/b/${encodeURIComponent(bucket)}/o?uploadType=media&name=${encodedObject}`;
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "text/plain; charset=utf-8");
|
||||
const authed = applyGCPAuth(url, headers, authType, authStringObj, quotaProject);
|
||||
const authed = applyGCPAuth(url, headers);
|
||||
|
||||
const response = await fetch(authed.url, {
|
||||
method: "POST",
|
||||
@ -117,15 +132,12 @@ export async function writeGCSFile(bucket, objectPath, content, authType, authSt
|
||||
*
|
||||
* @param {string} operationName - The operation ID (numeric string from the API response).
|
||||
* @param {string} pollUrl - The base polling URL (e.g. https://speech.googleapis.com/v1/operations/).
|
||||
* @param {string} authType - "API Key" or "OAuth Token".
|
||||
* @param {Object|string} authStringObj - The auth credential.
|
||||
* @param {string} quotaProject - Optional quota project.
|
||||
* @param {number} maxMs - Maximum wait time in milliseconds (default 30 minutes).
|
||||
* @param {number} intervalMs - Poll interval in milliseconds (default 10 seconds).
|
||||
* @param {Function} onProgress - Optional callback(elapsedSeconds) called on each poll tick.
|
||||
* @returns {Promise<Object>} The completed operation response object.
|
||||
*/
|
||||
export async function pollLongRunningOperation(operationName, pollUrl, authType, authStringObj, quotaProject, maxMs = 30 * 60 * 1000, intervalMs = 10000, onProgress = null) {
|
||||
export async function pollLongRunningOperation(operationName, pollUrl, maxMs = 30 * 60 * 1000, intervalMs = 10000, onProgress = null) {
|
||||
const startTime = Date.now();
|
||||
const url = `${pollUrl}${operationName}`;
|
||||
|
||||
@ -136,7 +148,7 @@ export async function pollLongRunningOperation(operationName, pollUrl, authType,
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
const authed = applyGCPAuth(url, headers, authType, authStringObj, quotaProject);
|
||||
const authed = applyGCPAuth(url, headers);
|
||||
const response = await fetch(authed.url, { method: "GET", headers: authed.headers, mode: "cors", cache: "no-cache" });
|
||||
|
||||
let data;
|
||||
@ -158,52 +170,26 @@ export async function pollLongRunningOperation(operationName, pollUrl, authType,
|
||||
}
|
||||
|
||||
/**
|
||||
* Common arguments for Google Cloud Platform operations
|
||||
*
|
||||
* Spread these into the `args` array of any new GCP operation.
|
||||
*/
|
||||
export const GCP_AUTH_ARGS = [
|
||||
{
|
||||
"name": "Auth Type",
|
||||
"type": "option",
|
||||
"value": ["API Key", "OAuth Token"]
|
||||
},
|
||||
{
|
||||
"name": "GCP Auth String",
|
||||
"type": "toggleString",
|
||||
"value": "",
|
||||
"toggleValues": ["UTF8", "Latin1", "Base64", "Hex"]
|
||||
},
|
||||
{
|
||||
"name": "Quota Project (ADC only)",
|
||||
"type": "string",
|
||||
"value": ""
|
||||
}
|
||||
];
|
||||
|
||||
/**
|
||||
* Validates and applies GCP authentication to a URL and Headers object.
|
||||
* Validates and applies GCP authentication to a URL and Headers object
|
||||
* using the globally cached credentials from AuthenticateGoogleCloud.
|
||||
*
|
||||
* @param {string} url - The base URL of the API endpoint.
|
||||
* @param {Headers} headers - The Headers object for the request.
|
||||
* @param {string} authType - "API Key" or "OAuth Token".
|
||||
* @param {Object|string} authStringObj - The authentication string (can be a toggleString object).
|
||||
* @param {string} quotaProject - Optional quota project for ADC OAuth tokens.
|
||||
* @returns {Object} An object containing the modified { url, headers }
|
||||
*/
|
||||
export function applyGCPAuth(url, headers, authType, authStringObj, quotaProject) {
|
||||
const authString = typeof authStringObj === "string" ? authStringObj : (authStringObj.string || "");
|
||||
export function applyGCPAuth(url, headers) {
|
||||
const creds = get_gcp_credentials();
|
||||
|
||||
if (!authString) {
|
||||
throw new OperationError("Error: Please provide a valid GCP Auth String (API Key or OAuth Token).");
|
||||
if (!creds || !creds.authString) {
|
||||
throw new OperationError("No Google Cloud credentials found. Please add the 'Authenticate Google Cloud' operation before this one.");
|
||||
}
|
||||
|
||||
if (authType === "API Key") {
|
||||
url += `${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(authString)}`;
|
||||
} else if (authType === "OAuth Token") {
|
||||
headers.set("Authorization", `Bearer ${authString}`);
|
||||
if (quotaProject) {
|
||||
headers.set("x-goog-user-project", quotaProject);
|
||||
if (creds.authType === "API Key") {
|
||||
url += `${url.includes('?') ? '&' : '?'}key=${encodeURIComponent(creds.authString)}`;
|
||||
} else if (creds.authType === "OAuth 2.0 (Web Application: PKCE)" || creds.authType === "Personal Access Token (PAT)") {
|
||||
headers.set("Authorization", `Bearer ${creds.authString}`);
|
||||
if (creds.quotaProject) {
|
||||
headers.set("x-goog-user-project", creds.quotaProject);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
167
src/core/operations/AuthenticateGoogleCloud.mjs
Normal file
167
src/core/operations/AuthenticateGoogleCloud.mjs
Normal file
@ -0,0 +1,167 @@
|
||||
/**
|
||||
* @author CyberChefCloud
|
||||
* @copyright Crown Copyright 2026
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { set_gcp_credentials, get_gcp_credentials } from "../lib/GoogleCloud.mjs";
|
||||
import { isWorkerEnvironment } from "../Utils.mjs";
|
||||
|
||||
/**
|
||||
* Authenticate Google Cloud operation
|
||||
*/
|
||||
class AuthenticateGoogleCloud extends Operation {
|
||||
|
||||
/**
|
||||
* AuthenticateGoogleCloud constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Authenticate Google Cloud";
|
||||
this.module = "Cloud";
|
||||
this.description = [
|
||||
"Authenticates with Google Cloud Platform.",
|
||||
"<br><br>",
|
||||
"This operation should be placed at the top of your recipe. It securely manages credentials for downstream Google Cloud operations (e.g. List Bucket, Read File, Speech-to-Text).",
|
||||
"<br><br>",
|
||||
"You can authenticate using:",
|
||||
"<ul>",
|
||||
"<li><b>OAuth 2.0 (Web Application: PKCE)</b>: The recommended, secure method. Provide your Web Application Client ID. CyberChef will popup a secure Google login window. The token is stored per-session and cleared when you close the tab.</li>",
|
||||
"<li><b>Personal Access Token (PAT)</b>: Provide a short-lived bearer token (e.g. from <code>gcloud auth print-access-token</code>).</li>",
|
||||
"<li><b>API Key</b>: Provide a Google Cloud API key. (Less secure, ensure it is restricted).</li>",
|
||||
"</ul>"
|
||||
].join("\n");
|
||||
this.infoURL = "https://cloud.google.com/docs/authentication";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.manualBake = true; // AutoBake must be disabled to prevent spamming the OAuth API
|
||||
this.args = [
|
||||
{
|
||||
"name": "Auth Type",
|
||||
"type": "option",
|
||||
"value": ["OAuth 2.0 (Web Application: PKCE)", "Personal Access Token (PAT)", "API Key"]
|
||||
},
|
||||
{
|
||||
"name": "Credentials (Client ID, PAT, or API Key)",
|
||||
"type": "toggleString",
|
||||
"value": "",
|
||||
"toggleValues": ["UTF8", "Latin1", "Base64", "Hex"]
|
||||
},
|
||||
{
|
||||
"name": "Quota Project (OAuth only)",
|
||||
"type": "string",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"name": "Output Logs",
|
||||
"type": "boolean",
|
||||
"value": true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [authType, credObj, quotaProject, outputLogs] = args;
|
||||
const credString = typeof credObj === "string" ? credObj : (credObj.string || "");
|
||||
|
||||
if (!credString) {
|
||||
throw new OperationError("Please provide Google Cloud credentials (Client ID, PAT, or API Key).");
|
||||
}
|
||||
|
||||
let logs = "";
|
||||
const log = (msg) => {
|
||||
if (outputLogs) logs += msg + "\n";
|
||||
// Also send to the UI status bar if in worker
|
||||
if (isWorkerEnvironment()) self.sendStatusMessage(msg);
|
||||
};
|
||||
|
||||
log("Starting Google Cloud Authentication...");
|
||||
|
||||
// If not using the Web App PKCE Flow, just cache the PAT/API Key and return.
|
||||
if (authType !== "OAuth 2.0 (Web Application: PKCE)") {
|
||||
set_gcp_credentials({
|
||||
authType: authType,
|
||||
authString: credString,
|
||||
quotaProject: quotaProject
|
||||
});
|
||||
log(`Successfully configured ${authType}.`);
|
||||
return logs ? (logs + "\n" + input) : input;
|
||||
}
|
||||
|
||||
// --- OAuth 2.0 Web Application (PKCE) Flow ---
|
||||
|
||||
// 1. Check if we already have a valid token for this Client ID in the session
|
||||
const existingCreds = get_gcp_credentials();
|
||||
if (existingCreds && existingCreds.authType === "OAuth 2.0 (Web Application: PKCE)" && existingCreds.clientId === credString) {
|
||||
if (existingCreds.expiresAt > Date.now()) {
|
||||
log("Reusing valid existing OAuth session token.");
|
||||
return logs ? (logs + "\n" + input) : input;
|
||||
}
|
||||
log("Existing OAuth token expired. A new authorization is required.");
|
||||
}
|
||||
|
||||
// 2. Pause the Web Worker and ask the Main UI to pop the GIS login window
|
||||
log("Requesting Google Login Popup (Check your browser windows)...");
|
||||
|
||||
// We use a Promise to halt the `run` method until the UI sends back the token
|
||||
const tokenData = await new Promise((resolve, reject) => {
|
||||
|
||||
// Temporary message listener to catch the response from the UI
|
||||
const messageHandler = function (e) {
|
||||
const r = e.data;
|
||||
if (r.action === "gcpAuthResponse") {
|
||||
self.removeEventListener("message", messageHandler);
|
||||
if (r.data.error) {
|
||||
reject(new OperationError(`Google OAuth Error: ${r.data.error}`));
|
||||
} else if (r.data.token) {
|
||||
resolve(r.data);
|
||||
} else {
|
||||
reject(new OperationError("Google OAuth Error: UI returned unexpected empty token payload."));
|
||||
}
|
||||
}
|
||||
};
|
||||
self.addEventListener("message", messageHandler);
|
||||
|
||||
// Trigger the UI
|
||||
// Assuming this operation is executed within a ChefWorker, we bubble up `gcpAuthRequest`
|
||||
// and we must include `inputNum` so WorkerWaiter knows which worker to send the response back to.
|
||||
if (!isWorkerEnvironment()) {
|
||||
reject(new OperationError("OAuth PKCE Flow can only run in a Web Worker environment. For manual node testing, use PAT or API Key auth types."));
|
||||
return;
|
||||
}
|
||||
|
||||
self.postMessage({
|
||||
action: "gcpAuthRequest",
|
||||
data: {
|
||||
clientId: credString,
|
||||
inputNum: self.inputNum || 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
log("Authorization successful!");
|
||||
log(`Access token retrieved. Expires in ${tokenData.expires_in} seconds.`);
|
||||
|
||||
// 3. Cache the credentials for downstream operations
|
||||
set_gcp_credentials({
|
||||
authType: "OAuth 2.0 (Web Application: PKCE)",
|
||||
authString: tokenData.token,
|
||||
quotaProject: quotaProject,
|
||||
clientId: credString,
|
||||
expiresAt: Date.now() + (tokenData.expires_in * 1000)
|
||||
});
|
||||
|
||||
return logs ? (logs + "\n" + input) : input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AuthenticateGoogleCloud;
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { GCP_AUTH_ARGS, applyGCPAuth, listGCSBucket } from "../lib/GoogleCloud.mjs";
|
||||
import { listGCSBucket } from "../lib/GoogleCloud.mjs";
|
||||
|
||||
/**
|
||||
* GCloud List Bucket operation
|
||||
@ -44,8 +44,7 @@ class GCloudListBucket extends Operation {
|
||||
"name": "Output Format",
|
||||
"type": "option",
|
||||
"value": ["GCS URIs (one per line)", "Filenames only", "JSON"]
|
||||
},
|
||||
...GCP_AUTH_ARGS
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -55,7 +54,7 @@ class GCloudListBucket extends Operation {
|
||||
* @returns {string}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [prefix, outputFormat, authType, authStringObj, quotaProject] = args;
|
||||
const [prefix, outputFormat] = args;
|
||||
|
||||
if (!input || !input.trim()) throw new OperationError("Please provide a GCS bucket name.");
|
||||
|
||||
@ -63,7 +62,7 @@ class GCloudListBucket extends Operation {
|
||||
let bucket = input.trim().replace(/^gs:\/\//, "").split("/")[0];
|
||||
|
||||
try {
|
||||
const items = await listGCSBucket(bucket, prefix, authType, authStringObj, quotaProject);
|
||||
const items = await listGCSBucket(bucket, prefix);
|
||||
|
||||
if (items.length === 0) {
|
||||
return `No objects found in gs://${bucket}/${prefix || ""}`;
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { GCP_AUTH_ARGS, readGCSFile } from "../lib/GoogleCloud.mjs";
|
||||
import { readGCSFile } from "../lib/GoogleCloud.mjs";
|
||||
|
||||
/**
|
||||
* GCloud Read File operation
|
||||
@ -35,9 +35,7 @@ class GCloudReadFile extends Operation {
|
||||
this.inputType = "string";
|
||||
this.outputType = "ArrayBuffer";
|
||||
this.manualBake = true;
|
||||
this.args = [
|
||||
...GCP_AUTH_ARGS
|
||||
];
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,15 +44,13 @@ class GCloudReadFile extends Operation {
|
||||
* @returns {ArrayBuffer}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [authType, authStringObj, quotaProject] = args;
|
||||
|
||||
const uri = input.trim();
|
||||
if (!uri.startsWith("gs://")) {
|
||||
throw new OperationError("Input must be a GCS URI starting with gs://");
|
||||
}
|
||||
|
||||
try {
|
||||
return await readGCSFile(uri, authType, authStringObj, quotaProject);
|
||||
return await readGCSFile(uri);
|
||||
} catch (e) {
|
||||
if (e.name === "OperationError") throw e;
|
||||
throw new OperationError(e.message || e.toString());
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { GCP_AUTH_ARGS, applyGCPAuth, pollLongRunningOperation, writeGCSFile } from "../lib/GoogleCloud.mjs";
|
||||
import { applyGCPAuth, pollLongRunningOperation, writeGCSFile } from "../lib/GoogleCloud.mjs";
|
||||
|
||||
/**
|
||||
* GCloud Speech to Text operation
|
||||
@ -73,8 +73,7 @@ class GCloudSpeechToText extends Operation {
|
||||
"name": "Max Poll Minutes",
|
||||
"type": "number",
|
||||
"value": 30
|
||||
},
|
||||
...GCP_AUTH_ARGS
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -85,16 +84,12 @@ class GCloudSpeechToText extends Operation {
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [
|
||||
inputMode, languageCode, model, outputDest, outputBucket, maxPollMinutes,
|
||||
authType, authStringObj, quotaProject
|
||||
inputMode, languageCode, model, outputDest, outputBucket, maxPollMinutes
|
||||
] = args;
|
||||
|
||||
const uri = input.trim();
|
||||
if (!uri) throw new OperationError("Please provide a GCS URI or Base64 audio input.");
|
||||
|
||||
const authString = typeof authStringObj === "string" ? authStringObj : (authStringObj.string || "");
|
||||
if (!authString) throw new OperationError("Please provide a valid GCP Auth String (API Key or OAuth Token).");
|
||||
|
||||
const maxMs = maxPollMinutes * 60 * 1000;
|
||||
let transcript;
|
||||
|
||||
@ -102,10 +97,10 @@ class GCloudSpeechToText extends Operation {
|
||||
if (!uri.startsWith("gs://")) {
|
||||
throw new OperationError("Input Mode is set to GCS URI but input does not start with gs://");
|
||||
}
|
||||
transcript = await this._transcribeGcsUri(uri, languageCode, model, authType, authStringObj, quotaProject, maxMs);
|
||||
transcript = await this._transcribeGcsUri(uri, languageCode, model, maxMs);
|
||||
} else {
|
||||
// Raw audio bytes (Base64)
|
||||
transcript = await this._transcribeRawAudio(uri, languageCode, model, authType, authStringObj, quotaProject);
|
||||
transcript = await this._transcribeRawAudio(uri, languageCode, model);
|
||||
}
|
||||
|
||||
if (outputDest === "Write to GCS") {
|
||||
@ -115,7 +110,7 @@ class GCloudSpeechToText extends Operation {
|
||||
: "raw_audio";
|
||||
|
||||
const objectPath = `output/audio/${sourceFilename}/speech-to-text/text.txt`;
|
||||
const destUri = await writeGCSFile(outputBucket, objectPath, transcript, authType, authStringObj, quotaProject);
|
||||
const destUri = await writeGCSFile(outputBucket, objectPath, transcript);
|
||||
return destUri;
|
||||
}
|
||||
|
||||
@ -128,17 +123,14 @@ class GCloudSpeechToText extends Operation {
|
||||
* @param {string} gcsUri
|
||||
* @param {string} languageCode
|
||||
* @param {string} model
|
||||
* @param {string} authType
|
||||
* @param {Object|string} authStringObj
|
||||
* @param {string} quotaProject
|
||||
* @param {number} maxMs
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async _transcribeGcsUri(gcsUri, languageCode, model, authType, authStringObj, quotaProject, maxMs) {
|
||||
async _transcribeGcsUri(gcsUri, languageCode, model, maxMs) {
|
||||
let url = "https://speech.googleapis.com/v1/speech:longrunningrecognize";
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||
const authed = applyGCPAuth(url, headers, authType, authStringObj, quotaProject);
|
||||
const authed = applyGCPAuth(url, headers);
|
||||
|
||||
const body = JSON.stringify({
|
||||
config: {
|
||||
@ -174,9 +166,6 @@ class GCloudSpeechToText extends Operation {
|
||||
const completed = await pollLongRunningOperation(
|
||||
operationName,
|
||||
POLL_URL,
|
||||
authType,
|
||||
authStringObj,
|
||||
quotaProject,
|
||||
maxMs,
|
||||
10000,
|
||||
(elapsedSec) => {
|
||||
@ -195,16 +184,13 @@ class GCloudSpeechToText extends Operation {
|
||||
* @param {string} base64Audio
|
||||
* @param {string} languageCode
|
||||
* @param {string} model
|
||||
* @param {string} authType
|
||||
* @param {Object|string} authStringObj
|
||||
* @param {string} quotaProject
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async _transcribeRawAudio(base64Audio, languageCode, model, authType, authStringObj, quotaProject) {
|
||||
async _transcribeRawAudio(base64Audio, languageCode, model) {
|
||||
let url = "https://speech.googleapis.com/v1/speech:recognize";
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||
const authed = applyGCPAuth(url, headers, authType, authStringObj, quotaProject);
|
||||
const authed = applyGCPAuth(url, headers);
|
||||
|
||||
const body = JSON.stringify({
|
||||
config: {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation.mjs";
|
||||
import OperationError from "../errors/OperationError.mjs";
|
||||
import { GCP_AUTH_ARGS, applyGCPAuth } from "../lib/GoogleCloud.mjs";
|
||||
import { applyGCPAuth } from "../lib/GoogleCloud.mjs";
|
||||
|
||||
/**
|
||||
* Google Translate operation
|
||||
@ -41,8 +41,7 @@ class GoogleTranslate extends Operation {
|
||||
"name": "Target Language (ISO-639-1)",
|
||||
"type": "string",
|
||||
"value": "es"
|
||||
},
|
||||
...GCP_AUTH_ARGS
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@ -52,7 +51,7 @@ class GoogleTranslate extends Operation {
|
||||
* @returns {string}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [sourceLanguage, targetLanguage, authType, authStringObj, quotaProject] = args;
|
||||
const [sourceLanguage, targetLanguage] = args;
|
||||
|
||||
if (input.length === 0) return "";
|
||||
|
||||
@ -60,7 +59,7 @@ class GoogleTranslate extends Operation {
|
||||
let headers = new Headers();
|
||||
headers.set("Content-Type", "application/json; charset=utf-8");
|
||||
|
||||
({ url, headers } = applyGCPAuth(url, headers, authType, authStringObj, quotaProject));
|
||||
({ url, headers } = applyGCPAuth(url, headers));
|
||||
|
||||
const body = JSON.stringify({
|
||||
q: input,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -7,8 +7,8 @@
|
||||
|
||||
import LoaderWorker from "worker-loader?inline=no-fallback!../workers/LoaderWorker.js";
|
||||
import InputWorker from "worker-loader?inline=no-fallback!../workers/InputWorker.mjs";
|
||||
import Utils, {debounce} from "../../core/Utils.mjs";
|
||||
import {toBase64} from "../../core/lib/Base64.mjs";
|
||||
import Utils, { debounce } from "../../core/Utils.mjs";
|
||||
import { toBase64 } from "../../core/lib/Base64.mjs";
|
||||
import cptable from "codepage";
|
||||
|
||||
import {
|
||||
@ -40,9 +40,9 @@ import {
|
||||
highlightSelectionMatches
|
||||
} from "@codemirror/search";
|
||||
|
||||
import {statusBar} from "../utils/statusBar.mjs";
|
||||
import {fileDetailsPanel} from "../utils/fileDetails.mjs";
|
||||
import {eolCodeToSeq, eolCodeToName, renderSpecialChar} from "../utils/editorUtils.mjs";
|
||||
import { statusBar } from "../utils/statusBar.mjs";
|
||||
import { fileDetailsPanel } from "../utils/fileDetails.mjs";
|
||||
import { eolCodeToSeq, eolCodeToName, renderSpecialChar } from "../utils/editorUtils.mjs";
|
||||
|
||||
|
||||
/**
|
||||
@ -109,7 +109,7 @@ class InputWaiter {
|
||||
dropCursor(),
|
||||
bracketMatching(),
|
||||
highlightSelectionMatches(),
|
||||
search({top: true}),
|
||||
search({ top: true }),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
|
||||
// Custom extensions
|
||||
@ -191,7 +191,7 @@ class InputWaiter {
|
||||
* @param {string} eol
|
||||
* @param {boolean} [manual=false]
|
||||
*/
|
||||
eolChange(eol, manual=false) {
|
||||
eolChange(eol, manual = false) {
|
||||
const eolVal = eolCodeToSeq[eol];
|
||||
if (eolVal === undefined) return;
|
||||
|
||||
@ -236,7 +236,7 @@ class InputWaiter {
|
||||
* @param {boolean} [manual=false] - Flag to indicate the encoding was set by the user
|
||||
* @param {boolean} [internal=false] - Flag to indicate this was set internally, i.e. by loading from URI
|
||||
*/
|
||||
chrEncChange(chrEncVal, manual=false, internal=false) {
|
||||
chrEncChange(chrEncVal, manual = false, internal = false) {
|
||||
if (typeof chrEncVal !== "number") return;
|
||||
this.inputChrEnc = chrEncVal;
|
||||
this.encodingState = manual ? 2 : this.encodingState;
|
||||
@ -288,7 +288,7 @@ class InputWaiter {
|
||||
* @param {string} data
|
||||
* @param {boolean} [silent=false]
|
||||
*/
|
||||
setInput(data, silent=false) {
|
||||
setInput(data, silent = false) {
|
||||
const lineLengthThreshold = 131072; // 128KB
|
||||
let wrap = this.app.options.wordWrap;
|
||||
if (data.length > lineLengthThreshold) {
|
||||
@ -617,8 +617,8 @@ class InputWaiter {
|
||||
* @param {string} eolSequence
|
||||
* @param {boolean} [silent=false] - If false, fires the manager statechange event
|
||||
*/
|
||||
async set(inputNum, inputData, silent=false) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
async set(inputNum, inputData, silent = false) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
const activeTab = this.manager.tabs.getActiveTab("input");
|
||||
if (inputNum !== activeTab) {
|
||||
this.changeTab(inputNum, this.app.options.syncTabs);
|
||||
@ -775,7 +775,7 @@ class InputWaiter {
|
||||
* @param {number} inputNum
|
||||
* @param {string | ArrayBuffer} value
|
||||
*/
|
||||
updateInputValue(inputNum, value, force=false) {
|
||||
updateInputValue(inputNum, value, force = false) {
|
||||
// Prepare the value as a buffer (full value) and a string sample (up to 4096 bytes)
|
||||
let buffer;
|
||||
let stringSample = "";
|
||||
@ -913,7 +913,7 @@ class InputWaiter {
|
||||
else if (inputLength < 1000000) delay = 200;
|
||||
else delay = 500;
|
||||
|
||||
debounce(function(e) {
|
||||
debounce(function (e) {
|
||||
const value = this.getInput();
|
||||
const activeTab = this.manager.tabs.getActiveTab("input");
|
||||
|
||||
@ -1241,7 +1241,7 @@ class InputWaiter {
|
||||
}
|
||||
|
||||
if (loaded < total && autoRefresh) {
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
this.inputWorker.postMessage({
|
||||
action: "getLoadProgress",
|
||||
data: this.manager.tabs.getActiveTab("input")
|
||||
@ -1256,7 +1256,7 @@ class InputWaiter {
|
||||
* @param {number} inputNum - The inputNum of the tab to change to
|
||||
* @param {boolean} [changeOutput=false] - If true, also changes the output
|
||||
*/
|
||||
changeTab(inputNum, changeOutput=false) {
|
||||
changeTab(inputNum, changeOutput = false) {
|
||||
if (this.manager.tabs.getTabItem(inputNum, "input") !== null) {
|
||||
this.manager.tabs.changeTab(inputNum, "input");
|
||||
this.inputWorker.postMessage({
|
||||
@ -1368,7 +1368,7 @@ class InputWaiter {
|
||||
* Sends a message to the inputWorker to add a new input.
|
||||
* @param {boolean} [changeTab=false] - If true, changes the tab to the new input
|
||||
*/
|
||||
addInput(changeTab=false) {
|
||||
addInput(changeTab = false) {
|
||||
if (!this.inputWorker) return;
|
||||
this.inputWorker.postMessage({
|
||||
action: "addInput",
|
||||
@ -1411,7 +1411,7 @@ class InputWaiter {
|
||||
* @param {number} inputNum - The inputNum of the new tab
|
||||
* @param {boolean} [changeTab=true] - If true, changes to the new tab once it's been added
|
||||
*/
|
||||
addTab(inputNum, changeTab=true) {
|
||||
addTab(inputNum, changeTab = true) {
|
||||
const tabsWrapper = document.getElementById("input-tabs"),
|
||||
numTabs = tabsWrapper.children.length;
|
||||
|
||||
@ -1517,7 +1517,7 @@ class InputWaiter {
|
||||
this.mousedown = true;
|
||||
this.changeTabRight();
|
||||
const time = 200;
|
||||
const func = function(time) {
|
||||
const func = function (time) {
|
||||
if (this.mousedown) {
|
||||
this.changeTabRight();
|
||||
const newTime = (time > 50) ? time - 10 : 50;
|
||||
@ -1534,7 +1534,7 @@ class InputWaiter {
|
||||
this.mousedown = true;
|
||||
this.changeTabLeft();
|
||||
const time = 200;
|
||||
const func = function(time) {
|
||||
const func = function (time) {
|
||||
if (this.mousedown) {
|
||||
this.changeTabLeft();
|
||||
const newTime = (time > 50) ? time - 10 : 50;
|
||||
@ -1686,7 +1686,7 @@ class InputWaiter {
|
||||
*/
|
||||
handlePostMessage(e) {
|
||||
log.debug(e);
|
||||
if ("data" in e && "id" in e.data && "value" in e.data) {
|
||||
if (e && e.data && typeof e.data === "object" && "id" in e.data && "value" in e.data) {
|
||||
if (e.data.id === "setInput") {
|
||||
this.setInput(e.data.value);
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ class WorkerWaiter {
|
||||
if (index > 0) {
|
||||
docURL = docURL.substring(0, index);
|
||||
}
|
||||
newWorker.postMessage({"action": "docURL", "data": docURL});
|
||||
newWorker.postMessage({ "action": "docURL", "data": docURL });
|
||||
|
||||
|
||||
// Store the worker, whether or not it's active, and the inputNum as an object
|
||||
@ -132,7 +132,7 @@ class WorkerWaiter {
|
||||
* @param {boolean} [setActive=true] - If true, set the worker status to active
|
||||
* @returns {number} - The index of the ChefWorker
|
||||
*/
|
||||
getInactiveChefWorker(setActive=true) {
|
||||
getInactiveChefWorker(setActive = true) {
|
||||
for (let i = 0; i < this.chefWorkers.length; i++) {
|
||||
if (!this.chefWorkers[i].active) {
|
||||
this.chefWorkers[i].active = setActive;
|
||||
@ -252,6 +252,39 @@ class WorkerWaiter {
|
||||
case "highlightsCalculated":
|
||||
this.manager.highlighter.displayHighlights(r.data.pos, r.data.direction);
|
||||
break;
|
||||
case "gcpAuthRequest":
|
||||
log.debug("Initializing Google Identity Services for PKCE Auth...");
|
||||
try {
|
||||
const client = google.accounts.oauth2.initTokenClient({
|
||||
client_id: r.data.clientId,
|
||||
scope: "https://www.googleapis.com/auth/cloud-platform",
|
||||
callback: (response) => {
|
||||
if (response.error) {
|
||||
currentWorker.worker.postMessage({
|
||||
action: "gcpAuthResponse",
|
||||
data: { error: response.error_description || response.error }
|
||||
});
|
||||
} else if (response.access_token) {
|
||||
// Store temporarily in sessionStorage for anti-XSS posture
|
||||
sessionStorage.setItem("gcp_access_token", response.access_token);
|
||||
currentWorker.worker.postMessage({
|
||||
action: "gcpAuthResponse",
|
||||
data: {
|
||||
token: response.access_token,
|
||||
expires_in: response.expires_in
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
client.requestAccessToken();
|
||||
} catch (e) {
|
||||
currentWorker.worker.postMessage({
|
||||
action: "gcpAuthResponse",
|
||||
data: { error: `Failed to initialize Google Auth popup: ${e.message}` }
|
||||
});
|
||||
}
|
||||
break;
|
||||
default:
|
||||
log.error("Unrecognised message from ChefWorker", e);
|
||||
break;
|
||||
@ -350,7 +383,7 @@ class WorkerWaiter {
|
||||
* @param {boolean} [silent=false] - If true, don't set the output
|
||||
* @param {boolean} [killAll=false] - If true, kills all chefWorkers regardless of status
|
||||
*/
|
||||
cancelBake(silent=false, killAll=false) {
|
||||
cancelBake(silent = false, killAll = false) {
|
||||
const deactiveOutputs = new Set();
|
||||
|
||||
for (let i = this.chefWorkers.length - 1; i >= 0; i--) {
|
||||
@ -852,7 +885,7 @@ class WorkerWaiter {
|
||||
}
|
||||
|
||||
if (progress.total !== progress.baked) {
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
this.displayProgress();
|
||||
}.bind(this), 100);
|
||||
}
|
||||
|
||||
@ -24,13 +24,24 @@ module.exports = {
|
||||
},
|
||||
|
||||
"Google Translate: Missing Key Validation": function (browser) {
|
||||
browserUtils.loadRecipe(browser, "Google Translate", "Hello World", [
|
||||
"en",
|
||||
"es",
|
||||
"API Key",
|
||||
{ option: "UTF8", string: "" },
|
||||
""
|
||||
]);
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"API Key",
|
||||
{ option: "UTF8", string: "" },
|
||||
"",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "Google Translate",
|
||||
args: [
|
||||
"en",
|
||||
"es"
|
||||
]
|
||||
}
|
||||
], "Hello World");
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
@ -38,38 +49,50 @@ module.exports = {
|
||||
browser.execute(function () {
|
||||
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||
}, [], function ({ value }) {
|
||||
browser.assert.ok(value.includes("Please provide a valid GCP Auth String"));
|
||||
browser.assert.ok(value.includes("No Google Cloud credentials found") || value.includes("Please provide Google Cloud credentials"), "Expected auth missing error.");
|
||||
});
|
||||
},
|
||||
|
||||
"Google Translate: Successful OAuth Token Translation": function (browser) {
|
||||
|
||||
"Google Translate: Successful PAT Translation": function (browser) {
|
||||
let testToken = process.env.CYBERCHEF_GCP_TEST_TOKEN;
|
||||
|
||||
if (!testToken || testToken === "YOUR_OAUTH_TOKEN_HERE") {
|
||||
try {
|
||||
testToken = require('child_process').execSync('gcloud auth print-access-token', { stdio: 'pipe', encoding: 'utf-8' }).trim();
|
||||
} catch (e) {
|
||||
console.log("No valid CYBERCHEF_GCP_TEST_TOKEN found and gcloud failed. Skipping live API test.");
|
||||
console.log("No valid CYBERCHEF_GCP_TEST_TOKEN found and gcloud failed. Skipping live PAT API test.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
browserUtils.loadRecipe(browser, "Google Translate", "Hello", [
|
||||
"en",
|
||||
"es",
|
||||
"OAuth Token",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud"
|
||||
]);
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"Personal Access Token (PAT)",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "Google Translate",
|
||||
args: [
|
||||
"en",
|
||||
"es"
|
||||
]
|
||||
}
|
||||
], "Hello");
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
browser.pause(2000);
|
||||
browser.saveScreenshot("tests/browser/output/success_oauth_debug.png");
|
||||
browser.saveScreenshot("tests/browser/output/success_pat_debug.png");
|
||||
browser.execute(function () {
|
||||
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||
}, [], function ({ value }) {
|
||||
browser.assert.equal(value, "Hola");
|
||||
browser.assert.ok(value.includes("Hola"), "Expected translation 'Hola'");
|
||||
});
|
||||
},
|
||||
|
||||
@ -81,13 +104,24 @@ module.exports = {
|
||||
return;
|
||||
}
|
||||
|
||||
browserUtils.loadRecipe(browser, "Google Translate", "Hello", [
|
||||
"en",
|
||||
"es",
|
||||
"API Key",
|
||||
{ option: "UTF8", string: testKey },
|
||||
""
|
||||
]);
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"API Key",
|
||||
{ option: "UTF8", string: testKey },
|
||||
"",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "Google Translate",
|
||||
args: [
|
||||
"en",
|
||||
"es"
|
||||
]
|
||||
}
|
||||
], "Hello");
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
@ -96,20 +130,31 @@ module.exports = {
|
||||
browser.execute(function () {
|
||||
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||
}, [], function ({ value }) {
|
||||
browser.assert.equal(value, "Hola");
|
||||
browser.assert.ok(value.includes("Hola"), "Expected translation 'Hola'");
|
||||
});
|
||||
},
|
||||
|
||||
// ─── GCloud List Bucket ────────────────────────────────────────────────────
|
||||
|
||||
"GCloud List Bucket: Missing Key Validation": function (browser) {
|
||||
browserUtils.loadRecipe(browser, "GCloud List Bucket", "cyber-chef-cloud-examples", [
|
||||
"audio/",
|
||||
"GCS URIs (one per line)",
|
||||
"API Key",
|
||||
{ option: "UTF8", string: "" },
|
||||
""
|
||||
]);
|
||||
"GCloud List Bucket: Missing Creds Validation": function (browser) {
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"API Key",
|
||||
{ option: "UTF8", string: "" },
|
||||
"",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "GCloud List Bucket",
|
||||
args: [
|
||||
"audio/",
|
||||
"GCS URIs (one per line)"
|
||||
]
|
||||
}
|
||||
], "cyber-chef-cloud-examples");
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
@ -118,7 +163,7 @@ module.exports = {
|
||||
browser.execute(function () {
|
||||
return window.app.manager.output.outputEditorView.state.doc.toString();
|
||||
}, [], function ({ value }) {
|
||||
browser.assert.ok(value.includes("Please provide a valid GCP Auth String"));
|
||||
browser.assert.ok(value.includes("No Google Cloud credentials found") || value.includes("Please provide Google Cloud credentials"));
|
||||
});
|
||||
},
|
||||
|
||||
@ -134,13 +179,24 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
browserUtils.loadRecipe(browser, "GCloud List Bucket", "cyber-chef-cloud-examples", [
|
||||
"audio/",
|
||||
"GCS URIs (one per line)",
|
||||
"OAuth Token",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud"
|
||||
]);
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"Personal Access Token (PAT)",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "GCloud List Bucket",
|
||||
args: [
|
||||
"audio/",
|
||||
"GCS URIs (one per line)"
|
||||
]
|
||||
}
|
||||
], "cyber-chef-cloud-examples");
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
@ -170,17 +226,29 @@ module.exports = {
|
||||
|
||||
const gcsUri = "gs://cyber-chef-cloud-examples/audio/she_achieves_great_results_f55548.mp3";
|
||||
|
||||
browserUtils.loadRecipe(browser, "GCloud Speech to Text", gcsUri, [
|
||||
"GCS URI (gs://...)",
|
||||
"en-US",
|
||||
"latest_long",
|
||||
"Return to CyberChef",
|
||||
"cyber-chef-cloud-examples",
|
||||
30,
|
||||
"OAuth Token",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud"
|
||||
]);
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"Personal Access Token (PAT)",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "GCloud Speech to Text",
|
||||
args: [
|
||||
"GCS URI (gs://...)",
|
||||
"en-US",
|
||||
"latest_long",
|
||||
"Return to CyberChef",
|
||||
"cyber-chef-cloud-examples",
|
||||
30
|
||||
]
|
||||
}
|
||||
], gcsUri);
|
||||
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
@ -211,17 +279,28 @@ module.exports = {
|
||||
|
||||
const gcsUri = "gs://cyber-chef-cloud-examples/audio/she_achieves_great_results_f55548.mp3";
|
||||
|
||||
browserUtils.loadRecipe(browser, "GCloud Speech to Text", gcsUri, [
|
||||
"GCS URI (gs://...)",
|
||||
"en-US",
|
||||
"latest_long",
|
||||
"Write to GCS",
|
||||
"cyber-chef-cloud-examples",
|
||||
30,
|
||||
"OAuth Token",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud"
|
||||
]);
|
||||
browserUtils.loadRecipeConfig(browser, [
|
||||
{
|
||||
op: "Authenticate Google Cloud",
|
||||
args: [
|
||||
"Personal Access Token (PAT)",
|
||||
{ option: "UTF8", string: testToken },
|
||||
"cyberchefcloud",
|
||||
true
|
||||
]
|
||||
},
|
||||
{
|
||||
op: "GCloud Speech to Text",
|
||||
args: [
|
||||
"GCS URI (gs://...)",
|
||||
"en-US",
|
||||
"latest_long",
|
||||
"Write to GCS",
|
||||
"cyber-chef-cloud-examples",
|
||||
30
|
||||
]
|
||||
}
|
||||
], gcsUri);
|
||||
|
||||
browser.waitForElementNotVisible("#snackbar-container", 6000);
|
||||
browserUtils.bake(browser);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user