Skip to content

Commit

Permalink
Add tests for outbound TCP
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Levick <[email protected]>
  • Loading branch information
rylev committed Jun 21, 2024
1 parent a1c5f84 commit 0b6de09
Show file tree
Hide file tree
Showing 15 changed files with 392 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions components/tcp-sockets/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "tcp-sockets-test-component"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = { workspace = true }
anyhow = { workspace = true }
9 changes: 9 additions & 0 deletions components/tcp-sockets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# TCP Sockets

Tests the `wasi:sockets` TCP related interfaces

## Expectations

This test component expects the following to be true:
* It is provided the header `address`
* It has access to a TCP echo server on the address supplied in `address`
114 changes: 114 additions & 0 deletions components/tcp-sockets/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use bindings::{
exports::wasi::http0_2_0::incoming_handler::Guest,
wasi::{
http0_2_0::types::{
Headers, IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam,
},
io0_2_0::poll,
sockets0_2_0::{
instance_network,
network::{
ErrorCode, IpAddressFamily, IpSocketAddress, Ipv4SocketAddress, Ipv6SocketAddress,
},
tcp_create_socket,
},
},
};

use std::net::SocketAddr;

mod bindings {
wit_bindgen::generate!({
world: "http-trigger",
path: "../../wit",
});
use super::Component;
export!(Component);
}

struct Component;

impl Guest for Component {
fn handle(request: IncomingRequest, outparam: ResponseOutparam) {
// The request must have a "url" header.
let Some(address) = request.headers().entries().iter().find_map(|(k, v)| {
(k == "address")
.then_some(v)
.and_then(|v| std::str::from_utf8(v).ok())
.and_then(|v| v.parse().ok())
}) else {
// Otherwise, return a 400 Bad Request response.
return_response(outparam, 400, b"Bad Request");
return;
};

match make_request(address) {
Ok(()) => return_response(outparam, 200, b""),
Err(e) => return_response(outparam, 500, format!("{e}").as_bytes()),
}
}
}

fn make_request(address: SocketAddr) -> anyhow::Result<()> {
let client = tcp_create_socket::create_tcp_socket(IpAddressFamily::Ipv4)?;

client.start_connect(
&instance_network::instance_network(),
match address {
SocketAddr::V6(address) => {
let ip = address.ip().segments();
IpSocketAddress::Ipv6(Ipv6SocketAddress {
address: (ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7]),
port: address.port(),
flow_info: 0,
scope_id: 0,
})
}
SocketAddr::V4(address) => {
let ip = address.ip().octets();
IpSocketAddress::Ipv4(Ipv4SocketAddress {
address: (ip[0], ip[1], ip[2], ip[3]),
port: address.port(),
})
}
},
)?;

let (rx, tx) = loop {
match client.finish_connect() {
Err(ErrorCode::WouldBlock) => {
poll::poll(&[&client.subscribe()]);
}
result => break result?,
}
};

let message = b"So rested he by the Tumtum tree";
tx.blocking_write_and_flush(message)?;

let mut buffer = Vec::with_capacity(message.len());
while buffer.len() < message.len() {
let chunk = rx.blocking_read((message.len() - buffer.len()).try_into().unwrap())?;
buffer.extend(chunk);
}
assert_eq!(buffer.as_slice(), message);
Ok(())
}

fn write_outgoing_body(outgoing_body: OutgoingBody, message: &[u8]) {
assert!(message.len() <= 4096);
{
let outgoing_stream = outgoing_body.write().unwrap();
outgoing_stream.blocking_write_and_flush(message).unwrap();
// The outgoing stream must be dropped before the outgoing body is finished.
}
OutgoingBody::finish(outgoing_body, None).unwrap();
}

fn return_response(outparam: ResponseOutparam, status: u16, body: &[u8]) {
let response = OutgoingResponse::new(Headers::new());
response.set_status_code(status).unwrap();
write_outgoing_body(response.body().unwrap(), body);

ResponseOutparam::set(outparam, Ok(response));
}
6 changes: 6 additions & 0 deletions crates/conformance-tests/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ pub enum Precondition {
/// and it should update any references that test assets make to port 80 to
/// the port of the echo server.
HttpEcho,
/// The test expects outgoing TCP requests to be echoed back
///
/// The test runner should start a TCP server that echoes back the request
/// and it should update any references that test assets make to port 5000 to
/// the port of the echo server.
TcpEcho,
}

#[derive(Debug, Clone, serde::Deserialize)]
Expand Down
19 changes: 19 additions & 0 deletions tests/tcp-sockets-ip-range-variable-permission/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
spin_manifest_version = 2

[application]
name = "tcp-sockets"
authors = ["Fermyon Engineering <[email protected]>"]
version = "0.1.0"

[variables]
addr_prefix = { default = "127.0.0.0"}
prefix_len = { default = "24"}

[[trigger.http]]
route = "/"
component = "test"

[component.test]
source = "%{source=tcp-sockets}"
environment = { ADDRESS = "127.0.0.1:%{port=5000}" }
allowed_outbound_hosts = ["*://{{ addr_prefix }}/{{ prefix_len }}:%{port=5000}"]
32 changes: 32 additions & 0 deletions tests/tcp-sockets-ip-range-variable-permission/test.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"invocations": [
{
"request": {
"path": "/",
"headers": [
{
"name": "Host",
"value": "example.com"
},
{
"name": "address",
"value": "127.0.0.1:%{port=5000}"
}
]
},
"response": {
"headers": [
{
"name": "transfer-encoding",
"optional": true
},
{
"name": "Date",
"optional": true
}
]
}
}
],
"preconditions": [{"kind": "tcp-echo"}]
}
15 changes: 15 additions & 0 deletions tests/tcp-sockets-ip-range/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
spin_manifest_version = 2

[application]
name = "tcp-sockets"
authors = ["Fermyon Engineering <[email protected]>"]
version = "0.1.0"

[[trigger.http]]
route = "/"
component = "test"

[component.test]
source = "%{source=tcp-sockets}"
environment = { ADDRESS = "127.0.0.1:%{port=5000}" }
allowed_outbound_hosts = ["*://127.0.0.0/24:%{port=5000}"]
32 changes: 32 additions & 0 deletions tests/tcp-sockets-ip-range/test.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"invocations": [
{
"request": {
"path": "/",
"headers": [
{
"name": "Host",
"value": "example.com"
},
{
"name": "address",
"value": "127.0.0.1:%{port=5000}"
}
]
},
"response": {
"headers": [
{
"name": "transfer-encoding",
"optional": true
},
{
"name": "Date",
"optional": true
}
]
}
}
],
"preconditions": [{"kind": "tcp-echo"}]
}
16 changes: 16 additions & 0 deletions tests/tcp-sockets-no-ip-permission/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
spin_manifest_version = 2

[application]
name = "tcp-sockets"
authors = ["Fermyon Engineering <[email protected]>"]
version = "0.1.0"

[[trigger.http]]
route = "/"
component = "test"

[component.test]
source = "%{source=tcp-sockets}"
environment = { ADDRESS = "127.0.0.1:6001" }
# Component expects 127.0.0.1 but we only allow 127.0.0.2
allowed_outbound_hosts = ["*://127.0.0.2:6001"]
33 changes: 33 additions & 0 deletions tests/tcp-sockets-no-ip-permission/test.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"invocations": [
{
"request": {
"path": "/",
"headers": [
{
"name": "Host",
"value": "example.com"
},
{
"name": "address",
"value": "127.0.0.1:5000"
}
]
},
"response": {
"status": 500,
"headers": [
{
"name": "transfer-encoding",
"optional": true
},
{
"name": "Date",
"optional": true
}
],
"body": "access-denied (error 1)"
}
}
],
}
16 changes: 16 additions & 0 deletions tests/tcp-sockets-no-port-permission/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
spin_manifest_version = 2

[application]
name = "tcp-sockets"
authors = ["Fermyon Engineering <[email protected]>"]
version = "0.1.0"

[[trigger.http]]
route = "/"
component = "test"

[component.test]
source = "%{source=tcp-sockets}"
environment = { ADDRESS = "127.0.0.1:6001" }
# Component expects port 6001 but we allow 6002
allowed_outbound_hosts = ["*://127.0.0.1:6002"]
34 changes: 34 additions & 0 deletions tests/tcp-sockets-no-port-permission/test.json5
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"invocations": [
{
"request": {
"path": "/",
"headers": [
{
"name": "Host",
"value": "example.com"
},
{
"name": "address",
"value": "127.0.0.1:5000"
}
]
},
"response": {
"status": 500,
"headers": [
{
"name": "transfer-encoding",
"optional": true
},
{
"name": "Date",
"optional": true
}
],
"body": "access-denied (error 1)"
},
}
],
"preconditions": [{"kind": "tcp-echo"}]
}
15 changes: 15 additions & 0 deletions tests/tcp-sockets/spin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
spin_manifest_version = 2

[application]
name = "request-shape"
version = "0.1.0"
authors = ["Fermyon Engineering <[email protected]>"]

[[trigger.http]]
route = "/..."
component = "test"

[component.test]
source = "%{source=tcp-sockets}"
environment = { ADDRESS = "127.0.0.1:%{port=5000}" }
allowed_outbound_hosts = ["*://127.0.0.1:%{port=5000}"]
Loading

0 comments on commit 0b6de09

Please sign in to comment.