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

allow outbound http to components in same app by default #1710

Merged
merged 2 commits into from
Sep 5, 2023

Conversation

michelleN
Copy link
Member

@michelleN michelleN commented Aug 26, 2023

Copy link
Contributor

@itowlson itowlson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great - thanks for making it happen!

My comments on the functional side of it are, I think, entirely suggestions or nits, not blockers. I did, though, get a bit lost in the sample - could we change / rationalise some of the naming there please? Thanks!


[[component]]
id = "component-outbound-http"
source = "component-outbound-http/target/wasm32-wasi/release/http_rust_outbound_http.wasm"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to make the difference between the outbound-http and component-outbound-http directories clearer. Maybe outbound-http-to-other-component or outbound-http-to-same-app?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be because of trying to fit the "local component call" sample into the existing sample - if we're doing that then we should be careful not to lose the simplicity of the existing sample.


[[component]]
id = "hello"
source = "http-rust/target/wasm32-wasi/release/http_rust.wasm"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The http-rust directory name isn't very helpful, and doesn't obviously relate to the hello component when reading the code that makes the outbound request.

@@ -0,0 +1,19 @@
[package]
name = "http-rust-outbound-http"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another Cargo package already has this name - this has caused us problems in the past because when a user refers to the Rust SDK, Cargo scans the entire repo and complains about duplicate names.


let parsed = Url::parse(spin_full_url)?;

let beforepath = Url::parse(&parsed[..Position::BeforePath]).unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not know about Position! Thank you!

id = "component-outbound-http"
source = "component-outbound-http/target/wasm32-wasi/release/http_rust_outbound_http.wasm"
[component.trigger]
route = "/outbound-hello"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe align the route to the component ID? The hello and outbound-hello components feel a bit too similar to me.


[[component]]
id = "component-outbound-http"
source = "component-outbound-http/target/wasm32-wasi/release/http_rust_outbound_http.wasm"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest adding a comment highlighting that this component doesn't need an allowed_http_hosts because it only sends requests to other components within the same app, and those are always allowed.

Ok(self.allowed_hosts.allow(&url))
}

/// Check if the outbound request host and port are the same as origin
fn is_same_origin(&self, url: &Url) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe

Suggested change
fn is_same_origin(&self, url: &Url) -> bool {
fn is_same_as_origin(&self, url: &Url) -> bool {

? "Same origin" implies to me whether two things have the same origin, whereas you are interested in whether the URL (the destination) is the same as the origin.

@@ -31,8 +33,19 @@ impl OutboundHttp {
/// If `None` is passed, the guest module is not allowed to send the request.
fn is_allowed(&self, url: &str) -> Result<bool, HttpError> {
let url = Url::parse(url).map_err(|_| HttpError::InvalidUrl)?;

if self.is_same_origin(&url) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a possible sneaky gap here, if two applications are hosted on the same host but at different base URLs. I don't think that's possible with any current Spin implementation, so not a blocker (mentioning it mostly because this was the justification for have base in the first place).

@@ -21,6 +21,8 @@ pub const ALLOWED_HTTP_HOSTS_KEY: MetadataKey<Vec<String>> = MetadataKey::new("a
pub struct OutboundHttp {
/// List of hosts guest modules are allowed to make requests to.
pub allowed_hosts: AllowedHttpHosts,
/// origin is the host of the component that created this outbound HTTP request.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct isn't associated with a specific HTTP request, so we could make this more explicit:

Suggested change
/// origin is the host of the component that created this outbound HTTP request.
/// During an incoming HTTP request, origin is set to the host of that incoming HTTP request.
/// This is used to automatically allow outbound requests to the same host.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's really great wording thank you

@itowlson
Copy link
Contributor

@karthik2804 noted that Cloud might need a bit of work to support this behaviour. It would be good if that could happen before this goes into a Spin release, so as to avoid any "works locally but not in cloud" issues.

@karthik2804
Copy link
Contributor

This feature looks great! I have a question about how this works. From looking at the PR, component-to-component HTTP requests for the same app are enabled by default. This is a deviation from what Spin usually does as in that to allow something, some explicit configuration is needed. I am wondering if that is the desired behavior we would want?

From the linked issue I see there is a proposed option for a self configuration option that would allow for an explicit configuration (ref: #957 (comment)).

I wanted to bring this up and see what people thought.

@michelleN michelleN force-pushed the outboundhttp branch 3 times, most recently from 32f8662 to 46fd5bc Compare August 30, 2023 00:10
Ok(res)
}

fn base_url(req: Request) -> Result<Url> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it an option to provide a function like this in the SDKs, so users don't have to implement this in every component?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively we could reserve some fake host name to represent "self", e.g. https://self.local.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://my.ego :-) Demo time "So here I call my ego" 👍🏻

@lann
Copy link
Collaborator

lann commented Aug 30, 2023

I do think we should require e.g. allowed_http_hosts = ["self"]. Part of the component model security story is that you don't have to completely trust components in your own app, and while making a request to your own app isn't "as bad" as e.g. database access it still breaks that story.

let mut url = Url::parse(&req.uri).map_err(|_| HttpError::InvalidUrl)?;

if url.host_str() == Some("self.local") {
url.set_host(self.origin.host())
Copy link
Collaborator

@lann lann Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we update the scheme here too? Thinking about testing on http://localhost and then switching to https://my.spin-self-hosting.com.

Copy link
Member Author

@michelleN michelleN Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea right now the url here would have the same scheme specified in request made in the app so you'd have to update http://self.local to https://self.local before deploying and that seems like a paper cut. Let me try updating the scheme like you suggested.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the implementation so you don't need to use self.local in the uri when making a request and instead you can use relative paths and the correct origin info will be added to the uri as you originally suggested in #1533

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.local is now self because... one word is better than two!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may still want to allow http://self.local/abc (in addition to just /abc) for code that expects an absolute URL, like (I think?) the Go SDK.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not seeing where the Go SDK requires an absolute URL. Could you point me in right direction?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking that since we replace the default http client (here) it might break something in that path if you pass a relative URL, but I haven't tested.

@michelleN michelleN force-pushed the outboundhttp branch 10 times, most recently from 919c1cc to 6475e5c Compare August 31, 2023 16:56

func init() {
spinhttp.Handle(func(w http.ResponseWriter, r *http.Request) {
resp, err := spinhttp.Get("/hello")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If convenient, could you try this as just http.Get (like here)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that worked as well

Copy link
Contributor

@itowlson itowlson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks fine - I left some style suggestions but nothing blocking. Thanks!

fn is_allowed(&self, url: &str) -> Result<bool, HttpError> {
fn is_allowed(&mut self, url: &str) -> Result<bool, HttpError> {
if url.starts_with('/') {
if self.allowed_hosts.includes(AllowedHttpHost::host("self")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than requiring callers to know how AllowedHttpHosts represents the self-link permission (and requiring them to construct an AllowedHttpHost just in order to ask), consider putting this into a AllowedHttpHosts::allow_relative_url (or some better name) method.

Suggested change
if self.allowed_hosts.includes(AllowedHttpHost::host("self")) {
return self.allowed_hosts.allow_relative_url();

(and remove the includes method)

@@ -49,7 +61,14 @@ impl outbound_http::Host for OutboundHttp {
}

let method = method_from(req.method);
let url = Url::parse(&req.uri).map_err(|_| HttpError::InvalidUrl)?;

let req_url: Url = if req.uri.starts_with('/') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid having the parse and error map on both branches, I wonder if this could be reorganised as something like

Suggested change
let req_url: Url = if req.uri.starts_with('/') {
let abs_url = if req.uri.starts_with('/') {
format!(...)
} else {
req.uri.to_string()
};
let req_url = Url::parse(&abs_uri).map_err(...)?;

if let Some(outbound_http_handle) = engine
.engine
.find_host_component_handle::<std::sync::Arc<outbound_http::OutboundHttpComponent>>(
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh rustfmt no

(no action for you, I am just sad at the tool)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think rustfmt produced this, I think it gave up on this code for some reason...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it gave up because the function call was too long. If you add uses for Arc and OutboundHttpComponent it will happily reformat this.

Copy link
Member Author

@michelleN michelleN Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good tip! Moving the logic to its own function helped but I added the uses anyway for readability.


outbound_http_data.origin = format!("{}://{}", scheme, authority);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two consecutive close braces at the same indent feels weird to me but I suppose this is another rustfmt special...

let EitherInstance::Component(instance) = instance else {
unreachable!()
};

if let Some(authority) = req.uri().authority() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has grown to a big enough block that it rather breaks up the flow of the function - consider pulling it out into a set_http_origin_from_request(&mut store, &req) (or otherwise meaningfully named) function.

@@ -0,0 +1,2 @@
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that we are adding these in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. removed!

Copy link
Member Author

@michelleN michelleN Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

turns out we did need that go.sum file (integration tests failed and other examples include it) so I added this back in.

@melissaklein24 melissaklein24 added this to the 1.5.0 milestone Sep 1, 2023
+ use special value `self` in allowed_http_hosts
to allow components in the same spin app to make outbound
http requests to each other
+ make requests with relative routes in app once self is set
+ update example for rust outbound http to demonstrate capability
+ adds e2e
+ resolves fermyon#957
+ resolves fermyon#1533

Signed-off-by: Michelle Dhanani <michelle@fermyon.com>
Signed-off-by: Michelle Dhanani <michelle@fermyon.com>
@michelleN michelleN merged commit 089e135 into fermyon:main Sep 5, 2023
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
6 participants