11package xitrum .handler .outbound
22
33import java .io .RandomAccessFile
4- import scala .util .control .NonFatal
54
65import io .netty .buffer .Unpooled
7- import io .netty .channel .{ChannelOutboundHandlerAdapter , ChannelHandler , ChannelHandlerContext , ChannelFuture , ChannelPromise , DefaultFileRegion , ChannelFutureListener }
8- import io .netty .handler .codec .http .{FullHttpRequest , FullHttpResponse , HttpMethod , HttpHeaderNames , HttpHeaderValues , HttpResponseStatus , HttpUtil , LastHttpContent }
6+ import io .netty .channel .{ChannelOutboundHandlerAdapter , ChannelHandler , ChannelHandlerContext , ChannelFuture , ChannelPromise , DefaultFileRegion }
7+ import io .netty .handler .codec .http .{FullHttpResponse , HttpMethod , HttpHeaderNames , HttpHeaderValues , HttpResponseStatus , HttpUtil , LastHttpContent }
98import io .netty .handler .ssl .SslHandler
109import io .netty .handler .stream .ChunkedFile
1110import ChannelHandler .Sharable
@@ -14,23 +13,10 @@ import HttpHeaderValues._
1413import HttpMethod ._
1514import HttpResponseStatus ._
1615
17- import xitrum .Log
1816import xitrum .etag .{Etag , NotModified }
1917import xitrum .handler .{AccessLog , HandlerEnv , NoRealPipelining }
2018import xitrum .util .{ByteBufUtil , Gzip }
2119
22- // valid range is send by client
23- // xitrum should return 206 (Partial content)
24- private case class SatisfiableRange (startIndex : Long , endIndex : Long )
25-
26- // first-byte-pos value greater than the current length of the selected resource
27- // Xitrum should return 416 (Requested Range Not Satisfiable)
28- private case class UnsatisfiableRange ()
29-
30- // Unsupported format, include syntax error
31- // Xitrum should ignore range header
32- private case class UnsupportedRange ()
33-
3420// Based on https://github.com/netty/netty/tree/master/example/src/main/java/io/netty/example/http/file
3521object XSendFile {
3622 // setClientCacheAggressively should be called at PublicFileServer, not
@@ -91,7 +77,7 @@ object XSendFile {
9177 // headers, not a FullHttpResponse.
9278 Etag .forFile(path, mimeo, Gzip .isAccepted(request)) match {
9379 case Etag .NotFound =>
94- XSendFile . removeHeaders(response)
80+ removeHeaders(response)
9581
9682 response.setStatus(NOT_FOUND )
9783 NotModified .setNoClientCache(response)
@@ -106,7 +92,7 @@ object XSendFile {
10692 }
10793
10894 case Etag .Small (bytes, etag, mmo, gzipped) =>
109- XSendFile . removeHeaders(response)
95+ removeHeaders(response)
11096
11197 if (Etag .areEtagsIdentical(request, etag)) {
11298 response.setStatus(NOT_MODIFIED )
@@ -133,7 +119,7 @@ object XSendFile {
133119 // but it's still good to give it a try
134120 val lastModifiedRfc2822 = NotModified .formatRfc2822(file.lastModified)
135121 if (request.headers.get(IF_MODIFIED_SINCE ) == lastModifiedRfc2822) {
136- XSendFile . removeHeaders(response)
122+ removeHeaders(response)
137123
138124 response.setStatus(NOT_MODIFIED )
139125 response.content.clear()
@@ -145,12 +131,12 @@ object XSendFile {
145131
146132 val raf = new RandomAccessFile (path, " r" )
147133
148- val (offset, length) = getRangeFromRequest (request, raf.length) match {
134+ val (offset, length) = RangeParser .parse (request.headers.get( RANGE ) , raf.length) match {
149135 case UnsupportedRange =>
150136 (0L , raf.length) // 0L is for avoiding "type mismatch" compile error
151137
152138 case UnsatisfiableRange =>
153- // A server sending a response with status code 416 (Requested range notsatisfiable )
139+ // A server sending a response with status code 416 (Requested range not satisfiable )
154140 // SHOULD include a Content-Range field with a byte-range-resp-spec of "*".
155141 // The instance-length specifies the current length of
156142 response.setStatus(REQUESTED_RANGE_NOT_SATISFIABLE )
@@ -170,19 +156,12 @@ object XSendFile {
170156 if (mmo.isDefined) response.headers.set(CONTENT_TYPE , mmo.get)
171157 if (! noLog) AccessLog .logStaticFileAccess(remoteAddress, request, response)
172158
173- if (request.method == HEAD && response.status == OK ) {
174- XSendFile . removeHeaders(response)
159+ if (response.status == REQUESTED_RANGE_NOT_SATISFIABLE || ( request.method == HEAD && response.status == OK ) ) {
160+ removeHeaders(response)
175161
176162 // http://stackoverflow.com/questions/3854842/content-length-header-with-head-requests
177163 response.content.clear()
178- ctx.write(env, promise)
179- NoRealPipelining .if_keepAliveRequest_then_resumeReading_else_closeOnComplete(request, channel, promise)
180- return
181- }
182164
183- if (response.status == REQUESTED_RANGE_NOT_SATISFIABLE ) {
184- XSendFile .removeHeaders(response)
185- response.content.clear()
186165 ctx.write(env, promise)
187166 NoRealPipelining .if_keepAliveRequest_then_resumeReading_else_closeOnComplete(request, channel, promise)
188167 return
@@ -196,86 +175,20 @@ object XSendFile {
196175 // Cannot use zero-copy with HTTPS
197176 ctx
198177 .write(new ChunkedFile (raf, offset, length, CHUNK_SIZE ))
199- .addListener(new ChannelFutureListener {
200- def operationComplete (f : ChannelFuture ) { raf.close() }
201- })
178+ .addListener((_ : ChannelFuture ) => raf.close())
202179 } else {
203180 // No encryption - use zero-copy
204181 val region = new DefaultFileRegion (raf.getChannel, offset, length)
205182 ctx
206183 .write(region) // region will automatically be released
207- .addListener(new ChannelFutureListener {
208- def operationComplete (f : ChannelFuture ) { raf.close() }
209- })
184+ .addListener((_ : ChannelFuture ) => raf.close())
210185 }
211186
212187 // Write the end marker
213188 val future = ctx.writeAndFlush(LastHttpContent .EMPTY_LAST_CONTENT , promise)
214189 NoRealPipelining .if_keepAliveRequest_then_resumeReading_else_closeOnComplete(request, channel, future)
215190 }
216191 }
217-
218- /**
219- * "Range" request: http://tools.ietf.org/html/rfc2616#section-14.35
220- * For simplicity only these specs are supported:
221- * bytes=123-456
222- * bytes=123-
223- * If the last-byte-pos value is present, it MUST be greater than or
224- * equal to the first-byte-pos in that byte-range-spec, or the byte-
225- * range-spec is syntactically invalid. The recipient of a byte-range-
226- * set that includes one or more syntactically invalid byte-range-spec
227- * values MUST ignore the header field that includes that byte-range-
228- * set.
229- * If the last-byte-pos value is absent, or if the value is greater than
230- * or equal to the current length of the entity-body, last-byte-pos is
231- * taken to be equal to one less than the current length of the entity-
232- * body in bytes.
233- *
234- * @return SatisfiableRange(startIndex, endIndex) or UnsatisfiableRange or UnsupportedRange
235- */
236- private def getRangeFromRequest (request : FullHttpRequest , length : Long ) = {
237- val spec = request.headers.get(RANGE )
238- try {
239- if (spec == null ) {
240- UnsupportedRange
241- } else {
242- if (spec.length <= 6 ) {
243- Log .warn(" Unsupported Range spec: " + spec)
244- UnsupportedRange
245- } else {
246- val range = spec.substring(6 ) // Skip "bytes="
247- val se = range.split('-' )
248- if (se.length == 2 ) {
249- val s = se(0 ).toLong
250- val e = se(1 ).toLong
251- if (s > length - 1 ) {
252- UnsatisfiableRange
253- } else if (s <= e) {
254- SatisfiableRange (s, Math .min(e, length - 1 ))
255- } else {
256- Log .warn(" Unsupported Range, last-byte-pos MUST be greater than or equal to the first-byte-pos. spec: " + spec)
257- UnsupportedRange
258- }
259- } else if (se.length != 1 ) {
260- Log .warn(" Unsupported Range spec: " + spec)
261- UnsupportedRange
262- } else {
263- val s = se(0 ).toLong
264- val e = length - 1
265- if (s > length - 1 ) {
266- UnsatisfiableRange
267- } else {
268- SatisfiableRange (s, e)
269- }
270- }
271- }
272- }
273- } catch {
274- case NonFatal (e) =>
275- Log .warn(" Unsupported Range spec: " + spec)
276- UnsupportedRange
277- }
278- }
279192}
280193
281194/**
0 commit comments