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, ¶m_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