| /** @file |
| |
| ATS plugin to do combo handling. |
| |
| @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 <list> |
| #include <string> |
| #include <string_view> |
| #include <sstream> |
| #include <vector> |
| #include <algorithm> |
| #include <ctime> |
| #include <fstream> |
| #include <arpa/inet.h> |
| #include <limits> |
| #include <getopt.h> |
| |
| #include "ts/ts.h" |
| #include "ts/experimental.h" |
| #include "ts/remap.h" |
| #include "tscore/ink_defs.h" |
| #include "tscpp/util/TextView.h" |
| |
| #include "HttpDataFetcherImpl.h" |
| #include "gzip.h" |
| #include "Utils.h" |
| |
| using namespace std; |
| using namespace EsiLib; |
| |
| #define DEBUG_TAG "combo_handler" |
| |
| // Because STL vs. C library leads to ugly casting, fix it once. |
| inline int |
| length(std::string const &str) |
| { |
| return static_cast<int>(str.size()); |
| } |
| |
| constexpr unsigned DEFAULT_MAX_FILE_COUNT = 100; |
| constexpr int MAX_QUERY_LENGTH = 4096; |
| |
| unsigned MaxFileCount = DEFAULT_MAX_FILE_COUNT; |
| |
| // We hardcode "immutable" here because it's not yet defined in the ATS API |
| #define HTTP_IMMUTABLE "immutable" |
| |
| int arg_idx; |
| static string SIG_KEY_NAME; |
| static vector<string> HEADER_ALLOWLIST; |
| |
| #define DEFAULT_COMBO_HANDLER_PATH "admin/v1/combo" |
| static string COMBO_HANDLER_PATH{DEFAULT_COMBO_HANDLER_PATH}; |
| |
| #define LOG_ERROR(fmt, args...) \ |
| do { \ |
| TSError("[%s:%d] [%s] ERROR: " fmt, __FILE__, __LINE__, __FUNCTION__, ##args); \ |
| TSDebug(DEBUG_TAG, "[%s:%d] [%s] ERROR: " fmt, __FILE__, __LINE__, __FUNCTION__, ##args); \ |
| } while (0) |
| |
| #define LOG_DEBUG(fmt, args...) \ |
| do { \ |
| TSDebug(DEBUG_TAG, "[%s:%d] [%s] DEBUG: " fmt, __FILE__, __LINE__, __FUNCTION__, ##args); \ |
| } while (0) |
| |
| using StringList = list<string>; |
| |
| struct ClientRequest { |
| TSHttpStatus status = TS_HTTP_STATUS_OK; |
| const sockaddr *client_addr = nullptr; |
| StringList file_urls; |
| bool gzip_accepted = false; |
| string defaultBucket; // default Bucket will be set to HOST header |
| ClientRequest() : defaultBucket("l"){}; |
| }; |
| |
| struct InterceptData { |
| TSVConn net_vc; |
| TSCont contp; |
| |
| struct IoHandle { |
| TSVIO vio = nullptr; |
| TSIOBuffer buffer = nullptr; |
| TSIOBufferReader reader = nullptr; |
| |
| IoHandle() = default; |
| |
| ~IoHandle() |
| { |
| if (reader) { |
| TSIOBufferReaderFree(reader); |
| } |
| if (buffer) { |
| TSIOBufferDestroy(buffer); |
| } |
| }; |
| }; |
| |
| IoHandle input; |
| IoHandle output; |
| TSHttpParser http_parser; |
| |
| string body; |
| TSMBuffer req_hdr_bufp; |
| TSMLoc req_hdr_loc; |
| bool req_hdr_parsed; |
| bool initialized; |
| ClientRequest creq; |
| HttpDataFetcherImpl *fetcher; |
| bool read_complete; |
| bool write_complete; |
| string gzipped_data; |
| |
| InterceptData(TSCont cont) |
| : net_vc(nullptr), |
| contp(cont), |
| input(), |
| output(), |
| req_hdr_bufp(nullptr), |
| req_hdr_loc(nullptr), |
| req_hdr_parsed(false), |
| initialized(false), |
| fetcher(nullptr), |
| read_complete(false), |
| write_complete(false) |
| { |
| http_parser = TSHttpParserCreate(); |
| } |
| |
| bool init(TSVConn vconn); |
| void setupWrite(); |
| |
| ~InterceptData(); |
| }; |
| |
| /* |
| * This class is responsible for keeping track of and processing the various |
| * Cache-Control values between all the requested documents |
| */ |
| struct CacheControlHeader { |
| enum Publicity { PRIVATE, PUBLIC, DEFAULT }; |
| |
| // Update the object with a document's Cache-Control header |
| void update(TSMBuffer bufp, TSMLoc hdr_loc); |
| |
| // Return the Cache-Control for the combined document |
| string generate() const; |
| |
| // Cache-Control values we're keeping track of |
| unsigned int _max_age = numeric_limits<unsigned int>::max(); |
| Publicity _publicity = Publicity::DEFAULT; |
| bool _immutable = true; |
| }; |
| |
| class ContentTypeHandler |
| { |
| public: |
| ContentTypeHandler(std::string &resp_header_fields) : _resp_header_fields(resp_header_fields) {} |
| |
| // Returns false if _content_type_allowlist is not empty, and content-type field is either not present or not in the |
| // allowlist. Adds first Content-type field it encounters in the headers passed to this function. |
| // |
| bool nextObjectHeader(TSMBuffer bufp, TSMLoc hdr_loc); |
| |
| // Load allowlist from config file. |
| // |
| static void loadAllowList(std::string const &file_spec); |
| |
| private: |
| // Add Content-Type field to these. |
| // |
| std::string &_resp_header_fields; |
| |
| bool _added_content_type{false}; |
| |
| static vector<std::string> _content_type_allowlist; |
| }; |
| |
| vector<std::string> ContentTypeHandler::_content_type_allowlist; |
| |
| bool |
| InterceptData::init(TSVConn vconn) |
| { |
| if (initialized) { |
| LOG_ERROR("InterceptData already initialized!"); |
| return false; |
| } |
| |
| net_vc = vconn; |
| |
| input.buffer = TSIOBufferCreate(); |
| input.reader = TSIOBufferReaderAlloc(input.buffer); |
| input.vio = TSVConnRead(net_vc, contp, input.buffer, INT64_MAX); |
| |
| req_hdr_bufp = TSMBufferCreate(); |
| req_hdr_loc = TSHttpHdrCreate(req_hdr_bufp); |
| TSHttpHdrTypeSet(req_hdr_bufp, req_hdr_loc, TS_HTTP_TYPE_REQUEST); |
| |
| fetcher = new HttpDataFetcherImpl(contp, creq.client_addr, "combohandler_fetcher"); |
| |
| initialized = true; |
| LOG_DEBUG("InterceptData initialized!"); |
| return true; |
| } |
| |
| void |
| InterceptData::setupWrite() |
| { |
| TSAssert(output.buffer == nullptr); |
| output.buffer = TSIOBufferCreate(); |
| output.reader = TSIOBufferReaderAlloc(output.buffer); |
| output.vio = TSVConnWrite(net_vc, contp, output.reader, INT64_MAX); |
| } |
| |
| InterceptData::~InterceptData() |
| { |
| if (req_hdr_loc) { |
| TSHandleMLocRelease(req_hdr_bufp, TS_NULL_MLOC, req_hdr_loc); |
| } |
| if (req_hdr_bufp) { |
| TSMBufferDestroy(req_hdr_bufp); |
| } |
| if (fetcher) { |
| delete fetcher; |
| } |
| TSHttpParserDestroy(http_parser); |
| if (net_vc) { |
| TSVConnClose(net_vc); |
| } |
| } |
| |
| void |
| CacheControlHeader::update(TSMBuffer bufp, TSMLoc hdr_loc) |
| { |
| bool found_immutable = false; |
| bool found_private = false; |
| |
| // Load each value from the Cache-Control header into the vector values |
| TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, TS_MIME_FIELD_CACHE_CONTROL, TS_MIME_LEN_CACHE_CONTROL); |
| if (field_loc != TS_NULL_MLOC) { |
| int n_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); |
| if ((n_values != TS_ERROR) && (n_values > 0)) { |
| for (int i = 0; i < n_values; i++) { |
| // Grab this current header value |
| int _val_len = 0; |
| const char *val = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, i, &_val_len); |
| |
| // Update max-age if necessary |
| if (strncasecmp(val, TS_HTTP_VALUE_MAX_AGE, TS_HTTP_LEN_MAX_AGE) == 0) { |
| unsigned int max_age = 0; |
| char *ptr = const_cast<char *>(val); |
| ptr += TS_HTTP_LEN_MAX_AGE; |
| while ((*ptr == ' ') || (*ptr == '\t')) { |
| ptr++; |
| } |
| if (*ptr == '=') { |
| ptr++; |
| max_age = atoi(ptr); |
| } |
| if (max_age > 0 && max_age < _max_age) { |
| _max_age = max_age; |
| } |
| // If we find even a single occurrence of private, the whole response must be private |
| } else if (strncasecmp(val, TS_HTTP_VALUE_PRIVATE, TS_HTTP_LEN_PRIVATE) == 0) { |
| found_private = true; |
| // Every requested document must have immutable for the final response to be immutable |
| } else if (strncasecmp(val, HTTP_IMMUTABLE, strlen(HTTP_IMMUTABLE)) == 0) { |
| found_immutable = true; |
| } |
| } |
| } |
| TSHandleMLocRelease(bufp, hdr_loc, field_loc); |
| } |
| |
| if (!found_immutable) { |
| LOG_DEBUG("Did not see an immutable cache control. The response will be not be immutable"); |
| _immutable = false; |
| } |
| |
| if (found_private) { |
| LOG_DEBUG("Saw a private cache control. The response will be private"); |
| _publicity = Publicity::PRIVATE; |
| } |
| } |
| |
| string |
| CacheControlHeader::generate() const |
| { |
| unsigned int max_age; |
| char line_buf[256]; |
| const char *publicity; |
| const char *immutable; |
| |
| // Previously, all combo_cache documents were public. However, that's a bug. If any requested document is private the combo_cache |
| // document should private as well. |
| if (_publicity == Publicity::PUBLIC || _publicity == Publicity::DEFAULT) { |
| publicity = TS_HTTP_VALUE_PUBLIC; |
| } else { |
| publicity = TS_HTTP_VALUE_PRIVATE; |
| } |
| |
| immutable = (_immutable ? ", " HTTP_IMMUTABLE : ""); |
| max_age = (_max_age == numeric_limits<unsigned int>::max() ? 315360000 : _max_age); // default is 10 years |
| |
| sprintf(line_buf, "Cache-Control: max-age=%u, %s%s\r\n", max_age, publicity, immutable); |
| return string(line_buf); |
| } |
| |
| // forward declarations |
| static int handleReadRequestHeader(TSCont contp, TSEvent event, void *edata); |
| static bool isComboHandlerRequest(TSMBuffer bufp, TSMLoc hdr_loc, TSMLoc url_loc); |
| static void getClientRequest(TSHttpTxn txnp, TSMBuffer bufp, TSMLoc hdr_loc, TSMLoc url_loc, ClientRequest &creq); |
| static void parseQueryParameters(const char *query, int query_len, ClientRequest &creq); |
| static void checkGzipAcceptance(TSMBuffer bufp, TSMLoc hdr_loc, ClientRequest &creq); |
| static int handleServerEvent(TSCont contp, TSEvent event, void *edata); |
| static bool initRequestProcessing(InterceptData &int_data, void *edata, bool &write_response); |
| static bool readInterceptRequest(InterceptData &int_data); |
| static bool writeResponse(InterceptData &int_data); |
| static bool writeErrorResponse(InterceptData &int_data, int &n_bytes_written); |
| static bool writeStandardHeaderFields(InterceptData &int_data, int &n_bytes_written); |
| static void prepareResponse(InterceptData &int_data, ByteBlockList &body_blocks, string &resp_header_fields); |
| static bool getDefaultBucket(TSHttpTxn txnp, TSMBuffer bufp, TSMLoc hdr_obj, ClientRequest &creq); |
| |
| void |
| TSPluginInit(int argc, const char *argv[]) |
| { |
| TSPluginRegistrationInfo info; |
| info.plugin_name = "combo_handler"; |
| info.vendor_name = "Apache Software Foundation"; |
| info.support_email = "dev@trafficserver.apache.org"; |
| |
| if (TSPluginRegister(&info) != TS_SUCCESS) { |
| TSError("[combo_handler][%s] plugin registration failed", __FUNCTION__); |
| return; |
| } |
| |
| if (argc > 1) { |
| int c; |
| static const struct option longopts[] = { |
| {"max-files", required_argument, nullptr, 'f'}, |
| {nullptr, 0, nullptr, 0}, |
| }; |
| |
| int longindex = 0; |
| optind = 1; // Force restart to avoid problems with other plugins. |
| while ((c = getopt_long(argc, const_cast<char *const *>(argv), "f:", longopts, &longindex)) != -1) { |
| switch (c) { |
| case 'f': { |
| char *tmp = nullptr; |
| long n = strtol(optarg, &tmp, 0); |
| if (tmp == optarg) { |
| TSError("[%s] %s requires a numeric argument", DEBUG_TAG, longopts[longindex].name); |
| } else if (n < 1) { |
| TSError("[%s] %s must be a positive number", DEBUG_TAG, longopts[longindex].name); |
| } else { |
| MaxFileCount = n; |
| TSDebug(DEBUG_TAG, "Max files set to %u", MaxFileCount); |
| } |
| break; |
| } |
| default: |
| TSError("[%s] Unrecognized option '%s'", DEBUG_TAG, argv[optind - 1]); |
| break; |
| } |
| } |
| } |
| |
| if (argc >= optind && (argv[optind][0] != '-' || argv[optind][1])) { |
| COMBO_HANDLER_PATH = argv[optind]; |
| if (COMBO_HANDLER_PATH == "/") { |
| COMBO_HANDLER_PATH.clear(); |
| } else { |
| if (COMBO_HANDLER_PATH[0] == '/') { |
| COMBO_HANDLER_PATH.erase(0, 1); |
| } |
| if (COMBO_HANDLER_PATH[COMBO_HANDLER_PATH.size() - 1] == '/') { |
| COMBO_HANDLER_PATH.erase(COMBO_HANDLER_PATH.size() - 1, 1); |
| } |
| } |
| } |
| ++optind; |
| LOG_DEBUG("Combo handler path is [%.*s]", length(COMBO_HANDLER_PATH), COMBO_HANDLER_PATH.data()); |
| |
| SIG_KEY_NAME = (argc > optind && (argv[optind][0] != '-' || argv[optind][1])) ? argv[optind] : ""; |
| ++optind; |
| LOG_DEBUG("Signature key is [%.*s]", length(SIG_KEY_NAME), SIG_KEY_NAME.data()); |
| |
| if (argc > optind && (argv[optind][0] != '-' || argv[optind][1])) { |
| stringstream strstream(argv[optind++]); |
| string header; |
| while (getline(strstream, header, ':')) { |
| HEADER_ALLOWLIST.push_back(header); |
| } |
| } |
| ++optind; |
| |
| for (unsigned int i = 0; i < HEADER_ALLOWLIST.size(); i++) { |
| LOG_DEBUG("AllowList: %s", HEADER_ALLOWLIST[i].c_str()); |
| } |
| |
| std::string content_type_allowlist_filespec = (argc > optind && (argv[optind][0] != '-' || argv[optind][1])) ? argv[optind] : ""; |
| if (content_type_allowlist_filespec.empty()) { |
| LOG_DEBUG("No Content-Type allowlist file specified (all content types allowed)"); |
| } else { |
| // If we have a path and it's not an absolute path, make it relative to the |
| // configuration directory. |
| if (content_type_allowlist_filespec[0] != '/') { |
| content_type_allowlist_filespec = std::string(TSConfigDirGet()) + '/' + content_type_allowlist_filespec; |
| } |
| LOG_DEBUG("Content-Type allowlist file: %s", content_type_allowlist_filespec.c_str()); |
| ContentTypeHandler::loadAllowList(content_type_allowlist_filespec); |
| } |
| ++optind; |
| |
| TSCont rrh_contp = TSContCreate(handleReadRequestHeader, nullptr); |
| if (!rrh_contp) { |
| LOG_ERROR("Could not create read request header continuation"); |
| return; |
| } |
| |
| TSHttpHookAdd(TS_HTTP_OS_DNS_HOOK, rrh_contp); |
| |
| if (TSUserArgIndexReserve(TS_USER_ARGS_TXN, DEBUG_TAG, "will save plugin-enable flag here", &arg_idx) != TS_SUCCESS) { |
| LOG_ERROR("failed to reserve private data slot"); |
| return; |
| } else { |
| LOG_DEBUG("txn_arg_idx: %d", arg_idx); |
| } |
| |
| Utils::init(&TSDebug, &TSError); |
| LOG_DEBUG("Plugin started"); |
| } |
| |
| /* |
| Handle TS_EVENT_HTTP_OS_DNS event after TS_EVENT_HTTP_POST_REMAP and |
| TS_EVENT_HTTP_CACHE_LOOKUP_COMPLETE in order to make this plugin |
| "per-remap configurable", that is, we enable combo for specific channels, |
| and disable for other channels. |
| |
| In yahoo's original code, this function handle TS_EVENT_HTTP_READ_REQUEST_HDR. |
| Because TS_EVENT_HTTP_READ_REQUEST_HDR is before TSRemapDoRemap, we can not |
| read "plugin_enable" flag in the READ_REQUEST_HDR event. |
| */ |
| static int |
| handleReadRequestHeader(TSCont /* contp ATS_UNUSED */, TSEvent event, void *edata) |
| { |
| TSHttpTxn txnp = static_cast<TSHttpTxn>(edata); |
| |
| if (event != TS_EVENT_HTTP_OS_DNS) { |
| LOG_ERROR("unknown event for this plugin %d", event); |
| return 0; |
| } |
| |
| if (1 != reinterpret_cast<intptr_t>(TSUserArgGet(txnp, arg_idx))) { |
| LOG_DEBUG("combo is disabled for this channel"); |
| TSHttpTxnReenable(txnp, TS_EVENT_HTTP_CONTINUE); |
| return 0; |
| } |
| |
| LOG_DEBUG("combo is enabled for this channel"); |
| LOG_DEBUG("handling TS_EVENT_HTTP_OS_DNS event"); |
| |
| TSEvent reenable_to_event = TS_EVENT_HTTP_CONTINUE; |
| TSMBuffer bufp; |
| TSMLoc hdr_loc; |
| |
| if (TSHttpTxnClientReqGet(txnp, &bufp, &hdr_loc) == TS_SUCCESS) { |
| TSMLoc url_loc; |
| if (TSHttpHdrUrlGet(bufp, hdr_loc, &url_loc) == TS_SUCCESS) { |
| if (isComboHandlerRequest(bufp, hdr_loc, url_loc)) { |
| TSCont contp = TSContCreate(handleServerEvent, TSMutexCreate()); |
| if (!contp) { |
| LOG_ERROR("[%s] Could not create intercept request", __FUNCTION__); |
| reenable_to_event = TS_EVENT_HTTP_ERROR; |
| } else { |
| TSHttpTxnServerIntercept(contp, txnp); |
| InterceptData *int_data = new InterceptData(contp); |
| TSContDataSet(contp, int_data); |
| // todo: check if these two cacheable sets are required |
| TSHttpTxnReqCacheableSet(txnp, 1); |
| TSHttpTxnRespCacheableSet(txnp, 1); |
| getClientRequest(txnp, bufp, hdr_loc, url_loc, int_data->creq); |
| LOG_DEBUG("Setup server intercept to handle client request"); |
| } |
| } |
| TSHandleMLocRelease(bufp, hdr_loc, url_loc); |
| } else { |
| LOG_ERROR("Could not get request URL"); |
| } |
| TSHandleMLocRelease(bufp, TS_NULL_MLOC, hdr_loc); |
| } else { |
| LOG_ERROR("Could not get client request"); |
| } |
| |
| TSHttpTxnReenable(txnp, reenable_to_event); |
| return 1; |
| } |
| |
| static bool |
| isComboHandlerRequest(TSMBuffer bufp, TSMLoc hdr_loc, TSMLoc url_loc) |
| { |
| int method_len; |
| bool retval = false; |
| const char *method = TSHttpHdrMethodGet(bufp, hdr_loc, &method_len); |
| |
| if (!method) { |
| LOG_ERROR("Could not obtain method!"); |
| } else { |
| if ((method_len != TS_HTTP_LEN_GET) || (strncasecmp(method, TS_HTTP_METHOD_GET, TS_HTTP_LEN_GET) != 0)) { |
| LOG_DEBUG("Unsupported method [%.*s]", method_len, method); |
| } else { |
| retval = true; |
| } |
| |
| if (retval) { |
| int path_len; |
| const char *path = TSUrlPathGet(bufp, url_loc, &path_len); |
| if (!path) { |
| LOG_ERROR("Could not get path from request URL"); |
| retval = false; |
| } else { |
| retval = (path_len == length(COMBO_HANDLER_PATH)) && |
| (strncasecmp(path, COMBO_HANDLER_PATH.data(), COMBO_HANDLER_PATH.size()) == 0); |
| LOG_DEBUG("Path [%.*s] is %s combo handler path", path_len, path, (retval ? "a" : "not a")); |
| } |
| } |
| } |
| return retval; |
| } |
| |
| static bool |
| getDefaultBucket(TSHttpTxn /* txnp ATS_UNUSED */, TSMBuffer bufp, TSMLoc hdr_obj, ClientRequest &creq) |
| { |
| LOG_DEBUG("In getDefaultBucket"); |
| TSMLoc field_loc; |
| const char *host; |
| int host_len = 0; |
| bool defaultBucketFound = false; |
| |
| field_loc = TSMimeHdrFieldFind(bufp, hdr_obj, TS_MIME_FIELD_HOST, -1); |
| if (field_loc == TS_NULL_MLOC) { |
| LOG_ERROR("Host field not found"); |
| return false; |
| } |
| |
| host = TSMimeHdrFieldValueStringGet(bufp, hdr_obj, field_loc, -1, &host_len); |
| if (!host || host_len <= 0) { |
| LOG_ERROR("Error Extracting Host Header"); |
| TSHandleMLocRelease(bufp, hdr_obj, field_loc); |
| return false; |
| } |
| |
| LOG_DEBUG("host: %.*s", host_len, host); |
| creq.defaultBucket = string(host, host_len); |
| defaultBucketFound = true; |
| |
| /* |
| for(int i = 0 ; i < host_len; i++) |
| { |
| if (host[i] == '.') |
| { |
| creq.defaultBucket = string(host, i); |
| defaultBucketFound = true; |
| break; |
| } |
| } |
| */ |
| |
| TSHandleMLocRelease(bufp, hdr_obj, field_loc); |
| |
| LOG_DEBUG("defaultBucket: %s", creq.defaultBucket.data()); |
| return defaultBucketFound; |
| } |
| |
| static void |
| getClientRequest(TSHttpTxn txnp, TSMBuffer bufp, TSMLoc hdr_loc, TSMLoc url_loc, ClientRequest &creq) |
| { |
| int query_len; |
| const char *query = TSUrlHttpQueryGet(bufp, url_loc, &query_len); |
| |
| if (!query) { |
| LOG_ERROR("Could not get query from request URL"); |
| creq.status = TS_HTTP_STATUS_BAD_REQUEST; |
| return; |
| } else { |
| if (!getDefaultBucket(txnp, bufp, hdr_loc, creq)) { |
| LOG_ERROR("failed getting Default Bucket for the request"); |
| return; |
| } |
| if (query_len > MAX_QUERY_LENGTH) { |
| creq.status = TS_HTTP_STATUS_BAD_REQUEST; |
| LOG_ERROR("querystring too long"); |
| return; |
| } |
| parseQueryParameters(query, query_len, creq); |
| creq.client_addr = TSHttpTxnClientAddrGet(txnp); |
| checkGzipAcceptance(bufp, hdr_loc, creq); |
| } |
| } |
| |
| static void |
| parseQueryParameters(const char *query, int query_len, ClientRequest &creq) |
| { |
| creq.status = TS_HTTP_STATUS_OK; |
| int param_start_pos = 0; |
| bool sig_verified = false; |
| int colon_pos = -1; |
| string file_url("http://localhost/"); |
| size_t file_base_url_size = file_url.size(); |
| const char *common_prefix = nullptr; |
| int common_prefix_size = 0; |
| const char *common_prefix_path = nullptr; |
| int common_prefix_path_size = 0; |
| |
| for (int i = 0; i <= query_len; ++i) { |
| if ((i == query_len) || (query[i] == '&')) { |
| int param_len = i - param_start_pos; |
| if (param_len) { |
| const char *param = query + param_start_pos; |
| if ((param_len >= 4) && (strncmp(param, "sig=", 4) == 0)) { |
| if (SIG_KEY_NAME.size()) { |
| if (!param_start_pos) { |
| LOG_DEBUG("Signature cannot be the first parameter in query [%.*s]", query_len, query); |
| } else if (param_len == 4) { |
| LOG_DEBUG("Signature empty in query [%.*s]", query_len, query); |
| } else { |
| // TODO - really verify the signature |
| LOG_DEBUG("Verified signature successfully"); |
| sig_verified = true; |
| } |
| if (!sig_verified) { |
| LOG_DEBUG("Signature [%.*s] on query [%.*s] is invalid", param_len - 4, param + 4, param_start_pos, query); |
| } |
| } else { |
| LOG_DEBUG("Verification not configured, ignoring signature"); |
| } |
| break; // nothing useful after the signature |
| } |
| if ((param_len >= 2) && (param[0] == 'p') && (param[1] == '=')) { |
| common_prefix_size = param_len - 2; |
| common_prefix_path_size = 0; |
| if (common_prefix_size) { |
| common_prefix = param + 2; |
| for (int i = 0; i < common_prefix_size; ++i) { |
| if (common_prefix[i] == ':') { |
| common_prefix_path = common_prefix; |
| common_prefix_path_size = i; |
| ++i; // go beyond the ':' |
| common_prefix += i; |
| common_prefix_size -= i; |
| break; |
| } |
| } |
| } |
| LOG_DEBUG("Common prefix is [%.*s], common prefix path is [%.*s]", common_prefix_size, common_prefix, |
| common_prefix_path_size, common_prefix_path); |
| } else { |
| if (common_prefix_path_size) { |
| if (colon_pos >= param_start_pos) { // we have a colon in this param as well? |
| LOG_ERROR("Ambiguous 'bucket': [%.*s] specified in common prefix and [%.*s] specified in " |
| "current parameter [%.*s]", |
| common_prefix_path_size, common_prefix_path, colon_pos - param_start_pos, param, param_len, param); |
| creq.file_urls.clear(); |
| break; |
| } |
| file_url.append(common_prefix_path, common_prefix_path_size); |
| } else if (colon_pos >= param_start_pos) { // we have a colon |
| if ((colon_pos == param_start_pos) || (colon_pos == (i - 1))) { |
| LOG_ERROR("Colon-separated path [%.*s] has empty part(s)", param_len, param); |
| creq.file_urls.clear(); |
| break; |
| } |
| file_url.append(param, colon_pos - param_start_pos); // appending pre ':' part first |
| |
| // modify these to point to the "actual" file path |
| param_start_pos = colon_pos + 1; |
| param_len = i - param_start_pos; |
| param = query + param_start_pos; |
| } else { |
| file_url += creq.defaultBucket; // default path |
| } |
| file_url += '/'; |
| if (common_prefix_size) { |
| file_url.append(common_prefix, common_prefix_size); |
| } |
| file_url.append(param, param_len); |
| creq.file_urls.push_back(file_url); |
| LOG_DEBUG("Added file path [%s]", file_url.c_str()); |
| file_url.resize(file_base_url_size); |
| } |
| } |
| param_start_pos = i + 1; |
| } else if (query[i] == ':') { |
| colon_pos = i; |
| } |
| } |
| if (!creq.file_urls.size()) { |
| creq.status = TS_HTTP_STATUS_BAD_REQUEST; |
| } else if (SIG_KEY_NAME.size() && !sig_verified) { |
| LOG_DEBUG("Invalid/empty signature found; Need valid signature"); |
| creq.status = TS_HTTP_STATUS_FORBIDDEN; |
| creq.file_urls.clear(); |
| } |
| |
| if (creq.file_urls.size() > MaxFileCount) { |
| creq.status = TS_HTTP_STATUS_BAD_REQUEST; |
| LOG_ERROR("too many files in url"); |
| creq.file_urls.clear(); |
| } |
| } |
| |
| static void |
| checkGzipAcceptance(TSMBuffer bufp, TSMLoc hdr_loc, ClientRequest &creq) |
| { |
| creq.gzip_accepted = false; |
| TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, TS_MIME_FIELD_ACCEPT_ENCODING, TS_MIME_LEN_ACCEPT_ENCODING); |
| if (field_loc != TS_NULL_MLOC) { |
| const char *value; |
| int value_len; |
| int n_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); |
| |
| for (int i = 0; i < n_values; ++i) { |
| value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, i, &value_len); |
| if (value) { |
| if ((value_len == TS_HTTP_LEN_GZIP) && (strncasecmp(value, TS_HTTP_VALUE_GZIP, value_len) == 0)) { |
| creq.gzip_accepted = true; |
| } |
| } else { |
| LOG_DEBUG("Error while getting value # %d of header [%.*s]", i, TS_MIME_LEN_ACCEPT_ENCODING, TS_MIME_FIELD_ACCEPT_ENCODING); |
| } |
| if (creq.gzip_accepted) { |
| break; |
| } |
| } |
| TSHandleMLocRelease(bufp, hdr_loc, field_loc); |
| } |
| LOG_DEBUG("Client %s gzip encoding", (creq.gzip_accepted ? "accepts" : "does not accept")); |
| } |
| |
| static int |
| handleServerEvent(TSCont contp, TSEvent event, void *edata) |
| { |
| InterceptData *int_data = static_cast<InterceptData *>(TSContDataGet(contp)); |
| bool write_response = false; |
| |
| switch (event) { |
| case TS_EVENT_NET_ACCEPT_FAILED: |
| LOG_DEBUG("Received net accept failed event; going to abort continuation"); |
| int_data->read_complete = int_data->write_complete = true; |
| break; |
| |
| case TS_EVENT_NET_ACCEPT: |
| LOG_DEBUG("Received net accept event"); |
| if (!initRequestProcessing(*int_data, edata, write_response)) { |
| LOG_ERROR("Could not initialize request processing"); |
| return 0; |
| } |
| break; |
| |
| case TS_EVENT_VCONN_READ_READY: |
| LOG_DEBUG("Received read ready event"); |
| if (!readInterceptRequest(*int_data)) { |
| LOG_ERROR("Error while reading from input vio"); |
| return 0; |
| } |
| break; |
| |
| case TS_EVENT_VCONN_READ_COMPLETE: |
| case TS_EVENT_VCONN_EOS: |
| LOG_DEBUG("Received read complete/eos event %d", event); |
| int_data->read_complete = true; |
| break; |
| |
| case TS_EVENT_VCONN_WRITE_READY: |
| LOG_DEBUG("Received write ready event"); |
| break; |
| |
| case TS_EVENT_VCONN_WRITE_COMPLETE: |
| LOG_DEBUG("Received write complete event"); |
| int_data->write_complete = true; |
| break; |
| |
| case TS_EVENT_ERROR: |
| LOG_ERROR("Received error event!"); |
| break; |
| |
| default: |
| if (int_data->fetcher && int_data->fetcher->isFetchEvent(event)) { |
| if (!int_data->fetcher->handleFetchEvent(event, edata)) { |
| LOG_ERROR("Couldn't handle fetch request event %d", event); |
| } |
| write_response = int_data->fetcher->isFetchComplete(); |
| } else { |
| LOG_DEBUG("Unexpected event %d", event); |
| } |
| break; |
| } |
| |
| if (write_response) { |
| if (!writeResponse(*int_data)) { |
| LOG_ERROR("Couldn't write response"); |
| int_data->write_complete = true; |
| } else { |
| LOG_DEBUG("Wrote response successfully"); |
| } |
| } |
| |
| if (int_data->read_complete && int_data->write_complete) { |
| LOG_DEBUG("Completed request processing, shutting down"); |
| delete int_data; |
| TSContDestroy(contp); |
| } |
| |
| return 1; |
| } |
| |
| static bool |
| initRequestProcessing(InterceptData &int_data, void *edata, bool &write_response) |
| { |
| TSAssert(int_data.initialized == false); |
| if (!int_data.init(static_cast<TSVConn>(edata))) { |
| LOG_ERROR("Could not initialize intercept data!"); |
| return false; |
| } |
| |
| if (int_data.creq.status == TS_HTTP_STATUS_OK) { |
| for (StringList::iterator iter = int_data.creq.file_urls.begin(); iter != int_data.creq.file_urls.end(); ++iter) { |
| if (!int_data.fetcher->addFetchRequest(*iter)) { |
| LOG_ERROR("Couldn't add fetch request for URL [%s]", iter->c_str()); |
| } else { |
| LOG_DEBUG("Added fetch request for URL [%s]", iter->c_str()); |
| } |
| } |
| } else { |
| LOG_DEBUG("Client request status [%d] not ok; Not fetching URLs", int_data.creq.status); |
| write_response = true; |
| } |
| return true; |
| } |
| |
| static bool |
| readInterceptRequest(InterceptData &int_data) |
| { |
| TSAssert(!int_data.read_complete); |
| int avail = TSIOBufferReaderAvail(int_data.input.reader); |
| if (avail == TS_ERROR) { |
| LOG_ERROR("Error while getting number of bytes available"); |
| return false; |
| } |
| |
| int consumed = 0; |
| if (avail > 0) { |
| int64_t data_len; |
| const char *data; |
| TSIOBufferBlock block = TSIOBufferReaderStart(int_data.input.reader); |
| while (block != nullptr) { |
| data = TSIOBufferBlockReadStart(block, int_data.input.reader, &data_len); |
| const char *endptr = data + data_len; |
| if (TSHttpHdrParseReq(int_data.http_parser, int_data.req_hdr_bufp, int_data.req_hdr_loc, &data, endptr) == TS_PARSE_DONE) { |
| int_data.read_complete = true; |
| } |
| consumed += data_len; |
| block = TSIOBufferBlockNext(block); |
| } |
| } |
| LOG_DEBUG("Consumed %d bytes from input vio", consumed); |
| |
| TSIOBufferReaderConsume(int_data.input.reader, consumed); |
| |
| // Modify the input VIO to reflect how much data we've completed. |
| TSVIONDoneSet(int_data.input.vio, TSVIONDoneGet(int_data.input.vio) + consumed); |
| |
| if (!int_data.read_complete) { |
| LOG_DEBUG("Re-enabling input VIO as request header not completely read yet"); |
| TSVIOReenable(int_data.input.vio); |
| } |
| return true; |
| } |
| |
| static const string OK_REPLY_LINE("HTTP/1.0 200 OK\r\n"); |
| static const string BAD_REQUEST_RESPONSE("HTTP/1.0 400 Bad Request\r\n\r\n"); |
| static const string ERROR_REPLY_RESPONSE("HTTP/1.0 500 Internal Server Error\r\n\r\n"); |
| static const string FORBIDDEN_RESPONSE("HTTP/1.0 403 Forbidden\r\n\r\n"); |
| static const char GZIP_ENCODING_FIELD[] = {"Content-Encoding: gzip\r\n"}; |
| static const int GZIP_ENCODING_FIELD_SIZE = sizeof(GZIP_ENCODING_FIELD) - 1; |
| |
| static bool |
| writeResponse(InterceptData &int_data) |
| { |
| int_data.setupWrite(); |
| |
| ByteBlockList body_blocks; |
| string resp_header_fields; |
| prepareResponse(int_data, body_blocks, resp_header_fields); |
| |
| int n_bytes_written = 0; |
| if (int_data.creq.status != TS_HTTP_STATUS_OK) { |
| if (!writeErrorResponse(int_data, n_bytes_written)) { |
| LOG_ERROR("Couldn't write response error"); |
| return false; |
| } |
| } else { |
| n_bytes_written = OK_REPLY_LINE.size(); |
| if (TSIOBufferWrite(int_data.output.buffer, OK_REPLY_LINE.data(), n_bytes_written) == TS_ERROR) { |
| LOG_ERROR("Error while writing reply line"); |
| return false; |
| } |
| |
| if (!writeStandardHeaderFields(int_data, n_bytes_written)) { |
| LOG_ERROR("Could not write standard header fields"); |
| return false; |
| } |
| |
| if (resp_header_fields.size()) { |
| if (TSIOBufferWrite(int_data.output.buffer, resp_header_fields.data(), resp_header_fields.size()) == TS_ERROR) { |
| LOG_ERROR("Error while writing additional response header fields"); |
| return false; |
| } |
| n_bytes_written += resp_header_fields.size(); |
| } |
| |
| if (TSIOBufferWrite(int_data.output.buffer, "\r\n", 2) == TS_ERROR) { |
| LOG_ERROR("Error while writing header terminator"); |
| return false; |
| } |
| n_bytes_written += 2; |
| |
| for (ByteBlockList::iterator iter = body_blocks.begin(); iter != body_blocks.end(); ++iter) { |
| if (TSIOBufferWrite(int_data.output.buffer, iter->data, iter->data_len) == TS_ERROR) { |
| LOG_ERROR("Error while writing content"); |
| return false; |
| } |
| n_bytes_written += iter->data_len; |
| } |
| } |
| |
| LOG_DEBUG("Wrote reply of size %d", n_bytes_written); |
| TSVIONBytesSet(int_data.output.vio, n_bytes_written); |
| |
| TSVIOReenable(int_data.output.vio); |
| return true; |
| } |
| |
| static void |
| prepareResponse(InterceptData &int_data, ByteBlockList &body_blocks, string &resp_header_fields) |
| { |
| if (int_data.creq.status == TS_HTTP_STATUS_OK) { |
| HttpDataFetcherImpl::ResponseData resp_data; |
| TSMLoc field_loc; |
| time_t expires_time; |
| bool got_expires_time = false; |
| int num_headers = HEADER_ALLOWLIST.size(); |
| int flags_list[num_headers]; |
| CacheControlHeader cch; |
| |
| for (int i = 0; i < num_headers; i++) { |
| flags_list[i] = 0; |
| } |
| |
| ContentTypeHandler cth(resp_header_fields); |
| |
| for (StringList::iterator iter = int_data.creq.file_urls.begin(); iter != int_data.creq.file_urls.end(); ++iter) { |
| if (int_data.fetcher->getData(*iter, resp_data) && resp_data.status == TS_HTTP_STATUS_OK) { |
| body_blocks.push_back(ByteBlock(resp_data.content, resp_data.content_len)); |
| if (find(HEADER_ALLOWLIST.begin(), HEADER_ALLOWLIST.end(), TS_MIME_FIELD_CONTENT_TYPE) == HEADER_ALLOWLIST.end()) { |
| if (!cth.nextObjectHeader(resp_data.bufp, resp_data.hdr_loc)) { |
| LOG_ERROR("Content type missing or forbidden for requested URL [%s]", iter->c_str()); |
| int_data.creq.status = TS_HTTP_STATUS_FORBIDDEN; |
| break; |
| } |
| } |
| |
| // Load this document's Cache-Control header into our managing object |
| cch.update(resp_data.bufp, resp_data.hdr_loc); |
| |
| field_loc = TSMimeHdrFieldFind(resp_data.bufp, resp_data.hdr_loc, TS_MIME_FIELD_EXPIRES, TS_MIME_LEN_EXPIRES); |
| if (field_loc != TS_NULL_MLOC) { |
| time_t curr_field_expires_time; |
| int n_values = TSMimeHdrFieldValuesCount(resp_data.bufp, resp_data.hdr_loc, field_loc); |
| if ((n_values != TS_ERROR) && (n_values > 0)) { |
| curr_field_expires_time = TSMimeHdrFieldValueDateGet(resp_data.bufp, resp_data.hdr_loc, field_loc); |
| if (!got_expires_time) { |
| expires_time = curr_field_expires_time; |
| got_expires_time = true; |
| } else if (curr_field_expires_time < expires_time) { |
| expires_time = curr_field_expires_time; |
| } |
| } |
| TSHandleMLocRelease(resp_data.bufp, resp_data.hdr_loc, field_loc); |
| } |
| |
| for (int i = 0; i < num_headers; i++) { |
| if (flags_list[i]) { |
| continue; |
| } |
| |
| const string &header = HEADER_ALLOWLIST[i]; |
| |
| field_loc = TSMimeHdrFieldFind(resp_data.bufp, resp_data.hdr_loc, header.c_str(), header.size()); |
| if (field_loc != TS_NULL_MLOC) { |
| bool values_added = false; |
| const char *value; |
| int value_len; |
| int n_values = TSMimeHdrFieldValuesCount(resp_data.bufp, resp_data.hdr_loc, field_loc); |
| if ((n_values != TS_ERROR) && (n_values > 0)) { |
| for (int k = 0; k < n_values; k++) { |
| value = TSMimeHdrFieldValueStringGet(resp_data.bufp, resp_data.hdr_loc, field_loc, k, &value_len); |
| if (!values_added) { |
| resp_header_fields.append(header + ": "); |
| values_added = true; |
| } else { |
| resp_header_fields.append(", "); |
| } |
| resp_header_fields.append(value, value_len); |
| } |
| if (values_added) { |
| resp_header_fields.append("\r\n"); |
| flags_list[i] = 1; |
| } |
| } |
| TSHandleMLocRelease(resp_data.bufp, resp_data.hdr_loc, field_loc); |
| } |
| } |
| |
| } else { |
| LOG_ERROR("Could not get content for requested URL [%s]", iter->c_str()); |
| int_data.creq.status = TS_HTTP_STATUS_BAD_REQUEST; |
| break; |
| } |
| } |
| if (int_data.creq.status == TS_HTTP_STATUS_OK) { |
| // Add in Cache-Control header |
| if (find(HEADER_ALLOWLIST.begin(), HEADER_ALLOWLIST.end(), TS_MIME_FIELD_CACHE_CONTROL) == HEADER_ALLOWLIST.end()) { |
| resp_header_fields.append(cch.generate()); |
| } |
| if (find(HEADER_ALLOWLIST.begin(), HEADER_ALLOWLIST.end(), TS_MIME_FIELD_EXPIRES) == HEADER_ALLOWLIST.end()) { |
| if (got_expires_time) { |
| if (expires_time <= 0) { |
| resp_header_fields.append("Expires: 0\r\n"); |
| } else { |
| char line_buf[128]; |
| struct tm gm_expires_time; |
| int line_size = strftime(line_buf, 128, "Expires: %a, %d %b %Y %T GMT\r\n", gmtime_r(&expires_time, &gm_expires_time)); |
| resp_header_fields.append(line_buf, line_size); |
| } |
| } |
| } |
| LOG_DEBUG("Prepared response header field\n%s", resp_header_fields.c_str()); |
| } |
| } |
| |
| if ((int_data.creq.status == TS_HTTP_STATUS_OK) && int_data.creq.gzip_accepted) { |
| if (!gzip(body_blocks, int_data.gzipped_data)) { |
| LOG_ERROR("Could not gzip content!"); |
| int_data.creq.status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR; |
| } else { |
| body_blocks.clear(); |
| body_blocks.push_back(ByteBlock(int_data.gzipped_data.data(), int_data.gzipped_data.size())); |
| resp_header_fields.append(GZIP_ENCODING_FIELD, GZIP_ENCODING_FIELD_SIZE); |
| } |
| } |
| } |
| |
| bool |
| ContentTypeHandler::nextObjectHeader(TSMBuffer bufp, TSMLoc hdr_loc) |
| { |
| TSMLoc field_loc = TSMimeHdrFieldFind(bufp, hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE); |
| if (field_loc != TS_NULL_MLOC) { |
| bool values_added = false; |
| const char *value; |
| int value_len; |
| int n_values = TSMimeHdrFieldValuesCount(bufp, hdr_loc, field_loc); |
| for (int i = 0; i < n_values; ++i) { |
| value = TSMimeHdrFieldValueStringGet(bufp, hdr_loc, field_loc, i, &value_len); |
| ts::TextView tv{value, value_len}; |
| tv = tv.prefix(';').rtrim(std::string_view(" \t")); |
| if (_content_type_allowlist.empty()) { |
| ; |
| } else if (std::find_if(_content_type_allowlist.begin(), _content_type_allowlist.end(), [tv](ts::TextView tv2) -> bool { |
| return strcasecmp(tv, tv2) == 0; |
| }) == _content_type_allowlist.end()) { |
| return false; |
| } else if (tv.empty()) { |
| // allowlist is bad, contains an empty string. |
| return false; |
| } |
| if (!_added_content_type) { |
| if (!values_added) { |
| _resp_header_fields.append("Content-Type: "); |
| values_added = true; |
| } else { |
| _resp_header_fields.append(", "); |
| } |
| _resp_header_fields.append(value, value_len); |
| } |
| } |
| TSHandleMLocRelease(bufp, hdr_loc, field_loc); |
| if (values_added) { |
| _resp_header_fields.append("\r\n"); |
| |
| // Assume that the Content-type field from the first header covers all the responses being combined. |
| _added_content_type = true; |
| } |
| return true; |
| } |
| // No content type header field so doesn't pass allowlist if there is one. |
| return _content_type_allowlist.empty(); |
| } |
| |
| void |
| ContentTypeHandler::loadAllowList(std::string const &file_spec) |
| { |
| std::fstream fs; |
| char line_buffer[256]; |
| bool extra_junk_on_line{false}; |
| int line_num = 0; |
| |
| fs.open(file_spec); |
| if (fs.good()) { |
| for (;;) { |
| ++line_num; |
| fs.getline(line_buffer, sizeof(line_buffer)); |
| if (!fs.good()) { |
| break; |
| } |
| constexpr std::string_view bs{" \t"sv}; |
| ts::TextView line{line_buffer, std::size_t(fs.gcount() - 1)}; |
| line.ltrim(bs); |
| if (line.empty() || line[0] == '#') { |
| // Empty/comment line. |
| continue; |
| } |
| ts::TextView content_type{line.take_prefix_at(bs)}; |
| line.trim(bs); |
| if (line.size() && (line[0] != '#')) { |
| extra_junk_on_line = true; |
| break; |
| } |
| _content_type_allowlist.emplace_back(content_type); |
| } |
| } |
| if (fs.fail() && !(fs.eof() && (fs.gcount() == 0))) { |
| LOG_ERROR("Error reading Content-Type allowlist config file %s, line %d", file_spec.c_str(), line_num); |
| } else if (extra_junk_on_line) { |
| LOG_ERROR("More than one type on line %d in Content-Type allowlist config file %s", line_num, file_spec.c_str()); |
| } else if (_content_type_allowlist.empty()) { |
| LOG_ERROR("Content-type allowlist config file %s must have at least one entry", file_spec.c_str()); |
| } else { |
| // End of file. |
| return; |
| } |
| _content_type_allowlist.clear(); |
| // An empty string marks object as bad. |
| _content_type_allowlist.emplace_back(""); |
| } |
| |
| static const char INVARIANT_FIELD_LINES[] = {"Vary: Accept-Encoding\r\n"}; |
| static const char INVARIANT_FIELD_LINES_SIZE = sizeof(INVARIANT_FIELD_LINES) - 1; |
| |
| static bool |
| writeStandardHeaderFields(InterceptData &int_data, int &n_bytes_written) |
| { |
| if (find(HEADER_ALLOWLIST.begin(), HEADER_ALLOWLIST.end(), TS_MIME_FIELD_VARY) == HEADER_ALLOWLIST.end()) { |
| if (TSIOBufferWrite(int_data.output.buffer, INVARIANT_FIELD_LINES, INVARIANT_FIELD_LINES_SIZE) == TS_ERROR) { |
| LOG_ERROR("Error while writing invariant fields"); |
| return false; |
| } |
| n_bytes_written += INVARIANT_FIELD_LINES_SIZE; |
| } |
| |
| if (find(HEADER_ALLOWLIST.begin(), HEADER_ALLOWLIST.end(), TS_MIME_FIELD_LAST_MODIFIED) == HEADER_ALLOWLIST.end()) { |
| time_t time_now = static_cast<time_t>(TShrtime() / 1000000000); // it returns nanoseconds! |
| char last_modified_line[128]; |
| struct tm gmnow; |
| int last_modified_line_size = |
| strftime(last_modified_line, 128, "Last-Modified: %a, %d %b %Y %T GMT\r\n", gmtime_r(&time_now, &gmnow)); |
| if (TSIOBufferWrite(int_data.output.buffer, last_modified_line, last_modified_line_size) == TS_ERROR) { |
| LOG_ERROR("Error while writing last-modified fields"); |
| return false; |
| } |
| n_bytes_written += last_modified_line_size; |
| } |
| |
| return true; |
| } |
| |
| static bool |
| writeErrorResponse(InterceptData &int_data, int &n_bytes_written) |
| { |
| const string *response; |
| switch (int_data.creq.status) { |
| case TS_HTTP_STATUS_BAD_REQUEST: |
| response = &BAD_REQUEST_RESPONSE; |
| break; |
| case TS_HTTP_STATUS_FORBIDDEN: |
| response = &FORBIDDEN_RESPONSE; |
| break; |
| default: |
| response = &ERROR_REPLY_RESPONSE; |
| break; |
| } |
| if (TSIOBufferWrite(int_data.output.buffer, response->data(), response->size()) == TS_ERROR) { |
| LOG_ERROR("Error while writing error response"); |
| return false; |
| } |
| n_bytes_written += response->size(); |
| return true; |
| } |
| |
| TSRemapStatus |
| TSRemapDoRemap(void *ih, TSHttpTxn rh, TSRemapRequestInfo *rri) |
| { |
| TSUserArgSet(rh, arg_idx, (void *)1); /* Save for later hooks */ |
| return TSREMAP_NO_REMAP; /* Continue with next remap plugin in chain */ |
| } |
| |
| /* |
| Initialize the plugin as a remap plugin. |
| */ |
| TSReturnCode |
| TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size) |
| { |
| if (!api_info) { |
| strncpy(errbuf, "[TSRemapInit] - Invalid TSRemapInterface argument", errbuf_size - 1); |
| return TS_ERROR; |
| } |
| |
| if (api_info->size < sizeof(TSRemapInterface)) { |
| strncpy(errbuf, "[TSRemapInit] - Incorrect size of TSRemapInterface structure", errbuf_size - 1); |
| return TS_ERROR; |
| } |
| |
| TSDebug(DEBUG_TAG, "%s plugin's remap part is initialized", DEBUG_TAG); |
| |
| return TS_SUCCESS; |
| } |
| |
| TSReturnCode |
| TSRemapNewInstance(int argc, char *argv[], void **ih, char *errbuf, int errbuf_size) |
| { |
| *ih = nullptr; |
| |
| TSDebug(DEBUG_TAG, "%s Remap Instance for '%s' created", DEBUG_TAG, argv[0]); |
| return TS_SUCCESS; |
| } |
| |
| void |
| TSRemapDeleteInstance(void *ih) |
| { |
| return; |
| } |