| /** @file |
| |
| This file contains a set of utility routines that are used throughout the |
| logging implementation. |
| |
| @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 <tscore/ink_align.h> |
| #include "tscore/ink_config.h" |
| #include "tscore/ink_string.h" |
| #include <tscore/ink_assert.h> |
| |
| #include "tsutil/ts_bw_format.h" |
| |
| #ifdef TEST_LOG_UTILS |
| |
| #include "unit-tests/test_LogUtils.h" |
| |
| #else |
| |
| #include "proxy/hdrs/MIME.h" |
| |
| #endif |
| |
| #include <cassert> |
| #include <cstdio> |
| #include <cstdlib> |
| #include <cstdarg> |
| #include <cstring> |
| #include <ctime> |
| #include <string_view> |
| #include <cstdint> |
| |
| using namespace std::literals; |
| |
| #include <sys/time.h> |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <unistd.h> |
| #include <sys/socket.h> |
| #include <netinet/in.h> |
| #include <arpa/inet.h> |
| #include <netdb.h> |
| |
| #include "proxy/logging/LogUtils.h" |
| #include "proxy/logging/LogLimits.h" |
| |
| /*------------------------------------------------------------------------- |
| LogUtils::timestamp_to_str |
| |
| This routine will convert a timestamp (seconds) into a short string, |
| of the format "%Y%m%d.%Hh%Mm%Ss". |
| |
| Since the resulting buffer is passed in, this routine is thread-safe. |
| Return value is the number of characters placed into the array, not |
| including the NULL. |
| -------------------------------------------------------------------------*/ |
| |
| int |
| LogUtils::timestamp_to_str(long timestamp, char *buf, int size) |
| { |
| static const char *format_str = "%Y%m%d.%Hh%Mm%Ss"; |
| struct tm res; |
| struct tm *tms; |
| tms = ink_localtime_r((const time_t *)×tamp, &res); |
| return strftime(buf, size, format_str, tms); |
| } |
| |
| /*------------------------------------------------------------------------- |
| LogUtils::timestamp_to_netscape_str |
| |
| This routine will convert a timestamp (seconds) into a string compatible |
| with the Netscape logging formats. |
| |
| This routine is intended to be called from the (single) logging thread, |
| and is therefore NOT MULTITHREADED SAFE. There is a single, static, |
| string buffer that the time string is constructed into and returned. |
| -------------------------------------------------------------------------*/ |
| |
| char * |
| LogUtils::timestamp_to_netscape_str(long timestamp) |
| { |
| static thread_local char timebuf[64]; |
| static thread_local long last_timestamp = 0; |
| |
| // safety check |
| if (timestamp < 0) { |
| static char bad_time[] = "Bad timestamp"; |
| return bad_time; |
| } |
| // |
| // since we may have many entries per second, lets only do the |
| // formatting if we actually have a new timestamp. |
| // |
| |
| if (timestamp != last_timestamp) { |
| // |
| // most of this garbage is simply to find out the offset from GMT, |
| // taking daylight savings into account. |
| // |
| struct tm res; |
| struct tm *tms = ink_localtime_r((const time_t *)×tamp, &res); |
| long zone = -tms->tm_gmtoff; // double negative! |
| int offset; |
| char sign; |
| |
| if (zone >= 0) { |
| offset = zone / 60; |
| sign = '-'; |
| } else { |
| offset = zone / -60; |
| sign = '+'; |
| } |
| |
| static char gmtstr[16]; |
| int glen = snprintf(gmtstr, 16, "%c%.2d%.2d", sign, offset / 60, offset % 60); |
| |
| strftime(timebuf, 64 - glen, "%d/%b/%Y:%H:%M:%S ", tms); |
| ink_strlcat(timebuf, gmtstr, sizeof(timebuf)); |
| last_timestamp = timestamp; |
| } |
| return timebuf; |
| } |
| |
| /*------------------------------------------------------------------------- |
| LogUtils::timestamp_to_date_str |
| |
| This routine will convert a timestamp (seconds) into a W3C compatible |
| date string. |
| -------------------------------------------------------------------------*/ |
| |
| char * |
| LogUtils::timestamp_to_date_str(long timestamp) |
| { |
| static thread_local char timebuf[64]; |
| static thread_local long last_timestamp = -1L; |
| |
| // safety check |
| if (timestamp < 0) { |
| static char bad_time[] = "Bad timestamp"; |
| return bad_time; |
| } |
| // |
| // since we may have many entries per second, lets only do the |
| // formatting if we actually have a new timestamp. |
| // |
| |
| if (timestamp != last_timestamp) { |
| struct tm res; |
| struct tm *tms = ink_localtime_r((const time_t *)×tamp, &res); |
| strftime(timebuf, 64, "%Y-%m-%d", tms); |
| last_timestamp = timestamp; |
| } |
| return timebuf; |
| } |
| |
| /*------------------------------------------------------------------------- |
| LogUtils::timestamp_to_time_str |
| |
| This routine will convert a timestamp (seconds) into a W3C compatible |
| time string. |
| -------------------------------------------------------------------------*/ |
| |
| char * |
| LogUtils::timestamp_to_time_str(long timestamp) |
| { |
| static thread_local char timebuf[64]; |
| static thread_local long last_timestamp = 0; |
| |
| // safety check |
| if (timestamp < 0) { |
| static char bad_time[] = "Bad timestamp"; |
| return bad_time; |
| } |
| // |
| // since we may have many entries per second, lets only do the |
| // formatting if we actually have a new timestamp. |
| // |
| |
| if (timestamp != last_timestamp) { |
| struct tm res; |
| struct tm *tms = ink_localtime_r((const time_t *)×tamp, &res); |
| strftime(timebuf, 64, "%H:%M:%S", tms); |
| last_timestamp = timestamp; |
| } |
| return timebuf; |
| } |
| |
| /*------------------------------------------------------------------------- |
| LogUtils::strip_trailing_newline |
| |
| This routine examines the given string buffer to see if the last |
| character before the trailing NULL is a newline ('\n'). If so, it will |
| be replaced with a NULL, thus stripping it and reducing the length of |
| the string by one. |
| -------------------------------------------------------------------------*/ |
| |
| void |
| LogUtils::strip_trailing_newline(char *buf) |
| { |
| if (buf != nullptr) { |
| int len = ::strlen(buf); |
| if (len > 0) { |
| if (buf[len - 1] == '\n') { |
| buf[len - 1] = '\0'; |
| } |
| } |
| } |
| } |
| |
| /*------------------------------------------------------------------------- |
| LogUtils::remove_content_type_attributes |
| |
| HTTP allows content types to have attributes following the main type and |
| subtype. For example, attributes of text/html might be charset=iso-8859. |
| The content type attributes are not logged, so this function strips them |
| from the given buffer, if present. |
| -------------------------------------------------------------------------*/ |
| |
| void |
| LogUtils::remove_content_type_attributes(char *type_str, int *type_len) |
| { |
| if (!type_str) { |
| *type_len = 0; |
| return; |
| } |
| // Look for a semicolon and cut out everything after that |
| // |
| char *p = static_cast<char *>(memchr(type_str, ';', *type_len)); |
| if (p) { |
| *type_len = p - type_str; |
| } |
| } |
| |
| /* |
| int |
| LogUtils::ip_to_str (unsigned ip, char *str, unsigned len) |
| { |
| int ret = snprintf (str, len, "%u.%u.%u.%u", |
| (ip >> 24) & 0xff, |
| (ip >> 16) & 0xff, |
| (ip >> 8) & 0xff, |
| ip & 0xff); |
| |
| return ((ret <= (int)len)? ret : (int)len); |
| } |
| */ |
| |
| // return the seconds remaining until the time of the next roll given |
| // the current time, the rolling offset, and the rolling interval |
| // |
| int |
| LogUtils::seconds_to_next_roll(time_t time_now, int rolling_offset, int rolling_interval) |
| { |
| struct tm lt; |
| ink_localtime_r((const time_t *)&time_now, <); |
| int sidl = lt.tm_sec + lt.tm_min * 60 + lt.tm_hour * 3600; |
| int tr = rolling_offset * 3600; |
| return ((tr >= sidl ? (tr - sidl) % rolling_interval : (86400 - (sidl - tr)) % rolling_interval)); |
| } |
| |
| swoc::TextView |
| LogUtils::get_unrolled_filename(swoc::TextView rolled_filename) |
| { |
| auto unrolled_name = rolled_filename; |
| |
| // A rolled log will look something like: |
| // squid.log_some.hostname.com.20191029.18h15m02s-20191029.18h30m02s.old |
| auto suffix = rolled_filename; |
| |
| if (auto idx = suffix.find('.'); idx != swoc::TextView::npos) { |
| suffix.remove_prefix(idx + 1); |
| // Using the above squid.log example, suffix now looks like: |
| // log_some.hostname.com.20191029.18h15m02s-20191029.18h30m02s.old |
| |
| // Some suffixes do not have the hostname. Rolled diags.log files will look |
| // something like this, for example: |
| // diags.log.20191114.21h43m16s-20191114.21h43m17s.old |
| // |
| // For these, the second delimiter will be a period. For this reason, we also |
| // split_prefix_at with a period as well. |
| if (swoc::TextView::npos != (idx = suffix.find_first_of("_."))) { |
| suffix.remove_prefix(idx + 1); |
| // ' + 1' to remove the '_' or second '.': |
| return unrolled_name.remove_suffix(suffix.size() + 1); |
| } |
| } |
| // If there isn't a '.' or an '_' after the first '.', then this |
| // doesn't look like a rolled file. |
| return unrolled_name; |
| } |
| |
| // Checks if the file pointed to by full_filename either is a regular |
| // file or a pipe and has write permission, or, if the file does not |
| // exist, if the path prefix of full_filename names a directory that |
| // has both execute and write permissions, so there will be no problem |
| // creating the file. If the size_bytes pointer is not NULL, it returns |
| // the size of the file through it. |
| // Also checks the current size limit for the file. If there is a |
| // limit and has_size_limit is not null, *has_size_limit is set to |
| // true. If there is no limit and has_size_limit is not null, |
| // *has_size_limit is set to false. If there is a limit and if the |
| // current_size_limit_bytes pointer is not null, it returns the limit |
| // through it. |
| // |
| // returns: |
| // 0 on success |
| // -1 on system error (no permission, etc.) |
| // 1 if the file full_filename points to is neither a regular file |
| // nor a pipe |
| // |
| int |
| LogUtils::file_is_writeable(const char *full_filename, off_t *size_bytes, bool *has_size_limit, uint64_t *current_size_limit_bytes) |
| { |
| int ret_val = 0; |
| int e; |
| struct stat stat_data; |
| |
| e = stat(full_filename, &stat_data); |
| if (e == 0) { |
| // stat succeeded, check if full_filename points to a regular |
| // file/fifo and if so, check if file has write permission |
| // |
| if (!(S_ISREG(stat_data.st_mode) || S_ISFIFO(stat_data.st_mode))) { |
| ret_val = 1; |
| } else if (!(stat_data.st_mode & S_IWUSR)) { |
| errno = EACCES; |
| ret_val = -1; |
| } |
| if (size_bytes) { |
| *size_bytes = stat_data.st_size; |
| } |
| } else { |
| // stat failed |
| // |
| if (errno != ENOENT) { |
| // can't stat file |
| // |
| ret_val = -1; |
| } else { |
| // file does not exist, check that the prefix is a directory with |
| // write and execute permissions |
| |
| char *dir; |
| char *prefix = nullptr; |
| |
| // search for forward or reverse slash in full_filename |
| // starting from the end |
| // |
| const char *slash = strrchr(full_filename, '/'); |
| if (slash) { |
| size_t prefix_len = slash - full_filename + 1; |
| prefix = new char[prefix_len + 1]; |
| memcpy(prefix, full_filename, prefix_len); |
| prefix[prefix_len] = 0; |
| dir = prefix; |
| } else { |
| dir = (char *)"."; // full_filename has no prefix, use . |
| } |
| |
| // check if directory is executable and writeable |
| // |
| e = access(dir, X_OK | W_OK); |
| if (e < 0) { |
| ret_val = -1; |
| } else { |
| if (size_bytes) { |
| *size_bytes = 0; |
| } |
| } |
| |
| if (prefix) { |
| delete[] prefix; |
| } |
| } |
| } |
| |
| // check for the current filesize limit |
| // |
| if (ret_val == 0) { |
| struct rlimit limit_data; |
| e = getrlimit(RLIMIT_FSIZE, &limit_data); |
| if (e < 0) { |
| ret_val = -1; |
| } else { |
| if (limit_data.rlim_cur != static_cast<rlim_t>(RLIM_INFINITY)) { |
| if (has_size_limit) { |
| *has_size_limit = true; |
| } |
| if (current_size_limit_bytes) { |
| *current_size_limit_bytes = limit_data.rlim_cur; |
| } |
| } else { |
| if (has_size_limit) { |
| *has_size_limit = false; |
| } |
| } |
| } |
| } |
| |
| return ret_val; |
| } |
| |
| namespace |
| { |
| // Get a string out of a MIMEField using one of its member functions, and put it into a buffer writer, terminated with a nul. |
| // |
| void |
| marshalStr(swoc::FixedBufferWriter &bw, const MIMEField &mf, std::string_view (MIMEField::*get_func)() const) |
| { |
| auto data{(mf.*get_func)()}; |
| |
| if (!data.data() or (*data.data() == '\0')) { |
| // Empty string. This is a problem, since it would result in two successive nul characters, which indicates the end of the |
| // marshaled hearer. Change the string to a single blank character. |
| data = " "sv; |
| } |
| |
| bw << data << '\0'; |
| } |
| |
| void |
| unmarshalStr(swoc::FixedBufferWriter &bw, const char *&data) |
| { |
| bw << '{'; |
| |
| while (*data) { |
| bw << *(data++); |
| } |
| |
| // Skip over terminal nul. |
| ++data; |
| |
| bw << '}'; |
| } |
| |
| } // end anonymous namespace |
| |
| namespace LogUtils |
| { |
| // Marshals header tags and values together, with a single terminating nul character. Returns buffer space required. 'buf' points |
| // to where to put the marshaled data. If 'buf' is null, no data is marshaled, but the function returns the amount of space that |
| // would have been used. |
| // |
| int |
| marshalMimeHdr(MIMEHdr *hdr, char *buf) |
| { |
| std::size_t bwSize = buf ? SIZE_MAX : 0; |
| |
| swoc::FixedBufferWriter bw(buf, bwSize); |
| |
| if (hdr) { |
| for (auto const &mfp : *hdr) { |
| marshalStr(bw, mfp, &MIMEField::name_get); |
| marshalStr(bw, mfp, &MIMEField::value_get); |
| } |
| } |
| |
| bw << '\0'; |
| |
| return int(INK_ALIGN_DEFAULT(bw.extent())); |
| } |
| |
| // Unmarshaled/printable format is {{{tag1}:{value1}}{{tag2}:{value2}} ... } |
| // |
| int |
| unmarshalMimeHdr(char **buf, char *dest, int destLength) |
| { |
| ink_assert(buf != nullptr); |
| |
| const char *data = *buf; |
| |
| ink_assert(data != nullptr); |
| |
| swoc::FixedBufferWriter bw(dest, destLength); |
| |
| bw.write('{'); |
| |
| int pairEndFallback{0}, pairEndFallback2{0}, pairSeparatorFallback{0}; |
| |
| while (*data) { |
| if (!bw.error()) { |
| pairEndFallback2 = pairEndFallback; |
| pairEndFallback = bw.size(); |
| } |
| |
| // Add open bracket of pair. |
| // |
| bw.write('{'); |
| |
| // Unmarshal field name. |
| unmarshalStr(bw, data); |
| |
| bw.write(':'); |
| |
| if (!bw.error()) { |
| pairSeparatorFallback = bw.size(); |
| } |
| |
| // Unmarshal field value. |
| unmarshalStr(bw, data); |
| |
| // Add close bracket of pair. |
| bw.write('}'); |
| |
| } // end for loop |
| |
| bw.write('}'); |
| |
| if (bw.error()) { |
| // The output buffer wasn't big enough. |
| bw.discard(bw.extent() - destLength); // clip to max buffer size. |
| |
| static std::string_view FULL_ELLIPSES("...}}}"); |
| ink_assert(bw.size() == size_t(destLength)); |
| |
| if ((pairSeparatorFallback > pairEndFallback) and ((pairSeparatorFallback + FULL_ELLIPSES.size()) < size_t(destLength))) { |
| // In the report, we can show the existence of the last partial tag/value pair, and maybe part of the value. If we only |
| // show part of the value, we want to end it with an elipsis, to make it clear it's not complete. |
| |
| bw.discard(FULL_ELLIPSES.size()); |
| bw.write(FULL_ELLIPSES); |
| |
| } else if (pairEndFallback and (pairEndFallback < destLength)) { |
| bw.discard(destLength - pairEndFallback); |
| bw.write('}'); |
| |
| } else if ((pairSeparatorFallback > pairEndFallback2) and |
| ((pairSeparatorFallback + FULL_ELLIPSES.size()) < size_t(destLength))) { |
| bw.discard(FULL_ELLIPSES.size()); |
| bw.write(FULL_ELLIPSES); |
| |
| } else if (pairEndFallback2 and (pairEndFallback2 < destLength)) { |
| bw.discard(destLength - pairEndFallback2); |
| bw.write('}'); |
| |
| } else if (destLength > 1) { |
| bw.discard(1); |
| bw.write('}'); |
| } else { |
| bw.clear(); |
| } |
| } |
| |
| *buf += INK_ALIGN_DEFAULT(data - *buf + 1); |
| |
| return bw.size(); |
| } |
| |
| } // end namespace LogUtils |