diff --git a/akka-http-core/src/main/java/akka/http/model/japi/HttpCharset.java b/akka-http-core/src/main/java/akka/http/model/japi/HttpCharset.java index 1bd92f72a5..52e705005d 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/HttpCharset.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/HttpCharset.java @@ -6,7 +6,7 @@ package akka.http.model.japi; /** * Represents a charset in Http. See {@link HttpCharsets} for a set of predefined charsets and - * static constructors to create and register custom charsets. + * static constructors to create custom charsets. */ public abstract class HttpCharset { /** @@ -34,7 +34,7 @@ public abstract class HttpCharset { public abstract HttpCharsetRange withQValue(float qValue); /** - * Returns the predefined (and registered) alias names for this charset. + * Returns the predefined alias names for this charset. */ public abstract Iterable getAliases(); } diff --git a/akka-http-core/src/main/java/akka/http/model/japi/HttpCharsets.java b/akka-http-core/src/main/java/akka/http/model/japi/HttpCharsets.java index eb151901c6..b3bb027fc4 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/HttpCharsets.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/HttpCharsets.java @@ -21,13 +21,10 @@ public final class HttpCharsets { public static final HttpCharset UTF_16LE = akka.http.model.HttpCharsets.UTF$minus16LE(); /** - * Registers a custom charset. Returns Some(newCharset) if the charset is supported by this JVM. - * Returns None otherwise. + * Create and return a custom charset. */ - public static Option registerCustom(String value, String... aliases) { - scala.Option custom = akka.http.model.HttpCharset.custom(value, Util.convertArray(aliases)); - if (custom.isDefined()) return Option.some(akka.http.model.HttpCharsets.register(custom.get())); - else return Option.none(); + public static HttpCharset custom(String value, String... aliases) { + return akka.http.model.HttpCharset.custom(value, Util.convertArray(aliases)); } /** diff --git a/akka-http-core/src/main/java/akka/http/model/japi/HttpMethod.java b/akka-http-core/src/main/java/akka/http/model/japi/HttpMethod.java index fc2e18290d..a40085a624 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/HttpMethod.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/HttpMethod.java @@ -6,7 +6,7 @@ package akka.http.model.japi; /** * Represents an HTTP request method. See {@link HttpMethods} for a set of predefined methods - * and static constructors to build and register custom ones. + * and static constructors to create custom ones. */ public abstract class HttpMethod { /** diff --git a/akka-http-core/src/main/java/akka/http/model/japi/HttpMethods.java b/akka-http-core/src/main/java/akka/http/model/japi/HttpMethods.java index 8defa300aa..522a672c1e 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/HttpMethods.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/HttpMethods.java @@ -24,12 +24,15 @@ public final class HttpMethods { public static final HttpMethod TRACE = akka.http.model.HttpMethods.TRACE(); /** - * Register a custom method type. + * Create a custom method type. */ - public static HttpMethod registerCustom(String value, boolean safe, boolean idempotent, boolean entityAccepted) { - return akka.http.model.HttpMethods.register(akka.http.model.HttpMethod.custom(value, safe, idempotent, entityAccepted)); + public static HttpMethod custom(String value, boolean safe, boolean idempotent, boolean entityAccepted) { + return akka.http.model.HttpMethod.custom(value, safe, idempotent, entityAccepted); } + /** + * Looks up a predefined HTTP method with the given name. + */ public static Option lookup(String name) { return Util.lookupInRegistry(HttpMethods$.MODULE$, name); } diff --git a/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java b/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java index cef317ff55..a30835ccb3 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/MediaTypes.java @@ -179,16 +179,16 @@ public abstract class MediaTypes { public static final MediaType VIDEO_WEBM = akka.http.model.MediaTypes.video$divwebm(); /** - * Register a custom media type. + * Creates a custom media type. */ - public static MediaType registerCustom( + public static MediaType custom( String mainType, String subType, boolean compressible, boolean binary, Iterable fileExtensions, Map params) { - return akka.http.model.MediaTypes.register(akka.http.model.MediaType.custom(mainType, subType, compressible, binary, Util.convertIterable(fileExtensions), Util.convertMapToScala(params), false)); + return akka.http.model.MediaType.custom(mainType, subType, compressible, binary, Util.convertIterable(fileExtensions), Util.convertMapToScala(params), false); } /** diff --git a/akka-http-core/src/main/java/akka/http/model/japi/StatusCodes.java b/akka-http-core/src/main/java/akka/http/model/japi/StatusCodes.java index 1333dd233f..3e306da242 100644 --- a/akka-http-core/src/main/java/akka/http/model/japi/StatusCodes.java +++ b/akka-http-core/src/main/java/akka/http/model/japi/StatusCodes.java @@ -85,17 +85,17 @@ public final class StatusCodes { public static final StatusCode NETWORK_CONNECT_TIMEOUT = akka.http.model.StatusCodes.NetworkConnectTimeout(); /** - * Registers a custom status code. + * Create a custom status code. */ - public static StatusCode registerCustom(int intValue, String reason, String defaultMessage, boolean isSuccess, boolean allowsEntity) { - return akka.http.model.StatusCodes.registerCustom(intValue, reason, defaultMessage, isSuccess, allowsEntity); + public static StatusCode custom(int intValue, String reason, String defaultMessage, boolean isSuccess, boolean allowsEntity) { + return akka.http.model.StatusCodes.custom(intValue, reason, defaultMessage, isSuccess, allowsEntity); } /** - * Registers a custom status code. + * Create a custom status code. */ - public static StatusCode registerCustom(int intValue, String reason, String defaultMessage) { - return akka.http.model.StatusCodes.registerCustom(intValue, reason, defaultMessage); + public static StatusCode custom(int intValue, String reason, String defaultMessage) { + return akka.http.model.StatusCodes.custom(intValue, reason, defaultMessage); } /** diff --git a/akka-http-core/src/main/resources/reference.conf b/akka-http-core/src/main/resources/reference.conf index 8fe044046f..bd88d2e5fb 100644 --- a/akka-http-core/src/main/resources/reference.conf +++ b/akka-http-core/src/main/resources/reference.conf @@ -116,6 +116,7 @@ akka.http { parsing { # The limits for the various parts of the HTTP message parser. max-uri-length = 2k + max-method-length = 16 max-response-reason-length = 64 max-header-name-length = 64 max-header-value-length = 8k diff --git a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala index d41ce67af1..e1d46e4b78 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpRequestParser.scala @@ -40,18 +40,20 @@ private[http] class HttpRequestParser(_settings: ParserSettings, def parseMethod(input: ByteString, cursor: Int): Int = { @tailrec def parseCustomMethod(ix: Int = 0, sb: JStringBuilder = new JStringBuilder(16)): Int = - if (ix < 16) { // hard-coded maximum custom method length + if (ix < maxMethodLength) { byteChar(input, cursor + ix) match { case ' ' ⇒ - HttpMethods.getForKey(sb.toString) match { + customMethods(sb.toString) match { case Some(m) ⇒ method = m cursor + ix + 1 - case None ⇒ parseCustomMethod(Int.MaxValue, sb) + case None ⇒ throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method", sb.toString)) } case c ⇒ parseCustomMethod(ix + 1, sb.append(c)) } - } else throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method", sb.toString)) + } else throw new ParsingException(BadRequest, + ErrorInfo("Unsupported HTTP method", s"HTTP method too long (started with '${sb.toString}'). " + + "Increase `akka.http.server.parsing.max-method-length` to support HTTP methods with more characters.")) @tailrec def parseMethod(meth: HttpMethod, ix: Int = 1): Int = if (ix == meth.value.length) diff --git a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala index 571982e090..173f86f684 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/parsing/HttpResponseParser.scala @@ -57,7 +57,7 @@ private[http] class HttpResponseParser(_settings: ParserSettings, case 200 ⇒ StatusCodes.OK case _ ⇒ StatusCodes.getForKey(code) match { case Some(x) ⇒ x - case None ⇒ badStatusCode + case None ⇒ customStatusCodes(code) getOrElse badStatusCode } } cursor + 4 diff --git a/akka-http-core/src/main/scala/akka/http/engine/parsing/ParserSettings.scala b/akka-http-core/src/main/scala/akka/http/engine/parsing/ParserSettings.scala index 43f8f20d8e..34b954fc90 100644 --- a/akka-http-core/src/main/scala/akka/http/engine/parsing/ParserSettings.scala +++ b/akka-http-core/src/main/scala/akka/http/engine/parsing/ParserSettings.scala @@ -6,11 +6,12 @@ package akka.http.engine.parsing import com.typesafe.config.Config import scala.collection.JavaConverters._ -import akka.http.model.Uri +import akka.http.model.{ StatusCode, HttpMethod, Uri } import akka.http.util._ final case class ParserSettings( maxUriLength: Int, + maxMethodLength: Int, maxResponseReasonLength: Int, maxHeaderNameLength: Int, maxHeaderValueLength: Int, @@ -20,9 +21,12 @@ final case class ParserSettings( maxChunkSize: Int, uriParsingMode: Uri.ParsingMode, illegalHeaderWarnings: Boolean, - headerValueCacheLimits: Map[String, Int]) extends HttpHeaderParser.Settings { + headerValueCacheLimits: Map[String, Int], + customMethods: String ⇒ Option[HttpMethod], + customStatusCodes: Int ⇒ Option[StatusCode]) extends HttpHeaderParser.Settings { require(maxUriLength > 0, "max-uri-length must be > 0") + require(maxMethodLength > 0, "max-method-length must be > 0") require(maxResponseReasonLength > 0, "max-response-reason-length must be > 0") require(maxHeaderNameLength > 0, "max-header-name-length must be > 0") require(maxHeaderValueLength > 0, "max-header-value-length must be > 0") @@ -35,6 +39,15 @@ final case class ParserSettings( def headerValueCacheLimit(headerName: String): Int = headerValueCacheLimits.getOrElse(headerName, defaultHeaderValueCacheLimit) + + def withCustomMethods(methods: HttpMethod*): ParserSettings = { + val map = methods.map(m ⇒ m.name -> m).toMap + copy(customMethods = map.get) + } + def withCustomStatusCodes(codes: StatusCode*): ParserSettings = { + val map = codes.map(c ⇒ c.intValue -> c).toMap + copy(customStatusCodes = map.get) + } } object ParserSettings extends SettingsCompanion[ParserSettings]("akka.http.parsing") { @@ -43,6 +56,7 @@ object ParserSettings extends SettingsCompanion[ParserSettings]("akka.http.parsi apply( c getIntBytes "max-uri-length", + c getIntBytes "max-method-length", c getIntBytes "max-response-reason-length", c getIntBytes "max-header-name-length", c getIntBytes "max-header-value-length", @@ -52,7 +66,9 @@ object ParserSettings extends SettingsCompanion[ParserSettings]("akka.http.parsi c getIntBytes "max-chunk-size", Uri.ParsingMode(c getString "uri-parsing-mode"), c getBoolean "illegal-header-warnings", - cacheConfig.entrySet.asScala.map(kvp ⇒ kvp.getKey -> cacheConfig.getInt(kvp.getKey))(collection.breakOut)) + cacheConfig.entrySet.asScala.map(kvp ⇒ kvp.getKey -> cacheConfig.getInt(kvp.getKey))(collection.breakOut), + _ ⇒ None, + _ ⇒ None) } } diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala b/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala index 3fce87d9c3..c94582d416 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpCharset.scala @@ -4,11 +4,12 @@ package akka.http.model +import java.lang.{ Iterable ⇒ JIterable } import language.implicitConversions import scala.collection.immutable +import scala.util.Try import java.nio.charset.Charset import akka.http.util._ -import java.lang.{ Iterable ⇒ JIterable } /** * A charset range as encountered in `Accept-Charset`. Can either be a single charset, or `*` @@ -52,12 +53,14 @@ object HttpCharsetRange { final case class HttpCharset private[http] (override val value: String)(val aliases: immutable.Seq[String]) extends japi.HttpCharset with SingletonValueRenderable with WithQValue[HttpCharsetRange] { - @transient private[this] var _nioCharset: Charset = Charset.forName(value) - def nioCharset: Charset = _nioCharset + @transient private[this] var _nioCharset: Try[Charset] = HttpCharset.findNioCharset(value) + + /** Returns the Charset for this charset if available or throws an exception otherwise */ + def nioCharset: Charset = _nioCharset.get private def readObject(in: java.io.ObjectInputStream): Unit = { in.defaultReadObject() - _nioCharset = Charset.forName(value) + _nioCharset = HttpCharset.findNioCharset(value) } def withQValue(qValue: Float): HttpCharsetRange = HttpCharsetRange(this, qValue.toFloat) @@ -70,17 +73,15 @@ final case class HttpCharset private[http] (override val value: String)(val alia } object HttpCharset { - def custom(value: String, aliases: String*): Option[HttpCharset] = - try Some(HttpCharset(value)(immutable.Seq(aliases: _*))) - catch { - // per documentation all exceptions thrown by `Charset.forName` are IllegalArgumentExceptions - case e: IllegalArgumentException ⇒ None - } + def custom(value: String, aliases: String*): HttpCharset = + HttpCharset(value)(immutable.Seq(aliases: _*)) + + private[http] def findNioCharset(name: String): Try[Charset] = Try(Charset.forName(name)) } // see http://www.iana.org/assignments/character-sets object HttpCharsets extends ObjectRegistry[String, HttpCharset] { - def register(charset: HttpCharset): HttpCharset = { + private def register(charset: HttpCharset): HttpCharset = { charset.aliases.foreach(alias ⇒ register(alias.toRootLowerCase, charset)) register(charset.value.toRootLowerCase, charset) } diff --git a/akka-http-core/src/main/scala/akka/http/model/HttpMethod.scala b/akka-http-core/src/main/scala/akka/http/model/HttpMethod.scala index 1c0e9a05b1..c545380432 100644 --- a/akka-http-core/src/main/scala/akka/http/model/HttpMethod.scala +++ b/akka-http-core/src/main/scala/akka/http/model/HttpMethod.scala @@ -4,58 +4,49 @@ package akka.http.model -import akka.http.util.{ SingletonValueRenderable, ObjectRegistry } +import scala.collection.immutable +import akka.http.util._ /** * The method of an HTTP request. - * @param fingerprint unique Int value for faster equality checks (uniqueness is verified during registration) * @param isSafe true if the resource should not be altered on the server * @param isIdempotent true if requests can be safely (& automatically) repeated * @param isEntityAccepted true if meaning of request entities is properly defined */ final case class HttpMethod private[http] (override val value: String, - fingerprint: Int, isSafe: Boolean, isIdempotent: Boolean, isEntityAccepted: Boolean) extends japi.HttpMethod with SingletonValueRenderable { def name = value - - override def hashCode(): Int = fingerprint - override def equals(obj: Any): Boolean = - obj match { - case m: HttpMethod ⇒ fingerprint == m.fingerprint - case _ ⇒ false - } - override def toString: String = s"HttpMethod($value)" } object HttpMethod { - def custom(value: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = - custom(value, value.##, safe, idempotent, entityAccepted) - - def custom(value: String, fingerprint: Int, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = { - require(value.nonEmpty, "value must be non-empty") + def custom(name: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = { + require(name.nonEmpty, "value must be non-empty") require(!safe || idempotent, "An HTTP method cannot be safe without being idempotent") - apply(value, fingerprint, safe, idempotent, entityAccepted) + apply(name, safe, idempotent, entityAccepted) } + + /** + * Creates a custom method by name and assumes properties conservatively to be + * safe = idempotent = false and entityAccepted = true. + */ + def custom(name: String): HttpMethod = custom(name, safe = false, idempotent = false, entityAccepted = true) } object HttpMethods extends ObjectRegistry[String, HttpMethod] { - def register(method: HttpMethod): HttpMethod = { - require(registry.values.forall(_.fingerprint != method.fingerprint), "Method fingerprint collision") - register(method.value, method) - } + private def register(method: HttpMethod): HttpMethod = register(method.value, method) // format: OFF - val CONNECT = register(HttpMethod("CONNECT", 0x01, isSafe = false, isIdempotent = false, isEntityAccepted = false)) - val DELETE = register(HttpMethod("DELETE" , 0x02, isSafe = false, isIdempotent = true , isEntityAccepted = false)) - val GET = register(HttpMethod("GET" , 0x03, isSafe = true , isIdempotent = true , isEntityAccepted = false)) - val HEAD = register(HttpMethod("HEAD" , 0x04, isSafe = true , isIdempotent = true , isEntityAccepted = false)) - val OPTIONS = register(HttpMethod("OPTIONS", 0x05, isSafe = true , isIdempotent = true , isEntityAccepted = true)) - val PATCH = register(HttpMethod("PATCH" , 0x06, isSafe = false, isIdempotent = false, isEntityAccepted = true)) - val POST = register(HttpMethod("POST" , 0x07, isSafe = false, isIdempotent = false, isEntityAccepted = true)) - val PUT = register(HttpMethod("PUT" , 0x08, isSafe = false, isIdempotent = true , isEntityAccepted = true)) - val TRACE = register(HttpMethod("TRACE" , 0x09, isSafe = true , isIdempotent = true , isEntityAccepted = false)) + val CONNECT = register(HttpMethod("CONNECT", isSafe = false, isIdempotent = false, isEntityAccepted = false)) + val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , isEntityAccepted = false)) + val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , isEntityAccepted = false)) + val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , isEntityAccepted = false)) + val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , isEntityAccepted = true)) + val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, isEntityAccepted = true)) + val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, isEntityAccepted = true)) + val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , isEntityAccepted = true)) + val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , isEntityAccepted = false)) // format: ON } diff --git a/akka-http-core/src/main/scala/akka/http/model/MediaType.scala b/akka-http-core/src/main/scala/akka/http/model/MediaType.scala index 364d4c15ce..bf8f3cce11 100644 --- a/akka-http-core/src/main/scala/akka/http/model/MediaType.scala +++ b/akka-http-core/src/main/scala/akka/http/model/MediaType.scala @@ -195,9 +195,7 @@ sealed abstract class NonMultipartMediaType private[http] (_value: String, _main object MediaType { /** - * Allows the definition of custom media types. In order for your custom type to be properly used by the - * HTTP layer you need to create an instance, register it via `MediaTypes.register` and use this instance in - * your custom Marshallers and Unmarshallers. + * Create a custom media type. */ def custom(mainType: String, subType: String, compressible: Boolean = false, binary: Boolean = false, fileExtensions: immutable.Seq[String] = Nil, params: Map[String, String] = Map.empty, @@ -224,9 +222,9 @@ object MediaType { } object MediaTypes extends ObjectRegistry[(String, String), MediaType] { - @volatile private[this] var extensionMap = Map.empty[String, MediaType] + private[this] var extensionMap = Map.empty[String, MediaType] - def register(mediaType: MediaType): MediaType = synchronized { + private def register(mediaType: MediaType): MediaType = { def registerFileExtension(ext: String): Unit = { val lcExt = ext.toRootLowerCase require(!extensionMap.contains(lcExt), s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and '$mediaType'") diff --git a/akka-http-core/src/main/scala/akka/http/model/StatusCode.scala b/akka-http-core/src/main/scala/akka/http/model/StatusCode.scala index fcb1db21a2..2b648c9901 100644 --- a/akka-http-core/src/main/scala/akka/http/model/StatusCode.scala +++ b/akka-http-core/src/main/scala/akka/http/model/StatusCode.scala @@ -62,23 +62,22 @@ object StatusCodes extends ObjectRegistry[Int, StatusCode] { } /** - * Create and register a custom status code and allow full customization of behavior. The value of `allowsEntity` + * Create a custom status code and allow full customization of behavior. The value of `allowsEntity` * changes the parser behavior: If it is set to true, a response with this status code is required to include a * `Content-Length` header to be parsed correctly when keep-alive is enabled (which is the default in HTTP/1.1). * If `allowsEntity` is false, an entity is never expected. */ - def registerCustom(intValue: Int, reason: String, defaultMessage: String, isSuccess: Boolean, allowsEntity: Boolean): StatusCode = - reg(CustomStatusCode(intValue)(reason, defaultMessage, isSuccess, allowsEntity)) + def custom(intValue: Int, reason: String, defaultMessage: String, isSuccess: Boolean, allowsEntity: Boolean): StatusCode = + StatusCodes.CustomStatusCode(intValue)(reason, defaultMessage, isSuccess, allowsEntity) - /** Create and register a custom status code with default behavior for its value region. */ - def registerCustom(intValue: Int, reason: String, defaultMessage: String = ""): StatusCode = reg { + /** Create a custom status code with default behavior for its value region. */ + def custom(intValue: Int, reason: String, defaultMessage: String = ""): StatusCode = if (100 to 199 contains intValue) Informational(intValue)(reason, defaultMessage) else if (200 to 299 contains intValue) Success(intValue)(reason, defaultMessage) else if (300 to 399 contains intValue) Redirection(intValue)(reason, defaultMessage, defaultMessage) else if (400 to 499 contains intValue) ClientError(intValue)(reason, defaultMessage) else if (500 to 599 contains intValue) ServerError(intValue)(reason, defaultMessage) else throw new IllegalArgumentException("Can't register status code in non-standard region") - } import Informational.{apply => i} import Success .{apply => s} diff --git a/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala b/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala index 291a014417..cdf617bae8 100644 --- a/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala +++ b/akka-http-core/src/main/scala/akka/http/model/headers/HttpEncoding.scala @@ -48,11 +48,6 @@ object HttpEncoding { // see http://www.iana.org/assignments/http-parameters/http-parameters.xml object HttpEncodings extends ObjectRegistry[String, HttpEncoding] { - def register(encoding: HttpEncoding): HttpEncoding = - register(encoding.value.toRootLowerCase, encoding) - - private def register(value: String): HttpEncoding = register(HttpEncoding(value)) - // format: OFF val compress = register("compress") val chunked = register("chunked") @@ -62,4 +57,7 @@ object HttpEncodings extends ObjectRegistry[String, HttpEncoding] { val `x-compress` = register("x-compress") val `x-zip` = register("x-zip") // format: ON + + private def register(encoding: HttpEncoding): HttpEncoding = register(encoding.value.toRootLowerCase, encoding) + private def register(value: String): HttpEncoding = register(HttpEncoding(value)) } diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala b/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala index d4cbd6d128..b03916b4aa 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/CommonActions.scala @@ -35,6 +35,5 @@ private[parser] trait CommonActions { def getCharset(name: String): HttpCharset = HttpCharsets .getForKeyCaseInsensitive(name) - .orElse(HttpCharset.custom(name)) - .getOrElse(throw new ParsingException("Unsupported charset", name)) + .getOrElse(HttpCharset.custom(name)) } \ No newline at end of file diff --git a/akka-http-core/src/main/scala/akka/http/model/parser/CommonRules.scala b/akka-http-core/src/main/scala/akka/http/model/parser/CommonRules.scala index 9fb4e4ebda..d7db5252d2 100644 --- a/akka-http-core/src/main/scala/akka/http/model/parser/CommonRules.scala +++ b/akka-http-core/src/main/scala/akka/http/model/parser/CommonRules.scala @@ -408,7 +408,7 @@ private[parser] trait CommonRules { this: Parser with StringBuilding ⇒ token ~> { s ⇒ HttpMethods.getForKey(s) match { case Some(m) ⇒ m - case None ⇒ throw new ParsingException("Unknown HTTP method", s) + case None ⇒ HttpMethod.custom(s) } } } diff --git a/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala b/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala index 97463ecf7e..5db5fd14c6 100644 --- a/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala +++ b/akka-http-core/src/main/scala/akka/http/util/ObjectRegistry.scala @@ -7,21 +7,18 @@ package akka.http.util /** * INTERNAL API * - * A registry to keep track of singleton instances similar to what - * java.lang.Enum provides. + * A unsynchronized registry to keep track of singleton instances similar to what + * java.lang.Enum provides. `registry` should therefore only be used inside of singleton constructors. */ private[http] trait ObjectRegistry[K, V <: AnyRef] { - @volatile private[this] var _registry = Map.empty[K, V] + private[this] var _registry = Map.empty[K, V] - protected final def register(key: K, obj: V): obj.type = synchronized { + protected final def register(key: K, obj: V): obj.type = { require(!_registry.contains(key), s"ObjectRegistry for ${getClass.getSimpleName} already contains value for $key") _registry = _registry.updated(key, obj) obj } - - protected def registry: Map[K, V] = _registry - - def getForKey(key: K): Option[V] = registry.get(key) + def getForKey(key: K): Option[V] = _registry.get(key) def getForKeyCaseInsensitive(key: String)(implicit conv: String <:< K): Option[V] = getForKey(conv(key.toRootLowerCase)) diff --git a/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala index 384f66e51b..4b4e0f942d 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/parsing/RequestParserSpec.scala @@ -35,7 +35,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { implicit val system = ActorSystem(getClass.getSimpleName, testConf) import system.dispatcher - val BOLT = HttpMethods.register(HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true)) + val BOLT = HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true) implicit val materializer = FlowMaterializer() "The request parsing logic should" - { @@ -124,6 +124,9 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { } "with a custom HTTP method" in new Test { + override protected def parserSettings: ParserSettings = + super.parserSettings.withCustomMethods(BOLT) + """BOLT / HTTP/1.0 | |""" should parseTo(HttpRequest(BOLT, "/", protocol = `HTTP/1.0`)) @@ -266,6 +269,14 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { "GETX " should parseToError(NotImplemented, ErrorInfo("Unsupported HTTP method", "GETX")) } + "a too long HTTP method" in new Test { + "ABCDEFGHIJKLMNOPQ " should + parseToError(BadRequest, + ErrorInfo( + "Unsupported HTTP method", + "HTTP method too long (started with 'ABCDEFGHIJKLMNOP'). Increase `akka.http.server.parsing.max-method-length` to support HTTP methods with more characters.")) + } + "two Content-Length headers" in new Test { """GET / HTTP/1.1 |Content-Length: 3 @@ -375,7 +386,8 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { Await.result(future, 250.millis) } - private def newParser = new HttpRequestParser(ParserSettings(system), false)() + protected def parserSettings: ParserSettings = ParserSettings(system) + protected def newParser = new HttpRequestParser(parserSettings, false)() private def compactEntity(entity: RequestEntity): Deferrable[RequestEntity] = entity match { diff --git a/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala index 3f978c60b1..f8953cd402 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/parsing/ResponseParserSpec.scala @@ -34,7 +34,7 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { import system.dispatcher implicit val materializer = FlowMaterializer() - val ServerOnTheMove = StatusCodes.registerCustom(331, "Server on the move") + val ServerOnTheMove = StatusCodes.custom(331, "Server on the move") "The response parsing logic should" - { "properly parse" - { @@ -56,6 +56,9 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { } "a response with a custom status code" in new Test { + override def parserSettings: ParserSettings = + super.parserSettings.withCustomStatusCodes(ServerOnTheMove) + """HTTP/1.1 331 Server on the move |Content-Length: 0 | @@ -233,8 +236,9 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll { Await.result(future, 250.millis) } + def parserSettings: ParserSettings = ParserSettings(system) def newParser(requestMethod: HttpMethod = GET) = { - val parser = new HttpResponseParser(ParserSettings(system), + val parser = new HttpResponseParser(parserSettings, dequeueRequestMethodForNextResponse = () ⇒ requestMethod)() parser } diff --git a/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala b/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala index b34fd2e9e7..1cc7dce14b 100644 --- a/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/engine/rendering/ResponseRendererSpec.scala @@ -27,7 +27,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll implicit val system = ActorSystem(getClass.getSimpleName, testConf) import system.dispatcher - val ServerOnTheMove = StatusCodes.registerCustom(330, "Server on the move") + val ServerOnTheMove = StatusCodes.custom(330, "Server on the move") implicit val materializer = FlowMaterializer() "The response preparation logic should properly render" - { diff --git a/akka-http-core/src/test/scala/akka/http/model/SerializabilitySpec.scala b/akka-http-core/src/test/scala/akka/http/model/SerializabilitySpec.scala index 3756e2d0fa..07b1412f0b 100644 --- a/akka-http-core/src/test/scala/akka/http/model/SerializabilitySpec.scala +++ b/akka-http-core/src/test/scala/akka/http/model/SerializabilitySpec.scala @@ -31,7 +31,7 @@ class SerializabilitySpec extends WordSpec with Matchers { } "with accept-charset" in { HttpRequest().withHeaders(`Accept-Charset`(HttpCharsets.`UTF-16`)) should beSerializable - HttpRequest().withHeaders(`Accept-Charset`(HttpCharset.custom("utf8").get)) should beSerializable + HttpRequest().withHeaders(`Accept-Charset`(HttpCharset.custom("utf8"))) should beSerializable } "with accepted encodings" in { HttpRequest().withHeaders(`Accept-Encoding`(HttpEncodings.chunked)) should beSerializable diff --git a/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala b/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala index fd529767da..4bb1fc8bb3 100644 --- a/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala +++ b/akka-http-core/src/test/scala/akka/http/model/parser/HttpHeaderSpec.scala @@ -17,7 +17,8 @@ import HttpEncodings._ import HttpMethods._ class HttpHeaderSpec extends FreeSpec with Matchers { - val `application/vnd.spray` = MediaTypes.register(MediaType.custom("application/vnd.spray")) + val `application/vnd.spray` = MediaType.custom("application/vnd.spray") + val PROPFIND = HttpMethod.custom("PROPFIND") "The HTTP header model must correctly parse and render the headers" - { @@ -49,6 +50,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { `Accept-Charset`(`ISO-8859-1`, `UTF-16` withQValue 0, HttpCharsetRange.`*` withQValue 0.8).renderedTo( "ISO-8859-1, UTF-16;q=0.0, *;q=0.8") `Accept-Charset`(`UTF-16` withQValue 0.234567).toString shouldEqual "Accept-Charset: UTF-16;q=0.235" + "Accept-Charset: UTF-16, unsupported42" =!= `Accept-Charset`(`UTF-16`, HttpCharset.custom("unsupported42")) } "Access-Control-Allow-Credentials" in { @@ -61,6 +63,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Access-Control-Allow-Methods" in { "Access-Control-Allow-Methods: GET, POST" =!= `Access-Control-Allow-Methods`(GET, POST) + "Access-Control-Allow-Methods: GET, PROPFIND, POST" =!= `Access-Control-Allow-Methods`(GET, PROPFIND, POST) } "Access-Control-Allow-Origin" in { @@ -85,6 +88,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Access-Control-Request-Method" in { "Access-Control-Request-Method: POST" =!= `Access-Control-Request-Method`(POST) + "Access-Control-Request-Method: PROPFIND" =!= `Access-Control-Request-Method`(PROPFIND) } "Accept-Ranges" in { @@ -113,6 +117,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Allow" in { "Allow: " =!= Allow() "Allow: GET, PUT" =!= Allow(GET, PUT) + "Allow: GET, PROPFIND, PUT" =!= Allow(GET, PROPFIND, PUT) } "Authorization" in { @@ -177,7 +182,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers { "Content-Type: text/xml; version=3; charset=windows-1252" =!= `Content-Type`(ContentType(MediaType.custom("text", "xml", params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252"))) "Content-Type: text/plain; charset=fancy-pants" =!= - ErrorInfo("Illegal HTTP header 'Content-Type': Unsupported charset", "fancy-pants") + `Content-Type`(ContentType(`text/plain`, HttpCharset.custom("fancy-pants"))) "Content-Type: multipart/mixed; boundary=ABC123" =!= `Content-Type`(ContentType(`multipart/mixed` withBoundary "ABC123")) "Content-Type: multipart/mixed; boundary=\"ABC/123\"" =!=