You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficserver.apache.org by zw...@apache.org on 2020/09/28 19:50:18 UTC

[trafficserver] branch 9.0.x updated (8bc4d63 -> d6a5be1)

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

zwoop pushed a change to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git.


    from 8bc4d63  Follow redirection responses when refreshing stale cache objects. (#7213)
     new 4a566be  Adds new plugin: statichit (#7173)
     new d6a5be1  Docs cleanup (#7210)

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 doc/admin-guide/files/records.config.en.rst        |   4 +-
 doc/admin-guide/plugins/access_control.en.rst      |  20 +-
 .../cache-architecture/architecture.en.rst         |   4 +-
 plugins/Makefile.am                                |   1 +
 .../statichit}/Makefile.inc                        |   6 +-
 plugins/experimental/statichit/README.md           |  38 ++
 plugins/experimental/statichit/statichit.cc        | 637 +++++++++++++++++++++
 7 files changed, 693 insertions(+), 17 deletions(-)
 copy plugins/{escalate => experimental/statichit}/Makefile.inc (84%)
 create mode 100644 plugins/experimental/statichit/README.md
 create mode 100644 plugins/experimental/statichit/statichit.cc


[trafficserver] 02/02: Docs cleanup (#7210)

Posted by zw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit d6a5be13dca637d5e0a8d2a1f129e3db421a3613
Author: Randall Meyer <rr...@apache.org>
AuthorDate: Mon Sep 21 12:36:38 2020 -0700

    Docs cleanup (#7210)
    
    Remove references to ATS 2.x and 4.x, update examples for logging to YAML
    
    (cherry picked from commit b8eb8104937494bdc8a802e95ead29511b556a1d)
---
 doc/admin-guide/files/records.config.en.rst          |  4 ++--
 doc/admin-guide/plugins/access_control.en.rst        | 20 ++++++++++----------
 .../cache-architecture/architecture.en.rst           |  4 ++--
 plugins/experimental/statichit/statichit.cc          |  2 +-
 4 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/doc/admin-guide/files/records.config.en.rst b/doc/admin-guide/files/records.config.en.rst
index ca5e945..96ba3bc 100644
--- a/doc/admin-guide/files/records.config.en.rst
+++ b/doc/admin-guide/files/records.config.en.rst
@@ -537,10 +537,10 @@ Local Manager
    process as, which also has the effect of setting ownership of configuration
    and log files.
 
-   As of version 2.1.1 if the user_id is prefixed with pound character (``#``)
+   If the user_id is prefixed with pound character (``#``),
    the remainder of the string is considered to be a
    `numeric user identifier <http://en.wikipedia.org/wiki/User_identifier>`_.
-   If the value is set to ``#-1`` |TS| will not change the user during startup.
+   If the value is set to ``#-1``, |TS| will not change the user during startup.
 
    .. important::
 
diff --git a/doc/admin-guide/plugins/access_control.en.rst b/doc/admin-guide/plugins/access_control.en.rst
index d06d5ef..8fec83b 100644
--- a/doc/admin-guide/plugins/access_control.en.rst
+++ b/doc/admin-guide/plugins/access_control.en.rst
@@ -379,16 +379,16 @@ Configuration files
 
 * Format the ``access_control.log``
 
-.. code-block:: bash
-
-  access_control_format = format {
-    Format = '%<cqtq> sub=%<{@TokenSubject}cqh> tid=%<{@TokenId}cqh> status=%<{@TokenStatus}cqh> cache=%<{x-cache}psh> key=%<{x-cache-key}psh>'  }
-
-  log.ascii {
-    Filename = 'access_control',
-    Format = access_control_format
-  }
-
+.. code-block:: yaml
+
+   logging:
+     formats:
+     - format: '%<cqtq> sub=%<{@TokenSubject}cqh> tid=%<{@TokenId}cqh> status=%<{@TokenStatus}cqh> cache=%<{x-cache}psh> key=%<{x-cache-key}psh>'
+       name: access_control_format
+     logs:
+     - filename: access_control
+       format: access_control_format
+       mode: ascii
 
 * X-Debug plugin added to ``plugin.config``
 
diff --git a/doc/developer-guide/cache-architecture/architecture.en.rst b/doc/developer-guide/cache-architecture/architecture.en.rst
index 0f04102..2bc3463 100644
--- a/doc/developer-guide/cache-architecture/architecture.en.rst
+++ b/doc/developer-guide/cache-architecture/architecture.en.rst
@@ -95,8 +95,8 @@ assigned to a stripe (and in turn to a cache volume) automatically based on a
 hash of the URI used to retrieve the object from the :term:`origin server`. It
 is possible to configure this to a limited extent in :file:`hosting.config`,
 which supports content from specific hosts or domain to be stored on specific
-cache volumes. As of version 4.0.1 it is also possible to control which cache
-spans (and hence, which cache stripes) are contained in a specific cache volume.
+cache volumes. It's also possible to control which cache spans (and hence,
+which cache stripes) are contained in a specific cache volume.
 
 The layout and structure of the cache spans, the cache volumes, and the cache
 stripes that compose them are derived entirely from :file:`storage.config` and
diff --git a/plugins/experimental/statichit/statichit.cc b/plugins/experimental/statichit/statichit.cc
index cc481df..1520350 100644
--- a/plugins/experimental/statichit/statichit.cc
+++ b/plugins/experimental/statichit/statichit.cc
@@ -1,6 +1,6 @@
 /** @file
 
-  Static HIt Content Serving
+  Static Hit Content Serving
 
   @section license License
 


[trafficserver] 01/02: Adds new plugin: statichit (#7173)

Posted by zw...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

zwoop pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/trafficserver.git

commit 4a566becdeb82bb33b3771ef0e2c3f8c68d6c68b
Author: Randall Meyer <rr...@apache.org>
AuthorDate: Mon Sep 21 10:20:25 2020 -0700

    Adds new plugin: statichit (#7173)
    
    This is a simple plugin (based off of the `generator` plugin) to
    serve static content from a local filesystem. It shares some
    of the same functionality as the `healthchecks` plugin,
    but can be used in the remap context, making it reloadable.
    
    (cherry picked from commit e1585907bfec722c63e1bb200e1da1aff2bea585)
---
 plugins/Makefile.am                         |   1 +
 plugins/experimental/statichit/Makefile.inc |  20 +
 plugins/experimental/statichit/README.md    |  38 ++
 plugins/experimental/statichit/statichit.cc | 637 ++++++++++++++++++++++++++++
 4 files changed, 696 insertions(+)

diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 6cdb45d..4bf7678 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -81,6 +81,7 @@ include experimental/mp4/Makefile.inc
 include experimental/remap_stats/Makefile.inc
 include experimental/slice/Makefile.inc
 include experimental/sslheaders/Makefile.inc
+include experimental/statichit/Makefile.inc
 include experimental/stream_editor/Makefile.inc
 include experimental/system_stats/Makefile.inc
 include experimental/traffic_dump/Makefile.inc
diff --git a/plugins/experimental/statichit/Makefile.inc b/plugins/experimental/statichit/Makefile.inc
new file mode 100644
index 0000000..b45e5e0
--- /dev/null
+++ b/plugins/experimental/statichit/Makefile.inc
@@ -0,0 +1,20 @@
+#  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.
+
+pkglib_LTLIBRARIES += experimental/statichit/statichit.la
+
+experimental_statichit_statichit_la_SOURCES = \
+  experimental/statichit/statichit.cc
diff --git a/plugins/experimental/statichit/README.md b/plugins/experimental/statichit/README.md
new file mode 100644
index 0000000..cb7001d
--- /dev/null
+++ b/plugins/experimental/statichit/README.md
@@ -0,0 +1,38 @@
+This is a simple plugin to serve static content from a local filesystem. It shares some of the same functionality as the `healthchecks` plugin, but can be used in the remap context(thereby making it reloadable). It does not use fsnotify for watching the source files.
+
+If the file specified by the `--file-path` parameter is not found, the plugin will return the status code specified
+by the `--failure-code` parameter, defaulting to a 404. If the file is found, it will return the contents of the file
+setting the `Content-Type` header to the value specified on the `--mime-type` parameter.
+
+Metrics
+--------
+
+The plugin publishes the following metrics:
+
+statichit.response_bytes:
+  The total number of bytes emitted
+statichit.response_count:
+  The number of HTTP responses generated by the plugin
+
+Examples:
+-----------
+
+remap.config:
+```
+map /internal/healthcheck \
+   http://127.0.0.1 \
+   @plugin=statichit.so @pparam=--file-path=/var/run/trafficserver/healthcheck.txt \
+   @pparam=--mime-type=text/plain \
+   @pparam=--success-code=200 \
+   @pparam=--failure-code=403 \
+   @pparam=--max-age=0
+
+map http://content.example.com/content.txt \
+   http://127.0.0.1 \
+   @plugin=statichit.so @pparam=--file-path=/opt/ats/etc/trafficserver/static/content_source.txt \
+   @pparam=--failure-code=404 \
+   @pparam=--max-age=604800
+
+```
+
+NOTE: The remap origin is never contacted because this plugin intercepts the request and acts as the origin server. For that reason, the origin specification must be syntactically valid and resolvable in DNS.
diff --git a/plugins/experimental/statichit/statichit.cc b/plugins/experimental/statichit/statichit.cc
new file mode 100644
index 0000000..cc481df
--- /dev/null
+++ b/plugins/experimental/statichit/statichit.cc
@@ -0,0 +1,637 @@
+/** @file
+
+  Static HIt Content Serving
+
+  @section license License
+
+  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.
+ */
+
+#include <cerrno>
+#include <cinttypes>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <unistd.h>
+
+#include <fstream>
+#include <sstream>
+
+#include <string>
+#include <getopt.h>
+
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include "ts/ts.h"
+#include "ts/remap.h"
+
+constexpr char PLUGIN[] = "statichit";
+
+#define VDEBUG(fmt, ...) TSDebug(PLUGIN, fmt, ##__VA_ARGS__)
+
+#if DEBUG
+#define VERROR(fmt, ...) TSDebug(PLUGIN, fmt, ##__VA_ARGS__)
+#else
+#define VERROR(fmt, ...) TSError("[%s] %s: " fmt, PLUGIN, __FUNCTION__, ##__VA_ARGS__)
+#endif
+
+#define VIODEBUG(vio, fmt, ...)                                                                                              \
+  VDEBUG("vio=%p vio.cont=%p, vio.cont.data=%p, vio.vc=%p " fmt, (vio), TSVIOContGet(vio), TSContDataGet(TSVIOContGet(vio)), \
+         TSVIOVConnGet(vio), ##__VA_ARGS__)
+
+static TSCont TxnHook;
+
+static int StatCountBytes     = -1;
+static int StatCountResponses = -1;
+
+static int StaticHitInterceptHook(TSCont contp, TSEvent event, void *edata);
+static int StaticHitTxnHook(TSCont contp, TSEvent event, void *edata);
+
+struct StaticHitConfig {
+  explicit StaticHitConfig(const std::string &filePath, const std::string &mimeType) : filePath(filePath), mimeType(mimeType) {}
+
+  ~StaticHitConfig() {}
+
+  std::string filePath;
+  std::string mimeType;
+
+  int successCode = 200;
+  int failureCode = 404;
+  int maxAge      = 0;
+};
+
+struct StaticHitRequest;
+
+union argument_type {
+  void *ptr;
+  intptr_t ecode;
+  TSVConn vc;
+  TSVIO vio;
+  TSHttpTxn txn;
+  StaticHitRequest *trq;
+
+  argument_type(void *_p) : ptr(_p) {}
+};
+
+// This structure represents the state of a streaming I/O request. It
+// is directional (ie. either a read or a write). We need two of these
+// for each TSVConn; one to push data into the TSVConn and one to pull
+// data out.
+struct IOChannel {
+  TSVIO vio = nullptr;
+  TSIOBuffer iobuf;
+  TSIOBufferReader reader;
+
+  IOChannel() : iobuf(TSIOBufferSizedCreate(TS_IOBUFFER_SIZE_INDEX_32K)), reader(TSIOBufferReaderAlloc(iobuf)) {}
+  ~IOChannel()
+  {
+    if (this->reader) {
+      TSIOBufferReaderFree(this->reader);
+    }
+
+    if (this->iobuf) {
+      TSIOBufferDestroy(this->iobuf);
+    }
+  }
+
+  void
+  read(TSVConn vc, TSCont contp)
+  {
+    this->vio = TSVConnRead(vc, contp, this->iobuf, INT64_MAX);
+  }
+
+  void
+  write(TSVConn vc, TSCont contp)
+  {
+    this->vio = TSVConnWrite(vc, contp, this->reader, INT64_MAX);
+  }
+};
+
+struct StaticHitHttpHeader {
+  TSMBuffer buffer;
+  TSMLoc header;
+  TSHttpParser parser;
+
+  StaticHitHttpHeader()
+  {
+    this->buffer = TSMBufferCreate();
+    this->header = TSHttpHdrCreate(this->buffer);
+    this->parser = TSHttpParserCreate();
+  }
+
+  ~StaticHitHttpHeader()
+  {
+    if (this->parser) {
+      TSHttpParserDestroy(this->parser);
+    }
+
+    TSHttpHdrDestroy(this->buffer, this->header);
+    TSHandleMLocRelease(this->buffer, TS_NULL_MLOC, this->header);
+    TSMBufferDestroy(this->buffer);
+  }
+};
+
+struct StaticHitRequest {
+  StaticHitRequest() {}
+
+  off_t nbytes        = 0; // Number of bytes to generate.
+  unsigned maxAge     = 0; // Max age for cache responses.
+  unsigned statusCode = 200;
+  IOChannel readio;
+  IOChannel writeio;
+  StaticHitHttpHeader rqheader;
+
+  std::string body;
+  std::string mimeType;
+
+  static StaticHitRequest *
+  createStaticHitRequest(StaticHitConfig *tc)
+  {
+    StaticHitRequest *shr = new StaticHitRequest;
+    std::ifstream ifstr;
+
+    ifstr.open(tc->filePath);
+    if (!ifstr) {
+      shr->statusCode = tc->failureCode;
+      return shr;
+    }
+
+    std::stringstream sstr;
+    sstr << ifstr.rdbuf();
+    shr->body       = sstr.str();
+    shr->nbytes     = shr->body.size();
+    shr->mimeType   = tc->mimeType;
+    shr->statusCode = tc->successCode;
+    shr->maxAge     = tc->maxAge;
+
+    return shr;
+  }
+
+  ~StaticHitRequest() = default;
+};
+
+// Destroy a StaticHitRequest, including the per-txn continuation.
+static void
+StaticHitRequestDestroy(StaticHitRequest *trq, TSVIO vio, TSCont contp)
+{
+  if (vio) {
+    TSVConnClose(TSVIOVConnGet(vio));
+  }
+
+  TSContDestroy(contp);
+  delete trq;
+}
+
+// NOTE: This will always append a new "field_name: value"
+static void
+HeaderFieldDateSet(const StaticHitHttpHeader &http, const char *field_name, int64_t field_len, time_t value)
+{
+  TSMLoc field;
+
+  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, &field);
+  TSMimeHdrFieldValueDateSet(http.buffer, http.header, field, value);
+  TSMimeHdrFieldAppend(http.buffer, http.header, field);
+  TSHandleMLocRelease(http.buffer, http.header, field);
+}
+
+// NOTE: This will always append a new "field_name: value"
+static void
+HeaderFieldIntSet(const StaticHitHttpHeader &http, const char *field_name, int64_t field_len, int64_t value)
+{
+  TSMLoc field;
+
+  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, &field);
+  TSMimeHdrFieldValueInt64Set(http.buffer, http.header, field, -1, value);
+  TSMimeHdrFieldAppend(http.buffer, http.header, field);
+  TSHandleMLocRelease(http.buffer, http.header, field);
+}
+
+// NOTE: This will always append a new "field_name: value"
+static void
+HeaderFieldStringSet(const StaticHitHttpHeader &http, const char *field_name, int64_t field_len, const char *value)
+{
+  TSMLoc field;
+
+  TSMimeHdrFieldCreateNamed(http.buffer, http.header, field_name, field_len, &field);
+  TSMimeHdrFieldValueStringSet(http.buffer, http.header, field, -1, value, -1);
+  TSMimeHdrFieldAppend(http.buffer, http.header, field);
+  TSHandleMLocRelease(http.buffer, http.header, field);
+}
+
+static void
+WriteResponseHeader(StaticHitRequest *trq, TSCont contp, TSHttpStatus status)
+{
+  StaticHitHttpHeader response;
+
+  VDEBUG("writing response header");
+
+  TSReleaseAssert(TSHttpHdrTypeSet(response.buffer, response.header, TS_HTTP_TYPE_RESPONSE) == TS_SUCCESS);
+  TSReleaseAssert(TSHttpHdrVersionSet(response.buffer, response.header, TS_HTTP_VERSION(1, 1)) == TS_SUCCESS);
+  TSReleaseAssert(TSHttpHdrStatusSet(response.buffer, response.header, status) == TS_SUCCESS);
+
+  TSHttpHdrReasonSet(response.buffer, response.header, TSHttpHdrReasonLookup(status), -1);
+
+  if (status == TS_HTTP_STATUS_OK) {
+    // Set the Content-Length header.
+    HeaderFieldIntSet(response, TS_MIME_FIELD_CONTENT_LENGTH, TS_MIME_LEN_CONTENT_LENGTH, trq->nbytes);
+
+    // Set the Cache-Control header.
+    if (trq->maxAge > 0) {
+      char buf[64];
+
+      snprintf(buf, sizeof(buf), "max-age=%u", trq->maxAge);
+      HeaderFieldStringSet(response, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL, buf);
+      HeaderFieldDateSet(response, TS_MIME_FIELD_LAST_MODIFIED, TS_MIME_LEN_LAST_MODIFIED, time(nullptr));
+
+    } else {
+      HeaderFieldStringSet(response, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL, "no-cache");
+    }
+
+    HeaderFieldStringSet(response, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE, trq->mimeType.c_str());
+  }
+
+  // Write the header to the IO buffer. Set the VIO bytes so that we can get a WRITE_COMPLETE
+  // event when this is done.
+  int hdrlen = TSHttpHdrLengthGet(response.buffer, response.header);
+
+  TSHttpHdrPrint(response.buffer, response.header, trq->writeio.iobuf);
+  TSVIONBytesSet(trq->writeio.vio, hdrlen);
+  TSVIOReenable(trq->writeio.vio);
+
+  TSStatIntIncrement(StatCountBytes, hdrlen);
+}
+
+static bool
+StaticHitParseRequest(StaticHitRequest *trq)
+{
+  const char *path;
+  int pathsz;
+
+  // Make sure this is a GET request
+  path = TSHttpHdrMethodGet(trq->rqheader.buffer, trq->rqheader.header, &pathsz);
+  if (path != TS_HTTP_METHOD_GET) {
+    VDEBUG("%.*s method is not supported", pathsz, path);
+    return false;
+  }
+
+  return true;
+}
+
+// Handle events from TSHttpTxnServerIntercept. The intercept
+// starts with TS_EVENT_NET_ACCEPT, and then continues with
+// TSVConn events.
+static int
+StaticHitInterceptHook(TSCont contp, TSEvent event, void *edata)
+{
+  VDEBUG("StaticHitInterceptHook: %p ", edata);
+
+  argument_type arg(edata);
+
+  VDEBUG("contp=%p, event=%s (%d), edata=%p", contp, TSHttpEventNameLookup(event), event, arg.ptr);
+
+  switch (event) {
+  case TS_EVENT_NET_ACCEPT: {
+    // TS_EVENT_NET_ACCEPT will be delivered when the server intercept
+    // is set up by the core. We just need to allocate a statichit
+    // request state and start reading the VC.
+    StaticHitRequest *trq = static_cast<StaticHitRequest *>(TSContDataGet(contp));
+
+    TSStatIntIncrement(StatCountResponses, 1);
+    VDEBUG("allocated server intercept statichit trq=%p", trq);
+
+    // This continuation was allocated in StaticHitTxnHook. Reset the
+    // data to keep track of this generator request.
+    TSContDataSet(contp, trq);
+
+    // Start reading the request from the server intercept VC.
+    trq->readio.read(arg.vc, contp);
+    VIODEBUG(trq->readio.vio, "started reading statichit request");
+
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_NET_ACCEPT_FAILED: {
+    // TS_EVENT_NET_ACCEPT_FAILED will be delivered if the
+    // transaction is cancelled before we start tunnelling
+    // through the server intercept. One way that this can happen
+    // is if the intercept is attached early, and then we serve
+    // the document out of cache.
+
+    // There's nothing to do here except nuke the continuation
+    // that was allocated in StaticHitTxnHook().
+
+    StaticHitRequest *trq = static_cast<StaticHitRequest *>(TSContDataGet(contp));
+    delete trq;
+
+    TSContDestroy(contp);
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_READ_READY: {
+    argument_type cdata           = TSContDataGet(contp);
+    StaticHitHttpHeader &rqheader = cdata.trq->rqheader;
+
+    VDEBUG("reading vio=%p vc=%p, trq=%p", arg.vio, TSVIOVConnGet(arg.vio), cdata.trq);
+
+    TSIOBufferBlock blk;
+    ssize_t consumed     = 0;
+    TSParseResult result = TS_PARSE_CONT;
+
+    for (blk = TSIOBufferReaderStart(cdata.trq->readio.reader); blk; blk = TSIOBufferBlockNext(blk)) {
+      const char *ptr;
+      const char *end;
+      int64_t nbytes;
+      TSHttpStatus status = static_cast<TSHttpStatus>(cdata.trq->statusCode);
+
+      ptr = TSIOBufferBlockReadStart(blk, cdata.trq->readio.reader, &nbytes);
+      if (ptr == nullptr || nbytes == 0) {
+        continue;
+      }
+
+      end    = ptr + nbytes;
+      result = TSHttpHdrParseReq(rqheader.parser, rqheader.buffer, rqheader.header, &ptr, end);
+      switch (result) {
+      case TS_PARSE_ERROR:
+        // If we got a bad request, just shut it down.
+        VDEBUG("bad request on trq=%p, sending an error", cdata.trq);
+        StaticHitRequestDestroy(cdata.trq, arg.vio, contp);
+        return TS_EVENT_ERROR;
+
+      case TS_PARSE_DONE:
+        // Check the response.
+        VDEBUG("parsed request on trq=%p, sending a response", cdata.trq);
+        if (!StaticHitParseRequest(cdata.trq)) {
+          status = TS_HTTP_STATUS_METHOD_NOT_ALLOWED;
+        }
+
+        // Start the vconn write.
+        cdata.trq->writeio.write(TSVIOVConnGet(arg.vio), contp);
+        TSVIONBytesSet(cdata.trq->writeio.vio, 0);
+
+        WriteResponseHeader(cdata.trq, contp, status);
+        return TS_EVENT_NONE;
+
+      case TS_PARSE_CONT:
+        // We consumed the buffer we got minus the remainder.
+        consumed += (nbytes - std::distance(ptr, end));
+      }
+    }
+
+    TSReleaseAssert(result == TS_PARSE_CONT);
+
+    // Reenable the read VIO to get more events.
+    TSVIOReenable(arg.vio);
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_WRITE_READY: {
+    argument_type cdata = TSContDataGet(contp);
+
+    if (cdata.trq->nbytes) {
+      int64_t nbytes = cdata.trq->nbytes;
+
+      VIODEBUG(arg.vio, "writing %" PRId64 " bytes for trq=%p", nbytes, cdata.trq);
+      nbytes = TSIOBufferWrite(cdata.trq->writeio.iobuf, cdata.trq->body.c_str(), nbytes);
+
+      cdata.trq->nbytes -= nbytes;
+      TSStatIntIncrement(StatCountBytes, nbytes);
+
+      // Update the number of bytes to write.
+      TSVIONBytesSet(arg.vio, TSVIONBytesGet(arg.vio) + nbytes);
+      TSVIOReenable(arg.vio);
+    }
+
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_ERROR:
+  case TS_EVENT_VCONN_EOS: {
+    argument_type cdata = TSContDataGet(contp);
+
+    VIODEBUG(arg.vio, "received EOS or ERROR for trq=%p", cdata.trq);
+    StaticHitRequestDestroy(cdata.trq, arg.vio, contp);
+    return event == TS_EVENT_ERROR ? TS_EVENT_ERROR : TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_READ_COMPLETE:
+    // We read data forever, so we should never get a READ_COMPLETE.
+    VIODEBUG(arg.vio, "unexpected TS_EVENT_VCONN_READ_COMPLETE");
+    return TS_EVENT_NONE;
+
+  case TS_EVENT_VCONN_WRITE_COMPLETE: {
+    argument_type cdata = TSContDataGet(contp);
+
+    // If we still have bytes to write, kick off a new write operation, otherwise
+    // we are done and we can shut down the VC.
+    if (cdata.trq->nbytes) {
+      cdata.trq->writeio.write(TSVIOVConnGet(arg.vio), contp);
+      TSVIONBytesSet(cdata.trq->writeio.vio, cdata.trq->nbytes);
+    } else {
+      VIODEBUG(arg.vio, "TS_EVENT_VCONN_WRITE_COMPLETE %" PRId64 " todo", TSVIONTodoGet(arg.vio));
+      StaticHitRequestDestroy(cdata.trq, arg.vio, contp);
+    }
+
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_TIMEOUT: {
+    return TS_EVENT_NONE;
+  }
+
+  case TS_EVENT_VCONN_INACTIVITY_TIMEOUT:
+    VERROR("unexpected event %s (%d) edata=%p", TSHttpEventNameLookup(event), event, arg.ptr);
+    return TS_EVENT_ERROR;
+
+  default:
+    VERROR("unexpected event %s (%d) edata=%p", TSHttpEventNameLookup(event), event, arg.ptr);
+    return TS_EVENT_ERROR;
+  }
+}
+
+static void
+StaticHitSetupIntercept(StaticHitConfig *cfg, TSHttpTxn txn)
+{
+  StaticHitRequest *req = StaticHitRequest::createStaticHitRequest(cfg);
+  if (req == nullptr) {
+    VERROR("could not create request for %s", cfg->filePath.c_str());
+    return;
+  }
+
+  TSCont cnt = TSContCreate(StaticHitInterceptHook, TSMutexCreate());
+  TSContDataSet(cnt, req);
+
+  TSHttpTxnServerIntercept(cnt, txn);
+
+  return;
+}
+
+// Handle events that occur on the TSHttpTxn.
+static int
+StaticHitTxnHook(TSCont contp, TSEvent event, void *edata)
+{
+  argument_type arg(edata);
+
+  VDEBUG("contp=%p, event=%s (%d), edata=%p", contp, TSHttpEventNameLookup(event), event, edata);
+
+  switch (event) {
+  case TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE: {
+    int method_length, status;
+    TSMBuffer bufp;
+    TSMLoc hdr_loc;
+    const char *method;
+
+    TSReleaseAssert(TSHttpTxnCacheLookupStatusGet(arg.txn, &status) == TS_SUCCESS);
+
+    if (TSHttpTxnClientReqGet(arg.txn, &bufp, &hdr_loc) != TS_SUCCESS) {
+      VERROR("Couldn't retrieve client request header");
+      goto done;
+    }
+
+    method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_length);
+    if (NULL == method) {
+      VERROR("Couldn't retrieve client request method");
+      goto done;
+    }
+
+    if (status != TS_CACHE_LOOKUP_HIT_FRESH || method != TS_HTTP_METHOD_GET) {
+      StaticHitSetupIntercept(static_cast<StaticHitConfig *>(TSContDataGet(contp)), arg.txn);
+    }
+
+    break;
+  }
+
+  default:
+    VERROR("unexpected event %s (%d)", TSHttpEventNameLookup(event), event);
+    break;
+  }
+
+done:
+  TSHttpTxnReenable(arg.txn, TS_EVENT_HTTP_CONTINUE);
+  return TS_EVENT_NONE;
+}
+
+TSReturnCode
+TSRemapInit(TSRemapInterface * /* api_info */, char * /* errbuf */, int /* errbuf_size */)
+{
+  TxnHook = TSContCreate(StaticHitTxnHook, nullptr);
+
+  if (TSStatFindName("statichit.response_bytes", &StatCountBytes) == TS_ERROR) {
+    StatCountBytes = TSStatCreate("statichit.response_bytes", TS_RECORDDATATYPE_COUNTER, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_SUM);
+  }
+
+  if (TSStatFindName("statichit.response_count", &StatCountResponses) == TS_ERROR) {
+    StatCountResponses =
+      TSStatCreate("statichit.response_count", TS_RECORDDATATYPE_COUNTER, TS_STAT_NON_PERSISTENT, TS_STAT_SYNC_COUNT);
+  }
+  return TS_SUCCESS;
+}
+
+TSRemapStatus
+TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri)
+{
+  StaticHitConfig *cfg = static_cast<StaticHitConfig *>(ih);
+
+  if (!cfg) {
+    VERROR("No remap context available, check code / config");
+    TSHttpTxnStatusSet(rh, TS_HTTP_STATUS_INTERNAL_SERVER_ERROR);
+    return TSREMAP_NO_REMAP;
+  }
+
+  if (!cfg->maxAge) {
+    TSHttpTxnConfigIntSet(rh, TS_CONFIG_HTTP_CACHE_HTTP, 0);
+    StaticHitSetupIntercept(static_cast<StaticHitConfig *>(ih), rh);
+  } else {
+    TSHttpTxnHookAdd(rh, TS_HTTP_CACHE_LOOKUP_COMPLETE_HOOK, TxnHook);
+  }
+  TSContDataSet(TxnHook, ih);
+
+  return TSREMAP_NO_REMAP; // This plugin never rewrites anything.
+}
+
+TSReturnCode
+TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
+{
+  static const struct option longopt[] = {
+    {"file-path", required_argument, NULL, 'f'},    {"mime-type", required_argument, NULL, 'm'},
+    {"max-age", required_argument, NULL, 'a'},      {"failure-code", required_argument, NULL, 'c'},
+    {"success-code", required_argument, NULL, 's'}, {NULL, no_argument, NULL, '\0'}};
+
+  std::string filePath;
+  std::string mimeType = "text/plain";
+  int maxAge = 0, failureCode = 0, successCode = 0;
+
+  // argv contains the "to" and "from" URLs. Skip the first so that the
+  // second one poses as the program name.
+  --argc;
+  ++argv;
+  optind = 0;
+
+  while (true) {
+    int opt = getopt_long(argc, (char *const *)argv, "f:m:a:c:s:", longopt, NULL);
+
+    switch (opt) {
+    case 'f': {
+      filePath = std::string(optarg);
+    } break;
+    case 'm': {
+      mimeType = std::string(optarg);
+    } break;
+    case 'a': {
+      maxAge = atoi(optarg);
+    } break;
+    case 'c': {
+      failureCode = atoi(optarg);
+    } break;
+    case 's': {
+      successCode = atoi(optarg);
+    } break;
+    }
+
+    if (opt == -1) {
+      break;
+    }
+  }
+
+  if (filePath.size() == 0) {
+    printf("Need to specify --file-path\n");
+    return TS_ERROR;
+  }
+
+  StaticHitConfig *tc = new StaticHitConfig(filePath, mimeType);
+  if (maxAge > 0) {
+    tc->maxAge = maxAge;
+  }
+  if (failureCode > 0) {
+    tc->failureCode = failureCode;
+  }
+  if (successCode > 0) {
+    tc->successCode = successCode;
+  }
+
+  *ih = static_cast<void *>(tc);
+
+  return TS_SUCCESS;
+}
+
+void
+TSRemapDeleteInstance(void *ih)
+{
+  StaticHitConfig *tc = static_cast<StaticHitConfig *>(ih);
+  delete tc;
+}