Skip to content

Commit b4c3afe

Browse files
author
Anton Kanugalawattage
committed
extension: terminal
Signed-off-by: Anton Kanugalawattage <antonk@palantir.com>
1 parent 0866f05 commit b4c3afe

16 files changed

Lines changed: 1101 additions & 1 deletion

File tree

extensions/composer/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ require (
3030
gopkg.in/yaml.v3 v3.0.1
3131
)
3232

33+
require github.com/creack/pty v1.1.24
34+
3335
require (
3436
github.com/agnivade/levenshtein v1.2.1 // indirect
3537
github.com/cespare/xxhash/v2 v2.3.0 // indirect

extensions/composer/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ github.com/corazawaf/coraza/v3 v3.7.0 h1:LIQqu1r+l6e/U/gyiZeykWaNNBY1TzRLz+aaI+Q
2020
github.com/corazawaf/coraza/v3 v3.7.0/go.mod h1:dOSt5evqC7EstouEv6ghhui01+oVUwp9X1vybWwqTlo=
2121
github.com/corazawaf/libinjection-go v0.3.2 h1:9rrKt0lpg4WvUXt+lwS06GywfqRXXsa/7JcOw5cQLwI=
2222
github.com/corazawaf/libinjection-go v0.3.2/go.mod h1:Ik/+w3UmTWH9yn366RgS9D95K3y7Atb5m/H/gXzzPCk=
23+
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
24+
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
2325
github.com/crewjam/saml v0.5.1 h1:g+mfp0CrLuLRZCK793PgJcZeg5dS/0CDwoeAX2zcwNI=
2426
github.com/crewjam/saml v0.5.1/go.mod h1:r0fDkmFe5URDgPrmtH0IYokva6fac3AUdstiPhyEolQ=
2527
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

extensions/composer/plugins.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ import (
2525
_ "github.com/tetratelabs/built-on-envoy/extensions/composer/saml/embedded" // SAML SP plugin.
2626
_ "github.com/tetratelabs/built-on-envoy/extensions/composer/token-exchange/embedded" // OAuth2 Token Exchange plugin.
2727
_ "github.com/tetratelabs/built-on-envoy/extensions/composer/waf/embedded" // WAF plugin.
28+
_ "github.com/tetratelabs/built-on-envoy/extensions/composer/web-terminal/embedded" // Web Terminal plugin.
2829
)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://github.com/tetratelabs/built-on-envoy/extensions/composer/web-terminal/config.schema.json",
4+
"title": "Web Terminal Configuration",
5+
"description": "Configuration for the web-terminal extension. Streams a PTY to the browser over SSE and takes input via POST. A browser terminal is remote code execution by design: set a token unless the listener is otherwise protected.",
6+
"type": "object",
7+
"additionalProperties": false,
8+
"properties": {
9+
"command": {
10+
"type": "string",
11+
"description": "Executable to run inside the PTY (e.g. \"/bin/bash\").",
12+
"minLength": 1,
13+
"default": "/bin/bash"
14+
},
15+
"args": {
16+
"type": "array",
17+
"description": "Arguments passed to the command.",
18+
"items": { "type": "string" }
19+
},
20+
"token": {
21+
"type": "string",
22+
"description": "Shared secret required as the `token` query parameter on the stream/input/resize endpoints. When empty, access is open (local-demo only)."
23+
},
24+
"writable": {
25+
"type": "boolean",
26+
"description": "Whether client keystrokes are written to the PTY. Set to false for a read-only terminal.",
27+
"default": true
28+
},
29+
"serve_frontend": {
30+
"type": "boolean",
31+
"description": "Whether to serve the bundled xterm.js client at \"/\". Disabled by default; the /stream, /input and /resize endpoints are the primary interface. Set to true to serve the bundled client.",
32+
"default": false
33+
}
34+
}
35+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright Built On Envoy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// The full text of the Apache license is available in the LICENSE file at
4+
// the root of the repo.
5+
6+
package webterminal
7+
8+
import (
9+
"testing"
10+
11+
internaltesting "github.com/tetratelabs/built-on-envoy/extensions/composer/internal/testing"
12+
)
13+
14+
func TestConfigSchema(t *testing.T) {
15+
t.Run("empty config is valid (defaults apply)", func(t *testing.T) {
16+
internaltesting.AssertSchemaValid(t, "config.schema.json", `{}`)
17+
})
18+
t.Run("full config", func(t *testing.T) {
19+
internaltesting.AssertSchemaValid(t, "config.schema.json", `{
20+
"command": "/bin/bash",
21+
"args": ["-l"],
22+
"token": "secret",
23+
"writable": false
24+
}`)
25+
})
26+
t.Run("empty command is invalid", func(t *testing.T) {
27+
internaltesting.AssertSchemaInvalid(t, "config.schema.json", `{"command": ""}`)
28+
})
29+
t.Run("unknown property is invalid", func(t *testing.T) {
30+
internaltesting.AssertSchemaInvalid(t, "config.schema.json", `{"nope": true}`)
31+
})
32+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright Built On Envoy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// The full text of the Apache license is available in the LICENSE file at
4+
// the root of the repo.
5+
6+
// Package host contains the code to register the plugin with the host binary.
7+
package host
8+
9+
import (
10+
sdk "github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go"
11+
12+
impl "github.com/tetratelabs/built-on-envoy/extensions/composer/web-terminal"
13+
)
14+
15+
// Register this plugin to the host registry if this is built into the host binary.
16+
func init() {
17+
sdk.RegisterHttpFilterConfigFactories(impl.WellKnownHttpFilterConfigFactories())
18+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright Built On Envoy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// The full text of the Apache license is available in the LICENSE file at
4+
// the root of the repo.
5+
6+
package webterminal
7+
8+
import (
9+
_ "embed"
10+
11+
"github.com/envoyproxy/envoy/source/extensions/dynamic_modules/sdk/go/shared"
12+
)
13+
14+
// indexHTML is an optional, bundled browser client (xterm.js). It is a
15+
// convenience only; the terminal protocol is served independently of it and any
16+
// client can implement it. Serving is controlled by config.ServeFrontend.
17+
//
18+
//go:embed www/index.html
19+
var indexHTML []byte
20+
21+
func serveFrontend(handle shared.HttpFilterHandle) {
22+
handle.SendResponseHeaders([][2]string{
23+
{":status", "200"},
24+
{"content-type", "text/html; charset=utf-8"},
25+
}, false)
26+
handle.SendResponseData(indexHTML, true)
27+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Copyright Built On Envoy
2+
# SPDX-License-Identifier: Apache-2.0
3+
# The full text of the Apache license is available in the LICENSE file at
4+
# the root of the repo.
5+
6+
name: web-terminal
7+
parent: composer
8+
categories:
9+
- Misc
10+
author: Anton Kanugalawattage
11+
description: Host an interactive terminal inside Envoy, streamed over SSE.
12+
longDescription: |
13+
Hosts an interactive shell (default `/bin/bash`) inside Envoy and exposes it to
14+
the browser without any backend server. The PTY runs in the Envoy process; its
15+
output is streamed to the client over a never-closing HTTP response as
16+
Server-Sent Events, and input and resize events come back as short POST
17+
requests correlated by a session id. Because it runs as a composer HTTP filter,
18+
no upstream cluster or extra port is required — Envoy hosts the session directly.
19+
20+
## Protocol
21+
22+
The transport is plain HTTP and client-agnostic — any client can implement it:
23+
24+
- `GET /stream?sid=<id>&cols=<n>&rows=<n>[&token=<t>]`
25+
A `text/event-stream` where each `data:` event is a base64-encoded chunk of
26+
PTY output. Opening the stream spawns the PTY for `sid`; closing it (client
27+
disconnect) tears the PTY down. A final `event: exit` is sent on shell exit.
28+
- `POST /input?sid=<id>[&token=<t>]`
29+
The request body is written to the PTY.
30+
- `POST /resize?sid=<id>&cols=<n>&rows=<n>[&token=<t>]`
31+
Resizes the PTY.
32+
33+
## Frontend
34+
35+
A small `xterm.js` client is bundled but is not served by default — the
36+
endpoints above are the primary interface, so bring your own client (e.g. an
37+
application's own terminal view). Set `serve_frontend: true` to serve the
38+
bundled client at `/`.
39+
40+
## Security
41+
42+
A browser terminal is remote code execution by design. Set a `token` unless the
43+
listener is otherwise protected; clients then pass `?token=<token>` on each
44+
endpoint (the bundled frontend reads it from `/?token=<token>`).
45+
type: go
46+
tags:
47+
- go
48+
- http
49+
- terminal
50+
- pty
51+
- sse
52+
license: Apache-2.0
53+
examples:
54+
- title: API only (default), bring your own client
55+
description: |
56+
Drive the /stream, /input and /resize endpoints from your own client. The
57+
bundled frontend is not served.
58+
code: |
59+
boe run --extension web-terminal --config '{
60+
"command": "/bin/bash",
61+
"token": "change-me"
62+
}'
63+
- title: Serve the bundled xterm.js client
64+
description: |
65+
Enable the bundled frontend, then open it in the browser at
66+
http://localhost:10000/?token=change-me
67+
code: |
68+
boe run --extension web-terminal --config '{
69+
"command": "/bin/bash",
70+
"token": "change-me",
71+
"serve_frontend": true
72+
}'

0 commit comments

Comments
 (0)