1 /** @file
2 
3   This is a simple URL signature generator for AWS S3 services.
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 #include <ctime>
24 #include <cstring>
25 #include <getopt.h>
26 #include <sys/time.h>
27 
28 #include <cstdio>
29 #include <cstdlib>
30 #include <climits>
31 #include <cctype>
32 
33 #include <fstream> /* std::ifstream */
34 #include <string>
35 #include <unordered_map>
36 
37 #include <openssl/sha.h>
38 #include <openssl/hmac.h>
39 
40 #include <chrono>
41 #include <atomic>
42 #include <thread>
43 #include <mutex>
44 #include <shared_mutex>
45 
46 #include <ts/ts.h>
47 #include <ts/remap.h>
48 #include "tscore/ink_config.h"
49 
50 #include "aws_auth_v4.h"
51 
52 ///////////////////////////////////////////////////////////////////////////////
53 // Some constants.
54 //
55 static const char PLUGIN_NAME[] = "s3_auth";
56 static const char DATE_FMT[]    = "%a, %d %b %Y %H:%M:%S %z";
57 
58 /**
59  * @brief Rebase a relative path onto the configuration directory.
60  */
61 static String
makeConfigPath(const String & path)62 makeConfigPath(const String &path)
63 {
64   if (path.empty() || path[0] == '/') {
65     return path;
66   }
67 
68   return String(TSConfigDirGet()) + "/" + path;
69 }
70 
71 /**
72  * @brief a helper function which loads the entry-point to region from files.
73  * @param args classname + filename in '<classname>:<filename>' format.
74  * @return true if successful, false otherwise.
75  */
76 static bool
loadRegionMap(StringMap & m,const String & filename)77 loadRegionMap(StringMap &m, const String &filename)
78 {
79   static const char *EXPECTED_FORMAT = "<s3-entry-point>:<s3-region>";
80 
81   String path(makeConfigPath(filename));
82 
83   std::ifstream ifstr;
84   String line;
85 
86   ifstr.open(path.c_str());
87   if (!ifstr) {
88     TSError("[%s] failed to load s3-region map from '%s'", PLUGIN_NAME, path.c_str());
89     return false;
90   }
91 
92   TSDebug(PLUGIN_NAME, "loading region mapping from '%s'", path.c_str());
93 
94   m[""] = ""; /* set a default just in case if the user does not specify it */
95 
96   while (std::getline(ifstr, line)) {
97     String::size_type pos;
98 
99     // Allow #-prefixed comments.
100     pos = line.find_first_of('#');
101     if (pos != String::npos) {
102       line.resize(pos);
103     }
104 
105     if (line.empty()) {
106       continue;
107     }
108 
109     std::size_t d = line.find(':');
110     if (String::npos == d) {
111       TSError("[%s] failed to parse region map string '%s', expected format: '%s'", PLUGIN_NAME, line.c_str(), EXPECTED_FORMAT);
112       return false;
113     }
114 
115     String entrypoint(trimWhiteSpaces(String(line, 0, d)));
116     String region(trimWhiteSpaces(String(line, d + 1, String::npos)));
117 
118     if (region.empty()) {
119       TSDebug(PLUGIN_NAME, "<s3-region> in '%s' cannot be empty (skipped), expected format: '%s'", line.c_str(), EXPECTED_FORMAT);
120       continue;
121     }
122 
123     if (entrypoint.empty()) {
124       TSDebug(PLUGIN_NAME, "added default region %s", region.c_str());
125     } else {
126       TSDebug(PLUGIN_NAME, "added entry-point:%s, region:%s", entrypoint.c_str(), region.c_str());
127     }
128 
129     m[entrypoint] = region;
130   }
131 
132   if (m.at("").empty()) {
133     TSDebug(PLUGIN_NAME, "default region was not defined");
134   }
135 
136   ifstr.close();
137   return true;
138 }
139 
140 ///////////////////////////////////////////////////////////////////////////////
141 // Cache for the secrets file, to avoid reading / loading them repeatedly on
142 // a reload of remap.config. This gets cached for 60s (not configurable).
143 //
144 class S3Config;
145 
146 class ConfigCache
147 {
148 public:
149   S3Config *get(const char *fname);
150 
151 private:
152   struct _ConfigData {
153     // This is incremented before and after cnf and load_time are set.
154     // Thus, an odd value indicates an update is in progress.
155     std::atomic<unsigned> update_status{0};
156 
157     // A config from a file and the last time it was loaded.
158     // config should be written before load_time.  That way,
159     // if config is read after load_time, the load time will
160     // never indicate config is fresh when it isn't.
161     std::atomic<S3Config *> config;
162     std::atomic<time_t> load_time;
163 
_ConfigDataConfigCache::_ConfigData164     _ConfigData() {}
165 
_ConfigDataConfigCache::_ConfigData166     _ConfigData(S3Config *config_, time_t load_time_) : config(config_), load_time(load_time_) {}
167 
_ConfigDataConfigCache::_ConfigData168     _ConfigData(_ConfigData &&lhs)
169     {
170       update_status = lhs.update_status.load();
171       config        = lhs.config.load();
172       load_time     = lhs.load_time.load();
173     }
174   };
175 
176   std::unordered_map<std::string, _ConfigData> _cache;
177   static const int _ttl = 60;
178 };
179 
180 ConfigCache gConfCache;
181 
182 ///////////////////////////////////////////////////////////////////////////////
183 // One configuration setup
184 //
185 int event_handler(TSCont, TSEvent, void *); // Forward declaration
186 int config_reloader(TSCont, TSEvent, void *);
187 
188 class S3Config
189 {
190 public:
S3Config(bool get_cont=true)191   S3Config(bool get_cont = true)
192   {
193     if (get_cont) {
194       _cont = TSContCreate(event_handler, nullptr);
195       TSContDataSet(_cont, static_cast<void *>(this));
196 
197       _conf_rld = TSContCreate(config_reloader, TSMutexCreate());
198       TSContDataSet(_conf_rld, static_cast<void *>(this));
199     }
200   }
201 
~S3Config()202   ~S3Config()
203   {
204     _secret_len = _keyid_len = _token_len = 0;
205     TSfree(_secret);
206     TSfree(_keyid);
207     TSfree(_token);
208     TSfree(_conf_fname);
209     if (_conf_rld_act) {
210       TSActionCancel(_conf_rld_act);
211     }
212     if (_conf_rld) {
213       TSContDestroy(_conf_rld);
214     }
215     if (_cont) {
216       TSContDestroy(_cont);
217     }
218   }
219 
220   // Is this configuration usable?
221   bool
valid() const222   valid() const
223   {
224     /* Check mandatory parameters first */
225     if (!_secret || !(_secret_len > 0) || !_keyid || !(_keyid_len > 0) || (2 != _version && 4 != _version)) {
226       return false;
227     }
228 
229     /* Optional parameters, issue warning if v2 parameters are used with v4 and vice-versa (wrong parameters are ignored anyways) */
230     if (2 == _version) {
231       if (_v4includeHeaders_modified && !_v4includeHeaders.empty()) {
232         TSDebug("[%s] headers are not being signed with AWS auth v2, included headers parameter ignored", PLUGIN_NAME);
233       }
234       if (_v4excludeHeaders_modified && !_v4excludeHeaders.empty()) {
235         TSDebug("[%s] headers are not being signed with AWS auth v2, excluded headers parameter ignored", PLUGIN_NAME);
236       }
237       if (_region_map_modified && !_region_map.empty()) {
238         TSDebug("[%s] region map is not used with AWS auth v2, parameter ignored", PLUGIN_NAME);
239       }
240       if (nullptr != _token || _token_len > 0) {
241         TSDebug("[%s] session token support with AWS auth v2 is not implemented, parameter ignored", PLUGIN_NAME);
242       }
243     } else {
244       /* 4 == _version */
245       // NOTE: virtual host not used with AWS auth v4, parameter ignored
246     }
247     return true;
248   }
249 
250   // Used to copy relevant configurations that can be configured in a config file. Note: we intentionally
251   // don't override/use the assignment operator, since we only copy things IF they have been modified.
252   void
copy_changes_from(const S3Config * src)253   copy_changes_from(const S3Config *src)
254   {
255     if (src->_secret) {
256       TSfree(_secret);
257       _secret     = TSstrdup(src->_secret);
258       _secret_len = src->_secret_len;
259     }
260 
261     if (src->_keyid) {
262       TSfree(_keyid);
263       _keyid     = TSstrdup(src->_keyid);
264       _keyid_len = src->_keyid_len;
265     }
266 
267     if (src->_token) {
268       TSfree(_token);
269       _token     = TSstrdup(src->_token);
270       _token_len = src->_token_len;
271     }
272 
273     if (src->_version_modified) {
274       _version          = src->_version;
275       _version_modified = true;
276     }
277 
278     if (src->_virt_host_modified) {
279       _virt_host          = src->_virt_host;
280       _virt_host_modified = true;
281     }
282 
283     if (src->_v4includeHeaders_modified) {
284       _v4includeHeaders          = src->_v4includeHeaders;
285       _v4includeHeaders_modified = true;
286     }
287 
288     if (src->_v4excludeHeaders_modified) {
289       _v4excludeHeaders          = src->_v4excludeHeaders;
290       _v4excludeHeaders_modified = true;
291     }
292 
293     if (src->_region_map_modified) {
294       _region_map          = src->_region_map;
295       _region_map_modified = true;
296     }
297 
298     _expiration = src->_expiration;
299 
300     if (src->_conf_fname) {
301       TSfree(_conf_fname);
302       _conf_fname = TSstrdup(src->_conf_fname);
303     }
304   }
305 
306   // Getters
307   bool
virt_host() const308   virt_host() const
309   {
310     return _virt_host;
311   }
312 
313   const char *
secret() const314   secret() const
315   {
316     return _secret;
317   }
318 
319   const char *
keyid() const320   keyid() const
321   {
322     return _keyid;
323   }
324 
325   const char *
token() const326   token() const
327   {
328     return _token;
329   }
330 
331   int
secret_len() const332   secret_len() const
333   {
334     return _secret_len;
335   }
336 
337   int
keyid_len() const338   keyid_len() const
339   {
340     return _keyid_len;
341   }
342 
343   int
token_len() const344   token_len() const
345   {
346     return _token_len;
347   }
348 
349   int
version() const350   version() const
351   {
352     return _version;
353   }
354 
355   const StringSet &
v4includeHeaders()356   v4includeHeaders()
357   {
358     return _v4includeHeaders;
359   }
360 
361   const StringSet &
v4excludeHeaders()362   v4excludeHeaders()
363   {
364     return _v4excludeHeaders;
365   }
366 
367   const StringMap &
v4RegionMap()368   v4RegionMap()
369   {
370     return _region_map;
371   }
372 
373   long
expiration() const374   expiration() const
375   {
376     return _expiration;
377   }
378 
379   const char *
conf_fname() const380   conf_fname() const
381   {
382     return _conf_fname;
383   }
384 
385   int
incr_conf_reload_count()386   incr_conf_reload_count()
387   {
388     return _conf_reload_count++;
389   }
390 
391   // Setters
392   void
set_secret(const char * s)393   set_secret(const char *s)
394   {
395     TSfree(_secret);
396     _secret     = TSstrdup(s);
397     _secret_len = strlen(s);
398   }
399   void
set_keyid(const char * s)400   set_keyid(const char *s)
401   {
402     TSfree(_keyid);
403     _keyid     = TSstrdup(s);
404     _keyid_len = strlen(s);
405   }
406   void
set_token(const char * s)407   set_token(const char *s)
408   {
409     TSfree(_token);
410     _token     = TSstrdup(s);
411     _token_len = strlen(s);
412   }
413   void
set_virt_host(bool f=true)414   set_virt_host(bool f = true)
415   {
416     _virt_host          = f;
417     _virt_host_modified = true;
418   }
419   void
set_version(const char * s)420   set_version(const char *s)
421   {
422     _version          = strtol(s, nullptr, 10);
423     _version_modified = true;
424   }
425 
426   void
set_include_headers(const char * s)427   set_include_headers(const char *s)
428   {
429     ::commaSeparateString<StringSet>(_v4includeHeaders, s);
430     _v4includeHeaders_modified = true;
431   }
432 
433   void
set_exclude_headers(const char * s)434   set_exclude_headers(const char *s)
435   {
436     ::commaSeparateString<StringSet>(_v4excludeHeaders, s);
437     _v4excludeHeaders_modified = true;
438 
439     /* Exclude headers that are meant to be changed */
440     _v4excludeHeaders.insert("x-forwarded-for");
441     _v4excludeHeaders.insert("forwarded");
442     _v4excludeHeaders.insert("via");
443   }
444 
445   void
set_region_map(const char * s)446   set_region_map(const char *s)
447   {
448     loadRegionMap(_region_map, s);
449     _region_map_modified = true;
450   }
451 
452   void
set_expiration(const char * s)453   set_expiration(const char *s)
454   {
455     _expiration = strtol(s, nullptr, 10);
456   }
457 
458   void
set_conf_fname(const char * s)459   set_conf_fname(const char *s)
460   {
461     TSfree(_conf_fname);
462     _conf_fname = TSstrdup(s);
463   }
464 
465   void
reset_conf_reload_count()466   reset_conf_reload_count()
467   {
468     _conf_reload_count = 0;
469   }
470 
471   // Parse configs from an external file
472   bool parse_config(const std::string &filename);
473 
474   // This should be called from the remap plugin, to setup the TXN hook for
475   // SEND_REQUEST_HDR, such that we always attach the appropriate S3 auth.
476   void
schedule(TSHttpTxn txnp) const477   schedule(TSHttpTxn txnp) const
478   {
479     TSHttpTxnHookAdd(txnp, TS_HTTP_SEND_REQUEST_HDR_HOOK, _cont);
480   }
481 
482   void
schedule_conf_reload(long delay)483   schedule_conf_reload(long delay)
484   {
485     if (_conf_rld_act != nullptr && !TSActionDone(_conf_rld_act)) {
486       TSActionCancel(_conf_rld_act);
487     }
488     _conf_rld_act = TSContScheduleOnPool(_conf_rld, delay * 1000, TS_THREAD_POOL_NET);
489   }
490 
491   std::shared_mutex reload_mutex;
492   std::atomic_bool reload_waiting = false;
493 
494 private:
495   char *_secret            = nullptr;
496   size_t _secret_len       = 0;
497   char *_keyid             = nullptr;
498   size_t _keyid_len        = 0;
499   char *_token             = nullptr;
500   size_t _token_len        = 0;
501   bool _virt_host          = false;
502   int _version             = 2;
503   bool _version_modified   = false;
504   bool _virt_host_modified = false;
505   TSCont _cont             = nullptr;
506   TSCont _conf_rld         = nullptr;
507   TSAction _conf_rld_act   = nullptr;
508   StringSet _v4includeHeaders;
509   bool _v4includeHeaders_modified = false;
510   StringSet _v4excludeHeaders;
511   bool _v4excludeHeaders_modified = false;
512   StringMap _region_map;
513   bool _region_map_modified = false;
514   long _expiration          = 0;
515   char *_conf_fname         = nullptr;
516   int _conf_reload_count    = 0;
517 };
518 
519 bool
parse_config(const std::string & config_fname)520 S3Config::parse_config(const std::string &config_fname)
521 {
522   if (0 == config_fname.size()) {
523     TSError("[%s] called without a config file, this is broken", PLUGIN_NAME);
524     return false;
525   } else {
526     char line[512]; // These are long lines ...
527     FILE *file = fopen(config_fname.c_str(), "r");
528 
529     if (nullptr == file) {
530       TSError("[%s] unable to open %s", PLUGIN_NAME, config_fname.c_str());
531       return false;
532     }
533 
534     while (fgets(line, sizeof(line), file) != nullptr) {
535       char *pos1, *pos2;
536 
537       // Skip leading white spaces
538       pos1 = line;
539       while (*pos1 && isspace(*pos1)) {
540         ++pos1;
541       }
542       if (!*pos1 || ('#' == *pos1)) {
543         continue;
544       }
545 
546       // Skip trailing white spaces
547       pos2 = pos1;
548       pos1 = pos2 + strlen(pos2) - 1;
549       while ((pos1 > pos2) && isspace(*pos1)) {
550         *(pos1--) = '\0';
551       }
552       if (pos1 == pos2) {
553         continue;
554       }
555 
556       // Identify the keys (and values if appropriate)
557       if (0 == strncasecmp(pos2, "secret_key=", 11)) {
558         set_secret(pos2 + 11);
559       } else if (0 == strncasecmp(pos2, "access_key=", 11)) {
560         set_keyid(pos2 + 11);
561       } else if (0 == strncasecmp(pos2, "session_token=", 14)) {
562         set_token(pos2 + 14);
563       } else if (0 == strncasecmp(pos2, "version=", 8)) {
564         set_version(pos2 + 8);
565       } else if (0 == strncasecmp(pos2, "virtual_host", 12)) {
566         set_virt_host();
567       } else if (0 == strncasecmp(pos2, "v4-include-headers=", 19)) {
568         set_include_headers(pos2 + 19);
569       } else if (0 == strncasecmp(pos2, "v4-exclude-headers=", 19)) {
570         set_exclude_headers(pos2 + 19);
571       } else if (0 == strncasecmp(pos2, "v4-region-map=", 14)) {
572         set_region_map(pos2 + 14);
573       } else if (0 == strncasecmp(pos2, "expiration=", 11)) {
574         set_expiration(pos2 + 11);
575       } else {
576         // ToDo: warnings?
577       }
578     }
579 
580     fclose(file);
581   }
582 
583   return true;
584 }
585 
586 ///////////////////////////////////////////////////////////////////////////////
587 // Implementation for the ConfigCache, it has to go here since we have a sort
588 // of circular dependency. Note that we always parse / get the configuration
589 // for the file, either from cache or by making one. The user of this just
590 // has to copy the relevant portions, but should not use the returned object
591 // directly (i.e. it must be copied).
592 //
593 S3Config *
get(const char * fname)594 ConfigCache::get(const char *fname)
595 {
596   S3Config *s3;
597 
598   struct timeval tv;
599 
600   gettimeofday(&tv, nullptr);
601 
602   // Make sure the filename is an absolute path, prepending the config dir if needed
603   std::string config_fname = makeConfigPath(fname);
604 
605   auto it = _cache.find(config_fname);
606 
607   if (it != _cache.end()) {
608     unsigned update_status = it->second.update_status;
609     if (tv.tv_sec > (it->second.load_time + _ttl)) {
610       if (!(update_status & 1) && it->second.update_status.compare_exchange_strong(update_status, update_status + 1)) {
611         TSDebug(PLUGIN_NAME, "Configuration from %s is stale, reloading", config_fname.c_str());
612         s3 = new S3Config(false); // false == this config does not get the continuation
613 
614         if (s3->parse_config(config_fname)) {
615           s3->set_conf_fname(fname);
616         } else {
617           // Failed the configuration parse... Set the cache response to nullptr
618           delete s3;
619           s3 = nullptr;
620           TSAssert(!"Configuration parsing / caching failed");
621         }
622 
623         delete it->second.config;
624         it->second.config    = s3;
625         it->second.load_time = tv.tv_sec;
626 
627         // Update is complete.
628         ++it->second.update_status;
629       } else {
630         // This thread lost the race with another thread that is also reloading
631         // the config for this file. Wait for the other thread to finish reloading.
632         while (it->second.update_status & 1) {
633           // Hopefully yielding will sleep the thread at least until the next
634           // scheduler interrupt, preventing a busy wait.
635           std::this_thread::yield();
636         }
637         s3 = it->second.config;
638       }
639     } else {
640       TSDebug(PLUGIN_NAME, "Configuration from %s is fresh, reusing", config_fname.c_str());
641       s3 = it->second.config;
642     }
643   } else {
644     // Create a new cached file.
645     s3 = new S3Config(false); // false == this config does not get the continuation
646 
647     TSDebug(PLUGIN_NAME, "Parsing and caching configuration from %s, version:%d", config_fname.c_str(), s3->version());
648     if (s3->parse_config(config_fname)) {
649       s3->set_conf_fname(fname);
650       _cache.emplace(config_fname, _ConfigData(s3, tv.tv_sec));
651     } else {
652       delete s3;
653       s3 = nullptr;
654       TSAssert(!"Configuration parsing / caching failed");
655     }
656   }
657   return s3;
658 }
659 
660 ///////////////////////////////////////////////////////////////////////////////
661 // This class is used to perform the S3 auth generation.
662 //
663 class S3Request
664 {
665 public:
S3Request(TSHttpTxn txnp)666   S3Request(TSHttpTxn txnp) : _txnp(txnp), _bufp(nullptr), _hdr_loc(TS_NULL_MLOC), _url_loc(TS_NULL_MLOC) {}
~S3Request()667   ~S3Request()
668   {
669     TSHandleMLocRelease(_bufp, _hdr_loc, _url_loc);
670     TSHandleMLocRelease(_bufp, TS_NULL_MLOC, _hdr_loc);
671   }
672 
673   bool
initialize()674   initialize()
675   {
676     if (TS_SUCCESS != TSHttpTxnServerReqGet(_txnp, &_bufp, &_hdr_loc)) {
677       return false;
678     }
679     if (TS_SUCCESS != TSHttpHdrUrlGet(_bufp, _hdr_loc, &_url_loc)) {
680       return false;
681     }
682 
683     return true;
684   }
685 
686   TSHttpStatus authorizeV2(S3Config *s3);
687   TSHttpStatus authorizeV4(S3Config *s3);
688   TSHttpStatus authorize(S3Config *s3);
689   bool set_header(const char *header, int header_len, const char *val, int val_len);
690 
691 private:
692   TSHttpTxn _txnp;
693   TSMBuffer _bufp;
694   TSMLoc _hdr_loc, _url_loc;
695 };
696 
697 ///////////////////////////////////////////////////////////////////////////
698 // Set a header to a specific value. This will avoid going to through a
699 // remove / add sequence in case of an existing header.
700 // but clean.
701 bool
set_header(const char * header,int header_len,const char * val,int val_len)702 S3Request::set_header(const char *header, int header_len, const char *val, int val_len)
703 {
704   if (!header || header_len <= 0 || !val || val_len <= 0) {
705     return false;
706   }
707 
708   bool ret         = false;
709   TSMLoc field_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, header, header_len);
710 
711   if (!field_loc) {
712     // No existing header, so create one
713     if (TS_SUCCESS == TSMimeHdrFieldCreateNamed(_bufp, _hdr_loc, header, header_len, &field_loc)) {
714       if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(_bufp, _hdr_loc, field_loc, -1, val, val_len)) {
715         TSMimeHdrFieldAppend(_bufp, _hdr_loc, field_loc);
716         ret = true;
717       }
718       TSHandleMLocRelease(_bufp, _hdr_loc, field_loc);
719     }
720   } else {
721     TSMLoc tmp = nullptr;
722     bool first = true;
723 
724     while (field_loc) {
725       tmp = TSMimeHdrFieldNextDup(_bufp, _hdr_loc, field_loc);
726       if (first) {
727         first = false;
728         if (TS_SUCCESS == TSMimeHdrFieldValueStringSet(_bufp, _hdr_loc, field_loc, -1, val, val_len)) {
729           ret = true;
730         }
731       } else {
732         TSMimeHdrFieldDestroy(_bufp, _hdr_loc, field_loc);
733       }
734       TSHandleMLocRelease(_bufp, _hdr_loc, field_loc);
735       field_loc = tmp;
736     }
737   }
738 
739   if (ret) {
740     TSDebug(PLUGIN_NAME, "Set the header %.*s: %.*s", header_len, header, val_len, val);
741   }
742 
743   return ret;
744 }
745 
746 // dst points to starting offset of dst buffer
747 // dst_len remaining space in buffer
748 static size_t
str_concat(char * dst,size_t dst_len,const char * src,size_t src_len)749 str_concat(char *dst, size_t dst_len, const char *src, size_t src_len)
750 {
751   size_t to_copy = std::min(dst_len, src_len);
752 
753   if (to_copy > 0) {
754     strncat(dst, src, to_copy);
755   }
756 
757   return to_copy;
758 }
759 
760 TSHttpStatus
authorize(S3Config * s3)761 S3Request::authorize(S3Config *s3)
762 {
763   TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
764   switch (s3->version()) {
765   case 2:
766     status = authorizeV2(s3);
767     break;
768   case 4:
769     status = authorizeV4(s3);
770     break;
771   default:
772     break;
773   }
774   return status;
775 }
776 
777 TSHttpStatus
authorizeV4(S3Config * s3)778 S3Request::authorizeV4(S3Config *s3)
779 {
780   TsApi api(_bufp, _hdr_loc, _url_loc);
781   time_t now = time(nullptr);
782 
783   AwsAuthV4 util(api, &now, /* signPayload */ false, s3->keyid(), s3->keyid_len(), s3->secret(), s3->secret_len(), "s3", 2,
784                  s3->v4includeHeaders(), s3->v4excludeHeaders(), s3->v4RegionMap());
785   String payloadHash = util.getPayloadHash();
786   if (!set_header(X_AMZ_CONTENT_SHA256.c_str(), X_AMZ_CONTENT_SHA256.length(), payloadHash.c_str(), payloadHash.length())) {
787     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
788   }
789 
790   /* set x-amz-date header */
791   size_t dateTimeLen   = 0;
792   const char *dateTime = util.getDateTime(&dateTimeLen);
793   if (!set_header(X_AMX_DATE.c_str(), X_AMX_DATE.length(), dateTime, dateTimeLen)) {
794     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
795   }
796 
797   /* set X-Amz-Security-Token if we have a token */
798   if (nullptr != s3->token() && '\0' != *(s3->token()) &&
799       !set_header(X_AMZ_SECURITY_TOKEN.data(), X_AMZ_SECURITY_TOKEN.size(), s3->token(), s3->token_len())) {
800     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
801   }
802 
803   String auth = util.getAuthorizationHeader();
804   if (auth.empty()) {
805     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
806   }
807 
808   if (!set_header(TS_MIME_FIELD_AUTHORIZATION, TS_MIME_LEN_AUTHORIZATION, auth.c_str(), auth.length())) {
809     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
810   }
811 
812   return TS_HTTP_STATUS_OK;
813 }
814 
815 // Method to authorize the S3 request:
816 //
817 // StringToSign = HTTP-VERB + "\n" +
818 //    Content-MD5 + "\n" +
819 //    Content-Type + "\n" +
820 //    Date + "\n" +
821 //    CanonicalizedAmzHeaders +
822 //    CanonicalizedResource;
823 //
824 // ToDo:
825 // -----
826 //     1) UTF8
827 //     2) Support POST type requests
828 //     3) Canonicalize the Amz headers
829 //
830 //  Note: This assumes that the URI path has been appropriately canonicalized by remapping
831 //
832 TSHttpStatus
authorizeV2(S3Config * s3)833 S3Request::authorizeV2(S3Config *s3)
834 {
835   TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
836   TSMLoc host_loc = TS_NULL_MLOC, md5_loc = TS_NULL_MLOC, contype_loc = TS_NULL_MLOC;
837   int method_len = 0, path_len = 0, param_len = 0, host_len = 0, con_md5_len = 0, con_type_len = 0, date_len = 0;
838   const char *method = nullptr, *path = nullptr, *param = nullptr, *host = nullptr, *con_md5 = nullptr, *con_type = nullptr,
839              *host_endp = nullptr;
840   char date[128]; // Plenty of space for a Date value
841   time_t now = time(nullptr);
842   struct tm now_tm;
843 
844   // Start with some request resources we need
845   if (nullptr == (method = TSHttpHdrMethodGet(_bufp, _hdr_loc, &method_len))) {
846     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
847   }
848   if (nullptr == (path = TSUrlPathGet(_bufp, _url_loc, &path_len))) {
849     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
850   }
851 
852   // get matrix parameters
853   param = TSUrlHttpParamsGet(_bufp, _url_loc, &param_len);
854 
855   // Next, setup the Date: header, it's required.
856   if (nullptr == gmtime_r(&now, &now_tm)) {
857     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
858   }
859   if ((date_len = strftime(date, sizeof(date) - 1, DATE_FMT, &now_tm)) <= 0) {
860     return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
861   }
862 
863   // Add the Date: header to the request (this overwrites any existing Date header)
864   set_header(TS_MIME_FIELD_DATE, TS_MIME_LEN_DATE, date, date_len);
865 
866   // If the configuration is a "virtual host" (foo.s3.aws ...), extract the
867   // first portion into the Host: header.
868   if (s3->virt_host()) {
869     host_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, TS_MIME_FIELD_HOST, TS_MIME_LEN_HOST);
870     if (host_loc) {
871       host      = TSMimeHdrFieldValueStringGet(_bufp, _hdr_loc, host_loc, -1, &host_len);
872       host_endp = static_cast<const char *>(memchr(host, '.', host_len));
873     } else {
874       return TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
875     }
876   }
877 
878   // Just in case we add Content-MD5 if present
879   md5_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, TS_MIME_FIELD_CONTENT_MD5, TS_MIME_LEN_CONTENT_MD5);
880   if (md5_loc) {
881     con_md5 = TSMimeHdrFieldValueStringGet(_bufp, _hdr_loc, md5_loc, -1, &con_md5_len);
882   }
883 
884   // get the Content-Type if available - (buggy) clients may send it
885   // for GET requests too
886   contype_loc = TSMimeHdrFieldFind(_bufp, _hdr_loc, TS_MIME_FIELD_CONTENT_TYPE, TS_MIME_LEN_CONTENT_TYPE);
887   if (contype_loc) {
888     con_type = TSMimeHdrFieldValueStringGet(_bufp, _hdr_loc, contype_loc, -1, &con_type_len);
889   }
890 
891   // For debugging, lets produce some nice output
892   if (TSIsDebugTagSet(PLUGIN_NAME)) {
893     TSDebug(PLUGIN_NAME, "Signature string is:");
894     // ToDo: This should include the Content-MD5 and Content-Type (for POST)
895     TSDebug(PLUGIN_NAME, "%.*s", method_len, method);
896     if (con_md5) {
897       TSDebug(PLUGIN_NAME, "%.*s", con_md5_len, con_md5);
898     }
899 
900     if (con_type) {
901       TSDebug(PLUGIN_NAME, "%.*s", con_type_len, con_type);
902     }
903 
904     TSDebug(PLUGIN_NAME, "%.*s", date_len, date);
905 
906     const size_t left_size   = 1024;
907     char left[left_size + 1] = "/";
908     size_t loff              = 1;
909 
910     // ToDo: What to do with the CanonicalizedAmzHeaders ...
911     if (host && host_endp) {
912       loff += str_concat(&left[loff], (left_size - loff), host, static_cast<int>(host_endp - host));
913       loff += str_concat(&left[loff], (left_size - loff), "/", 1);
914     }
915 
916     loff += str_concat(&left[loff], (left_size - loff), path, path_len);
917 
918     if (param) {
919       loff += str_concat(&left[loff], (left_size - loff), ";", 1);
920       str_concat(&left[loff], (left_size - loff), param, param_len);
921     }
922 
923     TSDebug(PLUGIN_NAME, "%s", left);
924   }
925 
926 // Produce the SHA1 MAC digest
927 #ifndef HAVE_HMAC_CTX_NEW
928   HMAC_CTX ctx[1];
929 #else
930   HMAC_CTX *ctx;
931 #endif
932   unsigned int hmac_len;
933   size_t hmac_b64_len;
934   unsigned char hmac[SHA_DIGEST_LENGTH];
935   char hmac_b64[SHA_DIGEST_LENGTH * 2];
936 
937 #ifndef HAVE_HMAC_CTX_NEW
938   HMAC_CTX_init(ctx);
939 #else
940   ctx = HMAC_CTX_new();
941 #endif
942   HMAC_Init_ex(ctx, s3->secret(), s3->secret_len(), EVP_sha1(), nullptr);
943   HMAC_Update(ctx, (unsigned char *)method, method_len);
944   HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n"), 1);
945   HMAC_Update(ctx, (unsigned char *)con_md5, con_md5_len);
946   HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n"), 1);
947   HMAC_Update(ctx, (unsigned char *)con_type, con_type_len);
948   HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n"), 1);
949   HMAC_Update(ctx, reinterpret_cast<unsigned char *>(date), date_len);
950   HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("\n/"), 2);
951 
952   if (host && host_endp) {
953     HMAC_Update(ctx, (unsigned char *)host, host_endp - host);
954     HMAC_Update(ctx, reinterpret_cast<const unsigned char *>("/"), 1);
955   }
956 
957   HMAC_Update(ctx, (unsigned char *)path, path_len);
958   if (param) {
959     HMAC_Update(ctx, reinterpret_cast<const unsigned char *>(";"), 1); // TSUrlHttpParamsGet() does not include ';'
960     HMAC_Update(ctx, (unsigned char *)param, param_len);
961   }
962 
963   HMAC_Final(ctx, hmac, &hmac_len);
964 #ifndef HAVE_HMAC_CTX_NEW
965   HMAC_CTX_cleanup(ctx);
966 #else
967   HMAC_CTX_free(ctx);
968 #endif
969 
970   // Do the Base64 encoding and set the Authorization header.
971   if (TS_SUCCESS == TSBase64Encode(reinterpret_cast<const char *>(hmac), hmac_len, hmac_b64, sizeof(hmac_b64) - 1, &hmac_b64_len)) {
972     char auth[256]; // This is way bigger than any string we can think of.
973     int auth_len = snprintf(auth, sizeof(auth), "AWS %s:%.*s", s3->keyid(), static_cast<int>(hmac_b64_len), hmac_b64);
974 
975     if ((auth_len > 0) && (auth_len < static_cast<int>(sizeof(auth)))) {
976       set_header(TS_MIME_FIELD_AUTHORIZATION, TS_MIME_LEN_AUTHORIZATION, auth, auth_len);
977       status = TS_HTTP_STATUS_OK;
978     }
979   }
980 
981   // Cleanup
982   TSHandleMLocRelease(_bufp, _hdr_loc, contype_loc);
983   TSHandleMLocRelease(_bufp, _hdr_loc, md5_loc);
984   TSHandleMLocRelease(_bufp, _hdr_loc, host_loc);
985 
986   return status;
987 }
988 
989 ///////////////////////////////////////////////////////////////////////////////
990 // This is the main continuation.
991 int
event_handler(TSCont cont,TSEvent event,void * edata)992 event_handler(TSCont cont, TSEvent event, void *edata)
993 {
994   TSHttpTxn txnp       = static_cast<TSHttpTxn>(edata);
995   S3Config *s3         = static_cast<S3Config *>(TSContDataGet(cont));
996   TSEvent enable_event = TS_EVENT_HTTP_CONTINUE;
997 
998   {
999     S3Request request(txnp);
1000     TSHttpStatus status = TS_HTTP_STATUS_INTERNAL_SERVER_ERROR;
1001 
1002     switch (event) {
1003     case TS_EVENT_HTTP_SEND_REQUEST_HDR:
1004       if (request.initialize()) {
1005         while (s3->reload_waiting) {
1006           std::this_thread::yield();
1007         }
1008 
1009         std::shared_lock lock(s3->reload_mutex);
1010         status = request.authorize(s3);
1011       }
1012 
1013       if (TS_HTTP_STATUS_OK == status) {
1014         TSDebug(PLUGIN_NAME, "Successfully signed the AWS S3 URL");
1015       } else {
1016         TSDebug(PLUGIN_NAME, "Failed to sign the AWS S3 URL, status = %d", status);
1017         TSHttpTxnStatusSet(txnp, status);
1018         enable_event = TS_EVENT_HTTP_ERROR;
1019       }
1020       break;
1021     default:
1022       TSError("[%s] Unknown event for this plugin", PLUGIN_NAME);
1023       TSDebug(PLUGIN_NAME, "unknown event for this plugin");
1024       break;
1025     }
1026     // Most get S3Request out of scope in case the later plugins invalidate the TSAPI
1027     // objects it references.  Some cases were causing asserts from the destructor
1028   }
1029 
1030   TSHttpTxnReenable(txnp, enable_event);
1031   return 0;
1032 }
1033 
1034 // If the token has more than one hour to expire, reload is scheduled one hour before expiration.
1035 // If the token has less than one hour to expire, reload is scheduled 15 minutes before expiration.
1036 // If the token has less than 15 minutes to expire, reload is scheduled at the expiration time.
1037 static long
cal_reload_delay(long time_diff)1038 cal_reload_delay(long time_diff)
1039 {
1040   if (time_diff > 3600) {
1041     return time_diff - 3600;
1042   } else if (time_diff > 900) {
1043     return time_diff - 900;
1044   } else {
1045     return time_diff;
1046   }
1047 }
1048 
1049 int
config_reloader(TSCont cont,TSEvent event,void * edata)1050 config_reloader(TSCont cont, TSEvent event, void *edata)
1051 {
1052   TSDebug(PLUGIN_NAME, "reloading configs");
1053   S3Config *s3          = static_cast<S3Config *>(TSContDataGet(cont));
1054   S3Config *file_config = gConfCache.get(s3->conf_fname());
1055 
1056   if (!file_config || !file_config->valid()) {
1057     TSError("[%s] requires both shared and AWS secret configuration", PLUGIN_NAME);
1058     return TS_ERROR;
1059   }
1060 
1061   s3->reload_waiting = true;
1062   {
1063     std::unique_lock lock(s3->reload_mutex);
1064     s3->copy_changes_from(file_config);
1065   }
1066   s3->reload_waiting = false;
1067 
1068   if (s3->expiration() == 0) {
1069     TSDebug(PLUGIN_NAME, "disabling auto config reload");
1070   } else {
1071     // auto reload is scheduled to be 5 minutes before the expiration time to get some headroom
1072     long time_diff = s3->expiration() -
1073                      std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
1074     if (time_diff > 0) {
1075       long delay = cal_reload_delay(time_diff);
1076       TSDebug(PLUGIN_NAME, "scheduling config reload with %ld seconds delay", delay);
1077       s3->reset_conf_reload_count();
1078       s3->schedule_conf_reload(delay);
1079     } else {
1080       TSDebug(PLUGIN_NAME, "config expiration time is in the past, re-checking in 1 minute");
1081       if (s3->incr_conf_reload_count() == 10) {
1082         TSError("[%s] tried to reload config automatically but failed, please try manual reloading the config", PLUGIN_NAME);
1083       }
1084       s3->schedule_conf_reload(60);
1085     }
1086   }
1087 
1088   return TS_SUCCESS;
1089 }
1090 
1091 ///////////////////////////////////////////////////////////////////////////////
1092 // Initialize the plugin.
1093 //
1094 TSReturnCode
TSRemapInit(TSRemapInterface * api_info,char * errbuf,int errbuf_size)1095 TSRemapInit(TSRemapInterface *api_info, char *errbuf, int errbuf_size)
1096 {
1097   if (!api_info) {
1098     strncpy(errbuf, "[tsremap_init] - Invalid TSRemapInterface argument", errbuf_size - 1);
1099     return TS_ERROR;
1100   }
1101 
1102   if (api_info->tsremap_version < TSREMAP_VERSION) {
1103     snprintf(errbuf, errbuf_size, "[TSRemapInit] - Incorrect API version %ld.%ld", api_info->tsremap_version >> 16,
1104              (api_info->tsremap_version & 0xffff));
1105     return TS_ERROR;
1106   }
1107 
1108   TSDebug(PLUGIN_NAME, "plugin is successfully initialized");
1109   return TS_SUCCESS;
1110 }
1111 
1112 ///////////////////////////////////////////////////////////////////////////////
1113 // One instance per remap.config invocation.
1114 //
1115 TSReturnCode
TSRemapNewInstance(int argc,char * argv[],void ** ih,char *,int)1116 TSRemapNewInstance(int argc, char *argv[], void **ih, char * /* errbuf ATS_UNUSED */, int /* errbuf_size ATS_UNUSED */)
1117 {
1118   static const struct option longopt[] = {
1119     {const_cast<char *>("access_key"), required_argument, nullptr, 'a'},
1120     {const_cast<char *>("config"), required_argument, nullptr, 'c'},
1121     {const_cast<char *>("secret_key"), required_argument, nullptr, 's'},
1122     {const_cast<char *>("version"), required_argument, nullptr, 'v'},
1123     {const_cast<char *>("virtual_host"), no_argument, nullptr, 'h'},
1124     {const_cast<char *>("v4-include-headers"), required_argument, nullptr, 'i'},
1125     {const_cast<char *>("v4-exclude-headers"), required_argument, nullptr, 'e'},
1126     {const_cast<char *>("v4-region-map"), required_argument, nullptr, 'm'},
1127     {const_cast<char *>("session_token"), required_argument, nullptr, 't'},
1128     {nullptr, no_argument, nullptr, '\0'},
1129   };
1130 
1131   S3Config *s3          = new S3Config(true); // true == this config gets the continuation
1132   S3Config *file_config = nullptr;
1133 
1134   // argv contains the "to" and "from" URLs. Skip the first so that the
1135   // second one poses as the program name.
1136   --argc;
1137   ++argv;
1138 
1139   while (true) {
1140     int opt = getopt_long(argc, static_cast<char *const *>(argv), "", longopt, nullptr);
1141 
1142     switch (opt) {
1143     case 'c':
1144       file_config = gConfCache.get(optarg); // Get cached, or new, config object, from a file
1145       if (!file_config) {
1146         TSError("[%s] invalid configuration file, %s", PLUGIN_NAME, optarg);
1147         *ih = nullptr;
1148         return TS_ERROR;
1149       }
1150       break;
1151     case 'a':
1152       s3->set_keyid(optarg);
1153       break;
1154     case 's':
1155       s3->set_secret(optarg);
1156       break;
1157     case 't':
1158       s3->set_token(optarg);
1159       break;
1160     case 'h':
1161       s3->set_virt_host();
1162       break;
1163     case 'v':
1164       s3->set_version(optarg);
1165       break;
1166     case 'i':
1167       s3->set_include_headers(optarg);
1168       break;
1169     case 'e':
1170       s3->set_exclude_headers(optarg);
1171       break;
1172     case 'm':
1173       s3->set_region_map(optarg);
1174       break;
1175     }
1176 
1177     if (opt == -1) {
1178       break;
1179     }
1180   }
1181 
1182   // Copy the config file secret into our instance of the configuration.
1183   if (file_config) {
1184     s3->copy_changes_from(file_config);
1185   }
1186 
1187   // Make sure we got both the shared secret and the AWS secret
1188   if (!s3->valid()) {
1189     TSError("[%s] requires both shared and AWS secret configuration", PLUGIN_NAME);
1190     *ih = nullptr;
1191     return TS_ERROR;
1192   }
1193 
1194   if (s3->expiration() == 0) {
1195     TSDebug(PLUGIN_NAME, "disabling auto config reload");
1196   } else {
1197     // auto reload is scheduled to be 5 minutes before the expiration time to get some headroom
1198     long time_diff = s3->expiration() -
1199                      std::chrono::duration_cast<std::chrono::seconds>(std::chrono::system_clock::now().time_since_epoch()).count();
1200     if (time_diff > 0) {
1201       long delay = cal_reload_delay(time_diff);
1202       TSDebug(PLUGIN_NAME, "scheduling config reload with %ld seconds delay", delay);
1203       s3->reset_conf_reload_count();
1204       s3->schedule_conf_reload(delay);
1205     } else {
1206       TSDebug(PLUGIN_NAME, "config expiration time is in the past, re-checking in 1 minute");
1207       s3->schedule_conf_reload(60);
1208     }
1209   }
1210 
1211   *ih = static_cast<void *>(s3);
1212   TSDebug(PLUGIN_NAME, "New rule: access_key=%s, virtual_host=%s, version=%d", s3->keyid(), s3->virt_host() ? "yes" : "no",
1213           s3->version());
1214 
1215   return TS_SUCCESS;
1216 }
1217 
1218 void
TSRemapDeleteInstance(void * ih)1219 TSRemapDeleteInstance(void *ih)
1220 {
1221   S3Config *s3 = static_cast<S3Config *>(ih);
1222   delete s3;
1223 }
1224 
1225 ///////////////////////////////////////////////////////////////////////////////
1226 // This is the main "entry" point for the plugin, called for every request.
1227 //
1228 TSRemapStatus
TSRemapDoRemap(void * ih,TSHttpTxn txnp,TSRemapRequestInfo *)1229 TSRemapDoRemap(void *ih, TSHttpTxn txnp, TSRemapRequestInfo * /* rri */)
1230 {
1231   S3Config *s3 = static_cast<S3Config *>(ih);
1232 
1233   if (s3) {
1234     TSAssert(s3->valid());
1235     // Now schedule the continuation to update the URL when going to origin.
1236     // Note that in most cases, this is a No-Op, assuming you have reasonable
1237     // cache hit ratio. However, the scheduling is next to free (very cheap).
1238     // Another option would be to use a single global hook, and pass the "s3"
1239     // configs via a TXN argument.
1240     s3->schedule(txnp);
1241   } else {
1242     TSDebug(PLUGIN_NAME, "Remap context is invalid");
1243     TSError("[%s] No remap context available, check code / config", PLUGIN_NAME);
1244     TSHttpTxnStatusSet(txnp, TS_HTTP_STATUS_INTERNAL_SERVER_ERROR);
1245   }
1246 
1247   // This plugin actually doesn't do anything with remapping. Ever.
1248   return TSREMAP_NO_REMAP;
1249 }
1250