blob: 290a3b54e53b386bead01e12c301198787aadf2f [file] [log] [blame]
#!/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)