/
GZip.scala
112 lines (97 loc) · 4.17 KB
/
GZip.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
package org.http4s
package server
package middleware
import scala.annotation.tailrec
import java.util.zip.{CRC32, Deflater}
import javax.xml.bind.DatatypeConverter
import org.http4s.headers.{`Content-Type`, `Content-Length`, `Content-Encoding`, `Accept-Encoding`}
import org.log4s.getLogger
import scalaz.stream.{ Process0, Process1 }
import scalaz.stream.Process._
import scalaz.concurrent.Task
import scalaz.Kleisli.kleisli
import scodec.bits.ByteVector
object GZip {
private[this] val logger = getLogger
// TODO: It could be possible to look for Task.now type bodies, and change the Content-Length header after
// TODO zipping and buffering all the input. Just a thought.
def apply(service: HttpService, bufferSize: Int = 32 * 1024, level: Int = Deflater.DEFAULT_COMPRESSION): HttpService = Service.lift {
req: Request =>
req.headers.get(`Accept-Encoding`) match {
case Some(acceptEncoding) if acceptEncoding.satisfiedBy(ContentCoding.gzip)
|| acceptEncoding.satisfiedBy(ContentCoding.`x-gzip`) =>
service.map { resp =>
if (isZippable(resp)) {
logger.trace("GZip middleware encoding content")
// Need to add the Gzip header and trailer
val b = resp.body
.pipe(gzip(
level = level,
nowrap = true,
bufferSize = bufferSize
))
resp.removeHeader(`Content-Length`)
.putHeaders(`Content-Encoding`(ContentCoding.gzip))
.copy(body = b)
}
else resp // Don't touch it, Content-Encoding already set
}.apply(req)
case _ => service(req)
}
}
private[http4s] def gzip(level: Int = Deflater.DEFAULT_COMPRESSION,
nowrap: Boolean = false,
bufferSize: Int = 1024 * 32): Process1[ByteVector,ByteVector] = {
suspend {
val crc = new CRC32()
var length = 0L
val deflater = new Deflater(level, nowrap)
val buf = Array.ofDim[Byte](bufferSize)
@tailrec
def collect(flush: Int, acc: Vector[ByteVector] = Vector.empty): Vector[ByteVector] =
deflater.deflate(buf, 0, buf.length, flush) match {
case 0 => acc
case n => collect(flush, acc :+ ByteVector.view(buf.take(n)))
}
def go(): Process1[ByteVector,ByteVector] =
receive1 { bytes =>
val arr = bytes.toArray
crc.update(arr)
length += bytes.length
deflater.setInput(arr)
val chunks = collect(Deflater.NO_FLUSH)
emitAll(chunks) ++ go()
}
def flush(): Process0[ByteVector] = {
deflater.finish()
val vecs = collect(Deflater.FULL_FLUSH)
deflater.end()
emitAll(vecs) ++ emit(trailer)
}
def trailer =
ByteVector.view(DatatypeConverter.parseHexBinary("%08x".format(crc.getValue)).reverse) ++
ByteVector.view(DatatypeConverter.parseHexBinary("%08x".format(length % GZIP_LENGTH_MOD)).reverse)
emit(header) ++ (go() onComplete flush())
}
}
private def isZippable(resp: Response): Boolean = {
val contentType = resp.headers.get(`Content-Type`)
!Fallthrough[Response].isFallthrough(resp) &&
resp.headers.get(`Content-Encoding`).isEmpty &&
(contentType.isEmpty || contentType.get.mediaType.compressible ||
(contentType.get.mediaType eq MediaType.`application/octet-stream`))
}
private val GZIP_MAGIC_NUMBER = 0x8b1f
private val GZIP_LENGTH_MOD = Math.pow(2, 32).toLong
private val header: ByteVector = ByteVector(
GZIP_MAGIC_NUMBER.toByte, // Magic number (int16)
(GZIP_MAGIC_NUMBER >> 8).toByte, // Magic number c
Deflater.DEFLATED.toByte, // Compression method
0.toByte, // Flags
0.toByte, // Modification time (int32)
0.toByte, // Modification time c
0.toByte, // Modification time c
0.toByte, // Modification time c
0.toByte, // Extra flags
0.toByte) // Operating system
}