Permalink
Browse files

HTTP/2 support. (#38)

  • Loading branch information...
guillaumebort committed Oct 2, 2017
1 parent a581728 commit 3e86baf10996f14a72bbca1b3263b4eafc323003
View
@@ -4,4 +4,6 @@ project/target
stuff/
.idea/
api/
sync
alpn-boot.jar
.travis/pubring.gpg
.travis/secring.gpg
View
@@ -2,22 +2,45 @@
# lolhttp
A scala HTTP server & client library.
A scala HTTP & HTTP/2 server and client library.
## About
## About the library
Servers and clients are service functions. A service takes an HTTP request and eventually returns an HTTP response. Requests and responses are a set of HTTP headers along with a content body. The content body is a lazy stream of bytes based on [fs2](https://github.com/functional-streams-for-scala/fs2), making it easy to handle streaming scenarios if needed. For additional convenience, the library provides content encoders and decoders for the common scala types. All concepts are shared between servers and clients, making it simple to compose them. SSL is supported on both sides.
Both server and client are plain functions accepting an HTTP request and eventually giving back an HTTP response. Requests and responses are just HTTP metadata along with a lazy content body. The content body is a stream of bytes based on [fs2](https://github.com/functional-streams-for-scala/fs2), making it easy to handle streaming scenarios if needed. For additional convenience, the library provides content encoders and decoders for the common scala types. SSL is supported on both sides.
## Hello World
```scala
// Let's start an HTTP server
Server.listen(8888) {
case GET at "/hello" =>
Ok("Hello World!")
case _ =>
NotFound
}
// Let's connect with an HTTP client
Client.run(Get("http://localhost:8888/hello")) { res =>
res.readAs[String].map { contentBody =>
println(s"Received: $contentBody")
}
}
```
## About HTTP/2 support
HTTP/2 is supported on both server and client side. If SSL is enabled, the protocol negociation is done using ALPN. On plain connections however HTTP/2 is only supported with prior knowledge (clear text upgrade from HTTP/1.1 to HTTP/2 is ignored). Because of ALPN, HTTP/2 support over SSL requires running on Java 9 (_Running on Java 8 is still possible but you need to replace the default Java TLS implementation; see http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-starting_).
## Usage
The library is cross-built for __Scala 2.11__ and __Scala 2.12__.
The core module to use is `"com.criteo.lolhttp" %% "lolhttp" % "0.6.1"`.
The core module to use is `"com.criteo.lolhttp" %% "lolhttp" % "0.7.2"`.
There are also 2 optional companion libraries:
- `"com.criteo.lolhttp" %% "loljson" % "0.6.1"`, provides integration with the [circe](https://circe.github.io/circe/) JSON library.
- `"com.criteo.lolhttp" %% "lolhtml" % "0.6.1"`, provides minimal HTML templating.
- `"com.criteo.lolhttp" %% "loljson" % "0.7.2"`, provides integration with the [circe](https://circe.github.io/circe/) JSON library.
- `"com.criteo.lolhttp" %% "lolhtml" % "0.7.2"`, provides minimal HTML templating.
## Documentation
@@ -31,6 +54,7 @@ For those who prefer documentation by example, you can also follow these hands-o
- [A JSON web service](https://criteo.github.io/lolhttp/examples/JsonWebService.scala.html).
- [Reading large request streams](https://criteo.github.io/lolhttp/examples/LargeFileUpload.scala.html).
- [A simple reverse proxy](https://criteo.github.io/lolhttp/examples/ReverseProxy.scala.html).
- [An HTTP/2 server](https://criteo.github.io/lolhttp/examples/Http2Server.scala.html).
## License
View
@@ -1,10 +1,10 @@
val VERSION = "0.6.1"
val VERSION = "0.7.2"
lazy val commonSettings = Seq(
organization := "com.criteo.lolhttp",
version := VERSION,
scalaVersion := "2.12.2",
crossScalaVersions := Seq("2.11.11", "2.12.2"),
crossScalaVersions := Seq("2.11.11", "2.12.3"),
scalacOptions ++= Seq(
"-deprecation",
"-encoding", "UTF-8",
@@ -38,6 +38,12 @@ lazy val commonSettings = Seq(
"criteo-oss",
sys.env.getOrElse("SONATYPE_PASSWORD", "")
),
publishTo := Some(
if (isSnapshot.value)
Opts.resolver.sonatypeSnapshots
else
Opts.resolver.sonatypeStaging
),
pgpPassphrase := sys.env.get("SONATYPE_PASSWORD").map(_.toArray),
pgpSecretRing := file(".travis/secring.gpg"),
pgpPublicRing := file(".travis/pubring.gpg"),
@@ -89,7 +95,7 @@ lazy val lolhttp =
libraryDependencies ++= Seq(
"co.fs2" %% "fs2-core" % "0.10.0-M6",
"io.netty" % "netty-codec-http2" % "4.1.11.Final",
"io.netty" % "netty-codec-http2" % "4.1.16.Final",
"org.scalatest" %% "scalatest" % "3.0.1" % "test"
),
@@ -117,7 +123,7 @@ lazy val lolhttp =
val vendorised = (artifact in (Compile, assembly)).value
vendorised
},
pomPostProcess := removeDependencies("io.netty", "org.bouncycastle", "org.scalatest")
pomPostProcess := removeDependencies("io.netty", "org.scalatest")
).
settings(addArtifact(artifact in (Compile, assembly), assembly): _*)
@@ -153,7 +159,19 @@ lazy val examples: Project =
Defaults.itSettings,
publishArtifact := false,
fork in IntegrationTest := true,
// Running integration tests with Java 8 requires to install the right version of alpn-boot.
// See http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html#alpn-starting
javaOptions in IntegrationTest := {
Option(System.getProperty("java.version")).getOrElse("") match {
case noAlpn if noAlpn.startsWith("1.8") =>
Seq(s"""-Xbootclasspath/p:${file("alpn-boot.jar").getAbsolutePath}""")
case _ =>
Nil
}
},
connectInput in IntegrationTest := true
).
settings(
@@ -167,6 +185,7 @@ lazy val examples: Project =
"-P:socco:package_lol.http:https://criteo.github.io/lolhttp/api/",
"-P:socco:package_scala.concurrent:http://www.scala-lang.org/api/current/",
"-P:socco:package_io.circe:http://circe.github.io/circe/api/",
"-P:socco:package_cats.effect:https://oss.sonatype.org/service/local/repositories/releases/archive/org/typelevel/cats-effect_2.12/0.4/cats-effect_2.12-0.4-javadoc.jar/!/",
"-P:socco:package_fs2:https://oss.sonatype.org/service/local/repositories/releases/archive/co/fs2/fs2-core_2.12/0.9.4/fs2-core_2.12-0.9.4-javadoc.jar/!/"
)
)).getOrElse(Nil): _*
@@ -203,6 +222,10 @@ lazy val root =
jar -> "https://www.scala-lang.org/api/current/"
case (jar, module) if module.name == "fs2-core_2.12" =>
jar -> "https://oss.sonatype.org/service/local/repositories/releases/archive/co/fs2/fs2-core_2.12/0.9.5/fs2-core_2.12-0.9.5-javadoc.jar/!/"
case (jar, module) if module.name == "cats-effect_2.12" =>
jar -> "https://oss.sonatype.org/service/local/repositories/releases/archive/org/typelevel/cats-effect_2.12/0.4/cats-effect_2.12-0.4-javadoc.jar/!/"
case (jar, module) if module.name.startsWith("circe") =>
jar -> "https://circe.github.io/circe/api/"
}.
toMap.
mapValues(url => new java.net.URL(url))
@@ -21,12 +21,17 @@ import internal.{ withTimeout, KillableFuture }
* @param ioThreads the number of threads used for the IO work. Default to `min(availableProcessors, 2)`.
* @param tcpNoDelay if true disable Nagle's algorithm. Default `true`.
* @param bufferSize if defined used as a hint for the TCP buffer size. If none use the system default. Default to `None`.
* @param protocols the protocols to use to connect to the server. If SSL enabled the protocol is negociated using ALPN.
* Without SSL enabled only direct HTTP2 connections with prior knowledge are supported. Meaning that HTTP2
* will be used if it is the only option available. If HTTP is listed, the client will always fallback to HTTP/1.0
* for plain connections.
* @param debug if defined log the TCP traffic with the provided logger name. Default to `None`.
*/
case class ClientOptions(
ioThreads: Int = Math.min(Runtime.getRuntime.availableProcessors, 2),
tcpNoDelay: Boolean = true,
bufferSize: Option[Int] = None,
protocols: Set[String] = Set(HTTP),
debug: Option[String] = None
)
@@ -90,7 +95,7 @@ trait Client extends Service {
channel.config.setSendBufferSize(size)
}
Option(scheme).filter(_ == "https").foreach { _ =>
channel.pipeline.addLast("SSL", ssl.ctx.newHandler(channel.alloc()))
channel.pipeline.addLast("SSL", ssl.builder.build().newHandler(channel.alloc()))
}
}
})
@@ -143,7 +148,13 @@ trait Client extends Service {
liveConnections.incrementAndGet()
KillableFuture(
nettyClient.connect().
map(c => Netty.clientConnection(c.asInstanceOf[SocketChannel], options.debug)).
map { channel =>
Netty.clientConnection(
channel.asInstanceOf[SocketChannel],
options.debug,
if(options.protocols.contains(HTTP2) && !options.protocols.contains(HTTP)) HTTP2 else HTTP
)
}.
andThen {
case Success(c) =>
if(!connections.offer(c)) Panic.!!!()
@@ -16,13 +16,15 @@ import fs2.{ Stream }
* @param scheme the scheme such as `http` or `https`.
* @param content the request content.
* @param headers the HTTP headers.
* @param protocol the protocol version.
*/
case class Request(
method: HttpMethod,
url: String = "/",
scheme: String = "http",
content: Content = Content.empty,
headers: Map[HttpString,HttpString] = Map.empty
headers: Map[HttpString,HttpString] = Map.empty,
protocol: String = HTTP
) {
private lazy val (p, qs) = url.split("[?]").toList match {
@@ -1,27 +1,25 @@
package lol.http
import java.io.{ File }
import java.security.{ KeyStore }
import javax.net.ssl.{ TrustManagerFactory }
import io.netty.buffer.{ ByteBufAllocator }
import io.netty.handler.ssl.{ SslContext, SslContextBuilder }
import io.netty.handler.ssl.util.{ InsecureTrustManagerFactory, SelfSignedCertificate }
import io.netty.handler.ssl.{ SslContextBuilder }
import io.netty.handler.ssl.util.{
InsecureTrustManagerFactory,
SelfSignedCertificate }
/** lol SSL. */
object SSL {
private[http] class NettySslContext(private val ctx: SslContext) {
def newHandler(alloc: ByteBufAllocator) = ctx.newHandler(alloc)
}
/** SSL configuration for clients. */
class ClientConfiguration private[http] (private[http] val ctx: NettySslContext, name: String) {
override def toString = s"ClientConfiguration($ctx, $name)"
class ClientConfiguration private[http] (private[http] val builder: SslContextBuilder, name: String) {
override def toString = s"ClientConfiguration($name)"
}
/** SSL configuration for servers. */
class ServerConfiguration private[http] (private[http] val ctx: NettySslContext, name: String) {
override def toString = s"ServerConfiguration($ctx, $name)"
class ServerConfiguration private[http] (private[http] val builder: SslContextBuilder, name: String) {
override def toString = s"ServerConfiguration($name)"
}
/** Provides the default client SSL configuration. */
@@ -30,7 +28,7 @@ object SSL {
implicit lazy val default = new ClientConfiguration({
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
trustManagerFactory.init(null: KeyStore)
new NettySslContext(SslContextBuilder.forClient.trustManager(trustManagerFactory).build())
SslContextBuilder.forClient.trustManager(trustManagerFactory)
}, "default")
}
@@ -39,7 +37,7 @@ object SSL {
* insecure server.
*/
lazy val trustAll = new ClientConfiguration({
new NettySslContext(SslContextBuilder.forClient.trustManager(InsecureTrustManagerFactory.INSTANCE).build())
SslContextBuilder.forClient.trustManager(InsecureTrustManagerFactory.INSTANCE)
}, "trustAll")
/** Generate an SSL server configuration with a self-signed certificate.
@@ -48,7 +46,15 @@ object SSL {
*/
def selfSigned(fqdn: String = "localhost") = new ServerConfiguration({
val ssc = new SelfSignedCertificate(fqdn)
new NettySslContext(SslContextBuilder.forServer(ssc.certificate, ssc.privateKey).build())
SslContextBuilder.forServer(ssc.certificate, ssc.privateKey)
}, s"selfSigned for $fqdn")
def serverCertificate(certificate: File, privateKey: File, privateKeyPassword: String): ServerConfiguration =
new ServerConfiguration({
SslContextBuilder.forServer(certificate, privateKey, privateKeyPassword)
}, s"serverCertificate from $certificate")
def serverCertificate(certificatePath: String, privateKeyPath: String, privateKeyPassword: String): ServerConfiguration =
serverCertificate(new File(certificatePath), new File(privateKeyPath), privateKeyPassword)
}
Oops, something went wrong.

0 comments on commit 3e86baf

Please sign in to comment.