| #!/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) |