summaryrefslogtreecommitdiff
path: root/docs/plugins.md
blob: 78a68f7a1892719ed26d915d87fad492306a4647 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# Plugins

Shiv supports Lua plugins that hook into the request and response pipeline. Plugins are loaded from the `plugins/` subdirectory of your Shiv data directory. Any `.lua` file placed there is loaded automatically on startup.

You can also load a plugin at runtime from the Plugins tab without restarting.

## Plugin lifecycle

When a plugin is loaded, its top-level code runs first. If the plugin defines an `on_load` function, it is called immediately after. Plugins can be enabled or disabled from the Plugins tab without reloading the application.

Each plugin runs in its own isolated Lua VM. Plugins do not share state with each other.

Hook calls have a 500ms timeout. If a hook takes longer than that it is interrupted and an error is logged.

## Hooks

### on_load()

Called once when the plugin is first loaded. Use it for initialization.

```lua
function on_load()
    log("my plugin loaded")
end
```

### on_request(req)

Called for every HTTP request before it is forwarded to the server. The `req` table contains:

| Field | Type | Description |
|---|---|---|
| `method` | string | HTTP method |
| `url` | string | Full URL |
| `headers` | table | Request headers |
| `body` | string | Request body |
| `drop` | boolean | Set to `true` to drop the request |

Return the modified table to apply changes. Fields you do not modify are left as-is.

```lua
function on_request(req)
    -- add a header to every request
    req.headers["X-Proxy"] = "shiv"
    return req
end
```

To drop a request:

```lua
function on_request(req)
    if req.url:find("analytics") then
        req.drop = true
        return req
    end
    return req
end
```

### on_response(resp)

Called for every HTTP response after it is received from the server. The `resp` table contains:

| Field | Type | Description |
|---|---|---|
| `host` | string | Bare hostname |
| `method` | string | Request method |
| `url` | string | Request URL |
| `proto` | string | Protocol (HTTP/1.1, HTTP/2) |
| `status` | number | HTTP status code |
| `duration_ms` | number | Round-trip time in milliseconds |
| `tls` | boolean | Whether the connection was TLS |
| `req_headers` | table | Request headers |
| `req_body` | string | Request body |
| `resp_headers` | table | Response headers |
| `resp_body` | string | Response body |

The return value of `on_response` is ignored. This hook is read-only.

```lua
function on_response(resp)
    if resp.status == 500 then
        log("server error on " .. resp.url)
        db.loot_add("500 Error", "Low", resp.url, "", "")
    end
end
```

### on_websocket_frame(frame)

Called for every WebSocket frame in either direction. The `frame` table contains:

| Field | Type | Description |
|---|---|---|
| `connection_id` | number | Unique ID for the WebSocket connection |
| `direction` | number | 0 = client to server, 1 = server to client |
| `opcode` | number | WebSocket opcode (1 = text, 2 = binary, etc.) |
| `payload` | string | Frame payload |

Return the modified table to change the payload. Setting `frame.payload` to a different value rewrites the frame on the wire.

```lua
function on_websocket_frame(frame)
    log("frame: " .. frame.payload)
    return frame
end
```

## Built-in API

### log(message)

Writes a line to the plugin log visible in the Plugins tab.

```lua
log("something happened")
```

### http

Makes outbound HTTP requests from within a plugin. Note that these requests do not go through the Shiv proxy — they go directly to the target.

```lua
-- GET
local resp, err = http.get("https://example.com")
if err then
    log("error: " .. err)
else
    log("status: " .. resp.status)
    log("body: " .. resp.body)
end

-- POST
local resp, err = http.post("https://example.com/api", '{"key":"value"}', "application/json")

-- Custom request
local resp, err = http.request("PUT", "https://example.com/resource", "body", {
    ["Authorization"] = "Bearer token"
})
```

All three functions return a response table with `status` (number), `body` (string), and `headers` (table), or `nil` plus an error string on failure.

### db

Provides read and write access to the project database.

```lua
-- Read history (up to 100 most recent)
local txs, err = db.history(100)
for i, tx in ipairs(txs) do
    log(tx.method .. " " .. tx.url .. " -> " .. tx.status)
end

-- Filter history by host
local txs, err = db.history_filter("example.com", "", 0)

-- Scope management
local entries, err = db.scope_get()
db.scope_add("example.com")
db.scope_remove(entry_id)

-- Loot management
local entries, err = db.loot_get()
local id, err = db.loot_add("Title", "High", "Notes", raw_request, raw_response)
db.loot_delete(id)
```

Each `db` function returns its result on success, or `nil` plus an error string on failure.

## Example plugin

This plugin logs any response with a `Set-Cookie` header and automatically saves it to Loot if the cookie is missing the `HttpOnly` flag:

```lua
function on_load()
    log("cookie checker loaded")
end

function on_response(resp)
    local cookie = resp.resp_headers["Set-Cookie"]
    if not cookie then return end

    if not cookie:find("HttpOnly") then
        local notes = "Set-Cookie header missing HttpOnly flag:\n" .. cookie
        db.loot_add("Missing HttpOnly", "Low", notes, "", "")
        log("flagged missing HttpOnly on " .. resp.host)
    end
end
```