Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AP-Guides #28

Merged
merged 7 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .vitepress/sidebars/concepts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ export const conceptsSidebar: DefaultTheme.SidebarItem[] = [
},
],
},
{
text: "Frontend",
items: [
{
text: "UI Styling",
link: "/concepts/frontend/ui",
},
],
},
{
text: "Backend",
items: [
Expand Down
18 changes: 15 additions & 3 deletions .vitepress/sidebars/guides.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,39 @@ export const guidesSidebar: DefaultTheme.SidebarItem[] = [
text: "Customizing Context Menus",
link: "/guides/components/menu",
},
{
text: "Handling Backend Events",
link: "/guides/components/backend_events",
},
{
text: "Using the Component Library",
link: "/guides/components/styling",
},
],
},
{
text: "Backend",
items: [
{
text: "Creating a Custom Endpoint",
link: "/guides/components/endpoint",
text: "Creating and Calling a Custom Function",
link: "/guides/components/rpc",
},
{
text: "Sending HTTP Requests",
link: "/guides/components/request",
},
{
text: "Sending Events",
text: "Sending Events to the Frontend",
link: "/guides/components/events",
},
{
text: "Spawning a Process",
link: "/guides/components/spawning_process",
},
{
text: "Storing Data in SQLite",
link: "/guides/components/sqlite",
},
{
text: "Using Invalid UTF-8",
link: "/guides/components/utf",
Expand Down
Binary file added src/_images/toast_messages.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/concepts/frontend/ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# UI Styling

When using the Caido [community plugin template](https://github.com/caido-community/create-plugin), you have the option to customize the user-interface.

For this, Caido uses the following dependencies:

- [Vue.js](https://vuejs.org/): A JavaScript framework for building user-interfaces.
- [PrimeVue](https://primevue.org/): A library for Vue.js that provides a wide range of pre-built UI components.
- [TailWind CSS](https://tailwindcss.com/): A CSS framework that allows applications to be styled using predefined utility classes.

The `@caido/primevue` and `@caido/tailwindcss` packages are custom adaptations that are configured to align with the theme of the Caido application.

The `tailwindcss-primeui` package acts as a bridge, allowing PrimeVue components to be styled using Tailwind CSS.

Within the `tailwind.config.ts` file, both the `tailwindPrimeui` and `tailwindCaido` plugins are imported to inject the necessary classes.

By leveraging these technologies and creating relationships between them, we are able to customize and maintain consistency in the appearance of the frontend component of Caido plugins.

## What's next?

[Learn how to use the PrimeVue library to build a plugin frontend.](/guides/components/styling.md)
4 changes: 4 additions & 0 deletions src/concepts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

Here you will find explanations of the core concepts that underpin Caido plugins.

## Frontend

- [UI Styling](./frontend/ui.md) - The Caido user-interface.

## Backend

- [Dealing with Binary Data](./backend/binary.md) - Using invalid UTF-8 in Caido.
Expand Down
63 changes: 63 additions & 0 deletions src/guides/components/backend_events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Handling Backend Events

In this guide, you will learn how to handle backend events in the frontend of your Caido plugin.

This can be accomplished using the three event handlers provided by the SDK:

## onProjectChange

An event will be triggered when the active [Project](https://docs.caido.io/quickstart/beginner_guide/first_steps_with_caido/project.html) changes.

### /packages/backend/src/index.ts

``` ts
import type { DefineAPI, SDK } from "caido:plugin";

export type API = DefineAPI<{}>;

let previousProject: string | null = null;

export function init(sdk: SDK<API>) {
sdk.events.onProjectChange((sdk, project) => {
const newProjectName = project?.getName() ?? null;
sdk.console.log(`Project changed from "${previousProject}" to "${newProjectName}."`);
previousProject = newProjectName;
});
}
```

## The Result

``` txt
Project changed from "Caido" to "Example".
```

## onInterceptRequest and onInterceptResponse

An event will be triggered with `onInterceptRequest` and `onInterceptResponse` when a request or response is proxied through Caido respectively.

### /packages/backend/src/index.ts

``` ts
import type { DefineAPI, SDK } from "caido:plugin";
import type { Request, Response } from "caido:utils";

export type API = DefineAPI<{}>;

export function init(sdk: SDK<API>) {
sdk.events.onInterceptRequest((sdk, request: Request) => {
sdk.console.log(`Intercepted ${request.getMethod()} request to ${request.getUrl()}`);
});

sdk.events.onInterceptResponse((sdk, request: Request, response: Response) => {
sdk.console.log(`Intercepted response from ${request.getUrl()} with status ${response.getCode()}`);
});
}
```

## The Result

``` txt
Intercepted GET request to https://example.com/path
Intercepted response from https://example.com/path with status 304
```
2 changes: 1 addition & 1 deletion src/guides/components/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export type BackendEvents = DefineEvents<{
Next, we define an empty API type using `DefineAPI` to satisy the `SDK` parameter requirements.

::: tip
To learn how to create custom API endpoints, click [here](/guides/components/endpoint.html).
[Learn how to create and call custom backend functions.](/guides/components/rpc.html)
:::

``` ts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# Creating a Custom Endpoint
# Creating and Calling a Custom Function

When developing a plugin, there are two components to consider: the **frontend** and the **backend**.

In this guide, we'll cover how to create a custom API endpoint in a backend plugin, and call it from a frontend plugin.
In this guide, we'll cover how to create a custom endpoint in a backend plugin, and call it from a frontend plugin.

::: tip
::: info
For additional documentation on the differences between a frontend and backend plugin - click [here](/concepts/essentials/package.md).
:::

## Registering an API Endpoint
## Registering an Endpoint

Let's start by creating an API endpoint called `multiply` in our backend plugin.
Let's start by creating an endpoint called `multiply` in our backend plugin.

`multiply` will take two numbers, output the result in the backend logs, as well as return the result. This endpoint will used by the frontend later on.

Expand Down Expand Up @@ -68,7 +68,7 @@ export function init(sdk: SDK<API>) {
}
```

## Calling the API Endpoint
## Calling the Endpoint

Now that we've created our endpoint in the backend plugin, we can call `multiply` from our frontend plugin.

Expand Down
8 changes: 4 additions & 4 deletions src/guides/components/spawning_process.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ To do this, you can use the `spawn` function from the `child_process` module.
This module is similar to NodeJS's `child_process` module, but with some differences. You can find more information in the [child_process](/concepts/modules/child_process.md) documentation.
:::

## Launching a process
## Launching a Process

The code below spawns a process that echoes "Hello, world!" to the console.

Expand All @@ -34,7 +34,7 @@ child.stderr.on("data", (data) => {
});
```

## Waiting for exit
## Waiting for Exit

To wait for the child process to close and check the exit code, you can use the `close` event of the child process.

Expand All @@ -48,7 +48,7 @@ if (exitCode) {
}
```

## Driving a process to completion
## Driving a Process to Completion

Combining the two methods, we can drive any child process to completion and get its output.

Expand Down Expand Up @@ -82,7 +82,7 @@ export async function test() {
}
```

## Executing within in a shell
## Executing Within a Shell

You can use the `shell: true` or `shell: '/shell/path'` options to execute the command in a shell.

Expand Down
140 changes: 140 additions & 0 deletions src/guides/components/sqlite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# Storing Data in SQLite
ninjeeter marked this conversation as resolved.
Show resolved Hide resolved

For storing data generated by your plugin, Caido utilizes SQLite databases.

SQLite is a lightweight database engine made available via a small library. It requires no setup, administration or separate server. Instead, all data is stored in a single file.

## Getting a Database Path
ninjeeter marked this conversation as resolved.
Show resolved Hide resolved

The `sdk.meta.db()` utility provides a SQLite database specific to your plugin. You can view the location of the generated file using `sdk.meta.path()`:

``` ts
const db = await sdk.meta.db();

const dataPath = sdk.meta.path();
sdk.console.log(`Database will be stored in: ${dataPath}`);
```

ninjeeter marked this conversation as resolved.
Show resolved Hide resolved
## Creating Tables

You can run direct SQL statements by supplying them as an arguement to the `.exec()` method:

``` ts
// Create a new table if it doesn't exist.
// This will create a table named "test" with two columns: id and name.
await db.exec(`
CREATE TABLE IF NOT EXISTS test (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
`);
```

## Inserting Rows

Instead of using direct statements every time you want to add to a table, you can use `.prepare()` to create predefined statements with placeholders, marked with `(?)` for new entry values.

Then, you can execute these prepared statements with the `.run()` method that takes the placeholder values as arguements:

``` ts
const insertStatement = await db.prepare("INSERT INTO test (name) VALUES (?)");

// Execute the insert statement to add "Ninjeeter" as a name entry.
const result = await insertStatement.run("Ninjeeter");
```

## Retrieving Data

Using `.lastInsertRowid` will return the ID of the last inserted row:

``` ts
console.log(`Inserted row with ID: ${result.lastInsertRowid}`);
```
ninjeeter marked this conversation as resolved.
Show resolved Hide resolved

To select all the columns in a table and return every row, use the wildcard character `*` and the `.all()` method:

``` ts
const selectStatement = await db.prepare("SELECT * FROM test");
const rows = await selectStatement.all();
sdk.console.log("Current records: " + JSON.stringify(rows));
```

You can return specific rows by preparing a statement and using the `.get()` method which takes an arguement that will be used to match the table entry:

``` ts
// Prepare a statement to get a single row by ID
const getByIdStatement = await db.prepare("SELECT * FROM test WHERE id = ?");

// Returns the first matching row or undefined if none found.
const row = await getByIdStatement.get(1); // Get row with ID 1.

if (row) {
sdk.console.log(`Found record: ${JSON.stringify(row)}`);
} else {
sdk.console.log("No record found with that ID");
}
```

### /packages/backend/src/index.ts

``` ts
import type { DefineAPI, SDK } from "caido:plugin";

async function initDatabase(sdk: SDK) {
try {
const db = await sdk.meta.db();

const dataPath = sdk.meta.path();
sdk.console.log(`Database will be stored in: ${dataPath}`);

await db.exec(`
CREATE TABLE IF NOT EXISTS test (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
`);

const insertStatement = await db.prepare("INSERT INTO test (name) VALUES (?)");
const result = await insertStatement.run("foo");
sdk.console.log(`Inserted row with ID: ${result.lastInsertRowid}`);

const selectStatement = await db.prepare("SELECT * FROM test");
const rows = await selectStatement.all();
sdk.console.log("Current records: " + JSON.stringify(rows));

const getByIdStatement = await db.prepare("SELECT * FROM test WHERE id = ?");

const row = await getByIdStatement.get(1);

if (row) {
sdk.console.log(`Found record: ${JSON.stringify(row)}`);
} else {
sdk.console.log("No record found with that ID");
}

return db;
} catch (error) {
sdk.console.error(`Database initialization failed: ${error}`);
throw error;
}
}

export type API = DefineAPI<{
}>;

export async function init(sdk: SDK<API>) {
await initDatabase(sdk);
sdk.console.log("Database initialized.");
}
```

## The Result

In your backend logs, you will see the following entries:

``` txt
Database will be stored in: [~]\Caido\data\plugins\[PLUGIN UUID]
Inserted row with ID: 1
Current records: [{"id":1,"name":"Ninjeeter"}]
Found record: {"id":1,"name":"Ninjeeter"}
```
Sytten marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading