mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 17:12:13 -06:00
add cors controls + improve preflight + pw header
This commit is contained in:
parent
0be1e43451
commit
741d781c18
28
README.md
28
README.md
|
@ -88,6 +88,7 @@ try the **[read-only demo server](https://a.ocv.me/pub/demo/)** 👀 running fro
|
||||||
* [client-side](#client-side) - when uploading files
|
* [client-side](#client-side) - when uploading files
|
||||||
* [security](#security) - some notes on hardening
|
* [security](#security) - some notes on hardening
|
||||||
* [gotchas](#gotchas) - behavior that might be unexpected
|
* [gotchas](#gotchas) - behavior that might be unexpected
|
||||||
|
* [cors](#cors) - cross-site request config
|
||||||
* [recovering from crashes](#recovering-from-crashes)
|
* [recovering from crashes](#recovering-from-crashes)
|
||||||
* [client crashes](#client-crashes)
|
* [client crashes](#client-crashes)
|
||||||
* [frefox wsod](#frefox-wsod) - firefox 87 can crash during uploads
|
* [frefox wsod](#frefox-wsod) - firefox 87 can crash during uploads
|
||||||
|
@ -1147,11 +1148,11 @@ interact with copyparty using non-browser clients
|
||||||
* curl/wget: upload some files (post=file, chunk=stdin)
|
* curl/wget: upload some files (post=file, chunk=stdin)
|
||||||
* `post(){ curl -F act=bput -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}`
|
* `post(){ curl -F act=bput -F f=@"$1" http://127.0.0.1:3923/?pw=wark;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `post(){ curl -b cppwd=wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}`
|
* `post(){ curl -H pw:wark -H rand:8 -T "$1" http://127.0.0.1:3923/;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `post(){ wget --header='Cookie: cppwd=wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
* `post(){ wget --header='pw: wark' --post-file="$1" -O- http://127.0.0.1:3923/?raw;}`
|
||||||
`post movie.mkv`
|
`post movie.mkv`
|
||||||
* `chunk(){ curl -b cppwd=wark -T- http://127.0.0.1:3923/;}`
|
* `chunk(){ curl -H pw:wark -T- http://127.0.0.1:3923/;}`
|
||||||
`chunk <movie.mkv`
|
`chunk <movie.mkv`
|
||||||
|
|
||||||
* bash: when curl and wget is not available or too boring
|
* bash: when curl and wget is not available or too boring
|
||||||
|
@ -1175,7 +1176,7 @@ copyparty returns a truncated sha512sum of your PUT/POST as base64; you can gene
|
||||||
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
|
b512(){ printf "$((sha512sum||shasum -a512)|sed -E 's/ .*//;s/(..)/\\x\1/g')"|base64|tr '+/' '-_'|head -c44;}
|
||||||
b512 <movie.mkv
|
b512 <movie.mkv
|
||||||
|
|
||||||
you can provide passwords using cookie `cppwd=hunter2`, as a url-param `?pw=hunter2`, or with basic-authentication (either as the username or password)
|
you can provide passwords using header `PW: hunter2`, cookie `cppwd=hunter2`, url-param `?pw=hunter2`, or with basic-authentication (either as the username or password)
|
||||||
|
|
||||||
NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename
|
NOTE: curl will not send the original filename if you use `-T` combined with url-params! Also, make sure to always leave a trailing slash in URLs unless you want to override the filename
|
||||||
|
|
||||||
|
@ -1236,6 +1237,11 @@ when uploading files,
|
||||||
|
|
||||||
some notes on hardening
|
some notes on hardening
|
||||||
|
|
||||||
|
* set `--rproxy 0` if your copyparty is directly facing the internet (not through a reverse-proxy)
|
||||||
|
* cors doesn't work right otherwise
|
||||||
|
|
||||||
|
safety profiles:
|
||||||
|
|
||||||
* option `-s` is a shortcut to set the following options:
|
* option `-s` is a shortcut to set the following options:
|
||||||
* `--no-thumb` disables thumbnails and audio transcoding to stop copyparty from running `FFmpeg`/`Pillow`/`VIPS` on uploaded files, which is a [good idea](https://www.cvedetails.com/vulnerability-list.php?vendor_id=3611) if anonymous upload is enabled
|
* `--no-thumb` disables thumbnails and audio transcoding to stop copyparty from running `FFmpeg`/`Pillow`/`VIPS` on uploaded files, which is a [good idea](https://www.cvedetails.com/vulnerability-list.php?vendor_id=3611) if anonymous upload is enabled
|
||||||
* `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate
|
* `--no-mtag-ff` uses `mutagen` to grab music tags instead of `FFmpeg`, which is safer and faster but less accurate
|
||||||
|
@ -1269,6 +1275,20 @@ other misc notes:
|
||||||
behavior that might be unexpected
|
behavior that might be unexpected
|
||||||
|
|
||||||
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
|
* users without read-access to a folder can still see the `.prologue.html` / `.epilogue.html` / `README.md` contents, for the purpose of showing a description on how to use the uploader for example
|
||||||
|
* anyone with write access can upload a `README.md` with a `<script>` which runs for all visitors unless `--no-readme`
|
||||||
|
* anyone with move access can rename `some.html` to `.epilogue.html` so it'll run for all visitors unless either `--no-logues` or `--no-dot-ren`
|
||||||
|
|
||||||
|
|
||||||
|
## cors
|
||||||
|
|
||||||
|
cross-site request config
|
||||||
|
|
||||||
|
by default, except for `GET` and `HEAD` operations, all requests must either:
|
||||||
|
* not contain an `Origin` header at all
|
||||||
|
* or have an `Origin` matching the server domain
|
||||||
|
* or the header `PW` with your password as value
|
||||||
|
|
||||||
|
cors can be configured with `--acao` and `--acam`, or the protections entirely disabled with `--allow-csrf`
|
||||||
|
|
||||||
|
|
||||||
# recovering from crashes
|
# recovering from crashes
|
||||||
|
|
|
@ -818,6 +818,11 @@ def add_hooks(ap):
|
||||||
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
|
ap2.add_argument("--xm", metavar="CMD", type=u, action="append", help="execute CMD on message")
|
||||||
|
|
||||||
|
|
||||||
|
def add_yolo(ap):
|
||||||
|
ap2 = ap.add_argument_group('yolo options')
|
||||||
|
ap2.add_argument("--allow-csrf", action="store_true", help="disable csrf protections; let other domains/sites impersonate you through cross-site requests")
|
||||||
|
|
||||||
|
|
||||||
def add_optouts(ap):
|
def add_optouts(ap):
|
||||||
ap2 = ap.add_argument_group('opt-outs')
|
ap2 = ap.add_argument_group('opt-outs')
|
||||||
ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
|
ap2.add_argument("-nw", action="store_true", help="never write anything to disk (debug/benchmark)")
|
||||||
|
@ -852,6 +857,8 @@ def add_safety(ap, fk_salt):
|
||||||
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="no", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (disabled by default since turbo-up2k counts as 404s)")
|
ap2.add_argument("--ban-404", metavar="N,W,B", type=u, default="no", help="hitting more than \033[33mN\033[0m 404's in \033[33mW\033[0m minutes = ban for \033[33mB\033[0m minutes (disabled by default since turbo-up2k counts as 404s)")
|
||||||
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for MIN minutes (and also kill its active connections) -- disable with 0")
|
ap2.add_argument("--aclose", metavar="MIN", type=int, default=10, help="if a client maxes out the server connection limit, downgrade it from connection:keep-alive to connection:close for MIN minutes (and also kill its active connections) -- disable with 0")
|
||||||
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for B minutes; disable with [\033[32m0\033[0m]")
|
ap2.add_argument("--loris", metavar="B", type=int, default=60, help="if a client maxes out the server connection limit without sending headers, ban it for B minutes; disable with [\033[32m0\033[0m]")
|
||||||
|
ap2.add_argument("--acao", metavar="V[,V]", type=u, default="*", help="Access-Control-Allow-Origin; list of origins (domains) to accept requests from. Default (*) allows requests from any site but will ignore cookies and http-auth (except for ?pw=hunter2)")
|
||||||
|
ap2.add_argument("--acam", metavar="V[,V]", type=u, default="GET,HEAD", help="Access-Control-Allow-Methods; list of methods to accept from offsite ('*' behaves like described in --acao)")
|
||||||
|
|
||||||
|
|
||||||
def add_shutdown(ap):
|
def add_shutdown(ap):
|
||||||
|
@ -1027,9 +1034,10 @@ def run_argparse(
|
||||||
add_webdav(ap)
|
add_webdav(ap)
|
||||||
add_smb(ap)
|
add_smb(ap)
|
||||||
add_safety(ap, fk_salt)
|
add_safety(ap, fk_salt)
|
||||||
add_hooks(ap)
|
|
||||||
add_optouts(ap)
|
add_optouts(ap)
|
||||||
add_shutdown(ap)
|
add_shutdown(ap)
|
||||||
|
add_yolo(ap)
|
||||||
|
add_hooks(ap)
|
||||||
add_ui(ap, retry)
|
add_ui(ap, retry)
|
||||||
add_admin(ap)
|
add_admin(ap)
|
||||||
add_logging(ap)
|
add_logging(ap)
|
||||||
|
|
|
@ -157,7 +157,7 @@ class HttpCli(object):
|
||||||
self.trailing_slash = True
|
self.trailing_slash = True
|
||||||
self.out_headerlist: list[tuple[str, str]] = []
|
self.out_headerlist: list[tuple[str, str]] = []
|
||||||
self.out_headers = {
|
self.out_headers = {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Vary": "Origin, PW, Cookie",
|
||||||
"Cache-Control": "no-store; max-age=0",
|
"Cache-Control": "no-store; max-age=0",
|
||||||
}
|
}
|
||||||
h = self.args.html_head
|
h = self.args.html_head
|
||||||
|
@ -346,11 +346,12 @@ class HttpCli(object):
|
||||||
if zso:
|
if zso:
|
||||||
zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
|
zsll = [x.split("=", 1) for x in zso.split(";") if "=" in x]
|
||||||
cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
|
cookies = {k.strip(): unescape_cookie(zs) for k, zs in zsll}
|
||||||
for kc, ku in (("cppws", "pw"), ("cppwd", "pw"), ("b", "b")):
|
cookie_pw = cookies.get("cppws") or cookies.get("cppwd")
|
||||||
if kc in cookies and ku not in uparam:
|
if "b" in cookies and "b" not in uparam:
|
||||||
uparam[ku] = cookies[kc]
|
uparam["b"] = cookies["b"]
|
||||||
else:
|
else:
|
||||||
cookies = {}
|
cookies = {}
|
||||||
|
cookie_pw = ""
|
||||||
|
|
||||||
if len(uparam) > 10 or len(cookies) > 50:
|
if len(uparam) > 10 or len(cookies) > 50:
|
||||||
raise Pebkac(400, "u wot m8")
|
raise Pebkac(400, "u wot m8")
|
||||||
|
@ -363,25 +364,24 @@ class HttpCli(object):
|
||||||
if ANYWIN:
|
if ANYWIN:
|
||||||
ok = ok and not relchk(self.vpath)
|
ok = ok and not relchk(self.vpath)
|
||||||
|
|
||||||
if not ok:
|
if not ok and (self.vpath != "*" or self.mode != "OPTIONS"):
|
||||||
self.log("invalid relpath [{}]".format(self.vpath))
|
self.log("invalid relpath [{}]".format(self.vpath))
|
||||||
return self.tx_404() and self.keepalive
|
return self.tx_404() and self.keepalive
|
||||||
|
|
||||||
pwd = ""
|
|
||||||
zso = self.headers.get("authorization")
|
zso = self.headers.get("authorization")
|
||||||
|
bauth = ""
|
||||||
if zso:
|
if zso:
|
||||||
try:
|
try:
|
||||||
zb = zso.split(" ")[1].encode("ascii")
|
zb = zso.split(" ")[1].encode("ascii")
|
||||||
zs = base64.b64decode(zb).decode("utf-8")
|
zs = base64.b64decode(zb).decode("utf-8")
|
||||||
# try "pwd", "x:pwd", "pwd:x"
|
# try "pwd", "x:pwd", "pwd:x"
|
||||||
for zs in [zs] + zs.split(":", 1)[::-1]:
|
for bauth in [zs] + zs.split(":", 1)[::-1]:
|
||||||
if self.asrv.iacct.get(zs):
|
if self.asrv.iacct.get(bauth):
|
||||||
pwd = zs
|
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.pw = uparam.get("pw") or pwd
|
self.pw = uparam.get("pw") or self.headers.get("pw") or bauth or cookie_pw
|
||||||
self.uname = self.asrv.iacct.get(self.pw) or "*"
|
self.uname = self.asrv.iacct.get(self.pw) or "*"
|
||||||
self.rvol = self.asrv.vfs.aread[self.uname]
|
self.rvol = self.asrv.vfs.aread[self.uname]
|
||||||
self.wvol = self.asrv.vfs.awrite[self.uname]
|
self.wvol = self.asrv.vfs.awrite[self.uname]
|
||||||
|
@ -390,7 +390,10 @@ class HttpCli(object):
|
||||||
self.gvol = self.asrv.vfs.aget[self.uname]
|
self.gvol = self.asrv.vfs.aget[self.uname]
|
||||||
self.upvol = self.asrv.vfs.apget[self.uname]
|
self.upvol = self.asrv.vfs.apget[self.uname]
|
||||||
|
|
||||||
if self.pw:
|
if self.pw and (
|
||||||
|
self.pw != cookie_pw or self.conn.freshen_pwd + 30 < time.time()
|
||||||
|
):
|
||||||
|
self.conn.freshen_pwd = time.time()
|
||||||
self.get_pwd_cookie(self.pw)
|
self.get_pwd_cookie(self.pw)
|
||||||
|
|
||||||
if self.is_rclone:
|
if self.is_rclone:
|
||||||
|
@ -408,15 +411,22 @@ class HttpCli(object):
|
||||||
) = self.asrv.vfs.can_access(self.vpath, self.uname)
|
) = self.asrv.vfs.can_access(self.vpath, self.uname)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# getattr(self.mode) is not yet faster than this
|
cors_k = self._cors()
|
||||||
if self.mode in ["GET", "HEAD"]:
|
if self.mode in ("GET", "HEAD"):
|
||||||
return self.handle_get() and self.keepalive
|
return self.handle_get() and self.keepalive
|
||||||
elif self.mode == "POST":
|
if self.mode == "OPTIONS":
|
||||||
|
return self.handle_options() and self.keepalive
|
||||||
|
|
||||||
|
if not cors_k:
|
||||||
|
origin = self.headers.get("origin", "<?>")
|
||||||
|
self.log("cors-reject {} from {}".format(self.mode, origin), 3)
|
||||||
|
raise Pebkac(403, "no surfing")
|
||||||
|
|
||||||
|
# getattr(self.mode) is not yet faster than this
|
||||||
|
if self.mode == "POST":
|
||||||
return self.handle_post() and self.keepalive
|
return self.handle_post() and self.keepalive
|
||||||
elif self.mode == "PUT":
|
elif self.mode == "PUT":
|
||||||
return self.handle_put() and self.keepalive
|
return self.handle_put() and self.keepalive
|
||||||
elif self.mode == "OPTIONS":
|
|
||||||
return self.handle_options() and self.keepalive
|
|
||||||
elif self.mode == "PROPFIND":
|
elif self.mode == "PROPFIND":
|
||||||
return self.handle_propfind() and self.keepalive
|
return self.handle_propfind() and self.keepalive
|
||||||
elif self.mode == "DELETE":
|
elif self.mode == "DELETE":
|
||||||
|
@ -635,6 +645,60 @@ class HttpCli(object):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _cors(self) -> bool:
|
||||||
|
ih = self.headers
|
||||||
|
origin = ih.get("origin")
|
||||||
|
if not origin:
|
||||||
|
return True
|
||||||
|
|
||||||
|
oh = self.out_headers
|
||||||
|
origin = re.sub(r"(:[0-9]{1,5})?/?$", "", origin.lower())
|
||||||
|
methods = ", ".join(self.conn.hsrv.mallow)
|
||||||
|
good_origins = self.args.acao + [
|
||||||
|
"{}://{}".format(
|
||||||
|
"https" if self.is_https else "http",
|
||||||
|
self.host.lower().split(":")[0],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if origin in good_origins:
|
||||||
|
good_origin = True
|
||||||
|
bad_hdrs = ("",)
|
||||||
|
else:
|
||||||
|
good_origin = False
|
||||||
|
bad_hdrs = ("", "pw")
|
||||||
|
|
||||||
|
# '*' blocks all credentials (cookies, http-auth);
|
||||||
|
# exact-match for Origin is necessary to unlock those,
|
||||||
|
# however yolo-requests (?pw=) are always allowed
|
||||||
|
acah = ih.get("access-control-request-headers", "")
|
||||||
|
acao = (origin if good_origin else None) or (
|
||||||
|
"*" if "*" in good_origins else None
|
||||||
|
)
|
||||||
|
if self.args.allow_csrf:
|
||||||
|
acao = origin or acao or "*" # explicitly permit impersonation
|
||||||
|
acam = ", ".join(methods) # and all methods + headers
|
||||||
|
oh["Access-Control-Allow-Credentials"] = "true"
|
||||||
|
good_origin = True
|
||||||
|
else:
|
||||||
|
acam = ", ".join(self.args.acam)
|
||||||
|
# wash client-requested headers and roll with that
|
||||||
|
if "range" not in acah.lower():
|
||||||
|
acah += ",Range" # firefox
|
||||||
|
req_h = acah.split(",")
|
||||||
|
req_h = [x.strip() for x in req_h]
|
||||||
|
req_h = [x for x in req_h if x.lower() not in bad_hdrs]
|
||||||
|
acah = ", ".join(req_h)
|
||||||
|
|
||||||
|
if not acao:
|
||||||
|
return False
|
||||||
|
|
||||||
|
oh["Access-Control-Allow-Origin"] = acao
|
||||||
|
oh["Access-Control-Allow-Methods"] = acam.upper()
|
||||||
|
if acah:
|
||||||
|
oh["Access-Control-Allow-Headers"] = acah
|
||||||
|
|
||||||
|
return good_origin
|
||||||
|
|
||||||
def handle_get(self) -> bool:
|
def handle_get(self) -> bool:
|
||||||
if self.do_log:
|
if self.do_log:
|
||||||
logmsg = "{:4} {}".format(self.mode, self.req)
|
logmsg = "{:4} {}".format(self.mode, self.req)
|
||||||
|
@ -1088,26 +1152,16 @@ class HttpCli(object):
|
||||||
if self.do_log:
|
if self.do_log:
|
||||||
self.log("OPTIONS " + self.req)
|
self.log("OPTIONS " + self.req)
|
||||||
|
|
||||||
ret = {
|
oh = self.out_headers
|
||||||
"Allow": "GET, HEAD, POST, PUT, OPTIONS",
|
oh["Allow"] = ", ".join(self.conn.hsrv.mallow)
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Access-Control-Allow-Methods": "*",
|
|
||||||
"Access-Control-Allow-Headers": "*",
|
|
||||||
}
|
|
||||||
|
|
||||||
wd = {
|
|
||||||
"Dav": "1, 2",
|
|
||||||
"Ms-Author-Via": "DAV",
|
|
||||||
}
|
|
||||||
|
|
||||||
if not self.args.no_dav:
|
if not self.args.no_dav:
|
||||||
# PROPPATCH, LOCK, UNLOCK, COPY: noop (spec-must)
|
# PROPPATCH, LOCK, UNLOCK, COPY: noop (spec-must)
|
||||||
zs = ", PROPFIND, PROPPATCH, LOCK, UNLOCK, MKCOL, COPY, MOVE, DELETE"
|
oh["Dav"] = "1, 2"
|
||||||
ret["Allow"] += zs
|
oh["Ms-Author-Via"] = "DAV"
|
||||||
ret.update(wd)
|
|
||||||
|
|
||||||
# winxp-webdav doesnt know what 204 is
|
# winxp-webdav doesnt know what 204 is
|
||||||
self.send_headers(0, 200, headers=ret)
|
self.send_headers(0, 200)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def handle_delete(self) -> bool:
|
def handle_delete(self) -> bool:
|
||||||
|
|
|
@ -65,6 +65,7 @@ class HttpConn(object):
|
||||||
self.ico: Ico = Ico(self.args) # mypy404
|
self.ico: Ico = Ico(self.args) # mypy404
|
||||||
|
|
||||||
self.t0: float = time.time() # mypy404
|
self.t0: float = time.time() # mypy404
|
||||||
|
self.freshen_pwd: float = 0.0
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.nreq: int = -1 # mypy404
|
self.nreq: int = -1 # mypy404
|
||||||
self.nbyte: int = 0 # mypy404
|
self.nbyte: int = 0 # mypy404
|
||||||
|
|
|
@ -109,6 +109,11 @@ class HttpSrv(object):
|
||||||
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
zs = os.path.join(self.E.mod, "web", "deps", "prism.js.gz")
|
||||||
self.prism = os.path.exists(zs)
|
self.prism = os.path.exists(zs)
|
||||||
|
|
||||||
|
self.mallow = "GET HEAD POST PUT DELETE OPTIONS".split()
|
||||||
|
if not self.args.no_dav:
|
||||||
|
zs = "PROPFIND PROPPATCH LOCK UNLOCK MKCOL COPY MOVE"
|
||||||
|
self.mallow += zs.split()
|
||||||
|
|
||||||
if self.args.zs:
|
if self.args.zs:
|
||||||
from .ssdp import SSDPr
|
from .ssdp import SSDPr
|
||||||
|
|
||||||
|
|
|
@ -294,12 +294,25 @@ class SvcHub(object):
|
||||||
al.zs_on = al.zs_on or al.z_on
|
al.zs_on = al.zs_on or al.z_on
|
||||||
al.zm_off = al.zm_off or al.z_off
|
al.zm_off = al.zm_off or al.z_off
|
||||||
al.zs_off = al.zs_off or al.z_off
|
al.zs_off = al.zs_off or al.z_off
|
||||||
for n in ("zm_on", "zm_off", "zs_on", "zs_off"):
|
ns = "zm_on zm_off zs_on zs_off acao acam"
|
||||||
|
for n in ns.split(" "):
|
||||||
vs = getattr(al, n).split(",")
|
vs = getattr(al, n).split(",")
|
||||||
vs = [x.strip() for x in vs]
|
vs = [x.strip() for x in vs]
|
||||||
vs = [x for x in vs if x]
|
vs = [x for x in vs if x]
|
||||||
setattr(al, n, vs)
|
setattr(al, n, vs)
|
||||||
|
|
||||||
|
ns = "acao acam"
|
||||||
|
for n in ns.split(" "):
|
||||||
|
vs = getattr(al, n)
|
||||||
|
vd = {zs: 1 for zs in vs}
|
||||||
|
setattr(al, n, vd)
|
||||||
|
|
||||||
|
ns = "acao"
|
||||||
|
for n in ns.split(" "):
|
||||||
|
vs = getattr(al, n)
|
||||||
|
vs = [x.lower() for x in vs]
|
||||||
|
setattr(al, n, vs)
|
||||||
|
|
||||||
R = al.rp_loc
|
R = al.rp_loc
|
||||||
if "//" in R or ":" in R:
|
if "//" in R or ":" in R:
|
||||||
t = "found URL in --rp-loc; it should be just the location, for example /foo/bar"
|
t = "found URL in --rp-loc; it should be just the location, for example /foo/bar"
|
||||||
|
|
Loading…
Reference in a new issue