You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by zr...@apache.org on 2022/11/08 17:33:00 UTC

[trafficcontrol] branch master updated: DTP open sourcing (#7161)

This is an automated email from the ASF dual-hosted git repository.

zrhoffman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new a1a7ddeb7f DTP open sourcing (#7161)
a1a7ddeb7f is described below

commit a1a7ddeb7f57003d5a967c0bcf9534a4ab48ef8f
Author: Evan Zelkowitz <ev...@gmail.com>
AuthorDate: Tue Nov 8 10:32:53 2022 -0700

    DTP open sourcing (#7161)
    
    * DTP open sourcing
    
    * fix formatting
    
    * Replace line endings for sanitization
    
    * Fix up more codeql
    
    * Change back to non-sanitized since we can dismiss as test
    
    * review cleanup, remove commented code, add some constants
    
    * change timestamps to time.now, can print as UnixNano, and get ttms as time.Since.MS
---
 test/fakeOrigin/README.md                          |   1 +
 test/fakeOrigin/docs/Configuration.md              |   8 +-
 test/fakeOrigin/docs/Testing.md                    | 318 ++++++++++++++++++
 test/fakeOrigin/dtp/bin.go                         |  86 +++++
 test/fakeOrigin/dtp/config.go                      | 120 +++++++
 test/fakeOrigin/dtp/dtp.go                         | 127 ++++++++
 test/fakeOrigin/dtp/fwd_delay.go                   |  78 +++++
 test/fakeOrigin/dtp/fwd_posevt.go                  | 169 ++++++++++
 test/fakeOrigin/dtp/handler.go                     | 360 +++++++++++++++++++++
 test/fakeOrigin/dtp/num.go                         | 276 ++++++++++++++++
 test/fakeOrigin/dtp/plugutils.go                   | 100 ++++++
 test/fakeOrigin/dtp/text.go                        |  86 +++++
 test/fakeOrigin/dtp/type_bin.go                    |  79 +++++
 test/fakeOrigin/dtp/type_binf.go                   | 124 +++++++
 test/fakeOrigin/dtp/type_gen3s.go                  |  79 +++++
 .../endpoint_enums.go => dtp/type_hijack.go}       |  62 ++--
 test/fakeOrigin/dtp/type_tex.go                    | 181 +++++++++++
 test/fakeOrigin/dtp/type_txt.go                    |  77 +++++
 test/fakeOrigin/endpoint/endpoint.go               |  50 +--
 test/fakeOrigin/endpoint/endpoint_enums.go         |   3 +
 test/fakeOrigin/endpoint/type_jsonenums.go         |   3 +
 test/fakeOrigin/httpService/filter.go              |   5 +-
 test/fakeOrigin/httpService/httpService.go         |  77 ++++-
 test/fakeOrigin/httpService/rangerequest.go        |   2 +-
 test/fakeOrigin/version.go                         |   2 +-
 25 files changed, 2407 insertions(+), 66 deletions(-)

diff --git a/test/fakeOrigin/README.md b/test/fakeOrigin/README.md
index e6fed57ed5..fde1e40864 100644
--- a/test/fakeOrigin/README.md
+++ b/test/fakeOrigin/README.md
@@ -79,3 +79,4 @@ There is also another [set of instructions](build/README.md) if you're intereste
 * Optional in-memory caching
 * Support for arbitrary external commands to perform transcoding
   * Supports vod, live, and event m3u8 HLS manifest types
+* Support for testing various types of transaction elements using generated output when setting a type of "testing". See the [dtp](./docs/Testing.md) documentation
diff --git a/test/fakeOrigin/docs/Configuration.md b/test/fakeOrigin/docs/Configuration.md
index 9634f8e01c..8ef242ce72 100644
--- a/test/fakeOrigin/docs/Configuration.md
+++ b/test/fakeOrigin/docs/Configuration.md
@@ -33,10 +33,12 @@ The underlying data structures for these can be found in [endpoint.go](../endpoi
   "ssl_cert": "server.crt",
   "ssl_key": "server.key",
   "binding_address": "127.0.0.1",
-  "crossdomain_xml_file": "./example/crossdomain.xml"
+  "crossdomain_xml_file": "./example/crossdomain.xml",
+  "read_timeout": 0,
+  "write_timeout": 0,
 }
 ```
-This allows you to set what ports and IP addresses you'd like to listen on.  You can also supply where your SSL certificates and key should be located or created.  They should be in PEM format.
+This allows you to set what ports and IP addresses you'd like to listen on.  You can also supply where your SSL certificates and key should be located or created.  They should be in PEM format. The read and write timeouts are optional with a default of 0 that means no timeout, values should be in seconds and will impose a time limit on the read or write side of transactions.
 
 ## Endpoints
 This is where the meat of your config will be.
@@ -138,3 +140,5 @@ curl -vs4 -o /dev/null http://localhost:8080/SampleVideo/kelloggs.mp4 -H 'Fakeor
 < Transfer-Encoding: chunked
 <
 ```
+## Deterministic Testing Protocol
+This is a special type of endpoint when using type `testing`. The user can give various arguments in the path of the request on this endpoint in order to generate expected responses from the origin. Headers can be generated or manipulated and various conditions set like an initial stall or delay in response and various other things. See the [dtp](./docs/Testing.md) documentation
\ No newline at end of file
diff --git a/test/fakeOrigin/docs/Testing.md b/test/fakeOrigin/docs/Testing.md
new file mode 100644
index 0000000000..9f8d9f5a21
--- /dev/null
+++ b/test/fakeOrigin/docs/Testing.md
@@ -0,0 +1,318 @@
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+-->
+
+# dtp
+
+DTP is the Deterministic Test Protocol. DTP works as an HTTP web server that can generate HTTP responses based on parameters provided in the request. 
+
+### Getting Started
+  ___
+
+DTP is enabled by specifying an endpoint in the fakeOrigin configuration with a type of `testing`. The testing capabilities will then be available
+at an endpoint named after the `id` specified in the configuration of that endpoint. For example:
+```
+	"endpoints": [
+		{
+			"id": "testing_endpoint",
+			"type": "testing"
+		}
+	]
+```
+can then be reached at http://host:port/testing_endpoint/. Where you can then add any additional parameters to be tested as specified below.
+
+### Usage
+___
+
+Desired behavior is requested via url string, with the following format:
+
+```
+http://endpoint/~p.<type>/~<key0>.<val0>/~<key1>.<val1> ....
+```
+
+Options also passable via custom request header:
+```
+X-Dtp: ~<key0>.<val0>,~<key1>.<val1>
+```
+
+Or via query string:
+
+```
+http://endpoint/~p.<type>/&~<key0>.<val0>/~<key1>.<val1> ....
+```
+
+A special header, becomes Cache-Control
+
+```
+X-Dtp-Cc: public, max-age=200
+```
+
+This header explicitly tells DTP what Cache-Control headers to set for the response.
+
+#### Response Manipulation
+
+Currently implemented handler (has priority) (~h.<type>):
+
+- sc ('~sc') (status code)
+- hijack ('~payload', '~payload64')
+
+Initial modifier:
+- idelay ('~idelay') golang time.Duration
+- dly ('~dly') nanoseconds, can use random weighting.
+
+Data types (~p.<type>):
+- bin ('~s') - original binary
+- binf ('~s') - faster binary
+- txt ('~s') - plain txt file
+- tex ('~s') - repeated binary texture
+- gen3s ('~s') - all 3s
+
+  
+
+Modifiers (applies to Data types):
+- rnd ('~rnd') random number for cache control
+- rmhdrs ('~rmhdrs') remove specified request headers
+
+Forwarders (requires ~p.<type>) (~f.<type>):
+- posevt ('~posevt', 'etags', 'sc')
+- delay ('delay') delays all but first block (and header)
+
+Cache control:
+- ui ('~ui')
+- lm ('~lm')
+
+Size:
+~s -  size value (int64)
+
+```
+~s.100
+~s.10M
+~s.((1,30000r)25w,(100000,1000000r)45w,(1M)30w)
+```
+
+Random Seed:
+~rnd - seed value (int64)
+
+```
+~rnd.42
+```
+
+Remove specified headers before processing:
+~rmhdrs - List of headers
+```
+~rmhdrs.Range
+```
+
+Status Code:
+~sc - any valid http status code value
+
+```
+~sc.502
+```
+
+HTTP Payload:
+~payload - any simple string
+
+```
+~payload.Hello-World
+```
+
+Base 64 Mime Encoded Payload: 
+~payload64 - a base64 mime encoded payload string
+
+```
+~payload64.SGVsbG8sIFdvcmxkCg== (Hello, World\n)
+```
+
+Specifying byte position for event (very useful for range requests, EvalNumber):
+~posevt:
+
+- byte pos for event (very useful for range requests, EvalNumber).
+- close (closes connection)
+- sc (send back status code if range request contains)
+- todo: hang the connection
+- etags
+- first etag for range before
+- second etag for range containining/after
+
+```
+~f.posevt.1200000.close
+~f.posevt.1048577.sc.416
+~f.posevt.1000001.etags.foo.bar
+```
+
+  
+Delay:
+~delay- golang parseable delay applied to each block Read request, NOT the First block
+
+```
+~f.delay.250ms
+```
+ #### Header Manipulation
+Add Headers (can be combined):
+  
+~hdr - dot separated pairs
+
+```
+~hdr.Foo.Bar
+~hdr.Foo.Faz.Bar.Baz
+```
+
+~hdr64 - dot separated pairs, with contents base64 mime encoded
+
+```
+~hdr64.Cache-Control.bWF4LWFnZT00Mg==  (max-age=42)
+```
+
+#### Cache Control
+Max Age (Seconds):
+~ui - Last-Modified: (UTC / ui) * ui
+
+```
+~ui.2000
+```
+
+~lm - Last-Modified in UTC seconds. This either needs ~hdr.Cache-Control, ~hdr64.CacheControl or X-Dtp-Cc: header
+
+```
+~lm.1586266180
+```
+
+~etag - Add an etag header, autogen if blank/empty or as specified
+
+```
+~etag.foo
+~etag
+```
+
+~cksum_req - Standalone option which will md5sum the request header as `X-Request-Header-Cksum`
+
+#### Examples  
+500 MB plain text response:
+```
+curl "http://endpoint/~p.txt/~s.500M/"
+```
+
+Cacheable url that cycles every aligned 2000s (Last Modifed, Max-Age, etc):
+```
+curl "http://endpoint/~p.bin/~s.1G/&~ui.2000/"
+```
+
+Sample range call given (transfer 6 bytes then fail):
+
+```
+curl "http://endpoint/~p.txt/~f.posevt/~s.2M/&~posevt.1000005.close/" -r 999999-1049100
+```
+
+Slice sample call given slice block 1000000 (1 byte then 416 server response):
+```
+curl "http://endpoint/~p.txt/~f.posevt/~s.2M/&~posevt.1049076.sc.416/" -r 999999-1049100
+```
+
+
+Slice sample call given slice block 1000000 (transfer 6 bytes then change etag):
+```
+curl "http://endpoint/~p.txt/~s.2M/a&~f.posevt.1000005.etags.foo.bar/" -r 999999-1049100
+```
+
+Sample per 32k block delay parameter, not the first block:
+
+```
+curl "http://endpoint/~p.txt/~s.2M/&~f.delay.100ms/" -r 999999-1049100
+```
+
+Sample last modified header:
+
+```
+curl "http://endpoint/~p.txt/~s.1024/~hdr.foo.bar.Next-Modified.RnJpLCAwNyBGZWIgMjAyMCAxNTowNjo0MCBHTVQ="
+
+results in return headers:
+Foo: bar
+Next-Modified: RnJpLCAwNyBGZWIgMjAyMCAxNTowNjo0MCBHTVQ=
+```
+
+  
+
+```
+
+curl "http://endpoint/~p.txt/~s.1024/~hdr64.Next-Modified.RnJpLCAwNyBGZWIgMjAyMCAxNTowNjo0MCBHTVQ="
+
+results in return header:
+Next-Modified: Fri, 07 Feb 2020 15:06:40 GMT
+
+```
+#### Profiling
+DTP may be profiled if so desired.  If profiling is enabled, then DTP endpoints become available under:
+
+http://endpoint/debug/pprof/
+
+http://endpoint/debug/pprof/cmdline
+
+http://endpoint/debug/pprof/profile
+
+http://endpoint/debug/pprof/symbol
+
+http://endpoint/debug/pprof/trace
+
+Current DTP configuration may be dumped via endpoint:
+
+http://endpoint/config
+  
+Header logging can be flipped on or off with the following:
+
+http://endpoint/config?request_headers=true
+
+http://endpoint/config?response_headers=false
+
+http://endpoint/config?all_headers=true
+
+### Development
+___
+The "type" identifies which "plugin" to use for handling a response.
+This allows developers to drop in their own custom plugins.
+A developer needs to include a per connection function with signature:
+
+```
+func(http.ResponseWriter, *http.Request, map[string]string)
+```
+
+the file needs to register the function as:
+
+```
+func init() {
+GlobalHandlerFuncs[`type`] = TypeFunc
+}
+```
+
+A simple example is in "type_hijack.go"
+An example curl might be:
+```
+curl -v "http://endpoint/~h.hijack/&~payload.HelloWorld"
+```
+
+or
+
+```
+curl -v "http://endpoint/~h.hijack/&~payload64.SGVsbG8gV29ybGQK"
+(Hello World with a \n at the the end)
+```
+
+With debug turned on you should see the message "Connection hijacked"
+displayed.
+
+Another example is type_gen3s.go
diff --git a/test/fakeOrigin/dtp/bin.go b/test/fakeOrigin/dtp/bin.go
new file mode 100644
index 0000000000..339303cdfc
--- /dev/null
+++ b/test/fakeOrigin/dtp/bin.go
@@ -0,0 +1,86 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/binary"
+	"errors"
+	"hash/fnv"
+	"io"
+	"net/http"
+	"strconv"
+	"time"
+)
+
+type BinStampSeeker struct {
+	Size int64
+	Pos  int64
+	Seed int64
+	Rnd  int64
+}
+
+func (s *BinStampSeeker) Read(p []byte) (n int, err error) {
+	return ReadBlock(p, s.Pos, s.Size, 8, func(ct int64) []byte {
+		ct += s.Seed
+		b := make([]byte, 8)
+		if s.Rnd == 0 {
+			for i := 0; i < len(b); i++ {
+				b[i] = byte(ct & 0xf)
+				ct >>= 8
+			}
+		} else {
+			h := fnv.New64()
+			binary.LittleEndian.PutUint64(b, uint64(ct))
+			h.Write(b)
+			binary.LittleEndian.PutUint64(b, uint64(s.Rnd))
+			h.Write(b)
+			return h.Sum(nil)
+		}
+		return b
+	})
+}
+
+func (s *BinStampSeeker) Seek(off int64, whence int) (int64, error) {
+	var newPos int64
+	switch whence {
+	case io.SeekStart:
+		newPos = off
+	case io.SeekEnd:
+		newPos = s.Size - off
+	case io.SeekCurrent:
+		newPos += off
+		if newPos > s.Size {
+			newPos = s.Size
+		}
+	}
+	if newPos < 0 {
+		return s.Pos, errors.New(`unable to seek before file`)
+	}
+	s.Pos = newPos
+	return s.Pos, nil
+}
+
+func BinStamp(w http.ResponseWriter, r *http.Request, reqdat map[string]string) {
+	seed, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+	lastmod, _ := strconv.ParseInt(reqdat[`lm`], 10, 64)
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	w.Header()[`Content-Type`] = []string{`application/octet-stream`}
+	http.ServeContent(w, r, ``, time.Unix(lastmod, 0), &BinStampSeeker{Size: sz, Seed: lastmod, Rnd: seed})
+}
diff --git a/test/fakeOrigin/dtp/config.go b/test/fakeOrigin/dtp/config.go
new file mode 100644
index 0000000000..b771f6c320
--- /dev/null
+++ b/test/fakeOrigin/dtp/config.go
@@ -0,0 +1,120 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+type Log struct {
+	Access          bool   `json:"access"`
+	Path            string `json:"path"`
+	RequestHeaders  bool   `json:"request_headers"`
+	ResponseHeaders bool   `json:"response_headers"`
+}
+
+type Timeout struct {
+	Read  time.Duration `json:"read"`
+	Write time.Duration `json:"write"`
+	Idle  time.Duration `json:"idle"`
+}
+
+type Config struct {
+	Debug         bool          `json:"debug"`
+	EnablePprof   bool          `json:"enable_pprof"`
+	Log           Log           `json:"log"`
+	Timeout       Timeout       `json:"timeout"`
+	StallDuration time.Duration `json:"stall_duration"`
+}
+
+func NewConfig() Config {
+	var cfg Config
+	cfg.Log.Access = true
+	cfg.Log.Path = "dtp.log"
+	cfg.Log.RequestHeaders = false
+	cfg.Log.ResponseHeaders = false
+	cfg.EnablePprof = false
+	cfg.Timeout.Read = time.Duration(10) * time.Second
+	cfg.Timeout.Write = time.Duration(10) * time.Second
+	cfg.Timeout.Idle = time.Duration(10) * time.Second
+	cfg.StallDuration = time.Duration(0)
+	return cfg
+}
+
+var GlobalConfig = NewConfig()
+
+// handle api configuration endpoint
+func ConfigHandler(w http.ResponseWriter, r *http.Request) {
+	{
+		dbghdrs := r.URL.Query().Get("debug")
+		if dbghdrs != "" {
+			fmt.Println("processing debug", dbghdrs)
+			GlobalConfig.Debug = (dbghdrs == "true")
+			fmt.Println("debugging:", GlobalConfig.Debug)
+		}
+	}
+	{
+		reqhdrs := r.URL.Query().Get("request_headers")
+		if reqhdrs != "" {
+			GlobalConfig.Log.RequestHeaders = (reqhdrs == "true")
+			fmt.Println("req header logging:", GlobalConfig.Log.RequestHeaders)
+		}
+	}
+	{
+		reshdrs := r.URL.Query().Get("response_headers")
+		if reshdrs != "" {
+			GlobalConfig.Log.ResponseHeaders = (reshdrs == "true")
+			fmt.Println("resp header logging:", GlobalConfig.Log.ResponseHeaders)
+		}
+	}
+	{
+		hdrs := r.URL.Query().Get("all_headers")
+		if hdrs != "" {
+			if hdrs == "true" {
+				GlobalConfig.Log.RequestHeaders = true
+				GlobalConfig.Log.ResponseHeaders = true
+				fmt.Println("req/resp header logging:", true)
+			} else {
+				GlobalConfig.Log.RequestHeaders = false
+				GlobalConfig.Log.ResponseHeaders = false
+				fmt.Println("req/resp header logging:", false)
+			}
+		}
+	}
+	{
+		stallhdr := r.URL.Query().Get("stall_duration")
+		if stallhdr != "" {
+			dur, err := time.ParseDuration(stallhdr)
+			if nil == err {
+				GlobalConfig.StallDuration = dur
+				fmt.Println("stall_duration:", dur)
+			} else {
+				fmt.Println("error setting stall_duration", err)
+			}
+		}
+	}
+
+	// default is to dump current config
+	bytes, _ := json.Marshal(&GlobalConfig)
+	w.Write(bytes)
+}
diff --git a/test/fakeOrigin/dtp/dtp.go b/test/fakeOrigin/dtp/dtp.go
new file mode 100644
index 0000000000..c36159f829
--- /dev/null
+++ b/test/fakeOrigin/dtp/dtp.go
@@ -0,0 +1,127 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"crypto/tls"
+	"fmt"
+	"log"
+	"net/http"
+	"strings"
+	"time"
+)
+
+const ByteMask = 0x8000000000000000
+const ReadBlockSize = 32
+
+type LogRecorder struct {
+	http.ResponseWriter
+
+	Status       int
+	HeaderBytes  int64
+	ContentBytes int64
+}
+
+func (rec *LogRecorder) WriteHeader(code int) {
+	rec.Status = code
+	rec.ResponseWriter.WriteHeader(code)
+}
+
+func (rec *LogRecorder) Write(bytes []byte) (int, error) {
+	rec.ContentBytes += int64(len(bytes))
+	return rec.ResponseWriter.Write(bytes)
+}
+
+// this is mostly for hijack
+func isHandlerType(r *http.Request) bool {
+	if strings.Contains(r.URL.EscapedPath(), "~h.") {
+		return true
+	} else if strings.Contains(r.URL.RawQuery, "~h.") {
+		return true
+	} else {
+		for _, part := range r.Header[`X-Dtp`] {
+			if strings.Contains(part, "~h.") {
+				return true
+			}
+		}
+	}
+
+	return false
+}
+
+func Logger(alog *log.Logger, next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		timeStart := time.Now()
+
+		// the logger interferes with hijacking
+		if isHandlerType(r) {
+			next.ServeHTTP(w, r)
+			alog.Printf("%.3f %s \"%s\" %d b=%d ttms=%d uas=\"%s\" rr=\"%s\"\n",
+				float64(timeStart.UnixNano())/float64(1.e9),
+				r.Method,
+				r.URL.String(),
+				42, // status code -- why not?
+				0,  // bytes
+				0,  // ttms
+				r.UserAgent(),
+				r.Header.Get("Range"),
+			)
+			return
+		}
+
+		tlsstr := "-"
+		if r.TLS != nil {
+			tlsstr = tls.CipherSuiteName(r.TLS.CipherSuite)
+		}
+
+		rec := LogRecorder{w, 200, 0, 0}
+		next.ServeHTTP(&rec, r)
+		alog.Printf("%.3f %s \"%s\" %s %d b=%d ttms=%d uas=\"%s\" rr=\"%s\"\n",
+			float64(timeStart.UnixNano())/float64(1.e9),
+			r.Method,
+			r.URL.String(),
+			tlsstr,
+			rec.Status,
+			rec.ContentBytes,
+			time.Since(timeStart).Milliseconds(),
+			r.UserAgent(),
+			r.Header.Get("Range"),
+		)
+
+		if GlobalConfig.Log.RequestHeaders {
+			alog.Print(r.Header)
+		}
+		if GlobalConfig.Log.ResponseHeaders {
+			alog.Print(w.Header())
+		}
+	})
+}
+
+func DebugLog(s string) {
+	if GlobalConfig.Debug {
+		fmt.Println(s)
+	}
+}
+
+func DebugLogf(format string, args ...interface{}) {
+	if GlobalConfig.Debug {
+		fmt.Printf(format, args...)
+	}
+}
diff --git a/test/fakeOrigin/dtp/fwd_delay.go b/test/fakeOrigin/dtp/fwd_delay.go
new file mode 100644
index 0000000000..2ded02272a
--- /dev/null
+++ b/test/fakeOrigin/dtp/fwd_delay.go
@@ -0,0 +1,78 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"net/http"
+	"strconv"
+	"time"
+)
+
+func init() {
+	GlobalForwarderFuncs["delay"] = NewDelayForwardGen
+}
+
+type DelayForward struct {
+	Generator Generator
+	Latency   time.Duration // per request latency
+	FirstTime bool
+}
+
+func (ss *DelayForward) ContentType() string {
+	return ss.Generator.ContentType()
+}
+
+func (ss *DelayForward) Read(bufout []byte) (bytes int, err error) {
+	if ss.FirstTime {
+		ss.FirstTime = false
+		bytes, err = ss.Generator.Read(bufout)
+	} else {
+		timer := time.NewTimer(ss.Latency)
+		bytes, err = ss.Generator.Read(bufout)
+		<-timer.C
+	}
+
+	return bytes, err
+}
+
+func (ss *DelayForward) Seek(off int64, whence int) (int64, error) {
+	return ss.Generator.Seek(off, whence)
+}
+
+func NewDelayForwardGen(
+	w http.ResponseWriter,
+	r *http.Request,
+	gen Generator,
+	reqdat map[string]string, updated int64,
+) Generator {
+	delaystr := reqdat[`delay`]
+	latency, err := time.ParseDuration(delaystr)
+	if err != nil {
+		seed, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+		latencyns := EvalNumber(delaystr, seed)
+		latency = time.Duration(latencyns)
+		reqdat[`delay`] = latency.String() // save this for next time
+	}
+	return &DelayForward{
+		Generator: gen,
+		Latency:   latency,
+		FirstTime: true,
+	}
+}
diff --git a/test/fakeOrigin/dtp/fwd_posevt.go b/test/fakeOrigin/dtp/fwd_posevt.go
new file mode 100644
index 0000000000..0cb4dbdbdf
--- /dev/null
+++ b/test/fakeOrigin/dtp/fwd_posevt.go
@@ -0,0 +1,169 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"io"
+	"math"
+	"net/http"
+	"strconv"
+	"strings"
+)
+
+func init() {
+	GlobalForwarderFuncs["posevt"] = NewEvtForwardGen
+}
+
+type RangeBE struct {
+	Begin int64
+	End   int64
+}
+
+func parseRange(rangestr string, bsize int64) (RangeBE, error) {
+	var beg int64 = -1
+	var end int64 = -1
+
+	const bstr = `bytes=`
+	bpos := strings.Index(rangestr, bstr)
+	if 0 <= bpos {
+		rstr := rangestr[bpos+len(bstr):]
+		fpos := strings.Index(rstr, `,`)
+		if 0 < fpos {
+			rstr = rstr[0:fpos]
+		}
+		fields := strings.Split(rstr, `-`)
+		if len(fields) == 2 {
+			v0, ok0 := strconv.ParseInt(fields[0], 10, 64)
+			v1, ok1 := strconv.ParseInt(fields[1], 10, 64)
+
+			if nil == ok0 && nil == ok1 {
+				beg = v0
+				end = v1 + 1 // convert from min/max to begin/end
+			} else if nil == ok0 {
+				beg = v0
+				end = bsize
+			} else if nil == ok1 {
+				beg = bsize - v1
+				end = bsize
+			}
+		}
+	}
+
+	if beg < 0 || end < beg {
+		DebugLogf("bad range %d %d\n", beg, end)
+		return RangeBE{}, errors.New("bad range passed in")
+	} else {
+		DebugLogf("parsed range %d %d\n", beg, end)
+		return RangeBE{beg, end}, nil
+	}
+}
+
+func (rng *RangeBE) Contains(val int64) bool {
+	return rng.Begin <= val && val < rng.End
+}
+
+type EvtForward struct {
+	generator Generator
+	posevt    int64
+}
+
+func (ss *EvtForward) ContentType() string {
+	return ss.generator.ContentType()
+}
+
+func (ss *EvtForward) Read(bufout []byte) (n int, err error) {
+
+	posstart, _ := ss.generator.Seek(0, io.SeekCurrent)
+	bytes, err := ss.generator.Read(bufout)
+
+	errout := err
+
+	if nil == err && math.MaxInt64 != ss.posevt {
+		// determine if we need to cut the buffer short
+		failbytes := ss.posevt - posstart
+		if 0 <= failbytes && failbytes < int64(bytes) {
+
+			// fix up the underlying seeker< dump an EOF
+			ss.generator.Seek(posstart+failbytes, io.SeekStart)
+			bytes = int(failbytes)
+			errout = io.EOF
+		}
+	}
+
+	return bytes, errout
+}
+
+func (ss *EvtForward) Seek(off int64, whence int) (int64, error) {
+	return ss.generator.Seek(off, whence)
+}
+
+func NewEvtForwardGen(w http.ResponseWriter, r *http.Request, gen Generator, reqdat map[string]string, updated int64) Generator {
+
+	var posevt int64 = math.MaxInt64
+
+	posstr := reqdat[`posevt`]
+	posfields := strings.Split(posstr, `.`)
+
+	if 2 <= len(posfields) {
+		if posevt = EvalNumber(posfields[0], 0); 0 < posevt {
+			key := posfields[1]
+
+			// This one doesn't depend on range requests
+			if key == `close` {
+				return &EvtForward{generator: gen, posevt: posevt}
+			}
+
+			// shift off the byte pos field and key
+			posfields = posfields[2:]
+
+			if rreq, ok := r.Header[`Range`]; ok {
+				if sz, err := strconv.ParseInt(reqdat[`sz`], 10, 64); nil == err {
+					if rangebe, err := parseRange(rreq[0], sz); nil == err {
+						switch key {
+						case `sc`:
+							if rangebe.Contains(posevt) {
+								code, _ := strconv.Atoi(posfields[0])
+								DebugLogf("Sending posevt code %d\n", code)
+								w.WriteHeader(code)
+								return nil
+							}
+						case `etags`:
+							if 2 <= len(posfields) {
+								var etag string
+								if rangebe.End < posevt {
+									etag = posfields[0]
+								} else {
+									etag = posfields[1]
+								}
+
+								DebugLogf("Setting posevt etag %s\n", etag)
+								w.Header().Set(`ETag`, etag)
+								posevt = math.MaxInt64
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return gen
+}
diff --git a/test/fakeOrigin/dtp/handler.go b/test/fakeOrigin/dtp/handler.go
new file mode 100644
index 0000000000..1c5252c23f
--- /dev/null
+++ b/test/fakeOrigin/dtp/handler.go
@@ -0,0 +1,360 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"crypto/md5"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+	"time"
+)
+
+const XDtpHdrStr = `X-Dtp`
+const XDtpCcHdrStr = `X-Dtp-Cc`
+
+type DTPHandler struct{}
+
+func NewDTPHandler() DTPHandler {
+	return DTPHandler{}
+}
+
+// This is for content generation
+type Generator interface {
+	ContentType() string
+	io.ReadSeeker
+}
+
+type NewGeneratorFunc func(
+	map[string]string,
+	int64,
+) Generator
+
+var GlobalGeneratorFuncs = map[string]NewGeneratorFunc{}
+
+// No content generation
+type HandlerFunc func(
+	http.ResponseWriter,
+	*http.Request,
+	map[string]string,
+)
+
+var GlobalHandlerFuncs = map[string]HandlerFunc{}
+
+type NewForwardFunc func(
+	http.ResponseWriter,
+	*http.Request,
+	Generator,
+	map[string]string,
+	int64,
+) Generator
+
+var GlobalForwarderFuncs = map[string]NewForwardFunc{}
+
+/*
+func timeoutAndDrop(w http.ResponseWriter, r *http.Request, durstr string) {
+	hj, ok := w.(http.Hijacker)
+	if !ok {
+		http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
+		DebugLogf("Can't get a hijack\n")
+		return
+	}
+
+	conn, buf, err := hj.Hijack()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		DebugLogf("hijack failed: %s\n", err)
+		return
+	}
+
+	delaydur, err := time.ParseDuration(durstr)
+	if err == nil {
+		if 0 < delaydur {
+			time.Sleep(delaydur)
+		}
+	}
+
+	conn.Close()
+}
+*/
+
+func (h DTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	var cmds []string
+
+	/*
+		if GlobalConfig.Drop {
+			DebugLogf("Dropping request for '%s'\n", r.URL.String())
+			timeoutAndDrop(w, r, "60s")
+			return
+		}
+	*/
+	if time.Duration(0) != GlobalConfig.StallDuration {
+		DebugLogf("Stalling request for '%s'\n", r.URL.String())
+		time.Sleep(GlobalConfig.StallDuration)
+	}
+
+	DebugLogf("Serving request for '%s'\n", r.URL.String())
+	cmdFromStr := func(s string) string {
+		DebugLogf("Parsing `%s`\n", s)
+		begin, end := -1, -1
+		for i, c := range s {
+			if c == '~' {
+				if begin == -1 {
+					begin = i + 1
+				} else {
+					end = i
+					break
+				}
+			}
+		}
+		if begin != -1 && end != -1 {
+			return s[begin:end]
+		}
+		if begin != -1 {
+			return s[begin:]
+		}
+		return ""
+	}
+
+	/* Check the URL */
+	DebugLog(`Parsing path: ` + r.URL.EscapedPath())
+	for _, part := range strings.Split(r.URL.EscapedPath(), `/`) {
+		DebugLog("Parsing part: " + part)
+		s, err := url.PathUnescape(part)
+		if err != nil {
+			continue
+		}
+
+		cmd := cmdFromStr(s)
+		if cmd != `` {
+			cmds = append(cmds, cmd)
+		}
+	}
+
+	// Check the query
+	for _, part := range strings.Split(r.URL.RawQuery, `&`) {
+		s, err := url.PathUnescape(part)
+		if err != nil {
+			continue
+		}
+
+		cmd := cmdFromStr(s)
+		if cmd != `` {
+			cmds = append(cmds, cmd)
+		}
+	}
+
+	// Check the headers
+	for _, part := range r.Header[XDtpHdrStr] {
+		for _, hdr := range strings.Split(part, `,`) {
+			cmd := cmdFromStr(hdr)
+			if cmd != `` {
+				cmds = append(cmds, cmd)
+			}
+		}
+	}
+
+	// requests to map
+	reqdat := make(map[string]string)
+
+	// Check for special cache-control header
+	for _, part := range r.Header[XDtpCcHdrStr] {
+		reqdat[XDtpCcHdrStr] = part
+	}
+
+	for _, cmd := range cmds {
+		var key string
+		var val string
+		dotind := strings.IndexByte(cmd, '.')
+		if dotind <= 0 {
+			key = cmd
+		} else {
+			key = cmd[:dotind]
+			val = cmd[dotind+1:]
+		}
+		DebugLogf("Setting '%s' to '%s'\n", key, val)
+		reqdat[key] = val
+	}
+
+	// check for connection handler, direct code or hijack
+	if hcode, ok := reqdat[`h`]; ok {
+		if hfunc, ok := GlobalHandlerFuncs[hcode]; ok {
+			hfunc(w, r, reqdat)
+			return
+		}
+	}
+
+	// check for content generator, failover to text
+	pcode := reqdat[`p`]
+	genfunc, genok := GlobalGeneratorFuncs[pcode]
+	if !genok {
+		genfunc = GlobalGeneratorFuncs[`txt`]
+	}
+
+	// process headers
+	if !ProcessHeaders(w, r, reqdat) {
+		return
+	}
+
+	lastmod, _ := strconv.ParseInt(reqdat[`lm`], 10, 64)
+
+	generator := genfunc(reqdat, lastmod)
+	w.Header()[`Content-Type`] = []string{generator.ContentType()}
+
+	// look for events, byte position events, handle extra args
+	var forwarder NewForwardFunc = nil
+	if fwdarg, ok := reqdat[`f`]; ok {
+		fcode := fwdarg
+		if find := strings.Index(fwdarg, `.`); 0 < find {
+			fcode = fwdarg[0:find]
+			args := fwdarg[find+1:]
+			reqdat[fcode] = args
+			DebugLogf("Adding args for forwarder %s: %s\n", fcode, args)
+		}
+
+		var ok = false
+		if forwarder, ok = GlobalForwarderFuncs[fcode]; ok {
+			DebugLogf("Processing a forwarder %s\n", fcode)
+		}
+	}
+
+	if nil != forwarder {
+		generator = forwarder(w, r, generator, reqdat, lastmod)
+		if nil == generator {
+			return
+		}
+	}
+
+	http.ServeContent(w, r, "", time.Unix(lastmod, 0), generator)
+}
+
+func ProcessHeaders(w http.ResponseWriter, r *http.Request, reqdat map[string]string) bool {
+
+	if _, ok := reqdat["cksum_req"]; ok {
+		reqhdr := r.Header
+		hdrstr := fmt.Sprintf("%v", reqhdr)
+		hash := md5.Sum([]byte(hdrstr))
+		hashstr := hex.EncodeToString(hash[:])
+		w.Header().Set("X-Request-Header-Cksum", hashstr)
+		DebugLogf("cksum header: '%s'", hashstr)
+	}
+
+	var lastmod int64 = 0
+	var maxage int64 = 0
+
+	now := time.Now().Unix()
+
+	// handle last modified and cache control
+	// note that 'lm' will always be available downstream
+	if lmstr, ok := reqdat[`lm`]; ok {
+		lmsec, err := strconv.ParseInt(lmstr, 10, 64)
+		if nil == err && 0 < lmsec {
+			lastmod = lmsec
+		}
+	} else if uistr, ok := reqdat[`ui`]; ok {
+		uisec, err := strconv.ParseInt(uistr, 10, 64)
+		if nil == err && 0 < uisec {
+			lastmod = (now / uisec) * uisec
+			maxage = now - lastmod // expire at quanta
+			w.Header().Set(`Cache-Control`, fmt.Sprintf("max-age=%d", maxage))
+			reqdat[`lm`] = strconv.FormatInt(lastmod, 10)
+			reqdat[`ma`] = strconv.FormatInt(maxage, 10)
+		}
+	} else {
+		reqdat[`lm`] = `0`
+	}
+
+	seed, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+
+	// Evaluate the size and apply random factors
+	sz := EvalNumber(reqdat[`s`], seed^lastmod)
+	reqdat[`sz`] = strconv.FormatInt(sz, 10)
+
+	if etagstr, ok := reqdat[`etag`]; ok {
+		if len(etagstr) == 0 {
+			etagnum := lastmod ^ sz
+			etagstr = fmt.Sprintf("\"%d\"", etagnum)
+		}
+		w.Header().Set(`Etag`, etagstr)
+	}
+
+	// Special header to hard set cache control
+	if ccstr, ok := reqdat[XDtpCcHdrStr]; ok {
+		if 0 < len(ccstr) {
+			w.Header().Set(`Cache-Control`, ccstr)
+		}
+	}
+
+	// Initial delay, hard set
+	delaydur, err := time.ParseDuration(reqdat["idelay"])
+	if err == nil {
+		if 0 < delaydur {
+			time.Sleep(delaydur)
+		}
+	} else { // initial delay with rand
+		delay := EvalNumber(reqdat[`dly`], seed^now)
+		if 0 < delay {
+			time.Sleep(time.Duration(delay))
+		}
+	}
+
+	// These should override any of the previous
+	if hdr, ok := reqdat[`hdr`]; ok {
+		parts := strings.Split(hdr, `.`)
+		DebugLogf("Reading hdr: %v", parts)
+		for ind := 1; ind < len(parts); ind += 2 {
+			w.Header().Set(parts[ind-1], parts[ind])
+		}
+	}
+
+	if hdr, ok := reqdat[`hdr64`]; ok {
+		parts := strings.Split(hdr, `.`)
+		DebugLogf("Reading hdr64: %v", parts)
+		for ind := 1; ind < len(parts); ind += 2 {
+			val, err := base64.URLEncoding.DecodeString(parts[ind])
+			if err != nil {
+				DebugLogf("Failed to decode hdr64 '%s': %s.\n", parts[ind], err.Error())
+				continue
+			}
+			w.Header().Set(parts[ind-1], string(val))
+		}
+	}
+
+	sc := EvalNumber(reqdat[`sc`], seed^now)
+	if sc != 200 && sc != 0 {
+		w.WriteHeader(int(sc))
+		return false
+	}
+
+	// remove request headers
+	if rmhdrs, ok := reqdat[`rmhdrs`]; ok {
+		hdrs := strings.Split(rmhdrs, `.`)
+		for _, hdr := range hdrs {
+			r.Header.Del(hdr)
+		}
+	}
+
+	return true
+}
diff --git a/test/fakeOrigin/dtp/num.go b/test/fakeOrigin/dtp/num.go
new file mode 100644
index 0000000000..685b1f6c67
--- /dev/null
+++ b/test/fakeOrigin/dtp/num.go
@@ -0,0 +1,276 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"fmt"
+	"math/rand"
+	"strconv"
+	"unicode"
+)
+
+/* Operators:
+ *   a,+: add
+ *   s,-: subtract
+ *   m,*: multiply
+ *   d: divide
+ *   u: modulo
+ *   ,: separate
+ *   k: multiply by 1024
+ *   M: multiply by 1024^2
+ *   G: multiply by 1024^3
+ *   T: multiply by 1024^4
+ *   P: multiply by 1024^5
+ *   r: Linear Distribution
+ *   (: Start Selection
+ *   ): Perform Selection
+ *   w: Add weight
+ */
+
+func EvalNumber(numStr string, randSeed int64) int64 {
+	rnd := rand.New(rand.NewSource(randSeed))
+	ops := LexNumberStr(numStr)
+	DebugLogf("Evaluating %s: %v\n", numStr, ops)
+	var stack []Number
+	for _, op := range ops {
+		DebugLogf("Evaluating %v on %v ...", op, stack)
+		pop, push := op.Evaluate(stack, rnd)
+		DebugLogf("Popping %d, Pushing %v\n", pop, push)
+
+		posend := len(stack) - pop
+		if posend < 0 {
+			return 0
+		}
+
+		stack = stack[:posend]
+		stack = append(stack, push...)
+	}
+	if len(stack) == 0 {
+		return 0
+	}
+	return stack[len(stack)-1].Value
+}
+
+type Evaluable interface {
+	Evaluate(stack []Number, rnd *rand.Rand) (pop int, push []Number)
+}
+
+type Operator byte
+type Literal int64
+
+type Number struct {
+	Value    int64
+	Weight   int64
+	Sentinel bool
+}
+
+func (o Operator) String() string {
+	return string(byte(o))
+}
+
+func (n Number) String() string {
+	if n.Sentinel {
+		return "!"
+	}
+	if n.Weight != 1 {
+		return fmt.Sprintf("%d,%dw", n.Value, n.Weight)
+	}
+	return fmt.Sprint(n.Value)
+}
+
+func (o Operator) Evaluate(stack []Number, rnd *rand.Rand) (pop int, push []Number) {
+	rd := func(i int) Number {
+		if i > len(stack) || i < 1 {
+			return Number{Weight: 1}
+		}
+		return stack[len(stack)-i]
+	}
+	var c int64 = 1
+	switch o {
+	case 'a', '+':
+		a, b := rd(1), rd(2)
+		if a.Sentinel || b.Sentinel {
+			return 0, nil
+		}
+		return 2, []Number{
+			{
+				Value:  b.Value + a.Value,
+				Weight: a.Weight,
+			},
+		}
+	case 's', '-':
+		a, b := rd(1), rd(2)
+		if a.Sentinel || b.Sentinel {
+			return 0, nil
+		}
+		return 2, []Number{
+			{
+				Value:  b.Value - a.Value,
+				Weight: a.Weight,
+			},
+		}
+	case 'm', '*':
+		a, b := rd(1), rd(2)
+		if a.Sentinel || b.Sentinel {
+			return 0, nil
+		}
+		return 2, []Number{
+			{
+				Value:  b.Value * a.Value,
+				Weight: a.Weight,
+			},
+		}
+	case 'd':
+		a, b := rd(1), rd(2)
+		if a.Sentinel || b.Sentinel {
+			return 0, nil
+		}
+		return 2, []Number{
+			{
+				Value:  b.Value / a.Value,
+				Weight: a.Weight,
+			},
+		}
+	case 'u':
+		a, b := rd(1), rd(2)
+		if a.Sentinel || b.Sentinel {
+			return 0, nil
+		}
+		return 2, []Number{
+			{
+				Value:  b.Value % a.Value,
+				Weight: a.Weight,
+			},
+		}
+	case ',':
+		return 0, nil
+	case 'P':
+		c *= 1024
+		fallthrough
+	case 'T':
+		c *= 1024
+		fallthrough
+	case 'G':
+		c *= 1024
+		fallthrough
+	case 'M':
+		c *= 1024
+		fallthrough
+	case 'k':
+		c *= 1024
+		a := rd(1)
+		if a.Sentinel {
+			return 0, nil
+		}
+		return 1, []Number{
+			{
+				Value:  a.Value * c,
+				Weight: a.Weight,
+			},
+		}
+	case 'r':
+		a, b := rd(1), rd(2)
+		if a.Sentinel || b.Sentinel {
+			return 0, nil
+		}
+		var min, max int64
+		if a.Value < b.Value {
+			min = a.Value
+			max = b.Value
+		} else {
+			min = b.Value
+			max = a.Value
+		}
+		return 2, []Number{
+			{
+				Value:  rnd.Int63n(max-min) + min,
+				Weight: a.Weight,
+			},
+		}
+	case '(':
+		return 0, []Number{{Sentinel: true}}
+	case ')':
+		var wt int64
+		var ct int
+		var sentinelFound int
+		for i := 1; i <= len(stack); i++ {
+			x := rd(i)
+			if x.Sentinel {
+				sentinelFound++
+				break
+			}
+
+			wt += x.Weight
+			ct++
+		}
+		wt_r := rnd.Int63n(wt)
+		DebugLogf("(w%d/%d*%d) ", wt_r, wt, ct)
+		var n int64
+		for i := int(1); i <= ct; i++ {
+			x := rd(i)
+			wt_r -= x.Weight
+			DebugLogf("(w%d/%d*%d %d) ", wt_r, wt, i, x.Value)
+			if wt_r < 0 {
+				n = x.Value
+				break
+			}
+		}
+		return ct + sentinelFound, []Number{{Value: n, Weight: 1}}
+	case 'w':
+		return 2, []Number{{Value: rd(2).Value, Weight: rd(1).Value}}
+	}
+	return 0, nil
+}
+
+func (n Literal) Evaluate(stack []Number, rnd *rand.Rand) (pop int, push []Number) {
+	return 0, []Number{
+		{
+			Value:  int64(n),
+			Weight: 1,
+		},
+	}
+}
+
+func LexNumberStr(s string) (ops []Evaluable) {
+	n := ``
+	addNum := func() {
+		if len(n) > 0 {
+			num, _ := strconv.ParseInt(n, 10, 64)
+			ops = append(ops, Literal(num))
+			n = ``
+		}
+	}
+	addOp := func(c byte) {
+		ops = append(ops, Operator(c))
+	}
+	for _, c := range s {
+		if c&^rune(0xff) != 0 {
+			continue
+		}
+		if !unicode.IsDigit(c) {
+			addNum()
+			addOp(byte(c))
+		} else {
+			n += string(c)
+		}
+	}
+	addNum()
+	return
+}
diff --git a/test/fakeOrigin/dtp/plugutils.go b/test/fakeOrigin/dtp/plugutils.go
new file mode 100644
index 0000000000..c14b4ddd6e
--- /dev/null
+++ b/test/fakeOrigin/dtp/plugutils.go
@@ -0,0 +1,100 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"errors"
+	"io"
+)
+
+func ReadBlock(
+	pbuf []byte,
+	at int64,
+	max int64,
+	blk_size int64,
+	blk_gen func(ct int64) []byte,
+) (n int, err error) {
+
+	sz := len(pbuf)
+	if (max - at) < int64(sz) {
+		sz = int(max - at)
+	}
+	DebugLogf("Reading at %d/%d%%%d for %d.\n", at, max, blk_size, sz)
+	if sz == 0 {
+		return 0, io.EOF
+	}
+
+	pidx := 0
+
+	blk_ct := at / blk_size
+	blk_off := at % blk_size
+
+	for {
+		blk_buf := blk_gen(blk_ct)
+		blk_len := int64(len(blk_buf))
+
+		DebugLogf("Got block %d: %v\n", blk_ct, blk_buf)
+
+		DebugLogf("Using block %d: %v\n", blk_ct, blk_buf)
+		DebugLogf("Copying into buffer at %d:%d from %d:%d.\n", pidx, sz, blk_off, blk_len)
+		ncopied := copy(pbuf[pidx:sz], blk_buf[blk_off:])
+		DebugLogf("Copied %d bytes into buffer at %d from %d.\n",
+			ncopied, pidx, blk_off)
+		if ncopied == 0 {
+			break
+		}
+		pidx += ncopied
+		DebugLogf("Now at %d/%d\n", pidx, sz)
+		if sz <= pidx {
+			break
+		}
+		blk_off = 0 /* Always zero after the first partial block */
+		blk_ct++
+	}
+	DebugLogf("%d/%d written.\n", at+int64(sz), max)
+	return sz, nil
+}
+
+func NewSeekPosFor(
+	off int64,
+	whence int,
+	posold int64,
+	size int64,
+) (int64, error) {
+
+	var posnew int64
+	switch whence {
+	case io.SeekStart:
+		posnew = off
+	case io.SeekEnd:
+		posnew = size - off
+	case io.SeekCurrent:
+		posnew = posold + off
+		if size < posnew {
+			posnew = size
+		}
+	}
+
+	DebugLogf("Seeking to %d via %d: %d\n", off, whence, posnew)
+	if posnew < 0 {
+		return posold, errors.New(`unable to seek before file`)
+	}
+	return posnew, nil
+}
diff --git a/test/fakeOrigin/dtp/text.go b/test/fakeOrigin/dtp/text.go
new file mode 100644
index 0000000000..985a34f517
--- /dev/null
+++ b/test/fakeOrigin/dtp/text.go
@@ -0,0 +1,86 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"hash/fnv"
+	"io"
+	"net/http"
+	"strconv"
+	"time"
+)
+
+type TextStampSeeker struct {
+	Size int64
+	Pos  int64
+	Seed int64
+	Rnd  int64
+}
+
+func (s *TextStampSeeker) Read(p []byte) (n int, err error) {
+	return ReadBlock(p, s.Pos, s.Size, ReadBlockSize, func(ct int64) []byte {
+		if s.Rnd == 0 {
+			return []byte(fmt.Sprintf("%31d\n", ct+s.Seed))
+		} else {
+			h := fnv.New64()
+			b := make([]byte, 8)
+			binary.LittleEndian.PutUint64(b, uint64(ct))
+			h.Write(b)
+			binary.LittleEndian.PutUint64(b, uint64(s.Seed))
+			h.Write(b)
+			binary.LittleEndian.PutUint64(b, uint64(s.Rnd))
+			h.Write(b)
+			return []byte(fmt.Sprintf("%31d\n", h.Sum64()&^ByteMask))
+		}
+	})
+}
+
+func (s *TextStampSeeker) Seek(off int64, whence int) (int64, error) {
+	var newPos int64
+	switch whence {
+	case io.SeekStart:
+		newPos = off
+	case io.SeekEnd:
+		newPos = s.Size - off
+	case io.SeekCurrent:
+		newPos += off
+		if newPos > s.Size {
+			newPos = s.Size
+		}
+	}
+	DebugLogf("Seeking to %d via %d: %d\n", off, whence, newPos)
+	if newPos < 0 {
+		return s.Pos, errors.New(`unable to seek before file`)
+	}
+	s.Pos = newPos
+	return s.Pos, nil
+}
+
+func TextStamp(w http.ResponseWriter, r *http.Request, reqdat map[string]string) {
+	seed, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+	lastmod, _ := strconv.ParseInt(reqdat[`lm`], 10, 64)
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	DebugLogf("Serving req of size %d.\n", sz)
+	w.Header()[`Content-Type`] = []string{`text/plain`}
+	http.ServeContent(w, r, ``, time.Unix(lastmod, 0), &TextStampSeeker{Size: sz, Seed: lastmod, Rnd: seed})
+}
diff --git a/test/fakeOrigin/dtp/type_bin.go b/test/fakeOrigin/dtp/type_bin.go
new file mode 100644
index 0000000000..77c06547d4
--- /dev/null
+++ b/test/fakeOrigin/dtp/type_bin.go
@@ -0,0 +1,79 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/binary"
+	"hash/fnv"
+	"strconv"
+)
+
+func init() {
+	GlobalGeneratorFuncs["bin"] = NewBinGen
+}
+
+type BinGen struct {
+	Size     int64
+	Pos      int64
+	Seed     int64
+	Rnd      int64
+	BufCache []byte
+}
+
+func (bs *BinGen) ContentType() string {
+	return "application/octet-stream"
+}
+
+func (bs *BinGen) Read(p []byte) (n int, err error) {
+	sz, err := ReadBlock(p, bs.Pos, bs.Size, 8, func(ct int64) []byte {
+		val := ct + bs.Seed
+		if bs.Rnd == 0 {
+			for index := 0; index < 8; index++ {
+				bs.BufCache[index] = byte(val & 0xff)
+				val >>= 8
+			}
+			return bs.BufCache
+		} else {
+			hash := fnv.New64()
+			binary.LittleEndian.PutUint64(bs.BufCache, uint64(val))
+			hash.Write(bs.BufCache)
+			binary.LittleEndian.PutUint64(bs.BufCache, uint64(bs.Rnd))
+			hash.Write(bs.BufCache)
+			return hash.Sum(nil)
+		}
+	})
+
+	bs.Pos += int64(sz)
+	return sz, err
+}
+
+func (s *BinGen) Seek(off int64, whence int) (int64, error) {
+	posnew, err := NewSeekPosFor(off, whence, s.Pos, s.Size)
+	if nil == err {
+		s.Pos = posnew
+	}
+	return s.Pos, nil
+}
+
+func NewBinGen(reqdat map[string]string, lastmod int64) Generator {
+	rnd, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	return &BinGen{Size: sz, Seed: lastmod, Rnd: rnd, BufCache: make([]byte, 8)}
+}
diff --git a/test/fakeOrigin/dtp/type_binf.go b/test/fakeOrigin/dtp/type_binf.go
new file mode 100644
index 0000000000..5c33290eb4
--- /dev/null
+++ b/test/fakeOrigin/dtp/type_binf.go
@@ -0,0 +1,124 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strconv"
+)
+
+func init() {
+	GlobalGeneratorFuncs["binf"] = NewBinFastGen
+}
+
+type xorshift128plusstate struct {
+	s0 uint64
+	s1 uint64
+}
+
+// https://en.wikipedia.org/wiki/Xorshift
+func (state *xorshift128plusstate) xorshift128plus() uint64 {
+	xx := state.s0
+	yy := state.s1
+	state.s0 = yy
+	xx ^= xx << 23                               // a
+	state.s1 = xx ^ yy ^ (xx >> 17) ^ (yy >> 26) // b, c
+	val := state.s1 + yy
+	return val
+}
+
+func (state *xorshift128plusstate) fixup() {
+	if state.s0 == 0 && state.s1 == 0 {
+		state.s0 = 1
+	}
+}
+
+type BinFastGen struct {
+	Size       int64
+	Pos        int64
+	Rnd        int64
+	Blockbytes int64
+}
+
+func (bs *BinFastGen) ContentType() string {
+	return "application/octet-stream"
+}
+
+func (bs *BinFastGen) Read(bufout []byte) (n int, err error) {
+
+	maxbytes := len(bufout)
+	posbeg := bs.Pos
+	posend := posbeg + int64(maxbytes)
+	if bs.Size < posend {
+		posend = bs.Size
+	}
+
+	blocknum := posbeg / bs.Blockbytes
+	posfile := blocknum * bs.Blockbytes
+
+	lenout := posend - posbeg
+	posout := posfile - posbeg
+
+	var state xorshift128plusstate
+
+	for posout < lenout {
+		if (posfile % bs.Blockbytes) == 0 {
+			blocknum = posfile / bs.Blockbytes
+			state.s0 = uint64(blocknum + bs.Rnd)
+			state.s1 = uint64(bs.Blockbytes)
+			state.fixup()
+		}
+
+		nextnum := state.xorshift128plus()
+
+		if posout <= -8 {
+			posout += 8
+		} else {
+			for posn := 0; posn < 8 && posout < lenout; posn++ {
+				if 0 <= posout {
+					bufout[posout] = byte(nextnum & 0xff)
+					bs.Pos++
+				}
+				nextnum >>= 8
+				posout++
+			}
+		}
+		posfile += 8
+	}
+
+	return int(lenout), nil
+}
+
+func (bs *BinFastGen) Seek(off int64, whence int) (int64, error) {
+	posnew, err := NewSeekPosFor(off, whence, bs.Pos, bs.Size)
+	if nil == err {
+		bs.Pos = posnew
+	}
+	return bs.Pos, nil
+}
+
+func NewBinFastGen(reqdat map[string]string, lastmod int64) Generator {
+	rnd, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	bs, _ := strconv.ParseInt(reqdat[`bs`], 10, 64)
+	if bs == 0 || (bs%8) != 0 {
+		bs = 1024
+	}
+	return &BinFastGen{Size: sz, Rnd: rnd, Blockbytes: bs}
+}
diff --git a/test/fakeOrigin/dtp/type_gen3s.go b/test/fakeOrigin/dtp/type_gen3s.go
new file mode 100644
index 0000000000..10456ef9a2
--- /dev/null
+++ b/test/fakeOrigin/dtp/type_gen3s.go
@@ -0,0 +1,79 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"strconv"
+	"sync"
+)
+
+func init() {
+	GlobalGeneratorFuncs["gen3s"] = NewGen3sGen
+}
+
+var Buf3Cache []byte
+var Buf3Mutex sync.Mutex
+
+type Gen3sGen struct {
+	Size int64
+	Pos  int64
+}
+
+func (s *Gen3sGen) ContentType() string {
+	return "text/plain"
+}
+
+func (s *Gen3sGen) Read(p []byte) (n int, err error) {
+	plen := len(p)
+
+	Buf3Mutex.Lock()
+
+	// we could be more clever about this lock but we
+	// expect it to only be hit a couple of times
+	// as 32k seems to be the observed max
+	if len(Buf3Cache) < plen {
+		Buf3Cache = make([]byte, plen)
+		for index := range Buf3Cache {
+			Buf3Cache[index] = byte('3')
+		}
+	}
+
+	cbuf := Buf3Cache
+
+	Buf3Mutex.Unlock()
+
+	copy(p, cbuf[:plen])
+	s.Pos += int64(plen)
+
+	return plen, nil
+}
+
+func (s *Gen3sGen) Seek(off int64, whence int) (int64, error) {
+	posnew, err := NewSeekPosFor(off, whence, s.Pos, s.Size)
+	if nil == err {
+		s.Pos = posnew
+	}
+	return s.Pos, nil
+}
+
+func NewGen3sGen(reqdat map[string]string, lastmod int64) Generator {
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	return &Gen3sGen{Size: sz}
+}
diff --git a/test/fakeOrigin/endpoint/endpoint_enums.go b/test/fakeOrigin/dtp/type_hijack.go
similarity index 50%
copy from test/fakeOrigin/endpoint/endpoint_enums.go
copy to test/fakeOrigin/dtp/type_hijack.go
index 8da24c2e81..32c379e6f0 100644
--- a/test/fakeOrigin/endpoint/endpoint_enums.go
+++ b/test/fakeOrigin/dtp/type_hijack.go
@@ -1,4 +1,4 @@
-package endpoint
+package dtp
 
 /*
  * Licensed to the Apache Software Foundation (ASF) under one
@@ -19,36 +19,40 @@ package endpoint
  * under the License.
  */
 
-//go:generate jsonenums -type=Type
+import (
+	"encoding/base64"
+	"net/http"
+)
 
-// Type models the supported types of endpoints
-type Type int
+func init() {
+	GlobalHandlerFuncs["hijack"] = Hijack
+}
 
-// Type models the supported types of endpoints
-const (
-	InvalidType Type = iota + 1
-	Vod
-	Live
-	Event
-	Static
-	Dir
-)
+func Hijack(w http.ResponseWriter, r *http.Request, reqdat map[string]string) {
+	hj, ok := w.(http.Hijacker)
+	if !ok {
+		http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
+		DebugLogf("Can't get a hijack\n")
+		return
+	}
+
+	conn, buf, err := hj.Hijack()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		DebugLogf("hijack failed: %s\n", err)
+		return
+	}
+
+	DebugLogf("Connection hijacked\n")
 
-func (e Type) String() string {
-	switch e {
-	case InvalidType:
-		return "invalid type"
-	case Vod:
-		return "vod"
-	case Live:
-		return "live"
-	case Event:
-		return "event"
-	case Static:
-		return "static"
-	case Dir:
-		return "dir"
-	default:
-		return "invalid type"
+	if str, ok := reqdat["payload"]; ok {
+		buf.WriteString(str)
+	} else if encoded, ok := reqdat["payload64"]; ok {
+		data, err := base64.StdEncoding.DecodeString(encoded)
+		if err == nil {
+			buf.Write(data)
+		}
 	}
+	buf.Flush()
+	conn.Close()
 }
diff --git a/test/fakeOrigin/dtp/type_tex.go b/test/fakeOrigin/dtp/type_tex.go
new file mode 100644
index 0000000000..b651fb3f7f
--- /dev/null
+++ b/test/fakeOrigin/dtp/type_tex.go
@@ -0,0 +1,181 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/base64"
+	"fmt"
+	"image/color"
+	"image/png"
+	"io"
+	"strconv"
+	"strings"
+	"sync"
+)
+
+// repeated buffer from a tilable 64x64 plasma png texture
+const texturebytes = `
+iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAAAAACPAi4CAAAG50lEQVRYw4XWS6ok23UA0P
+07nziRmffWKwmB8AAMAmOBEAb3DEIN2x231dOgjGfgmVk8eKqqzIw43723hxCjWAv/N3xz
+nrK3JWc6font6zP6oc9PC7dYofzuvzfhI1Gs78fPxUv4dVhhS+M3viG2r1O+PnPviT2Qlm
+Fc72Mtosin3Ag/6SP8k58NMhzhAV96qI9128awz/6lUoks9N6ph+BN+lAW1p9bvqG19cm2
+5czcv5ZHfExbKQ95hPvbW7gVb/odoD2q8F+njDEZIJTXqvOJbBJsId8iTI3BJFocBvlskW
+T4xwgAeRF6yVweS170bT3ek1z+Lr/MpwVeirc8Ywqw9Yw43uZYDIgmovI7v9B9iY11339J
+JOvsd3V9Bnx553Juy8dDiQJz5qK11aVhP3gE1K2Rue5wbFOQINj3r0ya9jXDWsDUHBRPBR
+8Y3AOxUY/wCHQc4ut5cDVlbvbcfRcP4fkz/QDpt0Xqg/1cUFYiNZY4RtyDKrxvqDqmaY2K
+yU7YNNL056Ol+6SnVAwUHDB7buMc/l4dQ3DqI+DCBk0W5vOOcbulsg2S9DSFPuOIP17P5q
+ip878Per/glAm7huoLnIGBOrEvGE0V1jBYSikMdz59i6MVM5uV110jfa9/Q3JfHQ9cOhWm
+m8rkWVnnsfEeFAlpdD9L2k5v7xa3DvWZKf1KbpP/85U1zDbmeVJfbpMVJwBHmIPCLjCi1d
+6CDgQ0HnEGDkrEmkuj9xCkGWulM8sannU9kGXyMZlX812tyvcs0fxefVIdCUmYcOb1yJ6f
+JUjNBmMovmN3tyTiuEixjxQwU3WzeLjxUDJeLXkxia1AfyBxRx4EzzENcdHyBckCVAg5MZ
+XN81gwOg3AOGQ6+hSkt45VjjGJ19F8qVQ7MwDpwsjUMi6eAVY5OodD/I0LcA3YF+LbNYAm
+dGzBt7fLhu5I3td5nGumzeOgBBgDj/xMcEBMnA6iCghjs8oYAoeREVkzcgP6Ps/Bf5rBfK
+0eqKCHJT5zoSXHyDqHf4/pqBItsa/KMacFK5gOpibaQXkTxAVjqU48M7PiVPc2jI3GnY9f
+m4rwse5rJLDcVsIzo3qDqNxuxC/+QyLTYRBN1W1BNkF0UKMtaupUywxOKlMcYHR0nxFXZZ
+UROx+T7q9JTM200Tl6UkxufThve1nU9sqvFDaJ+qUnmEmWpHvvKxwLjTbnTYbXNC2PlyzA
+0PDMc81iNWDZv6vfOkl2dHjFEdx3B8N48iJ/3mYzSkIzNvBxErQFBMyn2+3knb+JCvhBui
+m3dZu6GzarGcHiS0KNr7uQHfzPMdI8Gy8QZFCcFnDOGc1HQUPw+4vashliF7YRVoChBIAQ
+USCq3F8vNOHAorpxfks6N5mPdMIjTG/w5aN/xy8HnyMkcHD9Ib5W0oDKeuYuP0xJJsUxU9
+Y40KaNGHqhHKTkdyywMsNHjcRk+gJdi0fCBYm8R0r8+0q++n2KqeUZMCFpcNz6VIasP9ER
+k62FYTKa6kTMhlWOEtxUK/Hv4FwWAFDQexgacBHcPyk6ofE9nRFDEAN3lNZ9ukbFAdSm2D
+bz5D/WsCggonXxXpNn3GDTkM4Pu+9DytxToAKwjST8Js397kK2k0316fyPgELkQ3XN9+oT
+xSyVADHQdquPra40M+jMJOkM1gEkhYrJLWoAc/4XRCPqtlYYo6ydM69Pt5uVxSsmkNmCde
+6Ou/GkXvg2W7SU09poSyLSELFlda+6nlsrvva6k1LhKJk39Klux2dLVYessisAppM8COvm
+lf7vNGZT2tA+aPPM+QGMsd3XLclbzm/Sxhxk9oKxwxFf2yyfFo7o+ctv8/YP/D/1p0hQQp
+UIUcP4pFs2sw97NEtYwq2DjShMZHGN2e/WpkGKFffAe/lKV/5f/YH/68L/ePEHufLfL/4g
+zwv/7eIPdOX/1R+ELvzHiz/Qlf9Xf+D/uPDfLv5AV/5f/UGW3fzHaLGftEhvrzQC8poBjx
+Lf+bD83Hroc6xzlrkAVmpxJuTgp8YkLO8L/6/+IPXCf7v4A135f/UHOS/8v/oDXfl/9Qf+
+84X/dvEH8Qv/48Uf+F8v/O8Xf6Ar/6/+IHzh/9UfxC78Lxd/4H+78P+8+IN8XPj/q4s/yJ
+X/V38gfoedtOGGBE7JUArGU2sQmKv/mOMA6+22Ao0eOknmZAgAejx7z+kn6QlEVjMTlDYI
+XBEYZnxL8a1uo+ObKEyEpPreThA01nuBe4Nh/CXxXy78l4s/yDDsu08vdpeWbk+2yOUb7u
+rR2MwreETXhaBnpaV7cM+RgD/xVs6PLHk+SxvBJt377Sy1rNxa6ZG93i34kulK0N08tnXX
+jMizqD70VuFIpPJx4b9e/OH/AaPCnUo/dApAAAAAAElFTkSuQmCC
+`
+
+func texturePng() io.Reader {
+	return base64.NewDecoder(base64.StdEncoding, strings.NewReader(texturebytes))
+}
+
+var Texture []byte
+
+func init() {
+
+	img, err := png.Decode(texturePng())
+	if nil != err {
+		fmt.Println("tex: error decoding texture", err)
+		return
+	}
+
+	bounds := img.Bounds()
+	size := bounds.Size()
+	area := size.X * size.Y
+	if 0 < area {
+		Texture = make([]byte, area)
+
+		tindex := 0
+		for row := bounds.Min.Y; row < bounds.Max.Y; row++ {
+			for col := bounds.Min.X; col < bounds.Max.X; col++ {
+				pgray := img.At(row, col).(color.Gray)
+				Texture[tindex] = byte(pgray.Y)
+				tindex++
+			}
+		}
+
+		GlobalGeneratorFuncs["tex"] = NewTexGen
+	}
+}
+
+var TextureCache []byte
+var TextureCap int
+var TexMutex sync.Mutex
+
+type TexGen struct {
+	Size int64
+	Pos  int64
+	Rnd  int64
+}
+
+func (s *TexGen) ContentType() string {
+	return "application/octet-stream"
+}
+
+func (s *TexGen) Read(pbuf []byte) (n int, err error) {
+	plen := len(pbuf)
+	var leftover int = int(s.Size - s.Pos)
+
+	if leftover < plen {
+		plen = leftover
+	}
+
+	// fix up the texture cache
+	TexMutex.Lock()
+
+	texlen := len(Texture)
+
+	// we could be more clever about this lock but we
+	// expect it to only be hit a couple of times
+	// as 32k seems to be the observed max
+	if TextureCap < plen {
+		numtextures := plen / texlen
+		if (plen % texlen) != 0 {
+			numtextures++
+		}
+
+		// reset the texture capacity
+		TextureCap = numtextures * texlen
+
+		// pad the number required to handle any offset
+		numtextures++
+
+		// create new texture cache buffer
+		tcsize := numtextures * texlen
+		TextureCache = make([]byte, tcsize)
+
+		for indexb := 0; indexb < tcsize; indexb += texlen {
+			copy(TextureCache[indexb:indexb+texlen], Texture)
+		}
+	}
+
+	cbuf := TextureCache
+
+	TexMutex.Unlock()
+
+	offset := ((s.Pos + s.Rnd) % int64(texlen))
+
+	copy(pbuf, cbuf[offset:offset+int64(plen)])
+
+	s.Pos += int64(plen)
+
+	return plen, nil
+}
+
+func (s *TexGen) Seek(off int64, whence int) (int64, error) {
+	posnew, err := NewSeekPosFor(off, whence, s.Pos, s.Size)
+	if nil == err {
+		s.Pos = posnew
+	}
+	return s.Pos, nil
+}
+
+func NewTexGen(reqdat map[string]string, latmod int64) Generator {
+	seed, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	return &TexGen{Size: sz, Rnd: seed}
+}
diff --git a/test/fakeOrigin/dtp/type_txt.go b/test/fakeOrigin/dtp/type_txt.go
new file mode 100644
index 0000000000..685fa03c7f
--- /dev/null
+++ b/test/fakeOrigin/dtp/type_txt.go
@@ -0,0 +1,77 @@
+package dtp
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+	"encoding/binary"
+	"fmt"
+	"hash/fnv"
+	"strconv"
+)
+
+func init() {
+	GlobalGeneratorFuncs[`txt`] = NewTxtGen
+}
+
+type TxtGen struct {
+	Size int64
+	Pos  int64
+	Seed int64
+	Rnd  int64
+}
+
+func (s *TxtGen) ContentType() string {
+	return "text/plain"
+}
+
+func (s *TxtGen) Read(p []byte) (n int, err error) {
+	sz, err := ReadBlock(p, s.Pos, s.Size, ReadBlockSize, func(ct int64) []byte {
+		if s.Rnd == 0 {
+			return []byte(fmt.Sprintf("%31d\n", ct+s.Seed))
+		} else {
+			h := fnv.New64()
+			b := make([]byte, 8)
+			binary.LittleEndian.PutUint64(b, uint64(ct))
+			h.Write(b)
+			binary.LittleEndian.PutUint64(b, uint64(s.Seed))
+			h.Write(b)
+			binary.LittleEndian.PutUint64(b, uint64(s.Rnd))
+			h.Write(b)
+			return []byte(fmt.Sprintf("%31d\n", h.Sum64()&^ByteMask))
+		}
+	})
+
+	s.Pos += int64(sz)
+	return sz, err
+}
+
+func (s *TxtGen) Seek(off int64, whence int) (int64, error) {
+	posnew, err := NewSeekPosFor(off, whence, s.Pos, s.Size)
+	if nil == err {
+		s.Pos = posnew
+	}
+	return s.Pos, nil
+}
+
+func NewTxtGen(reqdat map[string]string, lastmod int64) Generator {
+	seed, _ := strconv.ParseInt(reqdat[`rnd`], 10, 64)
+	sz, _ := strconv.ParseInt(reqdat[`sz`], 10, 64)
+	return &TxtGen{Size: sz, Seed: lastmod, Rnd: seed}
+}
diff --git a/test/fakeOrigin/endpoint/endpoint.go b/test/fakeOrigin/endpoint/endpoint.go
index 14b4341598..948d145797 100644
--- a/test/fakeOrigin/endpoint/endpoint.go
+++ b/test/fakeOrigin/endpoint/endpoint.go
@@ -26,6 +26,7 @@ import (
 	"io/ioutil"
 	"path/filepath"
 	"strings"
+	"time"
 )
 
 // DefaultConfigFile is the default configuration file path
@@ -42,25 +43,34 @@ const DefaultHTTPSCertFile = "server.cert"
 
 // ServerInfo contains relevant info for serving content
 type ServerInfo struct {
-	HTTPListeningPort  int    `json:"http_port"`
-	HTTPSListeningPort int    `json:"https_port"`
-	SSLCert            string `json:"ssl_cert"`
-	SSLKey             string `json:"ssl_key"`
-	BindingAddress     string `json:"binding_address"`
-	CrossdomainFile    string `json:"crossdomain_xml_file"`
+	HTTPListeningPort  int           `json:"http_port"`
+	HTTPSListeningPort int           `json:"https_port"`
+	SSLCert            string        `json:"ssl_cert"`
+	SSLKey             string        `json:"ssl_key"`
+	BindingAddress     string        `json:"binding_address"`
+	CrossdomainFile    string        `json:"crossdomain_xml_file"`
+	ReadTimeout        time.Duration `json:"read_timeout"`
+	WriteTimeout       time.Duration `json:"write_timeout"`
 }
 
 // Endpoint defines all kinds of endpoints to be served
 type Endpoint struct {
 	ID              string              `json:"id"`
 	DiskID          string              `json:"override_disk_id,omitempty"`
-	Source          string              `json:"source"`
-	OutputDirectory string              `json:"outputdir,omitifempty"`
+	Source          string              `json:"source,omitempty"`
+	OutputDirectory string              `json:"outputdir,omitempty"`
 	EndpointType    Type                `json:"type"`
 	ManualCommand   []string            `json:"manual_command,omitempty"`
 	DefaultHeaders  map[string][]string `json:"default_headers,omitempty"`
 	NoCache         bool                `json:"no_cache,omitempty"`
 	ABRManifests    []string            `json:"abr_manifests,omitempty"`
+
+	// Testing endpoint specific config
+	LogReqHeaders  bool          `json:"log_request_headers,omitempty"`
+	LogRespHeaders bool          `json:"log_response_headers,omitempty"`
+	StallDuration  time.Duration `json:"stall_duration,omitempty"`
+	EnablePprof    bool          `json:"enable_pprof,omitempty"`
+	EnableDebug    bool          `json:"enable_debug,omitempty"`
 }
 
 // Config defines the application configuration
@@ -130,14 +140,16 @@ func ProcessConfig(out Config) (Config, error) {
 	for i := range out.Endpoints {
 		var err error
 		// Resolve relative paths to absolute paths
-		out.Endpoints[i].Source, err = filepath.Abs(out.Endpoints[i].Source)
-		if err != nil {
-			return Config{}, errors.New("resolving relative path: " + err.Error())
-		}
-		out.Endpoints[i].OutputDirectory, _ = filepath.Abs(out.Endpoints[i].OutputDirectory)
+		if out.Endpoints[i].EndpointType != Testing {
+			out.Endpoints[i].Source, err = filepath.Abs(out.Endpoints[i].Source)
+			if err != nil {
+				return Config{}, errors.New("resolving relative path: " + err.Error())
+			}
+			out.Endpoints[i].OutputDirectory, _ = filepath.Abs(out.Endpoints[i].OutputDirectory)
 
-		if out.Endpoints[i].OutputDirectory == "" {
-			out.Endpoints[i].OutputDirectory = DefaultOutputDirectory
+			if out.Endpoints[i].OutputDirectory == "" && out.Endpoints[i].EndpointType != Testing {
+				out.Endpoints[i].OutputDirectory = DefaultOutputDirectory
+			}
 		}
 		if out.Endpoints[i].DiskID == "" {
 			out.Endpoints[i].DiskID = out.Endpoints[i].ID
@@ -171,17 +183,17 @@ func LoadAndGenerateDefaultConfig(path string) (Config, error) {
 			//	return out, errors.New("Manual commands must include the %MASTERMANIFEST% token")
 			//}
 			if !contains(ep.ManualCommand, `%OUTPUTDIRECTORY%`) {
-				return out, errors.New(`Manual commands must include the %OUTPUTDIRECTORY% token`)
+				return out, errors.New(`manual commands must include the %OUTPUTDIRECTORY% token`)
 			}
 			if !contains(ep.ManualCommand, `%SOURCE%`) {
-				return out, errors.New(`Manual commands must include the %SOURCE% token`)
+				return out, errors.New(`manual commands must include the %SOURCE% token`)
 			}
 		}
 		if len(ep.ABRManifests) > 0 {
-			return out, errors.New("Paths of ABR Layer manifests must not be set via configuration")
+			return out, errors.New("paths of ABR Layer manifests must not be set via configuration")
 		}
 	}
-	out, err = ProcessConfig(out)
+	out, _ = ProcessConfig(out)
 	if err = WriteConfig(out, path); err != nil {
 		return out, errors.New("processing config file: " + err.Error())
 	}
diff --git a/test/fakeOrigin/endpoint/endpoint_enums.go b/test/fakeOrigin/endpoint/endpoint_enums.go
index 8da24c2e81..f85a326deb 100644
--- a/test/fakeOrigin/endpoint/endpoint_enums.go
+++ b/test/fakeOrigin/endpoint/endpoint_enums.go
@@ -32,6 +32,7 @@ const (
 	Event
 	Static
 	Dir
+	Testing
 )
 
 func (e Type) String() string {
@@ -48,6 +49,8 @@ func (e Type) String() string {
 		return "static"
 	case Dir:
 		return "dir"
+	case Testing:
+		return "testing"
 	default:
 		return "invalid type"
 	}
diff --git a/test/fakeOrigin/endpoint/type_jsonenums.go b/test/fakeOrigin/endpoint/type_jsonenums.go
index e251c8d307..3cdf5464fc 100644
--- a/test/fakeOrigin/endpoint/type_jsonenums.go
+++ b/test/fakeOrigin/endpoint/type_jsonenums.go
@@ -34,6 +34,7 @@ var (
 		"Event":       Event,
 		"Static":      Static,
 		"Dir":         Dir,
+		"Testing":     Testing,
 	}
 
 	_TypeValueToName = map[Type]string{
@@ -43,6 +44,7 @@ var (
 		Event:       "Event",
 		Static:      "Static",
 		Dir:         "Dir",
+		Testing:     "Testing",
 	}
 )
 
@@ -56,6 +58,7 @@ func init() {
 			interface{}(Event).(fmt.Stringer).String():       Event,
 			interface{}(Static).(fmt.Stringer).String():      Static,
 			interface{}(Dir).(fmt.Stringer).String():         Dir,
+			interface{}(Testing).(fmt.Stringer).String():     Testing,
 		}
 	}
 }
diff --git a/test/fakeOrigin/httpService/filter.go b/test/fakeOrigin/httpService/filter.go
index e2379a64da..af7869aa8a 100644
--- a/test/fakeOrigin/httpService/filter.go
+++ b/test/fakeOrigin/httpService/filter.go
@@ -101,7 +101,7 @@ func GenerateETag(source string) string {
 	return fmt.Sprintf("\"%d\"", h.Sum32())
 }
 
-func log(handler http.Handler) http.Handler {
+func logfo(handler http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		startTime := time.Now()
 		iw := &BodyInterceptor{w: w}
@@ -126,10 +126,11 @@ func log(handler http.Handler) http.Handler {
 		ims := strings.Replace(r.Header.Get("If-Modified-Since"), `"`, `\"`, -1)
 		ius := strings.Replace(r.Header.Get("If-Unmodified-Since"), `"`, `\"`, -1)
 		ir := strings.Replace(r.Header.Get("If-Range"), `"`, `\"`, -1)
-
+		fmt.Println("hit in log handler")
 		fmt.Printf("%s - [%s] \"%s %s %s\" %d %d %d \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\" \"%s\"\n", remoteAddr, finishTS, method, rURI, proto, rc, size, dur, refer, uas, im, inm, ims, ius, ir)
 	})
 }
+
 func strictTransportSecurity(handler http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		iw := &BodyInterceptor{w: w}
diff --git a/test/fakeOrigin/httpService/httpService.go b/test/fakeOrigin/httpService/httpService.go
index 5b1dd2c830..961f4b3d2a 100644
--- a/test/fakeOrigin/httpService/httpService.go
+++ b/test/fakeOrigin/httpService/httpService.go
@@ -25,7 +25,9 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"log"
 	"net/http"
+	"net/http/pprof"
 	"os"
 	"path"
 	"path/filepath"
@@ -33,6 +35,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/apache/trafficcontrol/test/fakeOrigin/dtp"
 	"github.com/apache/trafficcontrol/test/fakeOrigin/endpoint"
 	"github.com/apache/trafficcontrol/test/fakeOrigin/transcode"
 )
@@ -58,6 +61,13 @@ func GetRoutes(cfg endpoint.Config) (map[string]EndpointRoutes, error) {
 			allRoutes[ep.ID] = routes
 			continue
 		}
+
+		if ep.EndpointType == endpoint.Testing {
+			routes.MasterPath = path.Join("/", ep.ID)
+			allRoutes[ep.ID] = routes
+			continue
+		}
+
 		if ep.EndpointType == endpoint.Dir {
 			fileList := []string{}
 			err := filepath.Walk(ep.Source, func(path string, f os.FileInfo, err error) error {
@@ -152,12 +162,41 @@ func registerRoute(mux *http.ServeMux, e endpoint.Endpoint, httpPath string, isL
 			return errors.New("creating handler '" + httpPath + "': " + err.Error())
 		}
 		if isSSL {
-			mux.Handle(httpPath, log(strictTransportSecurity(originHeaderManipulation(h))))
-			mux.Handle(httpPath+"/", log(strictTransportSecurity(originHeaderManipulation(h))))
+			mux.Handle(httpPath, logfo(strictTransportSecurity(originHeaderManipulation(h))))
+			mux.Handle(httpPath+"/", logfo(strictTransportSecurity(originHeaderManipulation(h))))
 		} else {
-			mux.Handle(httpPath, log(originHeaderManipulation(h)))
-			mux.Handle(httpPath+"/", log(originHeaderManipulation(h)))
+			mux.Handle(httpPath, logfo(originHeaderManipulation(h)))
+			mux.Handle(httpPath+"/", logfo(originHeaderManipulation(h)))
+		}
+		fmt.Println("registered for static endpoint logfo for path: ", httpPath)
+		return nil
+	}
+
+	if e.EndpointType == endpoint.Testing {
+		alog := log.New(os.Stderr, "", 0)
+		dtpHandler := dtp.NewDTPHandler()
+
+		dtp.GlobalConfig.Log.RequestHeaders = e.LogReqHeaders
+		dtp.GlobalConfig.Log.ResponseHeaders = e.LogRespHeaders
+		dtp.GlobalConfig.StallDuration = e.StallDuration * time.Second
+		dtp.GlobalConfig.Debug = e.EnableDebug
+		dtp.GlobalConfig.EnablePprof = e.EnablePprof
+
+		// General DTP endpoints for testing
+		mux.Handle("/"+e.ID, dtp.Logger(alog, dtpHandler))
+		mux.Handle("/"+e.ID+"/", dtp.Logger(alog, dtpHandler))
+
+		// DTP endpoints for pprof output
+		if dtp.GlobalConfig.EnablePprof {
+			mux.HandleFunc("/"+e.ID+"/debug/pprof/", pprof.Index)
+			mux.HandleFunc("/"+e.ID+"/debug/pprof/cmdline", pprof.Cmdline)
+			mux.HandleFunc("/"+e.ID+"/debug/pprof/profile", pprof.Profile)
+			mux.HandleFunc("/"+e.ID+"/debug/pprof/symbol", pprof.Symbol)
+			mux.HandleFunc("/"+e.ID+"/debug/pprof/trace", pprof.Trace)
 		}
+
+		// DTP endpoint for setting various config values on the fly
+		mux.HandleFunc("/"+e.ID+"/config", dtp.ConfigHandler)
 		return nil
 	}
 
@@ -187,11 +226,11 @@ func registerRoute(mux *http.ServeMux, e endpoint.Endpoint, httpPath string, isL
 		return errors.New("registering route '" + httpPath + "': " + err.Error())
 	}
 	if isSSL {
-		mux.Handle(httpPath, log(strictTransportSecurity(originHeaderManipulation(cacheOptimization(h, startTime, ep)))))
-		mux.Handle(httpPath+"/", log(strictTransportSecurity(originHeaderManipulation(cacheOptimization(h, startTime, ep)))))
+		mux.Handle(httpPath, logfo(strictTransportSecurity(originHeaderManipulation(cacheOptimization(h, startTime, ep)))))
+		mux.Handle(httpPath+"/", logfo(strictTransportSecurity(originHeaderManipulation(cacheOptimization(h, startTime, ep)))))
 	} else {
-		mux.Handle(httpPath, log(originHeaderManipulation(cacheOptimization(h, startTime, ep))))
-		mux.Handle(httpPath+"/", log(originHeaderManipulation(cacheOptimization(h, startTime, ep))))
+		mux.Handle(httpPath, logfo(originHeaderManipulation(cacheOptimization(h, startTime, ep))))
+		mux.Handle(httpPath+"/", logfo(originHeaderManipulation(cacheOptimization(h, startTime, ep))))
 	}
 	return nil
 }
@@ -202,7 +241,13 @@ func registerRoutes(mux *http.ServeMux, conf endpoint.Config, routes map[string]
 		if !ok {
 			return errors.New("no routes found for endpoint '" + e.ID + "'")
 		}
-		if endpointRoutes.MasterPath != "" {
+		if e.EndpointType == endpoint.Testing {
+			err := registerRoute(mux, e, e.ID, false, ContentTypeJSON, isSSL)
+			if err != nil {
+				return errors.New("error registering endpoint '" + e.ID + "': " + err.Error())
+			}
+		}
+		if endpointRoutes.MasterPath != "" && e.EndpointType != endpoint.Testing {
 			err := registerRoute(mux, e, endpointRoutes.MasterPath, e.EndpointType == endpoint.Live && !endpointRoutes.IsABR, ContentTypeM3U8, isSSL)
 			if err != nil {
 				return errors.New("Error registering endpoint '" + e.ID + "': " + err.Error())
@@ -227,8 +272,8 @@ func registerRoutes(mux *http.ServeMux, conf endpoint.Config, routes map[string]
 			}
 		}
 	}
-	mux.Handle("/crossdomain.xml", log(crossdomainHandler(conf.ServerConf.CrossdomainFile)))
-	mux.Handle("/", log(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	mux.Handle("/crossdomain.xml", logfo(crossdomainHandler(conf.ServerConf.CrossdomainFile)))
+	mux.Handle("/", logfo(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		w.WriteHeader(http.StatusNotFound)
 		w.Write([]byte(http.StatusText(http.StatusNotFound)))
 	})))
@@ -242,7 +287,13 @@ func StartHTTPListener(conf endpoint.Config, routes map[string]EndpointRoutes) e
 		return errors.New("registering routes: " + err.Error())
 	}
 	fmt.Println("Serving HTTP on " + conf.ServerConf.BindingAddress + ":" + strconv.Itoa(conf.ServerConf.HTTPListeningPort))
-	return http.ListenAndServe(conf.ServerConf.BindingAddress+":"+strconv.Itoa(conf.ServerConf.HTTPListeningPort), mux)
+	srv := &http.Server{
+		Addr:         conf.ServerConf.BindingAddress + ":" + strconv.Itoa(conf.ServerConf.HTTPListeningPort),
+		Handler:      mux,
+		ReadTimeout:  conf.ServerConf.ReadTimeout * time.Second,
+		WriteTimeout: conf.ServerConf.ReadTimeout * time.Second,
+	}
+	return srv.ListenAndServe()
 }
 
 // StartHTTPSListener kicks off the HTTPS stack
@@ -271,6 +322,8 @@ func StartHTTPSListener(conf endpoint.Config, routes map[string]EndpointRoutes)
 		Handler:      mux,
 		TLSConfig:    cfg,
 		TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
+		ReadTimeout:  conf.ServerConf.ReadTimeout * time.Second,
+		WriteTimeout: conf.ServerConf.WriteTimeout * time.Second,
 	}
 	fmt.Println("Serving HTTPS on " + conf.ServerConf.BindingAddress + ":" + strconv.Itoa(conf.ServerConf.HTTPSListeningPort))
 	return srv.ListenAndServeTLS(conf.ServerConf.SSLCert, conf.ServerConf.SSLKey)
diff --git a/test/fakeOrigin/httpService/rangerequest.go b/test/fakeOrigin/httpService/rangerequest.go
index 6d330f3408..d8bf1ee64d 100644
--- a/test/fakeOrigin/httpService/rangerequest.go
+++ b/test/fakeOrigin/httpService/rangerequest.go
@@ -107,7 +107,7 @@ func clipToRange(ranges []httpRange, obody []byte, contentHeader string) ([]byte
 	// this also means we have to start dealing with slice string values
 
 	if len(ranges) == 0 {
-		return nil, nil, errors.New("No ranges supplied")
+		return nil, nil, errors.New("no ranges supplied")
 	} else if len(ranges) == 1 {
 		// single part ranges
 		r := ranges[0]
diff --git a/test/fakeOrigin/version.go b/test/fakeOrigin/version.go
index 33afdd8b91..b1b951872f 100644
--- a/test/fakeOrigin/version.go
+++ b/test/fakeOrigin/version.go
@@ -20,4 +20,4 @@ package main
  */
 
 // Version is the current version of the app, in string form.
-var Version = "0.2.0"
+var Version = "0.3.0"