1 /** @file
2
3 A plugin to parse Link headers from an origin server's response and initiate H2 Server Push for preload links.
4
5 @section license License
6
7 Licensed to the Apache Software Foundation (ASF) under one
8 or more contributor license agreements. See the NOTICE file
9 distributed with this work for additional information
10 regarding copyright ownership. The ASF licenses this file
11 to you under the Apache License, Version 2.0 (the
12 "License"); you may not use this file except in compliance
13 with the License. You may obtain a copy of the License at
14
15 http://www.apache.org/licenses/LICENSE-2.0
16
17 Unless required by applicable law or agreed to in writing, software
18 distributed under the License is distributed on an "AS IS" BASIS,
19 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 See the License for the specific language governing permissions and
21 limitations under the License.
22 */
23
24 #include <iostream>
25 #include <regex>
26 #include <set>
27 #include <sstream>
28 #include <ts/ts.h>
29 #include "tscpp/api/GlobalPlugin.h"
30 #include "tscpp/api/RemapPlugin.h"
31 #include "tscpp/api/TransactionPlugin.h"
32
33 #define PLUGIN_NAME "server_push_preload"
34 #define PRELOAD_PARAM "rel=preload"
35 #define NOPUSH_OPTION "nopush"
36
37 using namespace std;
38 using namespace atscppapi;
39
40 static regex linkRegexp("<([^>]+)>;(.+)");
41
42 namespace
43 {
44 GlobalPlugin *globalPlugin;
45 RemapPlugin *remapPlugin;
46 } // namespace
47
48 class ServerPushTransaction : public TransactionPlugin
49 {
50 public:
ServerPushTransaction(Transaction & transaction)51 explicit ServerPushTransaction(Transaction &transaction) : TransactionPlugin(transaction)
52 {
53 TransactionPlugin::registerHook(HOOK_SEND_RESPONSE_HEADERS);
54 }
55
56 void
handleSendResponseHeaders(Transaction & transaction)57 handleSendResponseHeaders(Transaction &transaction) override
58 {
59 serverPush(transaction);
60 transaction.resume();
61 }
62
63 void
serverPush(Transaction & transaction)64 serverPush(Transaction &transaction)
65 {
66 TSHttpTxn txnp = static_cast<TSHttpTxn>(transaction.getAtsHandle());
67 if (TSHttpTxnClientProtocolStackContains(txnp, "h2") == nullptr) {
68 return;
69 }
70
71 ClientRequest &request = transaction.getClientRequest();
72 Response &response = transaction.getClientResponse();
73 Headers &headers = response.getHeaders();
74
75 const Url &clientUrl = request.getPristineUrl();
76
77 for (header_field_iterator it = headers.find("Link"); it != headers.end(); it.nextDup()) {
78 HeaderField field = *it;
79
80 for (header_field_value_iterator hit = field.begin(); hit != field.end(); ++hit) {
81 const string &link = *hit;
82
83 TSDebug(PLUGIN_NAME, "Parsing link header: %s", link.c_str());
84 smatch matches;
85
86 if (regex_search(link, matches, linkRegexp)) {
87 string url = matches[1].str();
88 TSDebug(PLUGIN_NAME, "Found link header match: %s", url.c_str());
89
90 set<string> params = split(matches[2].str(), ';');
91 auto preload = params.find(PRELOAD_PARAM);
92 if (preload == params.end()) {
93 continue;
94 }
95
96 auto noPush = params.find(NOPUSH_OPTION);
97 if (noPush != params.end()) {
98 TSDebug(PLUGIN_NAME, "Skipping nopush link: %s", link.c_str());
99 continue;
100 }
101
102 Request request(url);
103 Url &linkUrl = request.getUrl();
104
105 if (linkUrl.getHost().empty()) {
106 linkUrl.setHost(clientUrl.getHost());
107 linkUrl.setScheme(clientUrl.getScheme());
108 }
109 if (0 != clientUrl.getPort()) {
110 linkUrl.setPort(clientUrl.getPort());
111 }
112 string lu = linkUrl.getUrlString();
113 TSDebug(PLUGIN_NAME, "Push preloaded content: %s", lu.c_str());
114 TSHttpTxnServerPush(txnp, lu.c_str(), lu.length());
115 } else {
116 TSDebug(PLUGIN_NAME, "No match found for link header: %s", link.c_str());
117 }
118 }
119 }
120 }
121
122 set<string>
split(const string & params,char delim)123 split(const string ¶ms, char delim)
124 {
125 stringstream ss(params);
126 string s;
127 set<string> tokens;
128 while (getline(ss, s, delim)) {
129 s.erase(find_if(s.rbegin(), s.rend(), [](unsigned char c) { return !std::isspace(c); }).base(), s.end()); // trim left
130 s.erase(s.begin(), find_if(s.begin(), s.end(), [](unsigned char c) { return !std::isspace(c); })); // trim right
131 tokens.insert(s);
132 }
133 return tokens;
134 }
135 };
136
137 class ServerPushRemap : public RemapPlugin
138 {
139 public:
ServerPushRemap(void ** instance_handle)140 explicit ServerPushRemap(void **instance_handle) : RemapPlugin(instance_handle) {}
141
142 Result
doRemap(const Url & map_from_url,const Url & map_to_url,Transaction & transaction,bool & redirect)143 doRemap(const Url &map_from_url, const Url &map_to_url, Transaction &transaction, bool &redirect) override
144 {
145 transaction.addPlugin(new ServerPushTransaction(transaction));
146 return RESULT_DID_REMAP;
147 }
148 };
149
150 class ServerPushGlobal : public GlobalPlugin
151 {
152 public:
ServerPushGlobal()153 ServerPushGlobal() { GlobalPlugin::registerHook(HOOK_READ_REQUEST_HEADERS_PRE_REMAP); }
154
155 void
handleReadRequestHeadersPreRemap(Transaction & transaction)156 handleReadRequestHeadersPreRemap(Transaction &transaction) override
157 {
158 transaction.addPlugin(new ServerPushTransaction(transaction));
159 transaction.resume();
160 }
161 };
162
163 void
TSPluginInit(int argc ATSCPPAPI_UNUSED,const char * argv[]ATSCPPAPI_UNUSED)164 TSPluginInit(int argc ATSCPPAPI_UNUSED, const char *argv[] ATSCPPAPI_UNUSED)
165 {
166 TSDebug(PLUGIN_NAME, "Init");
167 if (!RegisterGlobalPlugin("ServerPushPreloadPlugin", PLUGIN_NAME, "dev@trafficserver.apache.org")) {
168 return;
169 }
170 globalPlugin = new ServerPushGlobal();
171 }
172
173 TSReturnCode
TSRemapNewInstance(int argc ATSCPPAPI_UNUSED,char * argv[]ATSCPPAPI_UNUSED,void ** instance_handle,char * errbuf ATSCPPAPI_UNUSED,int errbuf_size ATSCPPAPI_UNUSED)174 TSRemapNewInstance(int argc ATSCPPAPI_UNUSED, char *argv[] ATSCPPAPI_UNUSED, void **instance_handle, char *errbuf ATSCPPAPI_UNUSED,
175 int errbuf_size ATSCPPAPI_UNUSED)
176 {
177 TSDebug(PLUGIN_NAME, "New Instance");
178 remapPlugin = new ServerPushRemap(instance_handle);
179 return TS_SUCCESS;
180 }