| /* |
| * 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 "normalize.h" |
| #include "common.h" |
| #include <cstring> |
| #include <cctype> |
| |
| #include <cstdio> |
| |
| #include "tsutil/LocalBuffer.h" |
| |
| /* Remove Dot Algorithm outlined in RFC3986 section 5.2.4 |
| * Function writes normalizes path and writes to ret_buffer */ |
| int |
| remove_dot_segments(const char *path, int path_ct, char *ret_buffer, int buff_ct) |
| { |
| /* Validate pointers and ensure buffer has room for the path plus NUL terminator */ |
| if (path == nullptr || ret_buffer == nullptr || path_ct < 0 || buff_ct <= path_ct) { |
| PluginDebug("Path buffer not large enough"); |
| return -1; |
| } |
| |
| /* Create an input buffer that we can change */ |
| size_t inBuff_size = static_cast<size_t>(path_ct) + 1; |
| ts::LocalBuffer<char> inBuff_storage(inBuff_size); |
| char *inBuff = inBuff_storage.data(); |
| memset(inBuff, 0, inBuff_size); |
| memcpy(inBuff, path, path_ct); |
| |
| const char *path_end = inBuff + path_ct; |
| char *seg_start = inBuff; |
| char *seg_end; |
| char *write_buffer = ret_buffer; |
| int seg_len; |
| |
| for (;;) { |
| if (seg_start == path_end) { |
| break; |
| } |
| seg_end = seg_start + 1; |
| |
| /* Parse such that Seg start/end contain the next full path segment */ |
| while (seg_end != path_end && *seg_end != '/') { |
| seg_end++; |
| } |
| |
| seg_len = seg_end - seg_start + 1; |
| |
| /* Remove starting ../ or ./ from input buffer */ |
| if (!strncmp(seg_start, "../", seg_len) || !strncmp(seg_start, "./", seg_len)) { |
| if (seg_end != path_end) { |
| seg_end++; |
| } |
| } |
| |
| /* Remove starting /./ or /. from input buffer and replace with '/' in output buffer */ |
| else if (!strncmp(seg_start, "/./", seg_len) || !strncmp(seg_start, "/.", seg_len)) { |
| *write_buffer = '/'; |
| write_buffer++; |
| if (seg_end != path_end) { |
| seg_end++; |
| } |
| } |
| |
| /* Replace /../ or /.. with / in write_buffer and remove preceding segment */ |
| else if (!strncmp(seg_start, "/../", seg_len) || !strncmp(seg_start, "/..", seg_len)) { |
| int prev_len = 0; |
| while (*write_buffer != '/' && write_buffer != ret_buffer) { |
| prev_len++; |
| write_buffer--; |
| } |
| memset(write_buffer, 0, prev_len); |
| |
| /* Replace segment with '/' in input buffer */ |
| if (seg_end != path_end) { |
| seg_start[seg_len - 1] = '/'; |
| } else { |
| seg_start[seg_len - 2] = '/'; |
| seg_end--; |
| } |
| } |
| |
| /* Remove starting '.' or '..' from input buffer */ |
| else if (!strncmp(seg_start, ".", seg_len) || !strncmp(seg_start, "..", seg_len)) { |
| if (seg_end != path_end) { |
| seg_end++; |
| } |
| } |
| /* Place the current path segment to the output buffer including initial '/' but not the next '/' */ |
| else { |
| /* Write first forward slash to buffer */ |
| if (*seg_start == '/') { |
| *write_buffer = *seg_start; |
| write_buffer++; |
| seg_start++; |
| } |
| |
| /* Write subsequent characters to buffer */ |
| while (*seg_start != '/' && *seg_start != '\0') { |
| *write_buffer = *seg_start; |
| write_buffer++; |
| seg_start++; |
| } |
| } |
| seg_start = seg_end; |
| } |
| |
| *write_buffer = '\0'; |
| PluginDebug("Normalized Path: %s", ret_buffer); |
| return static_cast<int>(write_buffer - ret_buffer); |
| } |
| |
| /* Function percent decodes uri_ct characters of the string uri and writes it to the decoded_uri |
| * buffer. If lower is true, it sets all characters including decoded ones to lower case. |
| * The function returns the length of the decoded string or -1 if there was a parsing error |
| * TODO: ADD functionality to ignore unicode non-standard characters and leave them encoded. Read RFC regarding normalization and |
| * determine if this is compliant. |
| */ |
| int |
| percent_decode(const char *uri, int uri_ct, char *decoded_uri, bool lower) |
| { |
| static const char *reserved_string = ":/?#[]@!$&\'()*+,;="; |
| |
| if (uri_ct <= 0) { |
| return 0; |
| } |
| |
| int offset = 0; |
| int i; |
| for (i = 0; i < uri_ct; i++) { |
| if (uri[i] == '%') { |
| /* The next two characters are interpreted as the hex encoded value. Store in encodedVal */ |
| if (uri_ct < i + 2) { |
| goto decode_failure; |
| } |
| char encodedVal[2] = {0}; |
| int j; |
| for (j = 0; j < 2; j++) { |
| if (isxdigit(uri[i + j + 1])) { |
| encodedVal[j] = uri[i + j + 1]; |
| } else { |
| goto decode_failure; |
| } |
| } |
| int hexVal = 0; |
| char decodeChar; |
| sscanf(encodedVal, "%2x", &hexVal); |
| decodeChar = static_cast<char>(hexVal); |
| /* If encoded value is a reserved char, leave encoded*/ |
| if (strchr(reserved_string, decodeChar)) { |
| decoded_uri[i - offset] = uri[i]; |
| decoded_uri[i + 1 - offset] = toupper(uri[i + 1]); |
| decoded_uri[i + 2 - offset] = toupper(uri[i + 2]); |
| } |
| /* If not a reserved char, decode using the decoded_uri buffer */ |
| else { |
| if (lower) { |
| decoded_uri[i - offset] = tolower(decodeChar); |
| } else { |
| decoded_uri[i - offset] = decodeChar; |
| } |
| offset = offset + 2; |
| } |
| i = i + 2; |
| } |
| /* Write non-encoded values to decoded buffer */ |
| else { |
| if (lower) { |
| decoded_uri[i - offset] = tolower(uri[i]); |
| } else { |
| decoded_uri[i - offset] = uri[i]; |
| } |
| } |
| } |
| |
| /* Return the size of the newly decoded string */ |
| return uri_ct - offset; |
| |
| decode_failure: |
| PluginDebug("ERROR Decoding URI"); |
| return -1; |
| } |
| |
| /* This function takes a uri and an initialized buffer to populate with the normalized uri. |
| * Returns non zero for error. |
| * |
| * The buffer provided must be at least uri_ct + 2 bytes: the normalized uri can be one |
| * character larger than the original (a trailing '/' added when the path is empty) plus |
| * one byte for the NUL terminator. |
| * |
| * The normalization function returns a string with the following modifications |
| * 1. Lowecase protocol/domain |
| * 2. Path segments .. and . are removed from path |
| * 3. Alphabetical percent encoded octet values are toupper |
| * 4. Non-reserved percent encoded octet values are decoded |
| * 5. The Port is removed if it is default |
| * 6. Defaults to a single forward slash for the path segment if path segment is empty |
| */ |
| int |
| normalize_uri(const char *uri, int uri_ct, char *normal_uri, int normal_ct) |
| { |
| /* Validate inputs before any pointer arithmetic or dereferences. |
| * The output buffer must hold uri_ct + 2 bytes: the URI itself plus a |
| * possible trailing '/' added when the authority has an empty path, plus |
| * the NUL terminator. The overflow-safe form of normal_ct < uri_ct + 2 |
| * is normal_ct - 1 <= uri_ct. |
| */ |
| if (uri == nullptr || normal_uri == nullptr || uri_ct < 0 || normal_ct < 2 || normal_ct - 1 <= uri_ct) { |
| PluginDebug("Buffer to Normalize URI not large enough."); |
| return -1; |
| } |
| |
| PluginDebug("Normalizing URI: %s", uri); |
| |
| /* Buffer provided must be large enough to store the uri plus one additional char */ |
| const char *uri_end = uri + uri_ct; |
| const char *buff_end = normal_uri + normal_ct; |
| |
| /* Initialize a path buffer to pass to path normalization function later on */ |
| ts::LocalBuffer<char> path_buffer_storage(normal_ct); |
| char *path_buffer = path_buffer_storage.data(); |
| memset(path_buffer, 0, normal_ct); |
| |
| /* Comp variables store starting/ending indexes for each uri component as uri is parsed. |
| * Write buffer traverses the normalized uri buffer as we build the normalized string. |
| */ |
| const char *comp_start = uri; |
| const char *comp_end = uri; |
| char *write_buffer = normal_uri; |
| bool https = false; |
| |
| /* Parse the protocol which will end with a colon */ |
| while (*comp_end != ':' && comp_end != uri_end) { |
| *write_buffer = tolower(*comp_end); |
| comp_end++; |
| write_buffer++; |
| } |
| |
| /* Set comp start/end to contain authority component */ |
| bool userInfo = false; |
| |
| if (comp_end == uri_end) { |
| PluginDebug("Reached End of String prematurely"); |
| goto normalize_failure; |
| } |
| |
| /* Copy the colon */ |
| *write_buffer = *comp_end; |
| comp_end++; |
| write_buffer++; |
| |
| /* Ensure the protocol is either http or https */ |
| if (strcmp("https:", normal_uri) == 0) { |
| https = true; |
| } else if (strcmp("http:", normal_uri)) { |
| PluginDebug("String is neither http or https"); |
| goto normalize_failure; |
| } |
| |
| /* Protocol must be terminated by two forward slashes */ |
| int i; |
| for (i = 0; i < 2; i++) { |
| if (comp_end == uri_end || *comp_end != '/') { |
| goto normalize_failure; |
| } |
| *write_buffer = *comp_end; |
| comp_end++; |
| write_buffer++; |
| } |
| |
| if (comp_end == uri_end) { |
| goto normalize_failure; |
| } |
| |
| /* Comp_start is index of start of authority component */ |
| int comp_ct; |
| comp_start = comp_end; |
| |
| while (comp_end != uri_end && *comp_end != '/' && *comp_end != '?' && *comp_end != '#') { |
| /* If we encounter userinfo, decode it without altering case and set comp_start/end to only include hostname/port */ |
| if (*comp_end == '@' && userInfo == false) { |
| comp_ct = comp_end - comp_start; |
| comp_ct = percent_decode(comp_start, comp_ct, write_buffer, false); |
| if (comp_ct < 0) { |
| goto normalize_failure; |
| } |
| comp_start = comp_end; |
| userInfo = true; |
| write_buffer = write_buffer + comp_ct; |
| } |
| comp_end++; |
| } |
| |
| /* UserInfo without a hostname is invalid */ |
| if (userInfo == true && comp_end == uri_end) { |
| goto normalize_failure; |
| } |
| |
| comp_ct = comp_end - comp_start; |
| |
| /* - comp start/end holds indices in original uri of hostname/port |
| * - write_buffer holds pointer to start of hostname/port written to the decode buffer |
| * - comp_ct holds size of hostname/port in original uri |
| */ |
| |
| /* Parse and decode the hostname and port and set to lower case */ |
| comp_ct = percent_decode(comp_start, comp_ct, write_buffer, true); |
| |
| if (comp_ct < 0) { |
| goto normalize_failure; |
| } |
| |
| /* Remove the port from the buffer if default */ |
| while (*write_buffer != 0) { |
| if (*write_buffer == ':') { |
| if (https == true && !strncmp(write_buffer, ":443", 5)) { |
| memset(write_buffer, 0, 4); |
| break; |
| } else if (https == false && !strncmp(write_buffer, ":80", 4)) { |
| memset(write_buffer, 0, 3); |
| break; |
| } |
| } |
| write_buffer++; |
| } |
| |
| comp_start = comp_end; |
| |
| /* If we have reached the end of the authority section with an empty path component, add a trailing backslash */ |
| if (*comp_end == 0 || *comp_end == '?' || *comp_end == '#') { |
| *write_buffer = '/'; |
| write_buffer++; |
| } |
| |
| /* If there is a path component, normalize it */ |
| else { |
| /* Set comp start/end pointers to contain the path component */ |
| while (*comp_end != '?' && *comp_end != '#' && *comp_end != 0) { |
| comp_end++; |
| } |
| /* Decode the path component without altering case and store it to the path_buffer*/ |
| comp_ct = comp_end - comp_start; |
| comp_ct = percent_decode(comp_start, comp_ct, path_buffer, false); |
| |
| if (comp_ct < 0) { |
| goto normalize_failure; |
| } |
| |
| /* Remove the . and .. segments from the path and write the now normalized path to the output buffer */ |
| PluginDebug("Removing Dot Segments"); |
| int buff_ct = buff_end - write_buffer; |
| comp_ct = remove_dot_segments(path_buffer, comp_ct, write_buffer, buff_ct); |
| |
| if (comp_ct < 0) { |
| PluginDebug("Failure removing dot segments from path"); |
| goto normalize_failure; |
| } |
| write_buffer = write_buffer + comp_ct; |
| } |
| |
| /* If there is any uri remaining after the path, decode and set case to lower */ |
| if (comp_end != uri_end) { |
| comp_start = comp_end; |
| comp_ct = uri_end - comp_start; |
| comp_ct = percent_decode(comp_start, comp_ct, write_buffer, false); |
| if (comp_ct < 0) { |
| goto normalize_failure; |
| } |
| } |
| |
| PluginDebug("Normalized URI: %s", normal_uri); |
| return 0; |
| |
| normalize_failure: |
| PluginDebug("URI Normalization Failure. URI does not fit http or https schemes."); |
| return -1; |
| } |