|
Play Framework/Scala example source code file (Http.scala)
The Http.scala Play Framework example source code/* * Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com> */ package play.api.mvc { import play.api._ import play.api.http.{ MediaType, MediaRange, HeaderNames } import play.api.i18n.Lang import play.api.libs.iteratee._ import play.api.libs.Crypto import scala.annotation._ import scala.util.control.NonFatal import scala.util.Try import java.net.{ URLDecoder, URLEncoder } import scala.concurrent.duration._ /** * The HTTP request header. Note that it doesn’t contain the request body yet. */ @implicitNotFound("Cannot find any HTTP Request Header here") trait RequestHeader { /** * The request ID. */ def id: Long /** * The request Tags. */ def tags: Map[String, String] /** * The complete request URI, containing both path and query string. */ def uri: String /** * The URI path. */ def path: String /** * The HTTP method. */ def method: String /** * The HTTP version. */ def version: String /** * The parsed query string. */ def queryString: Map[String, Seq[String]] /** * The HTTP headers. */ def headers: Headers /** * The client IP address. * * If the `X-Forwarded-For` header is present, then this method will return the value in that header * if either the local address is 127.0.0.1, or if `trustxforwarded` is configured to be true in the * application configuration file. */ def remoteAddress: String /** * Is the client using SSL? * * If the <code>X-Forwarded-Proto</code> header is present, then this method will return true * if the value in that header is "https", if either the local address is 127.0.0.1, or if * <code>trustxforwarded</code> is configured to be true in the application configuration file. */ def secure: Boolean // -- Computed /** * Helper method to access a queryString parameter. */ def getQueryString(key: String): Option[String] = queryString.get(key).flatMap(_.headOption) /** * The HTTP host (domain, optionally port) */ lazy val host: String = headers.get(HeaderNames.HOST).getOrElse("") /** * The HTTP domain */ lazy val domain: String = host.split(':').head /** * The Request Langs extracted from the Accept-Language header and sorted by preference (preferred first). */ lazy val acceptLanguages: Seq[play.api.i18n.Lang] = { val langs = RequestHeader.acceptHeader(headers, HeaderNames.ACCEPT_LANGUAGE).map(item => (item._1, Lang.get(item._2))) langs.sortWith((a, b) => a._1 > b._1).map(_._2).flatten } /** * @return The media types list of the request’s Accept header, sorted by preference (preferred first). */ lazy val acceptedTypes: Seq[play.api.http.MediaRange] = { headers.get(HeaderNames.ACCEPT).toSeq.flatMap(MediaRange.parse.apply) } /** * Check if this request accepts a given media type. * @return true if `mimeType` matches the Accept header, otherwise false */ def accepts(mimeType: String): Boolean = { acceptedTypes.isEmpty || acceptedTypes.find(_.accepts(mimeType)).isDefined } /** * The HTTP cookies. */ lazy val cookies: Cookies = Cookies(headers.get(play.api.http.HeaderNames.COOKIE)) /** * Parses the `Session` cookie and returns the `Session` data. */ lazy val session: Session = Session.decodeFromCookie(cookies.get(Session.COOKIE_NAME)) /** * Parses the `Flash` cookie and returns the `Flash` data. */ lazy val flash: Flash = Flash.decodeFromCookie(cookies.get(Flash.COOKIE_NAME)) /** * Returns the raw query string. */ lazy val rawQueryString: String = uri.split('?').drop(1).mkString("?") /** * The media type of this request. Same as contentType, except returns a fully parsed media type with parameters. */ lazy val mediaType: Option[MediaType] = headers.get(HeaderNames.CONTENT_TYPE).flatMap(MediaType.parse.apply) /** * Returns the value of the Content-Type header (without the parameters (eg charset)) */ lazy val contentType: Option[String] = mediaType.map(mt => mt.mediaType + "/" + mt.mediaSubType) /** * Returns the charset of the request for text-based body */ lazy val charset: Option[String] = for { mt <- mediaType param <- mt.parameters.find(_._1.equalsIgnoreCase("charset")) charset <- param._2 } yield charset /** * Copy the request. */ def copy( id: Long = this.id, tags: Map[String, String] = this.tags, uri: String = this.uri, path: String = this.path, method: String = this.method, version: String = this.version, queryString: Map[String, Seq[String]] = this.queryString, headers: Headers = this.headers, remoteAddress: String = this.remoteAddress, secure: Boolean = this.secure): RequestHeader = { val (_id, _tags, _uri, _path, _method, _version, _queryString, _headers, _remoteAddress, _secure) = (id, tags, uri, path, method, version, queryString, headers, remoteAddress, secure) new RequestHeader { val id = _id val tags = _tags val uri = _uri val path = _path val method = _method val version = _version val queryString = _queryString val headers = _headers val remoteAddress = _remoteAddress val secure = _secure } } override def toString = { method + " " + uri } } object RequestHeader { // “The first "q" parameter (if any) separates the media-range parameter(s) from the accept-params.” val qPattern = ";\\s*q=([0-9.]+)".r /** * @return The items of an Accept* header, with their q-value. */ private[play] def acceptHeader(headers: Headers, headerName: String): Seq[(Double, String)] = { for { header <- headers.get(headerName).toSeq value0 <- header.split(',') value = value0.trim } yield { RequestHeader.qPattern.findFirstMatchIn(value) match { case Some(m) => (m.group(1).toDouble, m.before.toString) case None => (1.0, value) // “The default value is q=1.” } } } } /** * The complete HTTP request. * * @tparam A the body content type. */ @implicitNotFound("Cannot find any HTTP Request here") trait Request[+A] extends RequestHeader { self => /** * The body content. */ def body: A /** * Transform the request body. */ def map[B](f: A => B): Request[B] = new Request[B] { def id = self.id def tags = self.tags def uri = self.uri def path = self.path def method = self.method def version = self.version def queryString = self.queryString def headers = self.headers def remoteAddress = self.remoteAddress def secure = self.secure lazy val body = f(self.body) } } object Request { def apply[A](rh: RequestHeader, a: A) = new Request[A] { def id = rh.id def tags = rh.tags def uri = rh.uri def path = rh.path def method = rh.method def version = rh.version def queryString = rh.queryString def headers = rh.headers lazy val remoteAddress = rh.remoteAddress lazy val secure = rh.secure def username = None val body = a } } /** * Wrap an existing request. Useful to extend a request. */ class WrappedRequest[+A](request: Request[A]) extends Request[A] { def id = request.id def tags = request.tags def body = request.body def headers = request.headers def queryString = request.queryString def path = request.path def uri = request.uri def method = request.method def version = request.version def remoteAddress = request.remoteAddress def secure = request.secure } /** * Defines a `Call`, which describes an HTTP request and can be used to create links or fill redirect data. * * These values are usually generated by the reverse router. * * @param method the request HTTP method * @param url the request URL */ case class Call(method: String, url: String) extends play.mvc.Call { /** * Transform this call to an absolute URL. * * {{{ * import play.api.mvc.{ Call, RequestHeader } * * implicit val req: RequestHeader = myRequest * val url: String = Call("GET", "/url").absoluteURL() * // == "http://$host/url", or "https://$host/url" if secure * }}} */ def absoluteURL()(implicit request: RequestHeader): String = absoluteURL(request.secure) /** * Transform this call to an absolute URL. */ def absoluteURL(secure: Boolean)(implicit request: RequestHeader): String = "http" + (if (secure) "s" else "") + "://" + request.host + this.url /** * Transform this call to an WebSocket URL. * * {{{ * import play.api.mvc.{ Call, RequestHeader } * * implicit val req: RequestHeader = myRequest * val url: String = Call("GET", "/url").webSocketURL() * // == "ws://$host/url", or "wss://$host/url" if secure * }}} */ def webSocketURL()(implicit request: RequestHeader): String = webSocketURL(request.secure) /** * Transform this call to an WebSocket URL. */ def webSocketURL(secure: Boolean)(implicit request: RequestHeader): String = "ws" + (if (secure) "s" else "") + "://" + request.host + this.url override def toString = url } /** * The HTTP headers set. */ trait Headers { /** * Optionally returns the first header value associated with a key. */ def get(key: String): Option[String] = getAll(key).headOption /** * Retrieves the first header value which is associated with the given key. */ def apply(key: String): String = get(key).getOrElse(scala.sys.error("Header doesn't exist")) /** * Retrieve all header values associated with the given key. */ def getAll(key: String): Seq[String] = toMap.get(key).getOrElse(Nil) /** * Retrieve all header keys */ def keys: Set[String] = { Set.empty ++ data.map(_._1) } /** * Transform the Headers to a Map */ lazy val toMap: Map[String, Seq[String]] = { import collection.immutable.TreeMap import play.core.utils.CaseInsensitiveOrdered TreeMap(data: _*)(CaseInsensitiveOrdered) } /** * The internal data structure here is a sequence of header to sequence of value pairs. Multiple * headers with the same name are not expected in the sequence. Instead the same header with multiple values * in the order that they appear in the http header is expected. */ protected val data: Seq[(String, Seq[String])] /** * Transform the Headers to a Map by ignoring multiple values. */ lazy val toSimpleMap: Map[String, String] = toMap.mapValues(_.headOption.getOrElse("")) override def toString = data.toString } /** * Trait that should be extended by the Cookie helpers. */ trait CookieBaker[T <: AnyRef] { /** * The cookie name. */ def COOKIE_NAME: String /** * Default cookie, returned in case of error or if missing in the HTTP headers. */ def emptyCookie: T /** * `true` if the Cookie is signed. Defaults to false. */ def isSigned: Boolean = false /** * `true` if the Cookie should have the httpOnly flag, disabling access from Javascript. Defaults to true. */ def httpOnly = true /** * The cookie expiration date in seconds, `None` for a transient cookie */ def maxAge: Option[Int] = None /** * The cookie domain. Defaults to None. */ def domain: Option[String] = None /** * `true` if the Cookie should have the secure flag, restricting usage to https. Defaults to false. */ def secure = false /** * The cookie path. */ def path = "/" /** * Encodes the data as a `String`. */ def encode(data: Map[String, String]): String = { val encoded = data.map { case (k, v) => URLEncoder.encode(k, "UTF-8") + "=" + URLEncoder.encode(v, "UTF-8") }.mkString("&") if (isSigned) Crypto.sign(encoded) + "-" + encoded else encoded } /** * Decodes from an encoded `String`. */ def decode(data: String): Map[String, String] = { def urldecode(data: String) = { data .split("&") .map(_.split("=", 2)) .map(p => URLDecoder.decode(p(0), "UTF-8") -> URLDecoder.decode(p(1), "UTF-8")) .toMap } // Do not change this unless you understand the security issues behind timing attacks. // This method intentionally runs in constant time if the two strings have the same length. // If it didn't, it would be vulnerable to a timing attack. def safeEquals(a: String, b: String) = { if (a.length != b.length) { false } else { var equal = 0 for (i <- Array.range(0, a.length)) { equal |= a(i) ^ b(i) } equal == 0 } } try { if (isSigned) { val splitted = data.split("-", 2) val message = splitted.tail.mkString("-") if (safeEquals(splitted(0), Crypto.sign(message))) urldecode(message) else Map.empty[String, String] } else urldecode(data) } catch { // fail gracefully is the session cookie is corrupted case NonFatal(_) => Map.empty[String, String] } } /** * Encodes the data as a `Cookie`. */ def encodeAsCookie(data: T): Cookie = { val cookie = encode(serialize(data)) Cookie(COOKIE_NAME, cookie, maxAge, path, domain, secure, httpOnly) } /** * Decodes the data from a `Cookie`. */ def decodeFromCookie(cookie: Option[Cookie]): T = { cookie.filter(_.name == COOKIE_NAME).map(c => deserialize(decode(c.value))).getOrElse(emptyCookie) } def discard = DiscardingCookie(COOKIE_NAME, path, domain, secure) /** * Builds the cookie object from the given data map. * * @param data the data map to build the cookie object * @return a new cookie object */ protected def deserialize(data: Map[String, String]): T /** * Converts the given cookie object into a data map. * * @param cookie the cookie object to serialize into a map * @return a new `Map` storing the key-value pairs for the given cookie */ protected def serialize(cookie: T): Map[String, String] } /** * HTTP Session. * * Session data are encoded into an HTTP cookie, and can only contain simple `String` values. */ case class Session(data: Map[String, String] = Map.empty[String, String]) { /** * Optionally returns the session value associated with a key. */ def get(key: String) = data.get(key) /** * Returns `true` if this session is empty. */ def isEmpty: Boolean = data.isEmpty /** * Adds a value to the session, and returns a new session. * * For example: * {{{ * session + ("username" -> "bob") * }}} * * @param kv the key-value pair to add * @return the modified session */ def +(kv: (String, String)) = { require(kv._2 != null, "Cookie values cannot be null") copy(data + kv) } /** * Removes any value from the session. * * For example: * {{{ * session - "username" * }}} * * @param key the key to remove * @return the modified session */ def -(key: String) = copy(data - key) /** * Retrieves the session value which is associated with the given key. */ def apply(key: String) = data(key) } /** * Helper utilities to manage the Session cookie. */ object Session extends CookieBaker[Session] { val COOKIE_NAME = Play.maybeApplication.flatMap(_.configuration.getString("session.cookieName")).getOrElse("PLAY_SESSION") val emptyCookie = new Session override val isSigned = true override def secure = Play.maybeApplication.flatMap(_.configuration.getBoolean("session.secure")).getOrElse(false) override val maxAge = Play.maybeApplication .flatMap(_.configuration.getMilliseconds("session.maxAge") .map(Duration(_, MILLISECONDS).toSeconds.toInt)) override val httpOnly = Play.maybeApplication.flatMap(_.configuration.getBoolean("session.httpOnly")).getOrElse(true) override def path = Play.maybeApplication.flatMap(_.configuration.getString("application.context")).getOrElse("/") override def domain = Play.maybeApplication.flatMap(_.configuration.getString("session.domain")) def deserialize(data: Map[String, String]) = new Session(data) def serialize(session: Session) = session.data } /** * HTTP Flash scope. * * Flash data are encoded into an HTTP cookie, and can only contain simple `String` values. */ case class Flash(data: Map[String, String] = Map.empty[String, String]) { /** * Optionally returns the flash value associated with a key. */ def get(key: String) = data.get(key) /** * Returns `true` if this flash scope is empty. */ def isEmpty: Boolean = data.isEmpty /** * Adds a value to the flash scope, and returns a new flash scope. * * For example: * {{{ * flash + ("success" -> "Done!") * }}} * * @param kv the key-value pair to add * @return the modified flash scope */ def +(kv: (String, String)) = { require(kv._2 != null, "Cookie values cannot be null") copy(data + kv) } /** * Removes a value from the flash scope. * * For example: * {{{ * flash - "success" * }}} * * @param key the key to remove * @return the modified flash scope */ def -(key: String) = copy(data - key) /** * Retrieves the flash value that is associated with the given key. */ def apply(key: String) = data(key) } /** * Helper utilities to manage the Flash cookie. */ object Flash extends CookieBaker[Flash] { val COOKIE_NAME = Play.maybeApplication.flatMap(_.configuration.getString("flash.cookieName")).getOrElse("PLAY_FLASH") override def path = Play.maybeApplication.flatMap(_.configuration.getString("application.context")).getOrElse("/") val emptyCookie = new Flash def deserialize(data: Map[String, String]) = new Flash(data) def serialize(flash: Flash) = flash.data } /** * An HTTP cookie. * * @param name the cookie name * @param value the cookie value * @param maxAge the cookie expiration date in seconds, `None` for a transient cookie, or a value less than 0 to expire a cookie now * @param path the cookie path, defaulting to the root path `/` * @param domain the cookie domain * @param secure whether this cookie is secured, sent only for HTTPS requests * @param httpOnly whether this cookie is HTTP only, i.e. not accessible from client-side JavaScipt code */ case class Cookie(name: String, value: String, maxAge: Option[Int] = None, path: String = "/", domain: Option[String] = None, secure: Boolean = false, httpOnly: Boolean = true) /** * A cookie to be discarded. This contains only the data necessary for discarding a cookie. * * @param name the name of the cookie to discard * @param path the path of the cookie, defaults to the root path * @param domain the cookie domain * @param secure whether this cookie is secured */ case class DiscardingCookie(name: String, path: String = "/", domain: Option[String] = None, secure: Boolean = false) { def toCookie = Cookie(name, "", Some(-86400), path, domain, secure) } /** * The HTTP cookies set. */ trait Cookies extends Traversable[Cookie] { /** * Optionally returns the cookie associated with a key. */ def get(name: String): Option[Cookie] /** * Retrieves the cookie that is associated with the given key. */ def apply(name: String): Cookie = get(name).getOrElse(scala.sys.error("Cookie doesn't exist")) } /** * Helper utilities to encode Cookies. */ object Cookies { import scala.collection.JavaConverters._ // We use netty here but just as an API to handle cookies encoding import play.core.netty.utils.{ CookieEncoder, CookieDecoder, DefaultCookie } /** * Extract cookies from the Set-Cookie header. */ def apply(header: Option[String]) = new Cookies { lazy val cookies: Map[String, Cookie] = header.map(Cookies.decode(_)).getOrElse(Seq.empty).groupBy(_.name).mapValues(_.head) def get(name: String) = cookies.get(name) override def toString = cookies.toString def foreach[U](f: (Cookie) => U) { cookies.values.foreach(f) } } /** * Encodes cookies as a proper HTTP header. * * @param cookies the Cookies to encode * @return a valid Set-Cookie header value */ def encode(cookies: Seq[Cookie]): String = { val encoder = new CookieEncoder(true) val newCookies = cookies.map { c => encoder.addCookie { val nc = new DefaultCookie(c.name, c.value) nc.setMaxAge(c.maxAge.getOrElse(Integer.MIN_VALUE)) nc.setPath(c.path) c.domain.map(nc.setDomain(_)) nc.setSecure(c.secure) nc.setHttpOnly(c.httpOnly) nc } encoder.encode() } newCookies.mkString("; ") } /** * Decodes a Set-Cookie header value as a proper cookie set. * * @param cookieHeader the Set-Cookie header value * @return decoded cookies */ private lazy val decoder = new CookieDecoder() def decode(cookieHeader: String): Seq[Cookie] = { Try { decoder.decode(cookieHeader).asScala.map { c => Cookie(c.getName, c.getValue, if (c.getMaxAge == Integer.MIN_VALUE) None else Some(c.getMaxAge), Option(c.getPath).getOrElse("/"), Option(c.getDomain), c.isSecure, c.isHttpOnly) }.toSeq }.getOrElse { Play.logger.debug(s"Couldn't decode the Cookie header containing: $cookieHeader") Nil } } /** * Merges an existing Set-Cookie header with new cookie values * * @param cookieHeader the existing Set-Cookie header value * @param cookies the new cookies to encode * @return a valid Set-Cookie header value */ def merge(cookieHeader: String, cookies: Seq[Cookie]): String = { val tupledCookies = (decode(cookieHeader) ++ cookies).map { c => // See rfc6265#section-4.1.2 // Secure and http-only attributes are not considered when testing if // two cookies are overlapping. (c.name, c.path, c.domain.map(_.toLowerCase)) -> c } // Put cookies in a map // Note: Seq.toMap do not preserve order val uniqCookies = scala.collection.immutable.ListMap(tupledCookies: _*) encode(uniqCookies.values.toSeq) } } } Other Play Framework source code examplesHere is a short list of links related to this Play Framework Http.scala source code file: |
... this post is sponsored by my books ... | |
#1 New Release! |
FP Best Seller |
Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.
A percentage of advertising revenue from
pages under the /java/jwarehouse
URI on this website is
paid back to open source projects.