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 {host} in proxy upstream #998

Closed
wants to merge 25 commits into from
Closed

Conversation

meyskens
Copy link

@meyskens meyskens commented Aug 4, 2016

This PR allow to use {host} in the proxy upstream to you can proxy the request to a specific server/path based on what hostname was requested. (discussed in #990 )

Note: I have barely any experience in Go, and the implementation I used seems very hacky to me. Any suggestions are welcome

@CLAassistant
Copy link

CLAassistant commented Aug 4, 2016

CLA assistant check
All committers have signed the CLA.

@mholt
Copy link
Member

mholt commented Aug 4, 2016

Cool, thanks for your work @meyskens! Will take a look at it later today.

In the meantime, run go fmt on your code which will automatically fix indentation and other formatting issues.

@mholt mholt added the under review 🧐 Review is pending before merging label Aug 4, 2016
@theblazehen
Copy link

I've been running this by reverse proxying to nginx behind caddy, would be great to have it implemented directly

@@ -126,6 +127,40 @@ func NewSingleHostReverseProxy(target *url.URL, without string) *ReverseProxy {
return rp
}

func NewDynamicHostReverseProxy(templateUrl string, without string) *ReverseProxy {
templateUrl = strings.Replace(strings.ToLower(templateUrl), "{host}", "HOST", -1)
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't generalize well, for example, if you want {hostonly}...

Copy link
Author

Choose a reason for hiding this comment

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

True. Maybe checking for {} would be better

@mholt
Copy link
Member

mholt commented Aug 5, 2016

Nice first attempt! This is not an easy problem to solve elegantly. I have been looking at this for about an hour trying to find a good solution independently as well. The director function is set up when the server is started, not at request-time. We do want to do as much processing as we can before actual requests start coming in in order to reduce latency. But that's what makes it a little hacky, because replacements require a *http.Request.

Will have some suggestions soon.

@mholt
Copy link
Member

mholt commented Aug 6, 2016

I think the most proper fix will involve making a httpserver.NewReplacer and parsing the upstream URL on each request, since the Director function needs it at request-time. This will introduce some overhead, but I haven't checked how much. Is that something we're willing to accept?

@meyskens
Copy link
Author

meyskens commented Aug 6, 2016

Seems like a good idea. Overhead is unavoidable in this situation imo

@meyskens
Copy link
Author

meyskens commented Aug 6, 2016

I think i know a way to do this. I'll try this out and update this PR

@meyskens
Copy link
Author

meyskens commented Aug 6, 2016

This should parse the URL in the director itself on every request, this adds a little overhead on every request but I don't think an url parse will kill your server 😛. It should also allow to use parameters everywhere in the URL (so you can also set the path different if you like). Support for Unix sockets is not included as it seems to me as a security risk.

targetQuery := target.RawQuery

req.URL.Scheme = target.Scheme
req.URL.Host = replacer.Replace(strings.Replace(target.Host, "HOST", "{host}", -1))
Copy link
Member

@mholt mholt Aug 6, 2016

Choose a reason for hiding this comment

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

How about doing the replacement before parsing the URL? Then you won't have to hack around the { } like this.

Copy link
Author

Choose a reason for hiding this comment

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

oops, it is already done but forgot to remove the old one

templateUrl = replacer.Replace(templateUrl)
target, _ := url.Parse(templateUrl)

Copy link
Member

Choose a reason for hiding this comment

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

Oh yeah 😄 ... is it weird that I read PRs from the bottom-up? 😛

Copy link
Author

Choose a reason for hiding this comment

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

I do that with magazines so no. (disclaimer: I'm weird)

rp := &ReverseProxy{Director: director, FlushInterval: 250 * time.Millisecond} // flushing good for streaming & server-sent events

return rp
}
Copy link
Member

Choose a reason for hiding this comment

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

This is the same thing as NewSingleHostReverseProxy, isn't it, but with a different director and without support for unix sockets?

Any chance we could reuse that code? i.e. call NewSingleHostReverseProxy somehow (maybe its signature has to change slightly?) and then just change the director?

Thanks for working through this. :) Not an easy change! But it's looking better already.

Once we get this looking good, it will need some tests...

Copy link
Author

Choose a reason for hiding this comment

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

Calling NewSingleHostReverseProxy seem hard as the URL has to change inside the director. The director is also where 90% of the code changes are. What I was thinking of is putting the property setters of the req inside a separate function.

Copy link
Member

Choose a reason for hiding this comment

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

That's a possibility too. I'll let you experiment with what is most elegant/clean!

Copy link
Author

Choose a reason for hiding this comment

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

oh yay 😛 If suddenly caddy has a bug that it burns down houses: don't blame me 😛

@meyskens
Copy link
Author

meyskens commented Aug 6, 2016

Moved almost all logic of the director in a function that sends back the req. Since there is unix sockets in there and you probably don't want to let /var/run/{host}.socket become /var/run/mysql-without-a-password.socket I added a check that they always go to SingleHostProxy.

@meyskens
Copy link
Author

meyskens commented Aug 6, 2016

Added the first part for the tests, now only finding out how to use it.

@@ -710,13 +710,18 @@ func basicAuthTestcase(t *testing.T, upstreamUser, clientUser *url.Userinfo) {
}

func newFakeUpstream(name string, insecure bool) *fakeUpstream {
uri, _ := url.Parse(name)
if strings.Contains(name,"{") && strings.Contains(name,"{") {
Copy link

Choose a reason for hiding this comment

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

I think the second strings.Contains call was meant to be with a } instead of {.

@meyskens
Copy link
Author

meyskens commented Aug 7, 2016

From testing on my server I noticed the health system isn't useful as are the fail counts. I've disabled them for dynamic hosts. Imo we need a property that tells if a backend is dynamic is the string.Contains gets overused. I also added some more explaining errors (maybe improve the logging, but that is for another PR).

On tests: anybody a hint what URL it tries to fetch 😛

@meyskens
Copy link
Author

meyskens commented Aug 7, 2016

Also a configurable tryDuration may be handy as not everybody can wait a minute for a 502.

@meyskens
Copy link
Author

meyskens commented Aug 7, 2016

Once I got this tested and wrote the tests it should be ready for review + merge. (may look into a tryDuration property in the caddyfile)

@@ -138,6 +138,9 @@ func (u *staticUpstream) NewHost(host string) (*UpstreamHost, error) {
if uh.Unhealthy {
return true
}
if uh.Dynamic {
return false
}
Copy link
Member

Choose a reason for hiding this comment

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

Backends with hostnames that depend on the request are always up?

Copy link
Author

Choose a reason for hiding this comment

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

yeah, since caddy can't see the difference between 2 different backends with this unfortunately.

Copy link
Member

Choose a reason for hiding this comment

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

Yikes. That is a problem... will think on this...

@mholt
Copy link
Member

mholt commented Aug 12, 2016

I will be able to get back to this in a little while... this is a bigger change than I thought it would be!

@mholt
Copy link
Member

mholt commented Aug 22, 2016

I am kind of wondering if this change would merit a re-design of the entire proxy middleware...

@meyskens
Copy link
Author

Depends on how you want it to work imo. The most efficient for dynamic will indeed be a redesign of the middleware.

@mholt mholt added help wanted 🆘 Extra attention is needed deferred ⏰ We'll come back to this later and removed under review 🧐 Review is pending before merging labels Aug 26, 2016
@mholt
Copy link
Member

mholt commented Aug 26, 2016

@meyskens I think without another one or two people proficient with writing proxy code in Go to review this (as I don't trust such a significant change to myself), I just don't feel comfortable with this change. It's disruptive because of the design of the existing proxy code.

I believe a better way to approach this might be to design the proxy middleware with this feature in mind, rather than kind of hacking it into place. I really appreciate your time and efforts on this issue, it made a lot of progress. But I think this is the end of it for now, until proxy is redesigned and built by contributors. Just leaving this open will not be doing you or I a favor, so I'm going to close it.

Thank you!! Hope you will still contribute in the future. 😄

@mholt mholt closed this Aug 26, 2016
@mholt mholt removed deferred ⏰ We'll come back to this later help wanted 🆘 Extra attention is needed labels Sep 10, 2019
mholt added a commit that referenced this pull request Oct 11, 2019
This PR enables the use of placeholders in an upstream's Dial address.

A Dial address must represent precisely one socket after replacements.

See also #998 and #1639.
@mholt
Copy link
Member

mholt commented Oct 11, 2019

This feature has been implemented into Caddy 2, as of 1e31be8.

It was possible in large part due to the experience gained from this pull request, so thank you for that!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants