Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit def17ee

Browse files
committed
Instrument proxy header behaviors
- Rewrite URL paths to as done in http1 - scrub hop-by-hop headers - insert the `via` header
1 parent 30bcc32 commit def17ee

File tree

10 files changed

+204
-18
lines changed

10 files changed

+204
-18
lines changed

linkerd/protocol/h2/src/main/scala/io/buoyant/linkerd/protocol/H2Config.scala

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,12 @@ class H2Config extends RouterConfig {
7171
@JsonDeserialize(using = classOf[H2IdentifierConfigDeserializer])
7272
var identifier: Option[Seq[H2IdentifierConfig]] = None
7373

74-
private[this] val combinedIdentifier: Option[H2.Identifier] = identifier.map { configs =>
75-
H2.Identifier { params =>
76-
RoutingFactory.Identifier.compose(configs.map(_.newIdentifier(params)))
74+
private[this] def combinedIdentifier: Option[H2.Identifier] =
75+
identifier.map { configs =>
76+
H2.Identifier { params =>
77+
RoutingFactory.Identifier.compose(configs.map(_.newIdentifier(params)))
78+
}
7779
}
78-
}
7980

8081
@JsonIgnore
8182
override def baseResponseClassifier =
@@ -119,10 +120,11 @@ class H2IdentifierConfigDeserializer extends JsonDeserializer[Option[Seq[H2Ident
119120
_c: DeserializationContext
120121
): Option[Seq[H2IdentifierConfig]] = {
121122
val codec = p.getCodec
123+
val klass = classOf[H2IdentifierConfig]
122124
codec.readTree[TreeNode](p) match {
123125
case n: JsonNode if n.isArray =>
124-
Some(n.asScala.toList.map(codec.treeToValue(_, classOf[H2IdentifierConfig])))
125-
case node => Some(Seq(codec.treeToValue(node, classOf[H2IdentifierConfig])))
126+
Some(n.asScala.toList.map(codec.treeToValue(_, klass)))
127+
case node => Some(Seq(codec.treeToValue(node, klass)))
126128
}
127129
}
128130

linkerd/protocol/h2/src/main/scala/io/buoyant/linkerd/protocol/h2/HeaderTokenIdentifier.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class HeaderTokenIdentifier(header: String, pfx: Path, baseDtab: () => Dtab)
3535

3636
override def apply(req: Request): Future[RequestIdentification[Request]] =
3737
req.headers.get(header).lastOption match {
38-
case None => Future.value(unidentified)
38+
case None | Some("") => Future.value(unidentified)
3939
case Some(token) =>
4040
val path = pfx ++ Path.Utf8(token)
4141
val dst = Dst.Path(path, baseDtab(), Dtab.local)

router/h2/src/test/scala/io/buoyant/router/h2/HeaderTokenIdentifierTest.scala renamed to linkerd/protocol/h2/src/test/scala/io/buoyant/linkerd/protocol/h2/HeaderTokenIdentifierTest.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package io.buoyant.router.h2
1+
package io.buoyant.linkerd.protocol.h2
22

33
import com.twitter.finagle.{Dtab, Path}
44
import com.twitter.finagle.buoyant.Dst

router/h2/src/main/scala/com/twitter/finagle/buoyant/h2/Message.scala

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ trait Headers {
3030
def get(k: String): Seq[String]
3131
def add(k: String, v: String): Unit
3232
def set(k: String, v: String): Unit
33-
def remove(key: String): Boolean
33+
def remove(key: String): Seq[String]
3434
def dup(): Headers
3535
}
3636

@@ -61,11 +61,11 @@ object Headers {
6161
current = current :+ (key -> v)
6262
}
6363
def remove(key: String) = synchronized {
64-
val filtered = current.filterNot { case (k, _) => key == k }
65-
val isUnchanged = filtered.length == current.length
66-
current = filtered
67-
isUnchanged
64+
val isMatch: ((String, String)) => Boolean = { case (k, _) => key == k }
65+
val (removed, updated) = current.partition(isMatch)
66+
removed.map(toVal)
6867
}
68+
private[this] val toVal: ((String, String)) => String = { case (_, v) => v }
6969
def dup() = new Impl(synchronized(current))
7070
}
7171
}

router/h2/src/main/scala/com/twitter/finagle/buoyant/h2/netty4/Netty4Message.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,11 @@ private[h2] object Netty4Message {
3636
underlying.set(key, value); ()
3737
}
3838

39-
override def remove(key: String): Boolean =
39+
override def remove(key: String): Seq[String] = {
40+
val removed = get(key)
4041
underlying.remove(key)
42+
removed
43+
}
4144

4245
override def dup(): Headers = {
4346
val headers = new DefaultHttp2Headers

router/h2/src/main/scala/io/buoyant/router/H2.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ object H2 extends Client[Request, Response]
8888

8989
object Router {
9090
val pathStack: Stack[ServiceFactory[Request, Response]] =
91-
StackRouter.newPathStack
91+
h2.ViaHeaderFilter.module +:
92+
h2.ScrubHopByHopHeadersFilter.module +:
93+
StackRouter.newPathStack
9294

9395
val boundStack: Stack[ServiceFactory[Request, Response]] =
9496
StackRouter.newBoundStack
@@ -128,13 +130,14 @@ object H2 extends Client[Request, Response]
128130

129131
object Server {
130132
val newStack: Stack[ServiceFactory[Request, Response]] = StackServer.newStack
133+
.insertAfter(StackServer.Role.protoTracing, h2.ProxyRewriteFilter.module)
131134

132135
val defaultParams = StackServer.defaultParams +
133136
param.ProtocolLibrary("h2")
134137
}
135138

136139
case class Server(
137-
stack: Stack[ServiceFactory[Request, Response]] = StackServer.newStack,
140+
stack: Stack[ServiceFactory[Request, Response]] = Server.newStack,
138141
params: Stack.Params = Server.defaultParams
139142
) extends StdStackServer[Request, Response, Server] {
140143

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.buoyant.router.h2
2+
3+
import com.twitter.finagle.{Filter, Service, ServiceFactory, SimpleFilter, Stack, Stackable}
4+
import com.twitter.finagle.buoyant.h2.{Request, Response, Message}
5+
import java.net.URI
6+
7+
/**
8+
* Coerces HTTP proxy requests to normal requests.
9+
*/
10+
object ProxyRewriteFilter {
11+
12+
private[this] val log = com.twitter.logging.Logger.get(getClass.getSimpleName)
13+
14+
/**
15+
* If the original URI is absolute
16+
* (e.g. scheme://host/path?query#fragment), drop the scheme, use
17+
* the authority to set the request's Host header, and rewrite the
18+
* URI to the remaining path, query, and fragment.
19+
*/
20+
private def rewriteIfProxy(req: Request): Unit =
21+
guessAbsolute(req.path) match {
22+
case null => // not absolute, nothing to do
23+
log.warning(s"proxy rewrite: not absolute ${req.path}")
24+
case uri if uri.isAbsolute =>
25+
log.warning(s"proxy rewrite: absolute ${req.path}")
26+
req.headers.set(":authority", uri.getAuthority)
27+
req.headers.set(":path", unproxifyUri(uri))
28+
case _ => // wasn't actually absolute after all
29+
log.warning(s"proxy rewrite: not absolute? ${req.path}")
30+
}
31+
32+
/**
33+
* If the uri _looks_ absolute, build a URI. This doesn't have to be
34+
* perfect, as we can know authoritatively after we've parsed the
35+
* URI. This lets us forego parsing if the URI doesn't have a scheme.
36+
*
37+
* We're using nulls here so we can forego a Some allocation when
38+
* the URI is absolute.
39+
*/
40+
private[this] def guessAbsolute(uri: String): URI =
41+
if (uri.contains("://")) new URI(uri)
42+
else null
43+
44+
/**
45+
* Return only the path, query, and fragment segments of a URI.
46+
*/
47+
private def unproxifyUri(uri: URI): String =
48+
new URI(null, null, uri.getPath, uri.getQuery, uri.getFragment).toString
49+
50+
val filter: Filter[Request, Response, Request, Response] =
51+
new SimpleFilter[Request, Response] {
52+
def apply(req: Request, service: Service[Request, Response]) = {
53+
log.warning(s"proxy rewrite: $req")
54+
rewriteIfProxy(req)
55+
service(req)
56+
}
57+
}
58+
59+
val module: Stackable[ServiceFactory[Request, Response]] =
60+
new Stack.Module0[ServiceFactory[Request, Response]] {
61+
val role = Stack.Role("ProxyRewriteFilter")
62+
63+
val description = "Rewrites proxied requests as direct requests, " +
64+
"overriding the Host header and removing canonical proxy headers"
65+
66+
def make(next: ServiceFactory[Request, Response]) = {
67+
log.warning("installing proxy rewrite filter")
68+
filter.andThen(next)
69+
}
70+
}
71+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.buoyant.router.h2
2+
3+
import com.twitter.finagle.{Service, ServiceFactory, SimpleFilter, Stack}
4+
import com.twitter.finagle.buoyant.h2.{Message, Request, Response}
5+
6+
object ScrubHopByHopHeadersFilter {
7+
8+
object HopByHopHeaders {
9+
val Connection = "connection"
10+
val ProxyAuthenticate = "proxy-authenticate"
11+
val ProxyAuthorization = "proxy-authorization"
12+
val ProxyConnection = "proxy-connection"
13+
val Te = "te"
14+
val Trailer = "trailer"
15+
val TransferEncoding = "transfer-encoding"
16+
val Upgrade = "upgrade"
17+
18+
def scrub(msg: Message): Unit = {
19+
msg.headers.remove(ProxyAuthenticate)
20+
msg.headers.remove(ProxyAuthorization)
21+
msg.headers.remove(Te)
22+
msg.headers.remove(Trailer)
23+
msg.headers.remove(TransferEncoding)
24+
msg.headers.remove(Upgrade)
25+
for {
26+
conn <- msg.headers.remove(Connection) ++ msg.headers.remove(ProxyConnection)
27+
header <- conn.split(", ")
28+
} {
29+
val h = header.trim
30+
if (h.nonEmpty) {
31+
msg.headers.remove(h); ()
32+
}
33+
}
34+
}
35+
}
36+
37+
/**
38+
* Removes all Hop-by-Hop headers and any header listed in `Connection` header from requests.
39+
*/
40+
object filter extends SimpleFilter[Request, Response] {
41+
42+
def apply(req: Request, svc: Service[Request, Response]) = {
43+
HopByHopHeaders.scrub(req)
44+
svc(req).map(scrubResponse)
45+
}
46+
47+
private[this] val scrubResponse: Response => Response = { rsp =>
48+
HopByHopHeaders.scrub(rsp)
49+
rsp
50+
}
51+
}
52+
53+
object module extends Stack.Module0[ServiceFactory[Request, Response]] {
54+
val role = Stack.Role("ScrubHopByHopHeadersFilter")
55+
val description =
56+
"Removes all Hop-by-Hop headers and any header listed in `Connection` header from requests"
57+
58+
def make(next: ServiceFactory[Request, Response]) =
59+
filter.andThen(next)
60+
}
61+
62+
}
63+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package io.buoyant.router.h2
2+
3+
import com.twitter.finagle.{Service, ServiceFactory, SimpleFilter, Stack, Stackable}
4+
import com.twitter.finagle.buoyant.h2.{Message, Request, Response}
5+
6+
/**
7+
* Appends the `via` header to all requests and responses.
8+
* See https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-9.9
9+
*/
10+
object ViaHeaderFilter {
11+
val Key = "via"
12+
13+
case class Param(via: String)
14+
implicit object Param extends Stack.Param[Param] {
15+
val default = Param("h2 linkerd")
16+
}
17+
18+
/**
19+
* Appends the 'via' header.
20+
*/
21+
class Filter(via: String) extends SimpleFilter[Request, Response] {
22+
def apply(req: Request, svc: Service[Request, Response]) =
23+
svc(setRequest(req)).map(setResponse)
24+
25+
private[this] def setMessage[T <: Message]: T => T = { msg =>
26+
val updated = msg.headers.get(Key) match {
27+
case Nil => via
28+
case vals => vals.mkString("", ", ", s", $via")
29+
}
30+
msg.headers.set(Key, updated); ()
31+
msg
32+
}
33+
private[this] val setRequest = setMessage[Request]
34+
private[this] val setResponse = setMessage[Response]
35+
}
36+
37+
val module: Stackable[ServiceFactory[Request, Response]] =
38+
new Stack.Module1[Param, ServiceFactory[Request, Response]] {
39+
val role = Stack.Role("ViaHeaderFilter")
40+
val description = "Appends the Via header to the request and response."
41+
def make(param: Param, next: ServiceFactory[Request, Response]) =
42+
new Filter(param.via).andThen(next)
43+
}
44+
}

router/h2/src/test/scala/com/twitter/finagle/buoyant/h2/ServerDispatcherTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class ServerDispatcherTest extends FunSuite with Eventually {
8585
def contains(k: String) = false
8686
def add(k: String, v: String) = {}
8787
def set(k: String, v: String) = {}
88-
def remove(k: String) = false
88+
def remove(k: String) = Nil
8989
def dup() = this
9090
}
9191
override val data: Stream = new Stream.Reader {
@@ -121,7 +121,7 @@ class ServerDispatcherTest extends FunSuite with Eventually {
121121
override def contains(k: String) = false
122122
override def add(k: String, v: String) = {}
123123
override def set(k: String, v: String) = {}
124-
override def remove(k: String) = false
124+
override def remove(k: String) = Nil
125125
override def dup() = this
126126
}
127127
override val data: Stream = new Stream.Reader {

0 commit comments

Comments
 (0)