|
68 | 68 | get_spd,
|
69 | 69 | guess_mime,
|
70 | 70 | gzip_orig_sz,
|
| 71 | + gzip_file_orig_sz, |
| 72 | + has_resource, |
71 | 73 | hashcopy,
|
72 | 74 | hidedir,
|
73 | 75 | html_bescape,
|
74 | 76 | html_escape,
|
75 | 77 | humansize,
|
76 | 78 | ipnorm,
|
| 79 | + load_resource, |
77 | 80 | loadpy,
|
78 | 81 | log_reloc,
|
79 | 82 | min_ex,
|
|
93 | 96 | sanitize_vpath,
|
94 | 97 | sendfile_kern,
|
95 | 98 | sendfile_py,
|
| 99 | + stat_resource, |
96 | 100 | ub64dec,
|
97 | 101 | ub64enc,
|
98 | 102 | ujoin,
|
@@ -1093,12 +1097,11 @@ def handle_get(self) -> bool:
|
1093 | 1097 | if self.vpath == ".cpr/metrics":
|
1094 | 1098 | return self.conn.hsrv.metrics.tx(self)
|
1095 | 1099 |
|
1096 |
| - path_base = os.path.join(self.E.mod, "web") |
1097 |
| - static_path = absreal(os.path.join(path_base, self.vpath[5:])) |
| 1100 | + static_path = os.path.join("web", self.vpath[5:]) |
1098 | 1101 | if static_path in self.conn.hsrv.statics:
|
1099 |
| - return self.tx_file(static_path) |
| 1102 | + return self.tx_res(static_path) |
1100 | 1103 |
|
1101 |
| - if not static_path.startswith(path_base): |
| 1104 | + if not undot(static_path).startswith("web"): |
1102 | 1105 | t = "malicious user; attempted path traversal [{}] => [{}]"
|
1103 | 1106 | self.log(t.format(self.vpath, static_path), 1)
|
1104 | 1107 | self.cbonk(self.conn.hsrv.gmal, self.req, "trav", "path traversal")
|
@@ -3300,6 +3303,129 @@ def _expand(self, txt: str, phs: list[str]) -> str:
|
3300 | 3303 |
|
3301 | 3304 | return txt
|
3302 | 3305 |
|
| 3306 | + def tx_res(self, req_path: str) -> bool: |
| 3307 | + status = 200 |
| 3308 | + logmsg = "{:4} {} ".format("", self.req) |
| 3309 | + logtail = "" |
| 3310 | + |
| 3311 | + editions = {} |
| 3312 | + file_ts = 0 |
| 3313 | + |
| 3314 | + if has_resource(self.E, req_path): |
| 3315 | + st = stat_resource(self.E, req_path) |
| 3316 | + if st: |
| 3317 | + file_ts = max(file_ts, st.st_mtime) |
| 3318 | + editions["plain"] = req_path |
| 3319 | + |
| 3320 | + if has_resource(self.E, req_path + ".gz"): |
| 3321 | + st = stat_resource(self.E, req_path + ".gz") |
| 3322 | + if st: |
| 3323 | + file_ts = max(file_ts, st.st_mtime) |
| 3324 | + if not st or st.st_mtime > file_ts: |
| 3325 | + editions[".gz"] = req_path + ".gz" |
| 3326 | + |
| 3327 | + if not editions: |
| 3328 | + return self.tx_404() |
| 3329 | + |
| 3330 | + # |
| 3331 | + # if-modified |
| 3332 | + |
| 3333 | + if file_ts > 0: |
| 3334 | + file_lastmod, do_send = self._chk_lastmod(int(file_ts)) |
| 3335 | + self.out_headers["Last-Modified"] = file_lastmod |
| 3336 | + if not do_send: |
| 3337 | + status = 304 |
| 3338 | + |
| 3339 | + if self.can_write: |
| 3340 | + self.out_headers["X-Lastmod3"] = str(int(file_ts * 1000)) |
| 3341 | + else: |
| 3342 | + do_send = True |
| 3343 | + |
| 3344 | + # |
| 3345 | + # Accept-Encoding and UA decides which edition to send |
| 3346 | + |
| 3347 | + decompress = False |
| 3348 | + supported_editions = [ |
| 3349 | + x.strip() |
| 3350 | + for x in self.headers.get("accept-encoding", "").lower().split(",") |
| 3351 | + ] |
| 3352 | + if ".gz" in editions: |
| 3353 | + is_compressed = True |
| 3354 | + selected_edition = ".gz" |
| 3355 | + |
| 3356 | + if "gzip" not in supported_editions: |
| 3357 | + decompress = True |
| 3358 | + else: |
| 3359 | + if re.match(r"MSIE [4-6]\.", self.ua) and " SV1" not in self.ua: |
| 3360 | + decompress = True |
| 3361 | + |
| 3362 | + if not decompress: |
| 3363 | + self.out_headers["Content-Encoding"] = "gzip" |
| 3364 | + else: |
| 3365 | + is_compressed = False |
| 3366 | + selected_edition = "plain" |
| 3367 | + |
| 3368 | + res_path = editions[selected_edition] |
| 3369 | + logmsg += "{} ".format(selected_edition.lstrip(".")) |
| 3370 | + |
| 3371 | + res = load_resource(self.E, res_path) |
| 3372 | + |
| 3373 | + if decompress: |
| 3374 | + file_sz = gzip_file_orig_sz(res) |
| 3375 | + res = gzip.open(res) |
| 3376 | + else: |
| 3377 | + res.seek(0, os.SEEK_END) |
| 3378 | + file_sz = res.tell() |
| 3379 | + res.seek(0, os.SEEK_SET) |
| 3380 | + |
| 3381 | + # |
| 3382 | + # send reply |
| 3383 | + |
| 3384 | + if is_compressed: |
| 3385 | + self.out_headers["Cache-Control"] = "max-age=604869" |
| 3386 | + else: |
| 3387 | + self.permit_caching() |
| 3388 | + |
| 3389 | + if "txt" in self.uparam: |
| 3390 | + mime = "text/plain; charset={}".format(self.uparam["txt"] or "utf-8") |
| 3391 | + elif "mime" in self.uparam: |
| 3392 | + mime = str(self.uparam.get("mime")) |
| 3393 | + else: |
| 3394 | + mime = guess_mime(req_path) |
| 3395 | + |
| 3396 | + logmsg += unicode(status) + logtail |
| 3397 | + |
| 3398 | + if self.mode == "HEAD" or not do_send: |
| 3399 | + res.close() |
| 3400 | + if self.do_log: |
| 3401 | + self.log(logmsg) |
| 3402 | + |
| 3403 | + self.send_headers(length=file_sz, status=status, mime=mime) |
| 3404 | + return True |
| 3405 | + |
| 3406 | + ret = True |
| 3407 | + self.send_headers(length=file_sz, status=status, mime=mime) |
| 3408 | + remains = sendfile_py( |
| 3409 | + self.log, |
| 3410 | + 0, |
| 3411 | + file_sz, |
| 3412 | + res, |
| 3413 | + self.s, |
| 3414 | + self.args.s_wr_sz, |
| 3415 | + self.args.s_wr_slp, |
| 3416 | + not self.args.no_poll, |
| 3417 | + ) |
| 3418 | + |
| 3419 | + if remains > 0: |
| 3420 | + logmsg += " \033[31m" + unicode(file_sz - remains) + "\033[0m" |
| 3421 | + ret = False |
| 3422 | + |
| 3423 | + spd = self._spd(file_sz - remains) |
| 3424 | + if self.do_log: |
| 3425 | + self.log("{}, {}".format(logmsg, spd)) |
| 3426 | + |
| 3427 | + return ret |
| 3428 | + |
3303 | 3429 | def tx_file(self, req_path: str, ptop: Optional[str] = None) -> bool:
|
3304 | 3430 | status = 200
|
3305 | 3431 | logmsg = "{:4} {} ".format("", self.req)
|
@@ -3815,15 +3941,11 @@ def tx_md(self, vn: VFS, fs_path: str) -> bool:
|
3815 | 3941 | return self.tx_404(True)
|
3816 | 3942 |
|
3817 | 3943 | tpl = "mde" if "edit2" in self.uparam else "md"
|
3818 |
| - html_path = os.path.join(self.E.mod, "web", "{}.html".format(tpl)) |
3819 | 3944 | template = self.j2j(tpl)
|
3820 | 3945 |
|
3821 | 3946 | st = bos.stat(fs_path)
|
3822 | 3947 | ts_md = st.st_mtime
|
3823 | 3948 |
|
3824 |
| - st = bos.stat(html_path) |
3825 |
| - ts_html = st.st_mtime |
3826 |
| - |
3827 | 3949 | max_sz = 1024 * self.args.txt_max
|
3828 | 3950 | sz_md = 0
|
3829 | 3951 | lead = b""
|
@@ -3857,7 +3979,7 @@ def tx_md(self, vn: VFS, fs_path: str) -> bool:
|
3857 | 3979 | fullfile = html_bescape(fullfile)
|
3858 | 3980 | sz_md = len(lead) + len(fullfile)
|
3859 | 3981 |
|
3860 |
| - file_ts = int(max(ts_md, ts_html, self.E.t0)) |
| 3982 | + file_ts = int(max(ts_md, self.E.t0)) |
3861 | 3983 | file_lastmod, do_send = self._chk_lastmod(file_ts)
|
3862 | 3984 | self.out_headers["Last-Modified"] = file_lastmod
|
3863 | 3985 | self.out_headers.update(NO_CACHE)
|
@@ -3896,7 +4018,7 @@ def tx_md(self, vn: VFS, fs_path: str) -> bool:
|
3896 | 4018 | zs = template.render(**targs).encode("utf-8", "replace")
|
3897 | 4019 | html = zs.split(boundary.encode("utf-8"))
|
3898 | 4020 | if len(html) != 2:
|
3899 |
| - raise Exception("boundary appears in " + html_path) |
| 4021 | + raise Exception("boundary appears in " + tpl) |
3900 | 4022 |
|
3901 | 4023 | self.send_headers(sz_md + len(html[0]) + len(html[1]), status)
|
3902 | 4024 |
|
|
0 commit comments