Skip to content

Commit 985c55e

Browse files
authored
feat: Shielding support (#1241)
1 parent 9390e8c commit 985c55e

File tree

15 files changed

+314
-0
lines changed

15 files changed

+314
-0
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
# `Shield()`
8+
9+
Load information about the given shield.
10+
11+
Returns an object representing the shield if it is active, or throws an exception if the string is malformed or the shield doesn’t exist.
12+
13+
Shield names are defined on [this webpage](https://www.fastly.com/documentation/guides/concepts/shielding/#shield-locations), in the “shield code” column. For example, the string “pdx-or-us” will look up our Portland, OR, USA shield site, while “paris-fr” will look up our Paris site.
14+
15+
If you are using a major cloud provider for your primary origin site, consider looking at the “Recommended for” column, to find the Fastly POP most closely located to the given cloud provider.
16+
17+
## Syntax
18+
19+
```js
20+
new Shield(name)
21+
```
22+
23+
> **Note:** `Shield()` can only be constructed with `new`. Attempting to call it without `new` throws a [`TypeError`](../../globals/TypeError/TypeError.mdx).
24+
25+
### Exceptions
26+
27+
- `TypeError`
28+
- Thrown if no Shield exists with the provided name
29+
30+
## Examples
31+
32+
In this example, we create a Shield instance for the Sydney, Australia shield POP. If the code is running on that shield POP, it fetches directly from origin. Otherwise, it routes the request through the shield using an encrypted connection.
33+
34+
```js
35+
/// <reference types="@fastly/js-compute" />
36+
37+
import { Shield } from "fastly:shielding";
38+
39+
async function app(event) {
40+
const shield = new Shield('wsi-australia-au');
41+
// If running on the shield POP, fetch from the origin directly
42+
if (shield.runningOn()) {
43+
return await fetch('https://http-me.fastly.com/anything', { backend: 'httpme' });
44+
}
45+
// Otherwise, route the request through the shield using an encrypted connection
46+
return await fetch(event.request, { backend: shield.encryptedBackend() });
47+
}
48+
49+
addEventListener("fetch", (event) => event.respondWith(app(event)))
50+
```
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
# Shield.prototype.encryptedBackend
8+
9+
**encryptedBackend**(): `Backend`
10+
11+
Returns a `Backend` representing an encrypted connection to the POP.
12+
13+
For reference, this is almost always the backend that you want to use. Only use [`Shield::unencryptedBackend`](./unencryptedBackend.mdx) in situations in which you are 100% sure that all the data you will send and receive over the backend is already encrypted.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
# Shield.prototype.runningOn
8+
9+
**runningOn**(): `boolean`
10+
11+
Returns whether we are currently operating on the given shield.
12+
13+
Technically, this may also return true in very isolated incidents in which Fastly is routing traffic from the target shield POP to the POP that this code is running on, but in these situations the results should be approximately identical.
14+
15+
(For example, it may be the case that you are asking to shield to ‘pdx-or-us’. But, for load balancing, performance, or other reasons, Fastly is temporarily shifting shielding traffic from Portland to Seattle. In that case, this function may return true for hosts running on ‘bfi-wa-us’, our Seattle site, because effectively the shield has moved to that location. This should give you a slightly faster experience than the alternative, in which this function would return false, you would try to forward your traffic to the Portland site, and then that traffic would be caught and redirected back to Seattle.)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
hide_title: false
3+
hide_table_of_contents: false
4+
pagination_next: null
5+
pagination_prev: null
6+
---
7+
# Shield.prototype.unencryptedBackend
8+
9+
**unencryptedBackend**(): `Backend`
10+
11+
Returns a `Backend` representing an unencrypted connection to the POP.
12+
13+
Generally speaking, we encourage users to use [`Shield::encryptedBackend`](./encryptedBackend.mdx) instead of this function. Data sent over this backend – the unencrypted version – will be sent over the open internet, with no protections. In most cases, this is not what you want. However, in some cases – such as when you want to ship large data blobs that you know are already encrypted — using these backends can prevent a double-encryption performance penalty.

integration-tests/js-compute/fixtures/app/__input_bundled.js.map

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/js-compute/fixtures/app/src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import './response-redirect.js';
4747
import './response.js';
4848
import './secret-store.js';
4949
import './server.js';
50+
import './shielding.js';
5051
import './tee.js';
5152
import './timers.js';
5253
import './urlsearchparams.js';
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { assert, assertThrows } from './assertions.js';
2+
import { routes } from './routes.js';
3+
import { Shield } from 'fastly:shielding';
4+
5+
routes.set('/shielding/invalid-shield', () => {
6+
assertThrows(() => new Shield('i-am-not-a-real-shield'));
7+
});

integration-tests/js-compute/fixtures/app/tests.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3136,5 +3136,8 @@
31363136
["link", "</style2.css>; rel=preload; as=style"]
31373137
]
31383138
}
3139+
},
3140+
"GET /shielding/invalid-shield": {
3141+
"environments": ["compute"]
31393142
}
31403143
}

runtime/fastly/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ add_builtin(fastly::edge_rate_limiter SRC builtins/edge-rate-limiter.cpp)
3232
add_builtin(fastly::config_store SRC builtins/config-store.cpp)
3333
add_builtin(fastly::secret_store SRC builtins/secret-store.cpp)
3434
add_builtin(fastly::image_optimizer SRC builtins/image-optimizer.cpp)
35+
add_builtin(fastly::shielding SRC builtins/shielding.cpp)
3536

3637
add_builtin(fastly::fetch
3738
SRC
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#include "shielding.h"
2+
#include "../../../StarlingMonkey/runtime/encode.h"
3+
#include "../common/validations.h"
4+
#include "../host-api/host_api_fastly.h"
5+
#include "backend.h"
6+
#include "fastly.h"
7+
8+
namespace fastly::shielding {
9+
const JSFunctionSpec Shield::static_methods[] = {
10+
JS_FS_END,
11+
};
12+
13+
const JSPropertySpec Shield::static_properties[] = {
14+
JS_PS_END,
15+
};
16+
17+
const JSFunctionSpec Shield::methods[] = {
18+
JS_FN("runningOn", runningOn, 0, JSPROP_ENUMERATE),
19+
JS_FN("unencryptedBackend", unencryptedBackend, 0, JSPROP_ENUMERATE),
20+
JS_FN("encryptedBackend", encryptedBackend, 0, JSPROP_ENUMERATE), JS_FS_END};
21+
22+
const JSPropertySpec Shield::properties[] = {JS_PS_END};
23+
24+
bool Shield::runningOn(JSContext *cx, unsigned argc, JS::Value *vp) {
25+
METHOD_HEADER(0);
26+
bool is_me = JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::IsMe)).toBoolean();
27+
args.rval().setBoolean(is_me);
28+
return true;
29+
}
30+
bool Shield::unencryptedBackend(JSContext *cx, unsigned argc, JS::Value *vp) {
31+
METHOD_HEADER(0)
32+
JS::RootedString target(
33+
cx, JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::PlainTarget)).toString());
34+
return backend_for_shield(cx, target, args.rval());
35+
}
36+
bool Shield::encryptedBackend(JSContext *cx, unsigned argc, JS::Value *vp) {
37+
METHOD_HEADER(0)
38+
JS::RootedString target(
39+
cx, JS::GetReservedSlot(self, static_cast<uint32_t>(Slots::SSLTarget)).toString());
40+
return backend_for_shield(cx, target, args.rval());
41+
}
42+
bool Shield::backend_for_shield(JSContext *cx, JS::HandleString target,
43+
JS::MutableHandleValue rval) {
44+
auto name = core::encode(cx, target);
45+
fastly_shielding_shield_backend_config config{nullptr, 0, 0};
46+
auto options_mask = 0;
47+
std::uint32_t backend_name_size_out = 0;
48+
constexpr std::size_t max_backend_name_size = 1024;
49+
std::string backend_name_out(max_backend_name_size, 0);
50+
auto status = fastly_shielding_backend_for_shield(name.ptr.get(), name.len, options_mask, &config,
51+
backend_name_out.data(), max_backend_name_size,
52+
&backend_name_size_out);
53+
if (status != 0) {
54+
HANDLE_ERROR(cx, status);
55+
return false;
56+
}
57+
backend_name_out.resize(backend_name_size_out);
58+
host_api::HostString backend_name(backend_name_out);
59+
return backend::Backend::get_from_valid_name(cx, std::move(backend_name), rval);
60+
}
61+
62+
bool Shield::constructor(JSContext *cx, unsigned argc, JS::Value *vp) {
63+
REQUEST_HANDLER_ONLY("The Shield builtin");
64+
CTOR_HEADER("Shield", 1);
65+
66+
JS::HandleValue name_arg = args.get(0);
67+
auto name = core::encode(cx, name_arg);
68+
69+
// Keep calling fastly_shielding_shield_info with a increasingly large buffer until it returns OK
70+
std::uint32_t buf_size = 1024;
71+
std::vector<char> out_buf(buf_size);
72+
while (true) {
73+
std::uint32_t used_amount = 0;
74+
auto status = fastly_shielding_shield_info(name.ptr.get(), name.len, out_buf.data(), buf_size,
75+
&used_amount);
76+
if (status == 0) {
77+
out_buf.resize(used_amount);
78+
break;
79+
} else if (status == FASTLY_HOST_ERROR_BUFFER_LEN) {
80+
buf_size *= 2;
81+
out_buf = std::vector<char>(1024);
82+
} else {
83+
HANDLE_ERROR(cx, status);
84+
return false;
85+
}
86+
}
87+
88+
if (out_buf.size() < 3) {
89+
return false;
90+
}
91+
92+
JS::RootedObject self(cx, JS_NewObjectWithGivenProto(cx, &class_, proto_obj));
93+
94+
bool is_me = out_buf[0] != 0;
95+
JS_SetReservedSlot(self, static_cast<uint32_t>(Slots::IsMe), JS::BooleanValue(is_me));
96+
JS_SetReservedSlot(self, static_cast<uint32_t>(Slots::PlainTarget),
97+
JS::StringValue(JS_NewStringCopyZ(cx, out_buf.data() + 1)));
98+
auto plain_bytes_end = std::find(begin(out_buf) + 1, end(out_buf), 0);
99+
JS_SetReservedSlot(self, static_cast<uint32_t>(Slots::SSLTarget),
100+
JS::StringValue(JS_NewStringCopyZ(cx, &*plain_bytes_end + 1)));
101+
102+
args.rval().setObject(*self);
103+
return true;
104+
}
105+
106+
bool install(api::Engine *engine) {
107+
RootedObject shielding_ns(engine->cx(), JS_NewObject(engine->cx(), nullptr));
108+
if (!Shield::init_class_impl(engine->cx(), shielding_ns)) {
109+
return false;
110+
}
111+
RootedObject shield_obj(engine->cx(), JS_GetConstructor(engine->cx(), Shield::proto_obj));
112+
RootedValue shield_val(engine->cx(), ObjectValue(*shield_obj));
113+
if (!JS_SetProperty(engine->cx(), shielding_ns, "Shield", shield_val)) {
114+
return false;
115+
}
116+
117+
RootedValue shielding_ns_val(engine->cx(), JS::ObjectValue(*shielding_ns));
118+
if (!engine->define_builtin_module("fastly:shielding", shielding_ns_val)) {
119+
return false;
120+
}
121+
122+
RootedObject fastly(engine->cx());
123+
if (!fastly::get_fastly_object(engine, &fastly)) {
124+
return false;
125+
}
126+
if (!JS_SetProperty(engine->cx(), fastly, "shielding", shielding_ns_val)) {
127+
return false;
128+
}
129+
130+
return true;
131+
}
132+
133+
} // namespace fastly::shielding

0 commit comments

Comments
 (0)