From 285770bb2dd43274d75fdad3b83e917e2ab27252 Mon Sep 17 00:00:00 2001 From: Walter Karas Date: Thu, 14 Jan 2021 17:03:19 -0600 Subject: [PATCH] Add command line utility to help convert remap plugin usage to ATS9. --- doc/appendices/command-line/cvtremappi.en.rst | 69 +++ tools/cvtremappi | 575 ++++++++++++++++++ tools/insnew | 67 ++ 3 files changed, 711 insertions(+) create mode 100644 doc/appendices/command-line/cvtremappi.en.rst create mode 100755 tools/cvtremappi create mode 100755 tools/insnew diff --git a/doc/appendices/command-line/cvtremappi.en.rst b/doc/appendices/command-line/cvtremappi.en.rst new file mode 100644 index 00000000000..264d474a9db --- /dev/null +++ b/doc/appendices/command-line/cvtremappi.en.rst @@ -0,0 +1,69 @@ +.. 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:: ../../common.defs + +.. _cvtremappi: + +cvtremappi +********** + +Description +=========== + +To help convert your remapping configuration from pre-ATS9 to ATS9 and later. It may be useful if you use any +of the core plugins regex_remap.so, header_rewrite.so or gzip.so. (For this script to work, the python3 +command has to be in your path.) You can specify where your remap configuration file is with the option: + +--filepath FILEPATH + +If this parameter is omitted, it defaults to ``./remap.config`` . The script will make necessary modifications +to this file, and any files it includes with ``.include`` . It will change `@plugin=gzip.so` to its new name, +`@plugin=compress.so` . When regex_remap.so is invoked as the first remap plugin, it will add the parameter +@pparam=pristine . (This makes it work the same as in pre-9 ATS, where the request URL is the pre-remapping +URL for the first plugin for a remap rule.) When `header_rewrite.so` is used as a remap plugin, no changes +are needed in the remap configuration line invoking it. However, changes may be necessary to the +configuration files passed to it as parameters. If a header rewrite configuration file is used for both the +invocation of header rewrite as the first plugin for remap rules, and for other invocations, it may be +necessary to generate two new versions of it. In these cases, the prefix `1st-` is added to file's name, +for the version used with header rewrite as the first plugin. If you prefer that a different prefix be added, +you can specify it with this option: + +--prefix PREFIX + +If you are also using header rewrite as a global plugin, you should also provide the filepath of the global +plugin configuration file with this option: + +--plugin PLUGIN + +(Note that, if the PLUGIN filepath is relative, it should be relative to the directory containing the remap +configuration file, not relative to the directory the script is run from. Note also that, if relative paths +for include files for header rewrite config files appear in the configuration files, they are assumed to be +relative to the directory containing the remap configuration file.) + +Header rewrite previously had some logic that has been eliminated in ATS9. If a line in a header rewrite +configuration file relies on this deprecated logic, an error message will be output to standard error. The +text `ERROR:` will be prepended to the line in the configuration file causing the error. + +The script writes, one per line, a list of the files it is changing or creating to the standard output. But +both new and changed files will be written into entirely new files with the suffix `.new` added to the filepath. +For example, if `remap.config` is changed by the script, it will put the changed version of the file in +`remap.config.new` . This gives you a chance to review the changes the script has made. You can then put the +changed files into effect with the tool script `insnew`. This script reads a list of filepaths, one per line, +from the standard input. For each filepath `FP`, if it specifies an existing file, it will rename it to +`FP.old`. It will then rename the file `FP.new` to `FP`. This second script should be run from the same +current directory as the first script was run from. diff --git a/tools/cvtremappi b/tools/cvtremappi new file mode 100755 index 00000000000..290a3b54e53 --- /dev/null +++ b/tools/cvtremappi @@ -0,0 +1,575 @@ +#!/usr/bin/env python3 + +# +# 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. + +# Automatically convert remapping configuration for use of header_rewrite.so, regex_remap.so and gzip.so +# plugins from pre-ATS9 to ATS9 and later. (See docs.trafficserver.apache.org, Command Line Utilities +# Appendix.) + +import argparse + +BACKSLASH = "\\" + +parser = argparse.ArgumentParser( + prog="cvt7to9", + description= "Convert remap configuration from ATS7 to ATS9" +) +parser.add_argument("--filepath", default="remap.config", help="path specifier of remap config file") +parser.add_argument("--prefix", default="1st-", help="prefix for new header_rewrite config files") +parser.add_argument( + "--plugin", default=None, help="path specifier (relative to FILEPATH) of (global) plugins config file" +) +args = parser.parse_args() + +import sys + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + +# Prefix to add to file relative pathspecs before opening file. +# +pathspec_prefix = None + +import os +import copy + +# Remap or header rewrite config file. +# +class CfgFile: + def __init__(self, param): + if isinstance(param, CfgFile): + # Making copy of header rewrite config file to use when header rewrite is first plugin for remap + # rule. + # + self.changed = True + d = os.path.dirname(param.pathspec) + if (d != "") and (d != "/"): + d += "/" + self.pathspec = d + args.prefix + os.path.basename(param.pathspec) + self.lines = copy.copy(param.lines) + + else: + # Opening existing config file, param is pathspec. + # + self.changed = False + self.pathspec = param + ps = param + if ps[:1] != "/": + ps = pathspec_prefix + ps + try: + fd = open(ps, "r") + except: + eprint(f"fatal error: failure opening {ps} for reading") + sys.exit(1) + + try: + self.lines = fd.readlines() + except: + eprint(f"fatal error: failure reading {ps}") + sys.exit(1) + + try: + fd.close() + except: + eprint(f"fatal error: failure closing {ps}") + sys.exit(1) + + # Write out new version of file if it's contents are new. ".new" is appended for the pathspec to get the + # pathspec of the new file. + # + def close(self): + if self.changed: + ps = self.pathspec + if ps[:1] != "/": + ps = pathspec_prefix + ps + print(ps) + ps += ".new" + try: + fd = open(ps, "w") + except: + eprint(f"fatal error: failure opening {ps} for writing") + sys.exit(1) + + for ln in self.lines: + if ln != None: + try: + fd.write(ln) + except: + eprint(f"fatal error: failure writing {ps}") + sys.exit(1) + + try: + fd.close() + except: + eprint(f"fatal error: failure closing {ps}") + sys.exit(1) + +# A generic object. +# +class Obj: + def __init__(self): + pass + +# A dictionary that is a mapping from pathspecs for header rewrite config files to generic objects. If the +# object has a "first" attribute, it is a list of 2-tuples. The first tuple entry is a reference to the +# the CfgFile object for a remap config file. The second tuple entry is the index into the list of lines, +# with the index of the line where header_rewrite.so is the first remap plugin, and the header rewrite +# config file is a parameter to it. If "other" is an attribute of the generic object, the config file is +# used as a parameter to calls to a global instance of header_rewrite.so, or instances of header_rewrite.so on +# a remap rule as the second or later remap plugin. (The value of the "other" attribute is always None because +# the value doesn't matter.) +# +header_rewrite_cfgs = dict() + +# Read in (global) header rewrite config file pathspecs from plugin.config file. +# +def handle_global(gbl_pathspec): + + if gbl_pathspec[:1] != "/": + gbl_pathspec = pathspec_prefix + gbl_pathspec + + try: + fd = open(gbl_pathspec, "r") + except: + eprint(f"fatal error: failure opening {gbl_pathspec} for reading") + sys.exit(1) + + try: + lines = fd.readlines() + except: + eprint(f"fatal error: failure reading {gbl_pathspec}") + sys.exit(1) + + try: + fd.close() + except: + eprint(f"fatal error: failure closing {gbl_pathspec}") + sys.exit(1) + + ln_num = 0 + while ln_num < len(lines): + ln = lines[ln_num] + + # Join continuation lines to the first line. + # + num_ln_joined = 1 + while ((ln_num + num_ln_joined) < len(lines)) and (ln[-2:] == (BACKSLASH + "\n")): + ln[:-2] += " " + lines[ln_num + num_ln_joined] + num_ln_joined += 1 + + ofst = ln.find("#") + if ofst >= 0: + # Remove comment. + # + ln = ln[:ofst] + + ln = ln.split() + + if (len(ln) > 0) and (ln[0] == "header_rewrite.so"): + for param in ln[1:]: + hr_obj = Obj() + hr_obj.other = None + header_rewrite_cfgs[param] = hr_obj + break + + ln_num += num_ln_joined + +# A list of CfgFile instances for the main remap config file and any include files it contains, directly or +# indirectly. +# +remap_cfgs = [] + +# Handle the remap config file, and call this recursively to handle include files. +# +def handle_remap(filepath): + + def skip_white(a_str): + if a_str[:1].isspace(): + return 1 + elif a_str.startswith(BACKSLASH + "\n"): + return 2 + return 0 + + rc = CfgFile(filepath) + + remap_cfgs.append(rc) + + ln_num = 0 + while ln_num < len(rc.lines): + ln = rc.lines[ln_num] + + # Join continuation lines to the first line, and make them None in the rc.lines array. + # + num_ln_joined = 1 + while ((ln_num + num_ln_joined) < len(rc.lines)) and (ln[-2:] == (BACKSLASH + "\n")): + ln += rc.lines[ln_num + num_ln_joined] + rc.lines[ln_num + num_ln_joined] = None + num_ln_joined += 1 + + rc.lines[ln_num] = ln + + len_content = ln.find("#") + if len_content < 0: + len_content = len(ln) - 1 + + # Only process lines with content. + # + if (len_content > 0) and not ln[:len_content].isspace(): + ofst = ln.find(".include") + if (ofst == 0) or ((ofst > 0) and ln[:ofst].isspace()): + ofst += len(".include") + start_ps = -1 + while ofst < len_content: + sw = skip_white(ln[ofst:]) + if sw == 0: + if start_ps < 0: + start_ps = ofst + ofst += 1 + else: + if start_ps >= 0: + handle_remap(ln[start_ps:ofst]) + start_ps = -1 + ofst += sw + + if start_ps >= 0: + handle_remap(ln[start_ps:len_content]) + else: + # First, gzip.so -> compress.so + # + lnc = ln[:len_content] + lncr = lnc.replace("@plugin=gzip.so", "@plugin=compress.so") + if lncr != lnc: + ln = lncr + ln[len_content:] + len_content = len(lncr) + rc.lines[ln_num] = ln + rc.changed = True + + ofst = ln[:len_content].find("@plugin=") + if ofst >= 0: + # Assuming it's some sort of remap line since it's got @plugin. + # + ofst += len("@plugin=") + + new_ln = None + + if ln[ofst:].startswith("header_rewrite.so"): + ofst += len("header_rewrite.so") + + while ofst < len_content: + ofst2 = ln[ofst:len_content].find("@") + if ofst2 < 0: + break; + + ofst += ofst2 + + if not ln[ofst:].startswith("@pparam="): + break + + ofst += len("@pparam=") + + ofst2 = ofst + while ofst2 < len_content: + sw = skip_white(ln[ofst2:]) + if sw != 0: + break + ofst2 += 1 + + hr_pathspec = ln[ofst:ofst2] + ofst = ofst2 + + if hr_pathspec not in header_rewrite_cfgs: + hr_obj = Obj() + header_rewrite_cfgs[hr_pathspec] = hr_obj; + else: + hr_obj = header_rewrite_cfgs[hr_pathspec] + + if not hasattr(hr_obj, "first"): + hr_obj.first = [] + + hr_obj.first.append((rc, ln_num)) + + elif ln[ofst:].startswith("regex_remap.so"): + ofst += len("regex_remap.so") + + ofst2 = ln[ofst:len_content].find("@plugin=") + + if ofst2 < 0: + ofst2 = len_content + else: + ofst2 += ofst + + if ((ln[ofst:ofst2].find("@pparam=pristine") < 0) and + (ln[ofst:ofst2].find("@pparam=no-pristine") < 0)): + + new_ln = ln[:ofst2] + + if not new_ln[-1].isspace(): + new_ln += " " + + new_ln += "@pparam=pristine" + + if not ln[ofst2].isspace(): + new_ln += " " + + ofst = len(new_ln) + + new_ln += ln[ofst2:] + + else: + ofst = ofst2 + + if new_ln: + rc.lines[ln_num] = new_ln + rc.changed = True + len_content += len(new_ln) - len(ln) + ln = new_ln + + # Handle it if header rewrite is called as a second or later plugin. + # + while ofst < len_content: + ofst2 = ln[ofst:len_content].find("@plugin=header_rewrite.so") + + if ofst2 < 0: + break + + ofst += ofst2 + len("@plugin=header_rewrite.so") + + while ofst < len_content: + ofst2 = ln[ofst:len_content].find("@") + if ofst2 < 0: + break + + ofst += ofst2 + 1 + + if not ln[ofst:].startswith("pparam="): + ofst -= 1 + break + + ofst += len("pparam=") + + ofst2 = ofst + while ofst2 < len_content: + sw = skip_white(ln[ofst2:]) + if sw != 0: + break + ofst2 += 1 + + hr_pathspec = ln[ofst:ofst2] + ofst = ofst2 + + if hr_pathspec not in header_rewrite_cfgs: + hr_obj = Obj() + header_rewrite_cfgs[hr_pathspec] = hr_obj; + else: + hr_obj = header_rewrite_cfgs[hr_pathspec] + + hr_obj.other = None # Only the existence of this attr matters, not its value. + + ln_num += num_ln_joined + +def handle_header_rewrite(pathspec, obj): + + # This class manages changes to a header rewrite config file. + # + class HRCfg: + # 'pathspec' is the key in header_rewrite_cfgs and 'obj' is the generic object value. + # + def __init__(self, pathspec, obj): + self._base = CfgFile(pathspec) + self._obj = obj + + def has_first(self): + return hasattr(self._obj, "first") + + def has_other(self): + return hasattr(self._obj, "other") + + def get_lines(self): + return self._base.lines + + # Assumes has_first is true. + # + def chg_ln_first(self, ln_num, ln): + if not hasattr(self, "_first"): + if self.has_other(): + # Must make two versions of header rewrite config file. + # + self._first = CfgFile(self._base) + else: + self._first = self._base + self._first.changed = True + + self._first.lines[ln_num] = ln + + # Assumes has_other is true. + # + def chg_ln_cmn(self, ln_num, ln): + if hasattr(self, "_first") and not (self._first is self._base): + self._first.lines[ln_num] = ln + + self._base.lines[ln_num] = ln + self._base.changed = True + + def close(self): + self._base.close() + if hasattr(self, "_first") and not (self._first is self._base): + self._first.close() + + old_pparam = "@pparam=" + self._base.pathspec + + # Backpatch remap configuration files with pathspec of new header rewrite config file for call + # to header rewrite as first remap plugin. + # + for pair in self._obj.first: + rc = pair[0] + rc.changed = True + ln = rc.lines[pair[1]] + ofst = ln.find(old_pparam) + len("@pparam=") + rc.lines[pair[1]] = ln[:ofst] + self._first.pathspec + ln[ofst + len(self._base.pathspec):] + + # Returns a pair: length of string before # comment, and a flag indicating if %< (beginning of a variable + # exapansion) was in any of the double-quoted strings. + # + def get_content_len(s): + # States. + # + COPYING = 0 + ESCAPE_COPYING = 1 + IN_QUOTED = 2 + ESCAPE_IN_QUOTED = 3 + PERCENT_IN_QUOTED = 4 + + state = COPYING + ofst = 0 + variable_expansion = False + + while ofst < (len(s) - 1): + if state == COPYING: + if s[ofst] == "#": + # Start of comment. + # + return (ofst, variable_expansion) + elif s[ofst] == '"': + state = IN_QUOTED + else: + if s[ofst] == BACKSLASH: + state = ESCAPE_COPYING + + elif state == IN_QUOTED: + if s[ofst] == BACKSLASH: + state = ESCAPE_IN_QUOTED + elif s[ofst] == '"': + state = COPYING + elif s[ofst] == '%': + state = PERCENT_IN_QUOTED + + elif state == ESCAPE_COPYING: + state = COPYING + + elif state == ESCAPE_IN_QUOTED: + if s[ofst] == '%': + state = PERCENT_IN_QUOTED + else: + state = IN_QUOTED + + elif state == PERCENT_IN_QUOTED: + if s[ofst] == '<': + variable_expansion = True + state = IN_QUOTED + elif s[ofst] == BACKSLASH: + state = ESCAPE_IN_QUOTED + elif s[ofst] == '"': + state = COPYING + elif s[ofst] == '%': + state = PERCENT_IN_QUOTED + else: + state = IN_QUOTED + + ofst += 1 + + return (ofst, variable_expansion) + + hrc = HRCfg(pathspec, obj) + lines = hrc.get_lines() + ln_num = 0 + while ln_num < len(lines): + ln = lines[ln_num] + prepend_error = False + + len_content, variable_expansion = get_content_len(ln) + + if variable_expansion: + eprint( + f"error: {pathspec}, line {ln_num + 1}: variable expansions cannot be automatically converted" + ) + prepend_error = True + + lnc = ln[:len_content] + lnc = lnc.replace("%{CLIENT-IP}", "%{INBOUND:REMOTE-ADDR}") + lnc = lnc.replace("%{INCOMING-PORT}", "%{INBOUND:LOCAL-PORT}") + lnc = lnc.replace("%{PATH}", "%{URL:PATH}") + lnc = lnc.replace("%{QUERY}", "%{URL:QUERY}") + + if hrc.has_other() and (prepend_error or (lnc != ln[:len_content])): + if prepend_error: + hrc.chg_ln_cmn(ln_num, "ERROR: " + lnc + ln[len_content:]) + else: + hrc.chg_ln_cmn(ln_num, lnc + ln[len_content:]) + + if hrc.has_first(): + lnc_save = lnc + ofst = lnc.find("set-destination") + if (ofst >= 0): + if (ofst == 0) or lnc[:ofst].isspace(): + ofst += len("set-destination") + ofst2 = lnc[ofst:].find("PATH") + if (ofst2 > 0) and lnc[ofst:(ofst + ofst2)].isspace(): + eprint( + f"error: {pathspec}, line {ln_num + 1}: the functionality of set-destination PATH" + + " for the first remap plugin does not exist in ATS9" + ) + prepend_error = True + + lnc = lnc.replace("%{URL:", "%{CLIENT-URL:") + if lnc != lnc_save or prepend_error: + if prepend_error: + hrc.chg_ln_first(ln_num, "ERROR: " + lnc + ln[len_content:]) + else: + hrc.chg_ln_first(ln_num, lnc + ln[len_content:]) + + ln_num += 1 + + hrc.close() + +pathspec_prefix = os.path.dirname(args.filepath) +if (pathspec_prefix != "") and (pathspec_prefix != "/"): + pathspec_prefix += "/" + +if not (args.plugin is None): + handle_global(args.plugin) + +handle_remap(os.path.basename(args.filepath)) + +for pathspec in header_rewrite_cfgs: + handle_header_rewrite(pathspec, header_rewrite_cfgs[pathspec]) + +# loop through remap_cfgs and close each one. +for rc in remap_cfgs: + rc.close() + +sys.exit(0) diff --git a/tools/insnew b/tools/insnew new file mode 100755 index 00000000000..94b1c8d8c23 --- /dev/null +++ b/tools/insnew @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +# +# 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. + +# For each filespec FS given (one per line) on stdin, rename FS (if it exists) to FS.old, then rename FS.new to +# FS. + +if [[ "$*" = "--help" ]] ; then + echo "For each filespec FS read from stdin, rename FS.new to FS. If FS already exists, rename to FS.old" >&2 + exit 0 +fi + +while read F +do +python3 - "$F" << THE_END + +import sys +import os + +def eprint(*args, **kwargs): + print(*args, file=sys.stderr, **kwargs) + +def rename_file(old_path, new_path): + try: + os.rename(old_path, new_path) + except: + eprint(f"fatal error: failure renaming {old_path} to {new_path}\n") + sys.exit(1) + +def rename_to_old(pathspec): + op = pathspec + ".old" + + if os.access(op, os.F_OK): + last = 2 + while os.access(op + ".{}".format(last), os.F_OK): + last += 1 + while last > 2: + rename_file(op + ".{}".format(last - 1), op + ".{}".format(last)) + last -= 1 + rename_file(op, op + ".2") + + rename_file(pathspec, op) + +filespec = sys.argv[1] +if os.access(filespec, os.F_OK): + rename_to_old(filespec) +rename_file(filespec + ".new", filespec) + +sys.exit(0) + +THE_END +done