From cf46ab887ff17d16c6da559d4dacfe0ca99dd79c Mon Sep 17 00:00:00 2001 From: Hawstein Date: Sun, 24 Jul 2016 23:56:39 +0800 Subject: [PATCH] +htc #20771 provide different options to deal with the illegal response header value (#20976) --- .../src/main/resources/reference.conf | 9 +++ .../client/OutgoingConnectionBlueprint.scala | 2 +- .../impl/engine/parsing/BodyPartParser.scala | 2 +- .../engine/parsing/HttpHeaderParser.scala | 60 +++++++++++----- .../engine/server/HttpServerBluePrint.scala | 2 +- .../engine/ws/WebSocketClientBlueprint.scala | 2 +- .../http/impl/model/parser/HeaderParser.scala | 15 ++-- .../impl/settings/ParserSettingsImpl.scala | 40 ++++++----- .../javadsl/settings/ParserSettings.scala | 2 + .../scaladsl/settings/ParserSettings.scala | 23 ++++++- .../LowLevelOutgoingConnectionSpec.scala | 69 ++++++++++++++++++- .../engine/parsing/HttpHeaderParserSpec.scala | 1 + .../parsing/HttpHeaderParserTestBed.scala | 2 +- .../engine/parsing/RequestParserSpec.scala | 4 +- .../engine/parsing/ResponseParserSpec.scala | 2 +- project/MiMa.scala | 6 +- 16 files changed, 185 insertions(+), 56 deletions(-) diff --git a/akka-http-core/src/main/resources/reference.conf b/akka-http-core/src/main/resources/reference.conf index 28012b6ba1..bdc03585ac 100644 --- a/akka-http-core/src/main/resources/reference.conf +++ b/akka-http-core/src/main/resources/reference.conf @@ -352,6 +352,15 @@ akka.http { # `full` : the full error details (potentially spanning several lines) are logged error-logging-verbosity = full + # Configures the processing mode when encountering illegal characters in + # header value of response. + # + # Supported mode: + # `error` : default mode, throw an ParsingException and terminate the processing + # `warn` : ignore the illegal characters in response header value and log a warning message + # `ignore` : just ignore the illegal characters in response header value + illegal-response-header-value-processing-mode = error + # limits for the number of different values per header type that the # header cache will hold header-cache { diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/client/OutgoingConnectionBlueprint.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/client/OutgoingConnectionBlueprint.scala index 3b61836874..a49a6e191b 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/client/OutgoingConnectionBlueprint.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/client/OutgoingConnectionBlueprint.scala @@ -83,7 +83,7 @@ private[http] object OutgoingConnectionBlueprint { val responseParsingMerge = b.add { // the initial header parser we initially use for every connection, // will not be mutated, all "shared copy" parsers copy on first-write into the header cache - val rootParser = new HttpResponseParser(parserSettings, HttpHeaderParser(parserSettings) { info ⇒ + val rootParser = new HttpResponseParser(parserSettings, HttpHeaderParser(parserSettings, log) { info ⇒ if (parserSettings.illegalHeaderWarnings) logParsingError(info withSummaryPrepended "Illegal response header", log, parserSettings.errorLoggingVerbosity) }) diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala index 19ac33da07..56eeb9ea98 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/BodyPartParser.scala @@ -58,7 +58,7 @@ private[http] final class BodyPartParser( private[this] val boyerMoore = new BoyerMoore(needle) // TODO: prevent re-priming header parser from scratch - private[this] val headerParser = HttpHeaderParser(settings) { errorInfo ⇒ + private[this] val headerParser = HttpHeaderParser(settings, log) { errorInfo ⇒ if (illegalHeaderWarnings) log.warning(errorInfo.withSummaryPrepended("Illegal multipart header").formatPretty) } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala index 63588995e8..867e39680b 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/parsing/HttpHeaderParser.scala @@ -7,6 +7,10 @@ package akka.http.impl.engine.parsing import java.nio.{ CharBuffer, ByteBuffer } import java.util.Arrays.copyOf import java.lang.{ StringBuilder ⇒ JStringBuilder } +import akka.event.LoggingAdapter +import akka.http.scaladsl.settings.ParserSettings.IllegalResponseHeaderValueProcessingMode +import akka.http.scaladsl.settings.ParserSettings + import scala.annotation.tailrec import akka.parboiled2.CharUtils import akka.util.ByteString @@ -60,6 +64,7 @@ import akka.http.impl.model.parser.CharacterClasses._ */ private[engine] final class HttpHeaderParser private ( val settings: HttpHeaderParser.Settings, + val log: LoggingAdapter, onIllegalHeader: ErrorInfo ⇒ Unit, private[this] var nodes: Array[Char] = new Array(512), // initial size, can grow as needed private[this] var nodeCount: Int = 0, @@ -85,7 +90,7 @@ private[engine] final class HttpHeaderParser private ( * Returns a copy of this parser that shares the trie data with this instance. */ def createShallowCopy(): HttpHeaderParser = - new HttpHeaderParser(settings, onIllegalHeader, nodes, nodeCount, branchData, branchDataCount, values, valueCount) + new HttpHeaderParser(settings, log, onIllegalHeader, nodes, nodeCount, branchData, branchDataCount, values, valueCount) /** * Parses a header line and returns the line start index of the subsequent line. @@ -145,12 +150,14 @@ private[engine] final class HttpHeaderParser private ( val colonIx = scanHeaderNameAndReturnIndexOfColon(input, lineStart, lineStart + 1 + maxHeaderNameLength)(cursor) val headerName = asciiString(input, lineStart, colonIx) try { - val valueParser = new RawHeaderValueParser(headerName, maxHeaderValueLength, headerValueCacheLimit(headerName)) + val valueParser = new RawHeaderValueParser(headerName, maxHeaderValueLength, + headerValueCacheLimit(headerName), log, illegalResponseHeaderValueProcessingMode) insert(input, valueParser)(cursor, colonIx + 1, nodeIx, colonIx) parseHeaderLine(input, lineStart)(cursor, nodeIx) } catch { case OutOfTrieSpaceException ⇒ // if we cannot insert we drop back to simply creating new header instances - val (headerValue, endIx) = scanHeaderValue(this, input, colonIx + 1, colonIx + maxHeaderValueLength + 3)() + val (headerValue, endIx) = scanHeaderValue(this, input, colonIx + 1, colonIx + maxHeaderValueLength + 3, + log, settings.illegalResponseHeaderValueProcessingMode)() resultHeader = RawHeader(headerName, headerValue.trim) endIx } @@ -413,6 +420,7 @@ private[http] object HttpHeaderParser { def maxHeaderValueLength: Int def headerValueCacheLimit(headerName: String): Int def customMediaTypes: MediaTypes.FindCustom + def illegalResponseHeaderValueProcessingMode: IllegalResponseHeaderValueProcessingMode } private def predefinedHeaders = Seq( @@ -426,16 +434,16 @@ private[http] object HttpHeaderParser { "Cache-Control: no-cache", "Expect: 100-continue") - def apply(settings: HttpHeaderParser.Settings)(onIllegalHeader: ErrorInfo ⇒ Unit = info ⇒ throw IllegalHeaderException(info)) = - prime(unprimed(settings, onIllegalHeader)) + def apply(settings: HttpHeaderParser.Settings, log: LoggingAdapter)(onIllegalHeader: ErrorInfo ⇒ Unit = info ⇒ throw IllegalHeaderException(info)) = + prime(unprimed(settings, log, onIllegalHeader)) - def unprimed(settings: HttpHeaderParser.Settings, warnOnIllegalHeader: ErrorInfo ⇒ Unit) = - new HttpHeaderParser(settings, warnOnIllegalHeader) + def unprimed(settings: HttpHeaderParser.Settings, log: LoggingAdapter, warnOnIllegalHeader: ErrorInfo ⇒ Unit) = + new HttpHeaderParser(settings, log, warnOnIllegalHeader) def prime(parser: HttpHeaderParser): HttpHeaderParser = { val valueParsers: Seq[HeaderValueParser] = HeaderParser.ruleNames.map { name ⇒ - new ModeledHeaderValueParser(name, parser.settings.maxHeaderValueLength, parser.settings.headerValueCacheLimit(name), parser.settings) + new ModeledHeaderValueParser(name, parser.settings.maxHeaderValueLength, parser.settings.headerValueCacheLimit(name), parser.log, parser.settings) }(collection.breakOut) def insertInGoodOrder(items: Seq[Any])(startIx: Int = 0, endIx: Int = items.size): Unit = if (endIx - startIx > 0) { @@ -470,11 +478,11 @@ private[http] object HttpHeaderParser { def cachingEnabled = maxValueCount > 0 } - private[parsing] class ModeledHeaderValueParser(headerName: String, maxHeaderValueLength: Int, maxValueCount: Int, settings: HeaderParser.Settings) + private[parsing] class ModeledHeaderValueParser(headerName: String, maxHeaderValueLength: Int, maxValueCount: Int, log: LoggingAdapter, settings: HeaderParser.Settings) extends HeaderValueParser(headerName, maxValueCount) { def apply(hhp: HttpHeaderParser, input: ByteString, valueStart: Int, onIllegalHeader: ErrorInfo ⇒ Unit): (HttpHeader, Int) = { // TODO: optimize by running the header value parser directly on the input ByteString (rather than an extracted String); seems done? - val (headerValue, endIx) = scanHeaderValue(hhp, input, valueStart, valueStart + maxHeaderValueLength + 2)() + val (headerValue, endIx) = scanHeaderValue(hhp, input, valueStart, valueStart + maxHeaderValueLength + 2, log, settings.illegalResponseHeaderValueProcessingMode)() val trimmedHeaderValue = headerValue.trim val header = HeaderParser.parseFull(headerName, trimmedHeaderValue, settings) match { case Right(h) ⇒ h @@ -486,10 +494,10 @@ private[http] object HttpHeaderParser { } } - private[parsing] class RawHeaderValueParser(headerName: String, maxHeaderValueLength: Int, maxValueCount: Int) - extends HeaderValueParser(headerName, maxValueCount) { + private[parsing] class RawHeaderValueParser(headerName: String, maxHeaderValueLength: Int, maxValueCount: Int, + log: LoggingAdapter, mode: IllegalResponseHeaderValueProcessingMode) extends HeaderValueParser(headerName, maxValueCount) { def apply(hhp: HttpHeaderParser, input: ByteString, valueStart: Int, onIllegalHeader: ErrorInfo ⇒ Unit): (HttpHeader, Int) = { - val (headerValue, endIx) = scanHeaderValue(hhp, input, valueStart, valueStart + maxHeaderValueLength + 2)() + val (headerValue, endIx) = scanHeaderValue(hhp, input, valueStart, valueStart + maxHeaderValueLength + 2, log, mode)() RawHeader(headerName, headerValue.trim) → endIx } } @@ -503,15 +511,16 @@ private[http] object HttpHeaderParser { } else fail(s"HTTP header name exceeds the configured limit of ${limit - start - 1} characters") - @tailrec private def scanHeaderValue(hhp: HttpHeaderParser, input: ByteString, start: Int, - limit: Int)(sb: JStringBuilder = null, ix: Int = start): (String, Int) = { + @tailrec private def scanHeaderValue(hhp: HttpHeaderParser, input: ByteString, start: Int, limit: Int, log: LoggingAdapter, + mode: IllegalResponseHeaderValueProcessingMode)(sb: JStringBuilder = null, ix: Int = start): (String, Int) = { + def appended(c: Char) = (if (sb != null) sb else new JStringBuilder(asciiString(input, start, ix))).append(c) def appended2(c: Int) = if ((c >> 16) != 0) appended(c.toChar).append((c >> 16).toChar) else appended(c.toChar) if (ix < limit) byteChar(input, ix) match { - case '\t' ⇒ scanHeaderValue(hhp, input, start, limit)(appended(' '), ix + 1) + case '\t' ⇒ scanHeaderValue(hhp, input, start, limit, log, mode)(appended(' '), ix + 1) case '\r' if byteChar(input, ix + 1) == '\n' ⇒ - if (WSP(byteChar(input, ix + 2))) scanHeaderValue(hhp, input, start, limit)(appended(' '), ix + 3) + if (WSP(byteChar(input, ix + 2))) scanHeaderValue(hhp, input, start, limit, log, mode)(appended(' '), ix + 3) else (if (sb != null) sb.toString else asciiString(input, start, ix), ix + 2) case c ⇒ var nix = ix + 1 @@ -544,8 +553,21 @@ private[http] object HttpHeaderParser { case -1 ⇒ if (sb != null) sb.append(c).append(byteChar(input, ix + 1)).append(byteChar(input, ix + 2)).append(byteChar(input, ix + 3)) else null case cc ⇒ appended2(cc) } - } else fail(s"Illegal character '${escape(c)}' in header value") - scanHeaderValue(hhp, input, start, limit)(nsb, nix) + } else { + mode match { + case ParserSettings.IllegalResponseHeaderValueProcessingMode.Error ⇒ + fail(s"Illegal character '${escape(c)}' in header value") + case ParserSettings.IllegalResponseHeaderValueProcessingMode.Warn ⇒ + // ignore the illegal character and log a warning message + log.warning(s"Illegal character '${escape(c)}' in header value") + sb + case ParserSettings.IllegalResponseHeaderValueProcessingMode.Ignore ⇒ + // just ignore the illegal character + sb + } + + } + scanHeaderValue(hhp, input, start, limit, log, mode)(nsb, nix) } else fail(s"HTTP header value exceeds the configured limit of ${limit - start - 2} characters") } diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala index bcc28bc828..301d542369 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/server/HttpServerBluePrint.scala @@ -211,7 +211,7 @@ private[http] object HttpServerBluePrint { // the initial header parser we initially use for every connection, // will not be mutated, all "shared copy" parsers copy on first-write into the header cache val rootParser = new HttpRequestParser(parserSettings, rawRequestUriHeader, - HttpHeaderParser(parserSettings) { info ⇒ + HttpHeaderParser(parserSettings, log) { info ⇒ if (parserSettings.illegalHeaderWarnings) logParsingError(info withSummaryPrepended "Illegal request header", log, parserSettings.errorLoggingVerbosity) }) diff --git a/akka-http-core/src/main/scala/akka/http/impl/engine/ws/WebSocketClientBlueprint.scala b/akka-http-core/src/main/scala/akka/http/impl/engine/ws/WebSocketClientBlueprint.scala index e193877496..c95ef042eb 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/engine/ws/WebSocketClientBlueprint.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/engine/ws/WebSocketClientBlueprint.scala @@ -62,7 +62,7 @@ object WebSocketClientBlueprint { new GraphStageLogic(shape) with InHandler with OutHandler { // a special version of the parser which only parses one message and then reports the remaining data // if some is available - val parser = new HttpResponseParser(settings.parserSettings, HttpHeaderParser(settings.parserSettings)()) { + val parser = new HttpResponseParser(settings.parserSettings, HttpHeaderParser(settings.parserSettings, log)()) { var first = true override def handleInformationalResponses = false override protected def parseMessage(input: ByteString, offset: Int): StateResult = { diff --git a/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala b/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala index 1d95e2f0b6..04c6e3a157 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/model/parser/HeaderParser.scala @@ -6,6 +6,7 @@ package akka.http.impl.model.parser import akka.http.scaladsl.settings.ParserSettings import akka.http.scaladsl.settings.ParserSettings.CookieParsingMode +import akka.http.scaladsl.settings.ParserSettings.IllegalResponseHeaderValueProcessingMode import akka.http.scaladsl.model.headers.HttpCookiePair import akka.stream.impl.ConstantFun import scala.util.control.NonFatal @@ -169,20 +170,26 @@ private[http] object HeaderParser { def uriParsingMode: Uri.ParsingMode def cookieParsingMode: ParserSettings.CookieParsingMode def customMediaTypes: MediaTypes.FindCustom + def illegalResponseHeaderValueProcessingMode: IllegalResponseHeaderValueProcessingMode } def Settings( - uriParsingMode: Uri.ParsingMode = Uri.ParsingMode.Relaxed, - cookieParsingMode: ParserSettings.CookieParsingMode = ParserSettings.CookieParsingMode.RFC6265, - customMediaTypes: MediaTypes.FindCustom = ConstantFun.scalaAnyTwoToNone): Settings = { + uriParsingMode: Uri.ParsingMode = Uri.ParsingMode.Relaxed, + cookieParsingMode: ParserSettings.CookieParsingMode = ParserSettings.CookieParsingMode.RFC6265, + customMediaTypes: MediaTypes.FindCustom = ConstantFun.scalaAnyTwoToNone, + mode: IllegalResponseHeaderValueProcessingMode = ParserSettings.IllegalResponseHeaderValueProcessingMode.Error): Settings = { + val _uriParsingMode = uriParsingMode val _cookieParsingMode = cookieParsingMode val _customMediaTypes = customMediaTypes + val _illegalResponseHeaderValueProcessingMode = mode new Settings { def uriParsingMode: Uri.ParsingMode = _uriParsingMode def cookieParsingMode: CookieParsingMode = _cookieParsingMode def customMediaTypes: MediaTypes.FindCustom = _customMediaTypes + def illegalResponseHeaderValueProcessingMode: IllegalResponseHeaderValueProcessingMode = + _illegalResponseHeaderValueProcessingMode } } val DefaultSettings: Settings = Settings() -} \ No newline at end of file +} diff --git a/akka-http-core/src/main/scala/akka/http/impl/settings/ParserSettingsImpl.scala b/akka-http-core/src/main/scala/akka/http/impl/settings/ParserSettingsImpl.scala index 4214065af4..09362ba630 100644 --- a/akka-http-core/src/main/scala/akka/http/impl/settings/ParserSettingsImpl.scala +++ b/akka-http-core/src/main/scala/akka/http/impl/settings/ParserSettingsImpl.scala @@ -5,7 +5,7 @@ package akka.http.impl.settings import akka.http.scaladsl.settings.ParserSettings -import akka.http.scaladsl.settings.ParserSettings.{ ErrorLoggingVerbosity, CookieParsingMode } +import akka.http.scaladsl.settings.ParserSettings.{ IllegalResponseHeaderValueProcessingMode, ErrorLoggingVerbosity, CookieParsingMode } import akka.stream.impl.ConstantFun import com.typesafe.config.Config import scala.collection.JavaConverters._ @@ -14,24 +14,25 @@ import akka.http.impl.util._ /** INTERNAL API */ private[akka] final case class ParserSettingsImpl( - maxUriLength: Int, - maxMethodLength: Int, - maxResponseReasonLength: Int, - maxHeaderNameLength: Int, - maxHeaderValueLength: Int, - maxHeaderCount: Int, - maxContentLength: Long, - maxChunkExtLength: Int, - maxChunkSize: Int, - uriParsingMode: Uri.ParsingMode, - cookieParsingMode: CookieParsingMode, - illegalHeaderWarnings: Boolean, - errorLoggingVerbosity: ParserSettings.ErrorLoggingVerbosity, - headerValueCacheLimits: Map[String, Int], - includeTlsSessionInfoHeader: Boolean, - customMethods: String ⇒ Option[HttpMethod], - customStatusCodes: Int ⇒ Option[StatusCode], - customMediaTypes: MediaTypes.FindCustom) + maxUriLength: Int, + maxMethodLength: Int, + maxResponseReasonLength: Int, + maxHeaderNameLength: Int, + maxHeaderValueLength: Int, + maxHeaderCount: Int, + maxContentLength: Long, + maxChunkExtLength: Int, + maxChunkSize: Int, + uriParsingMode: Uri.ParsingMode, + cookieParsingMode: CookieParsingMode, + illegalHeaderWarnings: Boolean, + errorLoggingVerbosity: ErrorLoggingVerbosity, + illegalResponseHeaderValueProcessingMode: IllegalResponseHeaderValueProcessingMode, + headerValueCacheLimits: Map[String, Int], + includeTlsSessionInfoHeader: Boolean, + customMethods: String ⇒ Option[HttpMethod], + customStatusCodes: Int ⇒ Option[StatusCode], + customMediaTypes: MediaTypes.FindCustom) extends akka.http.scaladsl.settings.ParserSettings { require(maxUriLength > 0, "max-uri-length must be > 0") @@ -76,6 +77,7 @@ object ParserSettingsImpl extends SettingsCompanion[ParserSettingsImpl]("akka.ht CookieParsingMode(c getString "cookie-parsing-mode"), c getBoolean "illegal-header-warnings", ErrorLoggingVerbosity(c getString "error-logging-verbosity"), + IllegalResponseHeaderValueProcessingMode(c getString "illegal-response-header-value-processing-mode"), cacheConfig.entrySet.asScala.map(kvp ⇒ kvp.getKey → cacheConfig.getInt(kvp.getKey))(collection.breakOut), c getBoolean "tls-session-info-header", noCustomMethods, diff --git a/akka-http-core/src/main/scala/akka/http/javadsl/settings/ParserSettings.scala b/akka-http-core/src/main/scala/akka/http/javadsl/settings/ParserSettings.scala index c977b4ead7..21279b3bfb 100644 --- a/akka-http-core/src/main/scala/akka/http/javadsl/settings/ParserSettings.scala +++ b/akka-http-core/src/main/scala/akka/http/javadsl/settings/ParserSettings.scala @@ -33,6 +33,7 @@ abstract class ParserSettings private[akka] () extends BodyPartParser.Settings { def getCookieParsingMode: ParserSettings.CookieParsingMode def getIllegalHeaderWarnings: Boolean def getErrorLoggingVerbosity: ParserSettings.ErrorLoggingVerbosity + def getIllegalResponseHeaderValueProcessingMode: ParserSettings.IllegalResponseHeaderValueProcessingMode def getHeaderValueCacheLimits: ju.Map[String, Int] def getIncludeTlsSessionInfoHeader: Boolean def headerValueCacheLimits: Map[String, Int] @@ -81,6 +82,7 @@ abstract class ParserSettings private[akka] () extends BodyPartParser.Settings { object ParserSettings extends SettingsCompanion[ParserSettings] { trait CookieParsingMode trait ErrorLoggingVerbosity + trait IllegalResponseHeaderValueProcessingMode override def create(config: Config): ParserSettings = ParserSettingsImpl(config) override def create(configOverrides: String): ParserSettings = ParserSettingsImpl(configOverrides) diff --git a/akka-http-core/src/main/scala/akka/http/scaladsl/settings/ParserSettings.scala b/akka-http-core/src/main/scala/akka/http/scaladsl/settings/ParserSettings.scala index fa4ae90d73..97dd767d49 100644 --- a/akka-http-core/src/main/scala/akka/http/scaladsl/settings/ParserSettings.scala +++ b/akka-http-core/src/main/scala/akka/http/scaladsl/settings/ParserSettings.scala @@ -34,6 +34,7 @@ abstract class ParserSettings private[akka] () extends akka.http.javadsl.setting def cookieParsingMode: ParserSettings.CookieParsingMode def illegalHeaderWarnings: Boolean def errorLoggingVerbosity: ParserSettings.ErrorLoggingVerbosity + def illegalResponseHeaderValueProcessingMode: ParserSettings.IllegalResponseHeaderValueProcessingMode def headerValueCacheLimits: Map[String, Int] def includeTlsSessionInfoHeader: Boolean def customMethods: String ⇒ Option[HttpMethod] @@ -56,6 +57,7 @@ abstract class ParserSettings private[akka] () extends akka.http.javadsl.setting override def getMaxUriLength = maxUriLength override def getMaxMethodLength = maxMethodLength override def getErrorLoggingVerbosity: js.ParserSettings.ErrorLoggingVerbosity = errorLoggingVerbosity + override def getIllegalResponseHeaderValueProcessingMode = illegalResponseHeaderValueProcessingMode override def getCustomMethods = new Function[String, Optional[akka.http.javadsl.model.HttpMethod]] { override def apply(t: String) = OptionConverters.toJava(customMethods(t)) @@ -100,10 +102,12 @@ abstract class ParserSettings private[akka] () extends akka.http.javadsl.setting val map = types.map(c ⇒ (c.mainType, c.subType) → c).toMap self.copy(customMediaTypes = (main, sub) ⇒ map.get((main, sub))) } + def withIllegalResponseHeaderValueProcessingMode(newValue: ParserSettings.IllegalResponseHeaderValueProcessingMode): ParserSettings = + self.copy(illegalResponseHeaderValueProcessingMode = newValue) } object ParserSettings extends SettingsCompanion[ParserSettings] { - trait CookieParsingMode extends akka.http.javadsl.settings.ParserSettings.CookieParsingMode + sealed trait CookieParsingMode extends akka.http.javadsl.settings.ParserSettings.CookieParsingMode object CookieParsingMode { case object RFC6265 extends CookieParsingMode case object Raw extends CookieParsingMode @@ -114,7 +118,7 @@ object ParserSettings extends SettingsCompanion[ParserSettings] { } } - trait ErrorLoggingVerbosity extends akka.http.javadsl.settings.ParserSettings.ErrorLoggingVerbosity + sealed trait ErrorLoggingVerbosity extends akka.http.javadsl.settings.ParserSettings.ErrorLoggingVerbosity object ErrorLoggingVerbosity { case object Off extends ErrorLoggingVerbosity case object Simple extends ErrorLoggingVerbosity @@ -129,6 +133,21 @@ object ParserSettings extends SettingsCompanion[ParserSettings] { } } + sealed trait IllegalResponseHeaderValueProcessingMode extends akka.http.javadsl.settings.ParserSettings.IllegalResponseHeaderValueProcessingMode + object IllegalResponseHeaderValueProcessingMode { + case object Error extends IllegalResponseHeaderValueProcessingMode + case object Warn extends IllegalResponseHeaderValueProcessingMode + case object Ignore extends IllegalResponseHeaderValueProcessingMode + + def apply(string: String): IllegalResponseHeaderValueProcessingMode = + string.toRootLowerCase match { + case "error" ⇒ Error + case "warn" ⇒ Warn + case "ignore" ⇒ Ignore + case x ⇒ throw new IllegalArgumentException(s"[$x] is not a legal `illegal-response-header-value-processing-mode` setting") + } + } + override def apply(config: Config): ParserSettings = ParserSettingsImpl(config) override def apply(configOverrides: String): ParserSettings = ParserSettingsImpl(configOverrides) } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/client/LowLevelOutgoingConnectionSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/client/LowLevelOutgoingConnectionSpec.scala index e2cea50f60..14a359351d 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/client/LowLevelOutgoingConnectionSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/client/LowLevelOutgoingConnectionSpec.scala @@ -4,6 +4,8 @@ package akka.http.impl.engine.client +import com.typesafe.config.ConfigFactory + import scala.concurrent.duration._ import scala.reflect.ClassTag import org.scalatest.Inside @@ -326,6 +328,65 @@ class LowLevelOutgoingConnectionSpec extends AkkaSpec("akka.loggers = []\n akka. } } + "process the illegal response header value properly" which { + + val illegalChar = '\u0001' + val escapeChar = "\\u%04x" format illegalChar.toInt + + "catch illegal response header value by default" in new TestSetup { + sendStandardRequest() + sendWireData( + s"""HTTP/1.1 200 OK + |Some-Header: value1$illegalChar + |Other-Header: value2 + | + |""") + + responsesSub.request(1) + val error @ IllegalResponseException(info) = responses.expectError() + info.summary shouldEqual s"""Illegal character '$escapeChar' in header value""" + netOut.expectError(error) + requestsSub.expectCancellation() + netInSub.expectCancellation() + } + + val ignoreConfig = + """ + akka.http.parsing.illegal-response-header-value-processing-mode = ignore + """ + "ignore illegal response header value if setting the config to ignore" in new TestSetup(config = ignoreConfig) { + sendStandardRequest() + sendWireData( + s"""HTTP/1.1 200 OK + |Some-Header: value1$illegalChar + |Other-Header: value2 + | + |""") + + val HttpResponse(_, headers, _, _) = expectResponse() + val headerStr = headers.map(h ⇒ s"${h.name}: ${h.value}").mkString(",") + headerStr shouldEqual "Some-Header: value1,Other-Header: value2" + } + + val warnConfig = + """ + akka.http.parsing.illegal-response-header-value-processing-mode = warn + """ + "ignore illegal response header value and log a warning message if setting the config to warn" in new TestSetup(config = warnConfig) { + sendStandardRequest() + sendWireData( + s"""HTTP/1.1 200 OK + |Some-Header: value1$illegalChar + |Other-Header: value2 + | + |""") + + val HttpResponse(_, headers, _, _) = expectResponse() + val headerStr = headers.map(h ⇒ s"${h.name}: ${h.value}").mkString(",") + headerStr shouldEqual "Some-Header: value1,Other-Header: value2" + } + } + "produce proper errors" which { "catch the request entity stream being shorter than the Content-Length" in new TestSetup { @@ -808,13 +869,14 @@ class LowLevelOutgoingConnectionSpec extends AkkaSpec("akka.loggers = []\n akka. } } - class TestSetup(maxResponseContentLength: Int = -1) { + class TestSetup(maxResponseContentLength: Int = -1, config: String = "") { val requests = TestPublisher.manualProbe[HttpRequest]() val responses = TestSubscriber.manualProbe[HttpResponse]() def settings = { - val s = ClientConnectionSettings(system) - .withUserAgentHeader(Some(`User-Agent`(List(ProductVersion("akka-http", "test"))))) + val s = ClientConnectionSettings( + ConfigFactory.parseString(config).withFallback(system.settings.config) + ).withUserAgentHeader(Some(`User-Agent`(List(ProductVersion("akka-http", "test"))))) if (maxResponseContentLength < 0) s else s.withParserSettings(s.parserSettings.withMaxContentLength(maxResponseContentLength)) } @@ -873,5 +935,6 @@ class LowLevelOutgoingConnectionSpec extends AkkaSpec("akka.loggers = []\n akka. responsesSub.request(1) responses.expectNext() } + } } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala index 4282538f0b..c632c74f3c 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserSpec.scala @@ -246,6 +246,7 @@ class HttpHeaderParserSpec extends WordSpec with Matchers with BeforeAndAfterAll val parser = { val p = HttpHeaderParser.unprimed( settings = ParserSettings(system), + system.log, warnOnIllegalHeader = info ⇒ system.log.warning(info.formatPretty)) if (primed) HttpHeaderParser.prime(p) else p } diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserTestBed.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserTestBed.scala index b8dcb95b9a..89c554d2fb 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserTestBed.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/HttpHeaderParserTestBed.scala @@ -15,7 +15,7 @@ object HttpHeaderParserTestBed extends App { val system = ActorSystem("HttpHeaderParserTestBed", testConf) val parser = HttpHeaderParser.prime { - HttpHeaderParser.unprimed(ParserSettings(system), warnOnIllegalHeader = info ⇒ system.log.warning(info.formatPretty)) + HttpHeaderParser.unprimed(ParserSettings(system), system.log, warnOnIllegalHeader = info ⇒ system.log.warning(info.formatPretty)) } println { diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala index c53f6cbdbf..ccf1b3cdc3 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/RequestParserSpec.scala @@ -300,7 +300,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { "support `rawRequestUriHeader` setting" in new Test { override protected def newParser: HttpRequestParser = - new HttpRequestParser(parserSettings, rawRequestUriHeader = true, headerParser = HttpHeaderParser(parserSettings)()) + new HttpRequestParser(parserSettings, rawRequestUriHeader = true, headerParser = HttpHeaderParser(parserSettings, system.log)()) """GET /f%6f%6fbar?q=b%61z HTTP/1.1 |Host: ping @@ -582,7 +582,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { .awaitResult(awaitAtMost) protected def parserSettings: ParserSettings = ParserSettings(system) - protected def newParser = new HttpRequestParser(parserSettings, false, HttpHeaderParser(parserSettings)()) + protected def newParser = new HttpRequestParser(parserSettings, false, HttpHeaderParser(parserSettings, system.log)()) private def compactEntity(entity: RequestEntity): Future[RequestEntity] = entity match { diff --git a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala index 5ad34b5d38..470d03c3e4 100644 --- a/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/impl/engine/parsing/ResponseParserSpec.scala @@ -320,7 +320,7 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { protected def parserSettings: ParserSettings = ParserSettings(system) def newParserStage(requestMethod: HttpMethod = GET) = { - val parser = new HttpResponseParser(parserSettings, HttpHeaderParser(parserSettings)()) + val parser = new HttpResponseParser(parserSettings, HttpHeaderParser(parserSettings, system.log)()) parser.setContextForNextResponse(HttpResponseParser.ResponseContext(requestMethod, None)) parser.stage } diff --git a/project/MiMa.scala b/project/MiMa.scala index 1081eed72a..106a7c14bc 100644 --- a/project/MiMa.scala +++ b/project/MiMa.scala @@ -907,7 +907,11 @@ object MiMa extends AutoPlugin { ), "2.4.9" -> Seq( // #20994 adding new decode method, since we're on JDK7+ now - ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.util.ByteString.decodeString") + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.util.ByteString.decodeString"), + + // #20976 provide different options to deal with the illegal response header value + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.javadsl.settings.ParserSettings.getIllegalResponseHeaderValueProcessingMode"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.http.scaladsl.settings.ParserSettings.illegalResponseHeaderValueProcessingMode") ) ) }