about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2012-02-08 02:07:07 +0000
committerEric Wong <normalperson@yhbt.net>2012-02-08 22:32:32 +0000
commitb74796a50f400419db19faccd69c433f9380beda (patch)
tree5bf5ffb42f5e9291166622bd8af17ae45c08cc0d
parent16038a4993f7156f04c221b045debfec706d4cfd (diff)
downloadcmogstored-b74796a50f400419db19faccd69c433f9380beda.tar.gz
This allows clients to support partial requests and
resume downloads.
-rw-r--r--Makefile.am2
-rw-r--r--cmogstored.h1
-rw-r--r--http_get.c180
-rw-r--r--http_parser.rl22
-rw-r--r--test/http-parser-1.c56
-rw-r--r--test/http_chunked_put.rb4
-rw-r--r--test/http_range.rb185
7 files changed, 413 insertions, 37 deletions
diff --git a/Makefile.am b/Makefile.am
index 3f19c68..8b24f03 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -101,7 +101,7 @@ RUBY = ruby
 TEST_EXTENSIONS = .rb .slowrb
 RB_LOG_COMPILER = $(RUBY) -I$(top_srcdir) -w
 SLOWRB_LOG_COMPILER = RUBY="$(RUBY)" top_srcdir="$(top_srcdir)"
-RB_TESTS_FAST = test/cmogstored-cfg.rb test/http_dav.rb
+RB_TESTS_FAST = test/cmogstored-cfg.rb test/http_dav.rb test/http_range.rb
 RB_TESTS_SLOW = test/mgmt-usage.rb test/mgmt.rb test/mgmt-iostat.rb \
  test/http.rb test/http_put.rb test/http_chunked_put.rb
 RB_TESTS = $(RB_TESTS_FAST) $(RB_TESTS_SLOW)
diff --git a/cmogstored.h b/cmogstored.h
index 84b8082..40bfe2d 100644
--- a/cmogstored.h
+++ b/cmogstored.h
@@ -163,6 +163,7 @@ struct mog_http {
         uint8_t path_end;
         uint16_t line_end;
         uint16_t tmp_tip;
+        uint16_t range_bytes_tip;
         struct mog_fd *forward;
         size_t offset;
         off_t range_beg;
diff --git a/http_get.c b/http_get.c
index 34db80a..b0637a0 100644
--- a/http_get.c
+++ b/http_get.c
@@ -34,15 +34,145 @@ static ssize_t linux_sendfile(int sockfd, int filefd, off_t *off, size_t count)
 #  include "compat_sendfile.h"
 #endif
 
-void mog_http_get_open(struct mog_http *http, char *buf, bool head_only)
+#define ERR416 "416 Requested Range Not Satisfiable"
+
+/* TODO: refactor this */
+static void http_get_resp_hdr(struct mog_http *http, struct stat *sb)
 {
-        struct stat sb;
+        char *modified;
         struct iovec iov;
+        struct mog_now *now = mog_now();
         int rc;
-        char *path = mog_http_path(http, buf);
-        char *modified;
-        struct mog_now *now;
 
+        /* single buffer so we can use MSG_MORE later... */
+        /* TODO: remove reliance on snprintf(), it eats stack */
+        iov.iov_base = mog_fsbuf_get(&iov.iov_len);
+        modified = (char *)iov.iov_base + iov.iov_len / 2;
+        assert(iov.iov_len / 2 > MOG_HTTPDATE_CAPA && "fsbuf too small");
+        mog_http_date(modified, MOG_HTTPDATE_CAPA, &sb->st_mtime);
+
+        /* validate ranges */
+        if (http->range_bytes_tip && ! http->has_range)
+                goto bad_range;
+        if (http->has_range) {
+                long long offset, count;
+
+                assert(http->range_bytes_tip && "range_bytes_tip not set");
+
+                if (http->range_end < 0 && http->range_beg < 0)
+                        goto bad_range;
+                if (http->range_beg >= sb->st_size)
+                        goto bad_range;
+
+                /* bytes=M-N where M > N */
+                if (http->range_beg >= 0 && http->range_end >= 0
+                    && http->range_beg > http->range_end)
+                        goto bad_range;
+
+                if (http->range_end < 0) { /* bytes=M- */
+                        /* bytes starting at M until EOF */
+                        assert(http->range_beg >= 0 && "should've sent 416");
+                        offset = (long long)http->range_beg;
+                        count = (long long)(sb->st_size - offset);
+                } else if (http->range_beg < 0) { /* bytes=-N */
+                        /* last N bytes */
+                        assert(http->range_end >= 0 && "should've sent 416");
+                        offset = (long long)(sb->st_size - http->range_end);
+
+                        /* serve the entire file if client requested too much */
+                        if (offset < 0)
+                                goto resp_200;
+                        count = (long long)(sb->st_size - offset);
+                } else { /* bytes=M-N*/
+                        assert(http->range_beg >= 0 && http->range_end >= 0
+                               && "should've sent 416");
+                        offset = (long long)http->range_beg;
+
+                        /* truncate responses to current file size */
+                        if (http->range_end >= sb->st_size)
+                                http->range_end = sb->st_size - 1;
+                        count = (long long)http->range_end + 1 - offset;
+                }
+
+                assert(count > 0 && "bad count for 206 response");
+                assert(offset >= 0 && "bad offset for 206 response");
+
+                if (http->forward) {
+                        struct mog_file *file = &http->forward->as.file;
+
+                        file->foff = offset;
+                        file->fsize = (off_t)(offset + count);
+                }
+
+                rc = snprintf(iov.iov_base, iov.iov_len,
+                        "HTTP/1.1 206 Partial Content\r\n"
+                        "Status: 206 Partial Content\r\n"
+                        "Date: %s\r\n"
+                        "Last-Modified: %s\r\n"
+                        "Content-Length: %lld\r\n"
+                        "Content-Type: application/octet-stream\r\n"
+                        "Content-Range: bytes %lld-%lld/%lld\n"
+                        "Connection: %s\r\n"
+                        "\r\n",
+                        now->httpdate,
+                        modified,
+                        count, /* Content-Length */
+                        offset, offset + count - 1, /* bytes M-N */
+                        (long long)sb->st_size,
+                        http->persistent ? "keep-alive" : "close");
+        } else {
+resp_200:
+                rc = snprintf(iov.iov_base, iov.iov_len,
+                        "HTTP/1.1 200 OK\r\n"
+                        "Status: 200 OK\r\n"
+                        "Date: %s\r\n"
+                        "Last-Modified: %s\r\n"
+                        "Content-Length: %lld\r\n"
+                        "Content-Type: application/octet-stream\r\n"
+                        "Accept-Ranges: bytes\r\n"
+                        "Connection: %s\r\n"
+                        "\r\n",
+                        now->httpdate,
+                        modified,
+                        (long long)sb->st_size,
+                        http->persistent ? "keep-alive" : "close");
+        }
+
+        /* TODO: put down the crack pipe and refactor this */
+        if (0) {
+bad_range:
+                if (http->forward) {
+                        mog_file_close(http->forward);
+                        http->forward = NULL;
+                } else {
+                        assert(http->http_method == MOG_HTTP_METHOD_HEAD
+                               && "not HTTP HEAD");
+                }
+                rc = snprintf(iov.iov_base, iov.iov_len,
+                        "HTTP/1.1 " ERR416 "\r\n"
+                        "Status: " ERR416 "\r\n"
+                        "Date: %s\r\n"
+                        "Content-Length: 0\r\n"
+                        "Content-Type: text/plain\r\n"
+                        "Content-Range: bytes */%lld\n"
+                        "Connection: %s\r\n"
+                        "\r\n",
+                        now->httpdate,
+                        (long long)sb->st_size,
+                        http->persistent ? "keep-alive" : "close");
+        }
+
+        assert(rc > 0 && rc < iov.iov_len && "we suck at snprintf");
+        iov.iov_len = rc;
+        assert(http->wbuf == NULL && "tried to write to a busy client");
+
+        http->wbuf = mog_trywritev(mog_fd_of(http)->fd, &iov, 1);
+}
+
+void mog_http_get_open(struct mog_http *http, char *buf, bool head_only)
+{
+        struct stat sb;
+        char *path = mog_http_path(http, buf);
         if (!path) goto forbidden; /* path traversal attack */
         assert(http->forward == NULL && "already have http->forward");
         assert(path[0] == '/' && "bad path");
@@ -75,33 +205,8 @@ void mog_http_get_open(struct mog_http *http, char *buf, bool head_only)
                 file->fsize = sb.st_size;
         }
 
-        /* single buffer so we can use MSG_MORE later... */
-        /* TODO: remove reliance on snprintf(), it eats stack */
-        iov.iov_base = mog_fsbuf_get(&iov.iov_len);
-        modified = (char *)iov.iov_base + iov.iov_len / 2;
-        assert(iov.iov_len / 2 > MOG_HTTPDATE_CAPA && "fsbuf too small");
-        mog_http_date(modified, MOG_HTTPDATE_CAPA, &sb.st_mtime);
-        now = mog_now();
-        rc = snprintf(iov.iov_base, iov.iov_len,
-                        "HTTP/1.1 200 OK\r\n"
-                        "Status: 200 OK\r\n"
-                        "Date: %s\r\n"
-                        "Last-Modified: %s\r\n"
-                        "Content-Length: %lld\r\n"
-                        "Content-Type: application/octet-stream\r\n"
-                        "Connection: %s\r\n"
-                        "\r\n",
-                        now->httpdate,
-                        modified,
-                        (long long)sb.st_size,
-                        http->persistent ? "keep-alive" : "close");
-        assert(rc > 0 && "we suck at snprintf");
-        iov.iov_len = rc;
-        assert(http->wbuf == NULL && "tried to write to a busy client");
-
-        http->wbuf = mog_trywritev(mog_fd_of(http)->fd, &iov, 1);
+        http_get_resp_hdr(http, &sb);
         return;
-
 err:
         switch (errno) {
         case EACCES:
@@ -123,13 +228,13 @@ enum mog_next mog_http_get_in_progress(struct mog_fd *mfd)
         struct mog_fd *file_mfd;
         struct mog_file *file;
         ssize_t w;
-        size_t count;
+        off_t count;
 
         /*
          * most readahead is 128K, so we try to send half of that to give
          * the kernel time to do readahead
          */
-        static const size_t max_sendfile = 64 * 1024; /* TODO: make tunable */
+        static const off_t max_sendfile = 64 * 1024; /* TODO: make tunable */
 
         assert(http->wbuf == NULL && "can't serve file with http->wbuf");
         assert(http->forward && http->forward != MOG_IOSTAT && "bad forward");
@@ -137,10 +242,13 @@ enum mog_next mog_http_get_in_progress(struct mog_fd *mfd)
         file = &file_mfd->as.file;
 
         assert(file->fsize >= 0 && "fsize is negative");
-        count = file->fsize > max_sendfile ? max_sendfile : file->fsize;
-        if (count == 0) goto done;
+        assert(file->foff >= 0 && "foff is negative");
+        count = file->fsize - file->foff;
+        count = count > max_sendfile ? max_sendfile : count;
+        if (count == 0)
+                goto done;
 retry:
-        w = sendfile(mfd->fd, file_mfd->fd, &file->foff, count);
+        w = sendfile(mfd->fd, file_mfd->fd, &file->foff, (size_t)count);
         if (w > 0) {
                 if (file->foff == file->fsize) goto done;
                 return MOG_NEXT_ACTIVE;
diff --git a/http_parser.rl b/http_parser.rl
index 1458fb7..f823b43 100644
--- a/http_parser.rl
+++ b/http_parser.rl
@@ -64,6 +64,27 @@ static bool length_incr(off_t *len, unsigned c)
                 $! { errno = EINVAL; fbreak; }
                 "/*"
                 eor > { http->has_content_range = 1; };
+        range = "Range:"i sep
+                "bytes" > { http->range_bytes_tip = to_u16(fpc - buf); }
+                '=' > {
+                        *p = ' ';
+                        http->range_beg = -1;
+                        http->range_end = -1;
+                }
+                (digit*) $ {
+                        if (http->range_beg < 0)
+                                http->range_beg = 0;
+                        if (!length_incr(&http->range_beg, fc))
+                                fbreak;
+                }
+                '-'
+                (digit*) $ {
+                        if (http->range_end < 0)
+                                http->range_end = 0;
+                        if (!length_incr(&http->range_end, fc))
+                                fbreak;
+                }
+                eor > { *p = 0; } @ { http->has_range = 1; };
         transfer_encoding_chunked = "Transfer-Encoding:"i sep
                 "chunked"i eor > { http->chunked = 1; };
         trailer = "Trailer:"i sep
@@ -77,6 +98,7 @@ static bool length_incr(off_t *len, unsigned c)
                 ( content_length |
                   transfer_encoding_chunked |
                   trailer |
+                  range |
                   content_range |
                   content_md5 |
                   connection ) $!
diff --git a/test/http-parser-1.c b/test/http-parser-1.c
index 18aa30e..7e513f1 100644
--- a/test/http-parser-1.c
+++ b/test/http-parser-1.c
@@ -18,6 +18,11 @@ static void assert_path_equal(const char *str)
         assert(http->path_end == http->path_tip + slen);
 }
 
+static void assert_range_bytes_equal(const char *str)
+{
+        assert(0 == strcmp(str, buf + http->range_bytes_tip));
+}
+
 static void reset(void)
 {
         free(buf);
@@ -199,6 +204,57 @@ int main(void)
                 assert_path_equal("/foo");
         }
 
+        if ("HTTP/1.1 Range (mid) GET request") {
+                buf_set("GET /foo HTTP/1.1\r\n"
+                        "Host: 127.6.6.6\r\n"
+                        "Range: bytes=5-55\r\n"
+                        "\r\n");
+                state = mog_http_parse(http, buf, len);
+                assert(http->has_range == 1);
+                assert(http->range_beg == 5 && "range_beg didn't match");
+                assert(http->range_end == 55 && "range_end didn't match");
+                assert(http->http_method == MOG_HTTP_METHOD_GET
+                       && "http_method should be GET");
+                assert(http->persistent == 1 && "should be persistent");
+                assert(state == MOG_PARSER_DONE && "parser not done");
+                assert_range_bytes_equal("bytes 5-55");
+                assert_path_equal("/foo");
+        }
+
+        if ("HTTP/1.1 Range (tip) GET request") {
+                buf_set("GET /foo HTTP/1.1\r\n"
+                        "Host: 127.6.6.6\r\n"
+                        "Range: bytes=-55\r\n"
+                        "\r\n");
+                state = mog_http_parse(http, buf, len);
+                assert(http->has_range == 1);
+                assert(http->range_beg == -1 && "range_beg didn't match");
+                assert(http->range_end == 55 && "range_end didn't match");
+                assert(http->http_method == MOG_HTTP_METHOD_GET
+                       && "http_method should be GET");
+                assert(http->persistent == 1 && "should be persistent");
+                assert(state == MOG_PARSER_DONE && "parser not done");
+                assert_range_bytes_equal("bytes -55");
+                assert_path_equal("/foo");
+        }
+
+        if ("HTTP/1.1 Range (end) GET request") {
+                buf_set("GET /foo HTTP/1.1\r\n"
+                        "Host: 127.6.6.6\r\n"
+                        "Range: bytes=55-\r\n"
+                        "\r\n");
+                state = mog_http_parse(http, buf, len);
+                assert(http->has_range == 1);
+                assert(http->range_beg == 55 && "range_beg didn't match");
+                assert(http->range_end == -1 && "range_end didn't match");
+                assert(http->http_method == MOG_HTTP_METHOD_GET
+                       && "http_method should be GET");
+                assert(http->persistent == 1 && "should be persistent");
+                assert(state == MOG_PARSER_DONE && "parser not done");
+                assert_range_bytes_equal("bytes 55-");
+                assert_path_equal("/foo");
+        }
+
         reset();
         return 0;
 }
diff --git a/test/http_chunked_put.rb b/test/http_chunked_put.rb
index d8f7b00..a492f9e 100644
--- a/test/http_chunked_put.rb
+++ b/test/http_chunked_put.rb
@@ -108,6 +108,7 @@ class TestHTTPChunkedPut < Test::Unit::TestCase
              "Last-Modified: #{EPOCH}\r\n" \
              "Content-Length: 5\r\n" \
              "Content-Type: application/octet-stream\r\n" \
+             "Accept-Ranges: bytes\r\n" \
              "Connection: close\r\n\r\nabcde"
     base = "PUT /zz HTTP/1.1\r\n" \
            "Host: #@host:#@port\r\n" \
@@ -242,6 +243,7 @@ class TestHTTPChunkedPut < Test::Unit::TestCase
              "Last-Modified: #{EPOCH}\r\n" \
              "Content-Length: 5\r\n" \
              "Content-Type: application/octet-stream\r\n" \
+             "Accept-Ranges: bytes\r\n" \
              "Connection: keep-alive\r\n\r\nabcde"
     resp = @client.read(expect.size)
     replace_dates!(resp)
@@ -267,6 +269,7 @@ class TestHTTPChunkedPut < Test::Unit::TestCase
              "Last-Modified: #{EPOCH}\r\n" \
              "Content-Length: 5\r\n" \
              "Content-Type: application/octet-stream\r\n" \
+             "Accept-Ranges: bytes\r\n" \
              "Connection: keep-alive\r\n\r\nabcde"
     resp = @client.read(expect.size)
     replace_dates!(resp)
@@ -295,6 +298,7 @@ class TestHTTPChunkedPut < Test::Unit::TestCase
              "Last-Modified: #{EPOCH}\r\n" \
              "Content-Length: 5\r\n" \
              "Content-Type: application/octet-stream\r\n" \
+             "Accept-Ranges: bytes\r\n" \
              "Connection: keep-alive\r\n\r\nabcde"
     resp = @client.read(expect.size)
     replace_dates!(resp)
diff --git a/test/http_range.rb b/test/http_range.rb
new file mode 100644
index 0000000..68f8d40
--- /dev/null
+++ b/test/http_range.rb
@@ -0,0 +1,185 @@
+#!/usr/bin/env ruby
+# -*- encoding: binary -*-
+# Copyright (C) 2012, Eric Wong <normalperson@yhbt.net>
+# License: GPLv3 or later (see COPYING for details)
+require 'test/test_helper'
+require 'net/http'
+require 'time'
+require 'webrick'
+require 'stringio'
+
+class TestHTTPRange < Test::Unit::TestCase
+  def setup
+    @tmpdir = Dir.mktmpdir('cmogstored-http-test')
+    @to_close = []
+    @host = TEST_HOST
+    srv = TCPServer.new(@host, 0)
+    @port = srv.addr[1]
+    srv.close
+    @err = Tempfile.new("stderr")
+    cmd = [ "cmogstored", "--docroot=#@tmpdir", "--httplisten=#@host:#@port",
+            "--maxconns=500" ]
+    vg = ENV["VALGRIND"] and cmd = vg.split(/\s+/).concat(cmd)
+    @pid = fork {
+      $stderr.reopen(@err)
+      @err.close
+      exec(*cmd)
+    }
+    @wblogger = StringIO.new
+    wbopts = {
+      :Port => 0,
+      :AccessLog => [],
+      :DocumentRoot => @tmpdir,
+      :ServerType => Thread,
+      :Logger => WEBrick::Log.new(@wblogger),
+    }
+    @webrick = WEBrick::HTTPServer.new(wbopts)
+    @webrick.start
+    @wport = @webrick.listeners[0].addr[1]
+    tries = 300
+    begin
+      sleep 0.05
+      @client = TCPSocket.new(@host, @port)
+      @to_close << @client
+      break
+    rescue
+    end while (tries-=1) > 0
+  end
+
+  def test_hello
+    full = File.open("/dev/urandom") { |fp| fp.read(666) }
+    File.open("#@tmpdir/hello", "wb") do |hello|
+      assert_equal 666, hello.write(full)
+    end
+    Net::HTTP.start(@host, @wport) do |wb|
+      Net::HTTP.start(@host, @port) do |cm|
+        [ Net::HTTP::Get, Net::HTTP::Head ].each do |meth|
+          r = meth.new("/hello")
+          cm_r = cm.request(r)
+          wb_r = wb.request(r)
+          assert_equal "666", cm_r["Content-Length"]
+          assert_equal "666", wb_r["Content-Length"]
+          assert_equal nil, cm_r["Content-Range"]
+          assert_equal nil, wb_r["Content-Range"]
+          assert_equal wb_r.body, cm_r.body
+          assert_equal nil, wb_r.body if Net::HTTP::Head === meth
+
+          r = meth.new("/hello")
+          r["Range"] = "bytes=66-70"
+          cm_r = cm.request(r)
+          wb_r = wb.request(r)
+          assert_equal wb_r["Content-Length"], cm_r["Content-Length"]
+          assert_equal "5", cm_r["Content-Length"]
+          assert_equal wb_r["Content-Range"], cm_r["Content-Range"]
+          assert_equal "bytes 66-70/666", cm_r["Content-Range"]
+          assert_equal wb_r.body, cm_r.body
+          assert_equal nil, wb_r.body if Net::HTTP::Head === meth
+
+          r["Range"] = "bytes=66-"
+          cm_r = cm.request(r)
+          wb_r = wb.request(r)
+          assert_equal wb_r["Content-Length"], cm_r["Content-Length"]
+          assert_equal "600", cm_r["Content-Length"]
+          assert_equal wb_r["Content-Range"], cm_r["Content-Range"]
+          assert_equal "bytes 66-665/666", cm_r["Content-Range"]
+          assert_equal wb_r.body, cm_r.body
+          assert_equal nil, wb_r.body if Net::HTTP::Head === meth
+
+          r["Range"] = "bytes=-66"
+          cm_r = cm.request(r)
+          wb_r = wb.request(r)
+          assert_equal wb_r["Content-Length"], cm_r["Content-Length"]
+          assert_equal "66", cm_r["Content-Length"]
+          assert_equal wb_r["Content-Range"], cm_r["Content-Range"]
+          assert_equal "bytes 600-665/666", cm_r["Content-Range"]
+          assert_equal wb_r.body, cm_r.body
+          assert_equal nil, wb_r.body if Net::HTTP::Head === meth
+
+          %w(0 665 123).each do |i|
+            r["Range"] = "bytes=#{i}-#{i}"
+            cm_r = cm.request(r)
+            wb_r = wb.request(r)
+            assert_equal wb_r["Content-Length"], cm_r["Content-Length"]
+            assert_equal "1", cm_r["Content-Length"]
+            assert_equal wb_r["Content-Range"], cm_r["Content-Range"]
+            assert_equal "bytes #{i}-#{i}/666", cm_r["Content-Range"]
+            assert_equal wb_r.body, cm_r.body
+            assert_equal nil, wb_r.body if Net::HTTP::Head === meth
+          end
+
+          # webrick doesn't seem to support this, follow nginx
+          r["Range"] = "bytes=666-666"
+          cm_r = cm.request(r)
+          assert_equal "0", cm_r["Content-Length"]
+          assert_equal "bytes */666", cm_r["Content-Range"]
+          assert_equal nil, cm_r.body if Net::HTTP::Head === meth
+          assert_equal 416, cm_r.code.to_i
+
+          # send the entire response as a 200 if the range is too far
+          # webrick doesn't seem to support this, follow nginx
+          %w(667 6666).each do |i|
+            r["Range"] = "bytes=-#{i}"
+            cm_r = cm.request(r)
+            assert_equal "666", cm_r["Content-Length"]
+            assert_nil cm_r["Content-Range"]
+            assert_equal nil, cm_r.body if Net::HTTP::Head === meth
+            assert_equal 200, cm_r.code.to_i
+          end
+
+          # send the entire request if the range is everything
+          r["Range"] = "bytes=-666"
+          cm_r = cm.request(r)
+          wb_r = wb.request(r)
+          assert_equal wb_r["Content-Range"], cm_r["Content-Range"]
+          assert_equal wb_r["Content-Length"], cm_r["Content-Length"]
+          assert_equal "666", cm_r["Content-Length"]
+          assert_equal wb_r.body, cm_r.body
+          assert_equal nil, wb_r.body if Net::HTTP::Head === meth
+          assert_equal wb_r.code.to_i, cm_r.code.to_i
+
+          # out-of-range requests
+          %w(666 6666).each do |i|
+            r["Range"] = "bytes=#{i}-"
+            cm_r = cm.request(r)
+            wb_r = wb.request(r)
+            assert_equal wb_r.code.to_i, cm_r.code.to_i
+            assert_equal 416, cm_r.code.to_i
+            assert_equal "bytes */666", cm_r["Content-Range"]
+            assert_equal nil, cm_r.body if Net::HTTP::Head === meth
+          end
+
+          %w(666 6666).each do |i|
+            r["Range"] = "bytes=#{i}-#{i}"
+            cm_r = cm.request(r)
+            wb_r = wb.request(r)
+            assert_equal wb_r.code.to_i, cm_r.code.to_i
+            assert_equal 416, cm_r.code.to_i
+            assert_equal "bytes */666", cm_r["Content-Range"]
+            assert_equal nil, cm_r.body if Net::HTTP::Head === meth
+          end
+
+          # bogus ranges
+          %w(-5-5 4qasdlkj 323).each do |bogus|
+            r["Range"] = "bytes=#{bogus}"
+            cm_r = cm.request(r)
+            assert_equal 416, cm_r.code.to_i
+            assert_equal "bytes */666", cm_r["Content-Range"]
+            assert_equal "0", cm_r["Content-Length"]
+            assert_equal nil, cm_r.body if Net::HTTP::Head === meth
+          end
+        end
+      end
+    end
+  end
+
+  def teardown
+    Process.kill(:QUIT, @pid) rescue nil
+    _, status = Process.waitpid2(@pid)
+    @to_close.each { |io| io.close unless io.closed? }
+    FileUtils.rm_rf(@tmpdir)
+    @err.rewind
+    $stderr.write(@err.read)
+    assert status.success?, status.inspect
+    @webrick.shutdown
+  end
+end