Re: internal vs. external hostname in Request.getHost9)
Glyph Lefkowitz <[email protected]> Tue, 14 Mar 2017 23:08:35 -0700
| Newsgroups | gmane.comp.python.twisted.web |
|---|---|
| Message-ID | <[email protected]> |
--===============1110597621251517107== Content-Type: multipart/alternative; boundary="Apple-Mail=_B4AE43CE-3654-4CCA-AACC-7A03B41F260F" --Apple-Mail=_B4AE43CE-3654-4CCA-AACC-7A03B41F260F Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 > On Mar 14, 2017, at 3:00 PM, Ilya Skriblovsky = <[email protected]> wrote: >=20 > Tickets you have mentioned and forwarded-for-5807 branch are mostly = about parsing X-Forwarded-For in order to obtain correct client IP. = While it is valuable task, it is not what strikes me right now. Sorry, I was pretty tired when I wrote that message, and I realize that = I was getting server identification and client identification mixed up. > I'm now more concerned with an absence of API for getting user-visible = server's name, not client's ip. Yes. My mistake. (Although totally fix those other bugs too. They're = also bad. :)) > Look, I'm currently porting my app from Django to Klein and noticed = strange behavior of Klein. For example: > @app.route('/alias', alias=3DTrue) > @app.route('/path') > def path(request): return b'42' >=20 > When /alias is requested werkzeug generates a redirect to /path. But = Klein is passing Request.getHost() to Werkzeug, so redirect gets = internal hostname and exposes backend's internal hostname and port to = the user. Seems like Klein is passing incorrect hostname to Werkzeug. = But how can we fix that? >=20 > There are two methods in Request: > =E2=80=A2 Request.getHost() =E2=80=94 "Get my originally requesting = transport's host" as doc says. Ok, seems like this method intentionally = returns server's internal address. > =E2=80=A2 Request.getRequestHostname() =E2=80=94doc says: > >> "Get the hostname that the user passed in to the request. This will = either use the Host: header (if it is available) or the host we are = listening on if the header is unavailable." > Cool, but why does this method only returns a hostname without a port? = It intentionally strips out the port number from Host header. What is = the point of such implementation? Request is one of the oldest parts of Twisted, so the likely reason is = "it looked like a good idea at the time". Request predates the = requirement for test coverage, documentation coverage, and, in many = cases, the author (me) having any idea what they were doing :). If you = find something that looks bad, it's probably just bad, there is unlikely = any deeper reason. Long term, we need to overhaul the API to have fewer methods and be = generally less confusing. See for example the infamous = https://twistedmatrix.com/trac/ticket/288 ticket. However, before we do = that, we should make all the stuff that is there already behave = correctly and be documented even in its weird shape; then we can = transition to a new good thing confident in the knowledge that no old = applications will break and that users can move over to the new APIs = without massive disruption. > This method is used only a couple of times inside Twisted itself, and = in both places Twisted gets what getRequestHostname() returned and mixes = it with request.getHost().port which is *definitely* incorrect, because = the former is user-visible while latter is internal. So if my backend = server is using different port than a fronend, it is impossible to use = getRequestHostname() to build user-visible URL. I think current = getRequestHostname() implementation is broken. >=20 > So I have two proposals: >=20 > Proposal #1 (fixing current behavior): > =E2=80=A2 Variant #1: Change Request.getRequestHostname() to return = b"hostname:port". I think this is the correct thing to do, but this is a = backward-incompatible change. > - or - > =E2=80=A2 Variant #2: Change Klein to use Request.getHeader(b'Host') = with fallback to Request.getHost() >=20 > Proposal #2 (adding new feature if Variant #1 is choosed): > =E2=80=A2 Add useXForwardedHost=3DFalse argument to = Request.getRequestHostname() and useXForwardedProto=3DFalse to = Request.isSecure(). If True is passed, these methods will obey = corresponding request headers that are de-facto standard for reverse = proxies. Also add corresponding options to Klein app. This can simplify = reverse proxy configuration a bit. I have a third proposal. Ideally if we want to know about the URL for the request, we could ask = the request to just give us the URL. And in fact the URL does have a = method, URLPath(), which is both (A) unambiguously broken (the case = could be made that getRequestHostname() is supposed to really just be a = host, not for URL generation, and maybe there is even a case where that = makes sense; origin comparisons perhaps) and (B) returning a data = structure which could be fixed to be correct without concern for client = compatibility. In the long term, we should get rid of all these methods and have a = single 'request.url()' method which cleanly and correctly returns a = https://twistedmatrix.com/documents/17.1.0/api/twisted.python.url.URL.html= object, which is better than a string or a URLPath (basically, it's = what URLPath should have been if we had designed it carefully). In the = meanwhile, without adding a bunch of new API surface and abandoning = existing methods, Request.URLPath() is the easiest place to put this = fix. getRequestHostname is, as you correctly called out, probably useless, = but we should just adjust its docstring to direct users to the URLPath = method instead. Klein should then be changed to use Request.URLPath() to build any URLs. What do you think of this proposal? Does my reasoning make sense? -glyph --Apple-Mail=_B4AE43CE-3654-4CCA-AACC-7A03B41F260F Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8 <html><head><meta http-equiv=3D"Content-Type" content=3D"text/html = charset=3Dutf-8"></head><body style=3D"word-wrap: break-word; = -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;" = class=3D""><br class=3D""><div><blockquote type=3D"cite" class=3D""><div = class=3D"">On Mar 14, 2017, at 3:00 PM, Ilya Skriblovsky <<a = href=3D"mailto:[email protected]" = class=3D"">[email protected]</a>> wrote:</div><br = class=3D"Apple-interchange-newline"><div class=3D""><div dir=3D"ltr" = class=3D"">Tickets you have mentioned and <span = style=3D"font-family:"helvetica neue","bitstream vera = sans",helvetica,arial,sans-serif" class=3D"">forwarded-for-5807 = branch</span> are mostly about parsing X-Forwarded-For in order to = obtain correct client IP. While it is valuable task, it is not what = strikes me right now.</div></div></blockquote><div><br = class=3D""></div><div>Sorry, I was pretty tired when I wrote that = message, and I realize that I was getting server identification and = client identification mixed up.</div><br class=3D""><blockquote = type=3D"cite" class=3D""><div class=3D""><div dir=3D"ltr" class=3D""><div = class=3D"">I'm now more concerned with an absence of API for getting = user-visible server's name, not client's = ip.</div></div></div></blockquote><div><br class=3D""></div><div>Yes. = My mistake. (Although totally fix those other bugs too. = They're also bad. :))</div><br class=3D""><blockquote type=3D"cite" = class=3D""><div class=3D""><div dir=3D"ltr" class=3D""><div = class=3D""><div class=3D"">Look, I'm currently porting my app from = Django to Klein and noticed strange behavior of Klein. For = example:</div><div class=3D"">@app.route('/alias', alias=3DTrue)</div><div= class=3D"">@app.route('/path')</div><div class=3D"">def path(request): = return b'42'</div><div class=3D""><br class=3D""></div><div = class=3D"">When /alias is requested werkzeug generates a redirect to = /path. But Klein is passing Request.getHost() to Werkzeug, so redirect = gets internal hostname and exposes backend's internal hostname and port = to the user. Seems like Klein is passing incorrect hostname to Werkzeug. = But how can we fix that?</div></div><div class=3D""><br = class=3D""></div><div class=3D"">There are two methods in = Request:</div><div class=3D"">=E2=80=A2 Request.getHost() =E2=80=94 "Get = my originally requesting transport's host" as doc says. Ok, seems like = this method intentionally returns server's internal address.</div><div = class=3D"">=E2=80=A2 Request.getRequestHostname() =E2=80=94doc = says:</div><div class=3D"">>> "Get the hostname that the user = passed in to the request. This will either use the Host: header (if it = is available) or the host we are listening on if the header is = unavailable."</div></div></div></blockquote><blockquote type=3D"cite" = class=3D""><div class=3D""><div dir=3D"ltr" class=3D""><div = class=3D"">Cool, but why does this method only returns a hostname = without a port? It intentionally strips out the port number from Host = header. What is the point of such = implementation?</div></div></div></blockquote><div><br = class=3D""></div><div>Request is one of the oldest parts of Twisted, so = the likely reason is "it looked like a good idea at the time". = Request predates the requirement for test coverage, documentation = coverage, and, in many cases, the author (me) having any idea what they = were doing :). If you find something that looks bad, it's probably = just bad, there is unlikely any deeper reason.</div><div><br = class=3D""></div><div>Long term, we need to overhaul the API to have = fewer methods and be generally less confusing. See for example the = infamous <a href=3D"https://twistedmatrix.com/trac/ticket/288" = class=3D"">https://twistedmatrix.com/trac/ticket/288</a> ticket. = However, before we do that, we should make all the stuff that is = there already behave correctly and be documented even in its weird = shape; then we can transition to a new good thing confident in the = knowledge that no old applications will break and that users can move = over to the new APIs without massive disruption.</div><br = class=3D""><blockquote type=3D"cite" class=3D""><div class=3D""><div = dir=3D"ltr" class=3D""><div class=3D"">This method is used only a couple = of times inside Twisted itself, and in both places Twisted gets what = getRequestHostname() returned and mixes it with request.getHost().port = which is *definitely* incorrect, because the former is user-visible = while latter is internal. So if my backend server is using different = port than a fronend, it is impossible to use getRequestHostname() to = build user-visible URL. I think current getRequestHostname() = implementation is broken.</div></div></div></blockquote><blockquote = type=3D"cite" class=3D""><div dir=3D"ltr" class=3D""><div class=3D""><br = class=3D""></div><div class=3D"">So I have two proposals:</div><div = class=3D""><br class=3D""></div><div class=3D"">Proposal #1 (fixing = current behavior):</div><div class=3D"">=E2=80=A2 Variant #1: Change = Request.getRequestHostname() to return b"hostname:port". I think this is = the correct thing to do, but this is a backward-incompatible = change.</div><div class=3D"">- or -</div><div class=3D"">=E2=80=A2 = Variant #2: Change Klein to use Request.getHeader(b'Host') with fallback = to Request.getHost()</div><div class=3D""><br class=3D""></div><div = class=3D"">Proposal #2 (adding new feature if Variant #1 is = choosed):</div><div class=3D"">=E2=80=A2 Add useXForwardedHost=3DFalse = argument to Request.getRequestHostname() and useXForwardedProto=3DFalse = to Request.isSecure(). If True is passed, these methods will obey = corresponding request headers that are de-facto standard for reverse = proxies. Also add corresponding options to Klein app. This can simplify = reverse proxy configuration a bit.</div></div></blockquote><br = class=3D""></div><div>I have a third proposal.</div><div><br = class=3D""></div><div>Ideally if we want to know about the URL for the = request, we could ask the request to just give us the URL. And in = fact the URL does have a method, URLPath(), which is both (A) <i = class=3D"">unambiguously</i> broken (the case could be made that = getRequestHostname() is supposed to really just be a host, not for URL = generation, and maybe there is even a case where that makes sense; = origin comparisons perhaps) and (B) returning a data structure which = could be fixed to be correct without concern for client = compatibility.</div><div><br class=3D""></div><div>In the long term, we = should get rid of all these methods and have a single 'request.url()' = method which cleanly and correctly returns a <a = href=3D"https://twistedmatrix.com/documents/17.1.0/api/twisted.python.url.= URL.html" = class=3D"">https://twistedmatrix.com/documents/17.1.0/api/twisted.python.u= rl.URL.html</a> object, which is better than a string or a URLPath = (basically, it's what URLPath should have been if we had designed it = carefully). In the meanwhile, without adding a bunch of new API = surface and abandoning existing methods, Request.URLPath() is the = easiest place to put this fix.</div><div><br = class=3D""></div><div>getRequestHostname is, as you correctly called = out, probably useless, but we should just adjust its docstring to direct = users to the URLPath method instead.</div><div><br = class=3D""></div><div>Klein should then be changed to use = Request.URLPath() to build any URLs.</div><div><br = class=3D""></div><div>What do you think of this proposal? Does my = reasoning make sense?</div><div><br = class=3D""></div><div>-glyph</div><div><br class=3D""></div></body></html>= --Apple-Mail=_B4AE43CE-3654-4CCA-AACC-7A03B41F260F-- --===============1110597621251517107== Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Content-Disposition: inline _______________________________________________ Twisted-web mailing list [email protected] http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-web --===============1110597621251517107==--