From 6961b4d7e99153a9319ebc14b3d603ea938d3b04 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 08:29:27 +0000 Subject: [PATCH 1/5] Add DNS-01 challenge support for Let's Encrypt Co-authored-by: mathieumoran1 --- python_video_server/README.md | 22 +++ .../__pycache__/letsencrypt.cpython-312.pyc | Bin 9565 -> 13403 bytes python_video_server/letsencrypt.py | 151 +++++++++++++++--- 3 files changed, 155 insertions(+), 18 deletions(-) diff --git a/python_video_server/README.md b/python_video_server/README.md index d7517f4..f8311a5 100644 --- a/python_video_server/README.md +++ b/python_video_server/README.md @@ -37,6 +37,28 @@ sudo -E python3 -m python_video_server.letsencrypt certonly \ --domains example.com,www.example.com ``` +### 1b) Issue a certificate (DNS-01 manual TXT record — no port 80, no root) + +If your host already uses port **80** (or you don't have permission to bind it), use DNS-01. +This does **not** require any inbound ports on the machine, but you must be able to edit DNS for the domain. + +```bash +python3 -m pip install -r python_video_server/requirements.txt + +# Add --production when staging works. +python3 -m python_video_server.letsencrypt certonly \ + --challenge dns \ + --email you@example.com \ + --domains example.com,www.example.com +``` + +During issuance, you'll be prompted to create a TXT record like: +- Name: `_acme-challenge.example.com` +- Type: `TXT` +- Value: (a token certbot prints) + +After it propagates, press Enter and certbot will continue. + Certificates will be written under: - `python_video_server/certs/config/live//fullchain.pem` - `python_video_server/certs/config/live//privkey.pem` diff --git a/python_video_server/__pycache__/letsencrypt.cpython-312.pyc b/python_video_server/__pycache__/letsencrypt.cpython-312.pyc index 0c8d9354c982a315afd6ba7eb77c056c5d98cb4d..db3748896b5552326550706679529c0304775b6b 100644 GIT binary patch delta 5416 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 Le6Vg#jgI~a?6tU6 delta 2036 zcmZWqOKclO7~Wa0?e#Np{EFi`wiD;!G@HgvRnpSZS5)1Jx%EIbt$HaO+8e2akR^n;A#nheLrVn&7o-Xy%F!i-iok*YU&~2=iT3;d`Cl`h z|C`Cs}0*o*JK9oJze%;+lzP{dbYN3V7SEUIT^->#1H}z3F$R_Hi4v@_@ z;s1X$K%L<8&>(ezYys&8>7}i-38asPXd7+5YM$_`0qS|ZR}h#>6x1Mm{w3T3j3rW+ zwztW=DH)#IVybw^X0b9Z-4#iq^1|{JAp@1q<+w<8@?Y#XE8qA&CFX6AG{SEL%E8Hc zx*$lPIN>u10$Ca4yMlR#R5q6_)I=p}=KF%}{Atj&AT<`GR^8gjEt%J>@&>DA&1$3e zX=mA5GBIb#N*%h~$eb37E-L?MN`>jl@&#WCN0bd#hHST1gM%co$+jF zc(<);R%bHV9BV^g7y9h8tU%|WRw2*2(b|ImT@b2LhD~cM1gZv`6Zp@eZqEoP<7oK| zU{R>KdAMzdv}sxP^HCWeFlT)n^-#c}miy zC{NkFJ=R9Mhyby%jZt-dJ$6A}kJURHk)<_qKwoTQRe4fndLf^iFFK(MdL~aZxk6qo z1{7tMsZ%OrDpe*gW^y?-Kdow2>7uUBuDTTkr81aOW(!PDN^Bo2u-+H89~dfdcQi^` z_&_w(4Re9Lf-nG3bz*jUFqOt^hFviV@TF+%B=SCKW(UxSJ+4>90#zq&_Ci5VV{0`t zm|{4XyY8H!>Bg^NC&5&LPkSKn;n?WOnnkdAm*v0{IkqgjaTfYPc3eB~uJm1V{9$wa zk+b8G+*$KB`z+6Wf<3z261dfOy>HnUUiL?pgYoA!v)x|vgL`qbmJnnok9YYiH{&re zu^pCS!w5SNb|SotunS=~!eRb>XL#x;s>mtUgfNO=91PODemZ+l+lw%U5CEuJDVtBT zxjZvY#=7BTjO2gQo}J(miJ0XKB;FSIV&a*&Dps_v--t}@wEdoQ$;3bGiJGG@i}AmD z6cXpj-nglQcJgDro#q7CyZ9Vv5zxB%d%cP;zRsf(+@565STV^z5|b5c@(5`^hg&pF z&d}3Iu_{qj)2lL5-Q!Xnak;#(Q9^w|D>es z8kGA4KJ5yR47{iu+(HRa`O;uGg&ue=GE7qq`oOa?udx(bt<$QW(REgTb8rD5bc(+i z>~U;?2}5`)@MP+p4uisFn6KJKvzmSuo~H8f zo~oQq=V3?bG*ftPC=$8~9{jhbeE?815kj7cJ|g|z&+iNk9d(j{T6;HXe&TS~%pl=D xx`Q<Jet=Uks3m$*XftpjWG_ANmTRBA{q}lG6W8 tuple[Path, Path, Path]: return config_dir, work_dir, logs_dir +def _ensure_dns_hook_scripts(work_dir: Path) -> tuple[Path, Path]: + """ + Create small executable hook scripts used by certbot's --manual DNS flow. + + Certbot runs these hooks as external executables; we generate them under the + certbot work dir so no special permissions are needed. + """ + + auth_hook = (work_dir / "certbot_dns_auth_hook.py").resolve() + cleanup_hook = (work_dir / "certbot_dns_cleanup_hook.py").resolve() + + auth_contents = """#!/usr/bin/env python3 +import os +import sys +import time + +def main() -> int: + domain = os.environ.get("CERTBOT_DOMAIN", "").strip() + validation = os.environ.get("CERTBOT_VALIDATION", "").strip() + if not domain or not validation: + print("Missing CERTBOT_DOMAIN/CERTBOT_VALIDATION in environment.", file=sys.stderr) + return 2 + + record_name = f"_acme-challenge.{domain}" + print("\\n=== DNS-01 challenge required ===") + print(f"Create/Update this TXT record:") + print(f" Name: {record_name}") + print(f" Type: TXT") + print(f" Value: {validation}") + print("\\nAfter the record is published and has propagated, press Enter to continue.") + + # If we're in a non-interactive environment, wait a bit and then continue. + if not sys.stdin.isatty(): + wait_s = int(os.environ.get("LE_DNS_WAIT_SECONDS", "60")) + print(f"(non-interactive stdin detected; waiting {wait_s}s then continuing...)") + time.sleep(max(0, wait_s)) + return 0 + + try: + input("> ") + except KeyboardInterrupt: + print("\\nAborted.", file=sys.stderr) + return 130 + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) +""" + + cleanup_contents = """#!/usr/bin/env python3 +import os + +def main() -> int: + domain = os.environ.get("CERTBOT_DOMAIN", "").strip() + if domain: + print("\\nDNS-01 cleanup:") + print(f"You may now remove the TXT record: _acme-challenge.{domain}") + return 0 + +if __name__ == "__main__": + raise SystemExit(main()) +""" + + auth_hook.write_text(auth_contents, encoding="utf-8") + cleanup_hook.write_text(cleanup_contents, encoding="utf-8") + os.chmod(auth_hook, 0o755) + os.chmod(cleanup_hook, 0o755) + + return auth_hook, cleanup_hook + + def _run_certbot(argv: Iterable[str]) -> int: try: # certbot is a Python package dependency (installed via pip). @@ -86,6 +165,7 @@ def _common_certbot_flags( domains: List[str], base_dir: Path, production: bool, + non_interactive: bool = True, ) -> List[str]: if not email.strip(): raise SystemExit("--email is required") @@ -95,7 +175,6 @@ def _common_certbot_flags( config_dir, work_dir, logs_dir = _ensure_dirs(base_dir) args: List[str] = [ - "--non-interactive", "--agree-tos", "--email", email.strip(), @@ -109,6 +188,8 @@ def _common_certbot_flags( "--key-type", "ecdsa", ] + if non_interactive: + args.insert(0, "--non-interactive") if production: args += ["--server", LE_PRODUCTION_DIRECTORY_URL] @@ -126,23 +207,51 @@ def cmd_certonly(args: argparse.Namespace) -> int: domains = _split_domains(args.domains) production = bool(args.production) - certbot_args = [ - "certonly", - "--standalone", - # HTTP-01 is the simplest/most compatible for typical VPS setups. - "--preferred-challenges", - "http", - # If port 80 is already in use, certbot can still succeed behind a reverse proxy - # using `--webroot`, but this wrapper defaults to standalone by design. - "--http-01-port", - str(int(args.http_01_port)), - *_common_certbot_flags( - email=args.email, - domains=domains, - base_dir=base_dir, - production=production, - ), - ] + challenge = (args.challenge or "http").strip().lower() + if challenge not in {"http", "dns"}: + raise SystemExit("--challenge must be one of: http, dns") + + if challenge == "dns": + # DNS-01 works without binding any local ports; suitable for shared hosting / no root. + _, work_dir, _ = _ensure_dirs(base_dir) + auth_hook, cleanup_hook = _ensure_dns_hook_scripts(work_dir) + certbot_args = [ + "certonly", + "--manual", + "--preferred-challenges", + "dns", + "--manual-public-ip-logging-ok", + "--manual-auth-hook", + str(auth_hook), + "--manual-cleanup-hook", + str(cleanup_hook), + *_common_certbot_flags( + email=args.email, + domains=domains, + base_dir=base_dir, + production=production, + non_interactive=True, + ), + ] + else: + certbot_args = [ + "certonly", + "--standalone", + # HTTP-01 is the simplest/most compatible for typical VPS setups. + "--preferred-challenges", + "http", + # If port 80 is already in use, certbot can still succeed behind a reverse proxy + # using `--webroot`, but this wrapper defaults to standalone by design. + "--http-01-port", + str(int(args.http_01_port)), + *_common_certbot_flags( + email=args.email, + domains=domains, + base_dir=base_dir, + production=production, + non_interactive=True, + ), + ] if args.force_renewal: certbot_args.append("--force-renewal") @@ -212,6 +321,12 @@ def _build_parser() -> argparse.ArgumentParser: type=int, help="Port to bind for HTTP-01 challenge (default: 80).", ) + c.add_argument( + "--challenge", + default=os.environ.get("LE_CHALLENGE", "http"), + choices=["http", "dns"], + help="ACME challenge type: http (standalone) or dns (manual TXT record). Default: http.", + ) c.add_argument("--force-renewal", action="store_true", help="Force issuance even if not close to expiry.") c.add_argument( "--test-cert", -- 2.54.0 From 02dc41d63fb41797a5653f4fbe264eeb2d7b3714 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 08:37:45 +0000 Subject: [PATCH 2/5] Refactor letsencrypt script to use /dev/tty for interactive prompts Co-authored-by: mathieumoran1 --- .../__pycache__/letsencrypt.cpython-312.pyc | Bin 13403 -> 14502 bytes python_video_server/letsencrypt.py | 78 ++++++++++++++---- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/python_video_server/__pycache__/letsencrypt.cpython-312.pyc b/python_video_server/__pycache__/letsencrypt.cpython-312.pyc index db3748896b5552326550706679529c0304775b6b..7d125dbac28e073c70a0cb4a46c0503154bc900a 100644 GIT binary patch delta 1563 zcmchW%TE(Q7{J|vqJ&Z!(4aB+Wf9YcS1W1^T9kko5~wjY>VbqzcW3E@?M$7WO2mb(kTs-N_Zc7(XVq)SP_VK;v`+ncY(#w18@8a>+ z1M=jXrpf2*oA&j1>`{xq5x?TivkIp3Lae6JqpcuMFHB%AicCNmt%89n!sQqoVKP?? z%NOPxm<1)q7M)8lY~rP1=?xa+26n)8FdtkZ4k_9grf<|DE|)L`M1fF&IWh!U!XUrO zgnGyZyh0q|3eO1T1evHzugs-x^IKn_wt994{;OPp?- zqk2QY5~u8_z|sNosEkJe);Km8H|MGReGs*gCWY0CGAZ0GL>0>UHnLr*T3bRkF_Dl3 z@9_11CT6{;k3I%rIqBB0d2$Xph)gt)5)0IThyzp(HP0w+v_UF{mN^kPC9fOyOx;z9 zgLb4MvS2bjN?kk3ieqHjgyNc^|0;RLQ|d-weU6S785h_blC?MD)vb2tQS#d0vm=@9 zZd-&R+m65y_(y3`0(Cr<+pw8aFDev+89%isV$Nt}BquoG$Tg0GRflcg!6h z$z=3&qhX z-wp@6Eb;R<knaXNY1YnHz0>PQw42rLp+uARxSGnoj?PZseQ6%e0hb@(N!2$&W>SCtLIHoy;QO##T_2nOCAQ zIZ8lX_LDIaxU~%#_5E%=|o5?Mg9uj%g*SMG7SusR~7tl?CmCfM$T*rL5qYrcj=$ zUX-eknWvDbke8pQ3v_#GQDSmQW?8C2YF=4pQGQ--YF>$sLV03l2~bTEh|Eg?nU<%J zoS#>cnOB;sH#to>M$F7WNfY8>Bu^_%zAfxNSx>}F8f;}rQKc1_0aB8gS5R7_p=75p zxkki?Nt0`GypZ_hw<69=ri{Q)Ri1oLG}JmJKQ}Qm51|JVmOxLr_yy}47%C*^q$cK- z7Fa>U1;yq(G2_WL0-Te(#Xm9jOs=2{gW7Dmp^r!;18GOBJ)HsWVyjM`jmx|WIEg^^MDvmeuBb&Glc D-yWQ6 diff --git a/python_video_server/letsencrypt.py b/python_video_server/letsencrypt.py index 4ed280e..0277f4f 100644 --- a/python_video_server/letsencrypt.py +++ b/python_video_server/letsencrypt.py @@ -89,32 +89,60 @@ import os import sys import time +def _open_tty(): + # Certbot may capture stdout/stderr of hooks; /dev/tty forces user-visible IO + # when an interactive terminal exists. + try: + return open("/dev/tty", "r+", encoding="utf-8", buffering=1) + except Exception: + return None + def main() -> int: domain = os.environ.get("CERTBOT_DOMAIN", "").strip() validation = os.environ.get("CERTBOT_VALIDATION", "").strip() if not domain or not validation: - print("Missing CERTBOT_DOMAIN/CERTBOT_VALIDATION in environment.", file=sys.stderr) + print("Missing CERTBOT_DOMAIN/CERTBOT_VALIDATION in environment.", file=sys.stderr, flush=True) return 2 record_name = f"_acme-challenge.{domain}" - print("\\n=== DNS-01 challenge required ===") - print(f"Create/Update this TXT record:") - print(f" Name: {record_name}") - print(f" Type: TXT") - print(f" Value: {validation}") - print("\\nAfter the record is published and has propagated, press Enter to continue.") + msg = ( + "\\n=== DNS-01 challenge required ===\\n" + "Create/Update this TXT record:\\n" + f" Name: {record_name}\\n" + " Type: TXT\\n" + f" Value: {validation}\\n" + "\\nAfter the record is published and has propagated, press Enter to continue.\\n" + "> " + ) - # If we're in a non-interactive environment, wait a bit and then continue. - if not sys.stdin.isatty(): - wait_s = int(os.environ.get("LE_DNS_WAIT_SECONDS", "60")) - print(f"(non-interactive stdin detected; waiting {wait_s}s then continuing...)") - time.sleep(max(0, wait_s)) + tty = _open_tty() + if tty is not None: + tty.write(msg) + tty.flush() + try: + tty.readline() + except KeyboardInterrupt: + tty.write("\\nAborted.\\n") + tty.flush() + return 130 + finally: + try: + tty.close() + except Exception: + pass return 0 + # Fallback: best-effort stdout + either wait or read stdin if interactive. + print(msg, end="", flush=True) + if not sys.stdin.isatty(): + wait_s = int(os.environ.get("LE_DNS_WAIT_SECONDS", "300")) + print(f"\\n(non-interactive stdin detected; waiting {wait_s}s then continuing...)", flush=True) + time.sleep(max(0, wait_s)) + return 0 try: - input("> ") + input("") except KeyboardInterrupt: - print("\\nAborted.", file=sys.stderr) + print("\\nAborted.", file=sys.stderr, flush=True) return 130 return 0 @@ -125,11 +153,27 @@ if __name__ == "__main__": cleanup_contents = """#!/usr/bin/env python3 import os +def _open_tty(): + try: + return open("/dev/tty", "w", encoding="utf-8", buffering=1) + except Exception: + return None + def main() -> int: domain = os.environ.get("CERTBOT_DOMAIN", "").strip() - if domain: - print("\\nDNS-01 cleanup:") - print(f"You may now remove the TXT record: _acme-challenge.{domain}") + if not domain: + return 0 + msg = "\\nDNS-01 cleanup:\\nYou may now remove the TXT record: _acme-challenge.%s\\n" % domain + tty = _open_tty() + if tty is not None: + tty.write(msg) + tty.flush() + try: + tty.close() + except Exception: + pass + return 0 + print(msg, end="", flush=True) return 0 if __name__ == "__main__": -- 2.54.0 From 98f40488aa6399b54c7601a933030626693fe274 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 08:40:25 +0000 Subject: [PATCH 3/5] feat: Add optional DNS hook mode for certonly Co-authored-by: mathieumoran1 --- python_video_server/README.md | 12 +++++- .../__pycache__/letsencrypt.cpython-312.pyc | Bin 14502 -> 15062 bytes python_video_server/letsencrypt.py | 40 ++++++++++++++---- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/python_video_server/README.md b/python_video_server/README.md index f8311a5..54d0eba 100644 --- a/python_video_server/README.md +++ b/python_video_server/README.md @@ -52,13 +52,23 @@ python3 -m python_video_server.letsencrypt certonly \ --domains example.com,www.example.com ``` -During issuance, you'll be prompted to create a TXT record like: +During issuance, certbot will prompt you to create a TXT record like: - Name: `_acme-challenge.example.com` - Type: `TXT` - Value: (a token certbot prints) After it propagates, press Enter and certbot will continue. +If you run into a situation where the prompt/token only shows up in logs, you can try the hook-based mode: + +```bash +python3 -m python_video_server.letsencrypt certonly \ + --challenge dns \ + --dns-hooks \ + --email you@example.com \ + --domains example.com,www.example.com +``` + Certificates will be written under: - `python_video_server/certs/config/live//fullchain.pem` - `python_video_server/certs/config/live//privkey.pem` diff --git a/python_video_server/__pycache__/letsencrypt.cpython-312.pyc b/python_video_server/__pycache__/letsencrypt.cpython-312.pyc index 7d125dbac28e073c70a0cb4a46c0503154bc900a..709b682ea4f5f9fddd475c29cdeed57670a93398 100644 GIT binary patch delta 1609 zcmaJ>Uu;@c80SDQh0BGq*4jeHxD;r)j!U6SX_vUpCG9el(P-^Dw?>BC+XAB(Jok2M z8C|o)2NpHydX{WWW|nAX7TcEvHH-hEuk%I2R6-g}_O=&OZ69VBbh z538@Ub1gX|)=_y%aW*t7s+6lfY5w!-JjiPV-KKtns4kT>?w{MEml8=esOeas1%O^bO)k)O~BV=dhLclBNK(r z_C~rms-laFP{~N?rJRoE=VQ`BEd2z1)!RkENgRxR=$*A3hp%EY$MC$Y#3Uh>&PXDh zBHoa509eqH^JPOtFGzAUk;$Bw(F5n4>nwIx7_#;o86_Hnb8-blA=xP$AmAtRDVNg_ z$Ht%dlRp9IJ5K<8?3!Nroz`~K1!mW*V$7>8H%!+|4-NJ$8+X@JvK_h_DI6;--rTp< zH+p}u)OX^0##soh=F3cX+3eafcdUkxsch~iR=8w#Z0jkT={FT+G!??zjLK+vxIa*S z)_Nm#EmiKemo1KRkGsseb`4q*rZt+bhPmzb|BG|)joWT7F1bHad#7>vPxL%5z$G3o zNL(9qV+OVmc#QzL4UjZGJcA}YHV{Sgo?#F}cRVbW!1OqJ;CW0l*uqT@c782>$wk$) z(t(NUK>mYA94^7VfxDiJdOz3yLf>ZpMhh|@v{N2|7C3ak>qMV>+coM`C;G!{174(f z9U5Nnp_^VOHGpIM=xeX@IEfByG8;H889Ojzjm< zT=5nDMF1r9p6_*xfBBATWvqb`IySHfPNB~RPBQbDsI(|cSrOA6{>v*>qBWk#B;%qC zCG5@_@F_|%U+tp`6CRI_MJA&sCMI5*tN?#5>%mhto`|K>qBJM6q-{BNN)}lnVC6VW zW)+!LGHezmB?TMjMTx~N%U+Bn6(2h$&c+tf3cDbSG8+%pnzLVKajXbqaV2>{WNTLd zFOZqfDn3pFvt%En)M|xr8r|^+z-jc8KMh1Q76^h_bSdxxFrqI4Ggf$pY?6^hB`VaC z$&hq7Odx(RKo_tX!w;PNsF)b!fV-^gEVuP+nV-2nd8_@lb=`XV!1{r^ru&+b<@r+Q zk&=0|Y+NZ`K6DY-E@5e|Ba&g`nR#5Jz{+A;j4PtRwZZ~wA36taA$jO+u!jB~>cQ9( zp0`LQ*Ue*^=^z>~xd5wH?K1e(nnAycvgc(h?v)d+nWv>TfXso@%W6g-R*>>qX; z@+6oi@D2(OhpSf_lD@`;eJ~uL*Re@nb@?0g^Dx>M0-$f_wAzAkWCR#@FQ6+U?tcKf Ch>2qW delta 1140 zcma)5O-vI(6y9kmZMWN^0xA+LblcKZ7bqaK#P}1v$WIeu6g6n6oh@3IR<}#U+UkWE z2nnDs#;anYi4qgii$^XV3@0Tq4e?+M9*Bw2;Kg|HO`8_HIobW@d*8hI-cH`^$MNSk z9V>P_&*2CePhVU2FFCrLybVJm$C1sNT*w(C_s;6|EQ5^t%;*p(2fNOb!-{yp3=Sy7x%)?CQopi{r|h;ZXew6)f%nwgW%_@A?i)gh*!df%V8uQiKm?AL4D@i>Z(MD77#Vzs?lz-!!x&u|7>67bqo+npV z>sqHBQ)DVIKlsVj^CqzBI`!V-oU+az`7G?tv&WqmcIJhHd7<%>P&IFz3FQTmF=16` z0P1b7m==Eca>u~XidMzo0)r3(iNR?GeGK{;T!K!in~Z=eHJV~5 z9EJzd25&`HxL7ndc_3^sP&XuHvBN`o>cvLl5*vie7~<7Mz8&?NQnc6%Z{;SVk4ms1 zi||n11^Zie*NW(K=pP?ryc%A}4K>ZBn}y!o8BUTzX$h7LQtnwxFCi*?Z@r8-&~~iy zCf3kYc-S^U&VtQ9AY4abtwO(jLqVDP@75FqbM$KRT2x7CD$73ykK6rZ2;Q};Bnn*} zU4+6|$8jRS>yF{tH6f+wVXDL;Yu64ezdnYBqc(LF-$!$S8AnT3`>Jz{S_LevBDsafV6K#&A&fbl|8 z_Axfcfc*wq1%pE03LQMg!U8Moo`bIer+v%Fpv}V0U=R!R1wFP2mMk-vf;+)}Tk$9? lo`erUKmQ0R9*0XUgS`iK1c`TZg47`f+KK&F9F}@Ke*xK20=EDF diff --git a/python_video_server/letsencrypt.py b/python_video_server/letsencrypt.py index 0277f4f..92d0489 100644 --- a/python_video_server/letsencrypt.py +++ b/python_video_server/letsencrypt.py @@ -257,26 +257,42 @@ def cmd_certonly(args: argparse.Namespace) -> int: if challenge == "dns": # DNS-01 works without binding any local ports; suitable for shared hosting / no root. - _, work_dir, _ = _ensure_dirs(base_dir) - auth_hook, cleanup_hook = _ensure_dns_hook_scripts(work_dir) + # + # Default: use certbot's built-in interactive manual prompt (most reliable). + # Optional: --dns-hooks to use our hook scripts (useful for automation). certbot_args = [ "certonly", "--manual", "--preferred-challenges", "dns", "--manual-public-ip-logging-ok", - "--manual-auth-hook", - str(auth_hook), - "--manual-cleanup-hook", - str(cleanup_hook), - *_common_certbot_flags( + ] + + if bool(args.dns_hooks): + _, work_dir, _ = _ensure_dirs(base_dir) + auth_hook, cleanup_hook = _ensure_dns_hook_scripts(work_dir) + certbot_args += [ + "--manual-auth-hook", + str(auth_hook), + "--manual-cleanup-hook", + str(cleanup_hook), + ] + certbot_args += _common_certbot_flags( email=args.email, domains=domains, base_dir=base_dir, production=production, non_interactive=True, - ), - ] + ) + else: + # IMPORTANT: manual DNS prompt requires interactive mode. + certbot_args += _common_certbot_flags( + email=args.email, + domains=domains, + base_dir=base_dir, + production=production, + non_interactive=False, + ) else: certbot_args = [ "certonly", @@ -371,6 +387,12 @@ def _build_parser() -> argparse.ArgumentParser: choices=["http", "dns"], help="ACME challenge type: http (standalone) or dns (manual TXT record). Default: http.", ) + c.add_argument( + "--dns-hooks", + action="store_true", + default=bool(int(os.environ.get("LE_DNS_HOOKS", "0"))), + help="(DNS challenge only) Use hook scripts to print token and wait. Default uses certbot's interactive manual prompt.", + ) c.add_argument("--force-renewal", action="store_true", help="Force issuance even if not close to expiry.") c.add_argument( "--test-cert", -- 2.54.0 From 7cffe9db583918c0937fad15de47ba0c29afea2c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 09:00:41 +0000 Subject: [PATCH 4/5] Refactor: Improve MongoDB connection and add certifi Co-authored-by: mathieumoran1 --- .../__pycache__/server.cpython-312.pyc | Bin 12923 -> 14907 bytes python_video_server/requirements.txt | 1 + python_video_server/server.py | 76 ++++++++++++++++-- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/python_video_server/__pycache__/server.cpython-312.pyc b/python_video_server/__pycache__/server.cpython-312.pyc index 9f3aa7bb0b58d768f9bd77820e82cbfc107a8a8d..a0f8ad139d09e71732d6e8f06f5a680c81a137ae 100644 GIT binary patch delta 6092 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 delta 4267 zcmaJDTWnm#_0HbiyYKbByz95ucKp~RUdOSW2joF85HK_#f`BYZSDT%?_By+Jow;}8 z+PPa(8Y!ejk=;QmB~TJXs%+Y6i$ar zk#wXKO-DBdql9TPlvF7Ry&$u=R(3x8-coo#)Q@0 z{DPgvfzU^rRAp758Rq@81q52v0id~6k2;_Z9lwoyvTIEZw2ubpyt(8pjxB6P0{3TBlbQi4of?oNJ+g_q6{*apj#9B(Iw0ouNwl#oJ*MuM)@u42q1vR5s$+CJ9hfBe zT^YQrNM0d9Bv3hxejZBImxd)Hlw29A&+~-tfDjF{hIMX-b>?uLZMW6Eqqm=v*8JE%M{+hI zf;uZD4_wDR9^p&&_xY^jJvfQ4DZ^cjKw~ig7CEEK@SjK|ZVe30YV4Fglg+6^{8?qd z(FKe!e@i)k8)83^SU3E!1OUsMnVvN=x|(CE!TNy`hfntdcvAX{H?sJ^HE-9u_Ab`V ziE}T1-RV8p3q00`VBLx*Q_{+lTBgDZY%8$Z;M-v2{%hXEyY_@o;Gd1emRnuT#EZTN zY3AVv-OZ$fA9IhjB!FrKR_)P~nR2$IvfV&T@E^OM=l6TBjyq6MXt$%VoSUVVlc~B^ zJfm**ESNb{psJS17mBK$p#{bcfX*0y%(stp^Jjf;9vg)Dyz``*WhzTtcfu?wvko-u zC~C7Rvpl+yWrjXmFs70=b`Uu-%g*X}I}(Xn82y=mWIjnXIU~p2=xtLoFK_6k?A8 z=Rw{S8Y11x_lAB%$RNKS?%9U>XLlkPM=%V)k_%f;FR;=3awO$ik6 ztpZ}#WwP@mLSiaL0mCd1t z*^-`gFox`d{9o}@-T0&mF+nTrEbz|oJDX+{;d*8H)uwag;Q(sj6|rQvGnPWtoJLj4 z^U&PHV~^a;n8qMID#%xA_i0mxKynuHbE`|72f3&C#g>cwZzFEU1kAqAzZ{A4L#=)M z!(NZ`sFbuHVNdeoZC!_|z=gBLi^YT!Q6vyH3V%8Qu$F|iTbL^r;C$Hy;9NwJUQRmN z{U3Q_i~2Qh>$~<=xXszlR^=OTF z_mqNBWas(zo|bVDS~t$WjzC<+wT!Wp?97Z>rj~tLE0ozcK_cm8ODOm~1bBg1ggBQOG@bm*-Zs0b z05a0Irw!f(@T-?&gC?0yVdxjaqP6!KSV(Fb z!MgL+(1tAq>QpmCBl|$o7+F=2Xw(c{X{;Z|QOOvqb4ilZh3_sap$~q}0 zjNyef(GAPem>IYv{m=$W#WyY0^q)(Cskj00tc?xsuAP+GU;AsnPgf@!2a@1Yq1=$4s9Y zF{5>7XuH{9hy6f%-FnB~VW=3ha4C*S1b&OQXA<9#k4dv6*?FX}2(@HBcwhNUwpgHv zl6Iz`CJK~c`GHE*x9yB_0?|Q!&-UMS{|wOiz<8~f3_hTiCylA=zX2i?LA>+qMLs&X zWdWkXugJ2luLGqQp~VeGPo)d+le56 z0IQpL7g(-*p-eMas6<_ALk6}(EdFtcVW$*LpY2CX^s{Czr;-cU`z)oT8dDlnD_Y1I zEQli5xmXwhRtnK-k}>vk#6=ks6>J%&6#$m36{w}uimv6XKUsDm%OQfuUP0Vldx*dj z#B%2=#bU-glQpK;o4{j_^IxPAO}J%?u+=V!^gIAP#&4vWJ6Hm_KHY#`S*ks1*zbY# zYZQ5dCx=o)p7?h=S0tC?FnR8tkHU#-;qIrMe{P6<6z=?>t@DF;>F*9X5?GOBe_+KW z1)}`iP_OHY5X!GamoE=_ZR8MtWw;}G>k9>mH-xEMIzBId;Jt6F^wORQyL@$Pe1gdD z*c}rACqJ-rF)Z!vKXFpD{z={YRtmUeB zv)E5+TCtYEn5(sJE3)%doSsH-7QwF&h+A@j@7dL+V3}ndJiTjhp$MoI$WoeF_t>(} zXpl(Qcr7<!T|pbQFFU* diff --git a/python_video_server/requirements.txt b/python_video_server/requirements.txt index 863ae3a..7f27c17 100644 --- a/python_video_server/requirements.txt +++ b/python_video_server/requirements.txt @@ -3,3 +3,4 @@ uvicorn[standard]==0.34.0 pymongo==4.10.1 PyJWT==2.10.1 certbot>=2.11.0,<3 +certifi==2025.11.12 diff --git a/python_video_server/server.py b/python_video_server/server.py index 70fb957..a57016c 100644 --- a/python_video_server/server.py +++ b/python_video_server/server.py @@ -1,6 +1,7 @@ import mimetypes import os import posixpath +from functools import lru_cache from datetime import datetime, timezone from pathlib import Path from typing import Generator, Optional, Tuple @@ -13,6 +14,7 @@ from fastapi import FastAPI, Header, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, StreamingResponse from pymongo import MongoClient +from pymongo.errors import PyMongoError def _env_str(name: str, default: str = "") -> str: @@ -49,6 +51,12 @@ def _mongo_db_name() -> str: except Exception: return "" +def _env_int(name: str, default: int) -> int: + try: + return int(_env_str(name, str(default))) + except Exception: + return default + def _video_files_dir() -> Path: # Read env at runtime (important on Windows/uvicorn worker reloads). @@ -143,7 +151,38 @@ def _mongo_client() -> MongoClient: uri = _mongo_uri() if not uri: raise HTTPException(status_code=500, detail="MONGO_URI is not configured") - return MongoClient(uri) + kwargs = { + # Fail fast instead of hanging for ~30s on each request. + "serverSelectionTimeoutMS": _env_int("MONGO_SERVER_SELECTION_TIMEOUT_MS", 8000), + "connectTimeoutMS": _env_int("MONGO_CONNECT_TIMEOUT_MS", 8000), + "socketTimeoutMS": _env_int("MONGO_SOCKET_TIMEOUT_MS", 20000), + } + + # On some hosts, the system CA store is missing/broken or overridden by env vars, + # causing TLS handshake failures to MongoDB Atlas. Prefer certifi's CA bundle. + ca_file = _env_str("MONGO_TLS_CA_FILE") + if ca_file: + kwargs["tlsCAFile"] = ca_file + else: + try: + import certifi # type: ignore + + kwargs["tlsCAFile"] = certifi.where() + except Exception: + # If certifi isn't available, fall back to system trust. + pass + + # Optional escape hatch (NOT recommended): allow invalid certs for debugging only. + if _env_str("MONGO_TLS_INSECURE", "0") in ("1", "true", "yes", "on"): + kwargs["tlsAllowInvalidCertificates"] = True + + return MongoClient(uri, **kwargs) + + +@lru_cache(maxsize=1) +def _mongo_client_cached() -> MongoClient: + # Reuse one client (connection pool) across requests. + return _mongo_client() app = FastAPI() @@ -211,15 +250,26 @@ def stream_video( except Exception: raise HTTPException(status_code=400, detail="Invalid movie id") - with _mongo_client() as client: - db_name = _mongo_db_name() - if not db_name: - raise HTTPException( - status_code=500, - detail="Mongo DB name not configured. Add it to MONGO_URI (…/dbname) or set MONGO_DB_NAME.", - ) + db_name = _mongo_db_name() + if not db_name: + raise HTTPException( + status_code=500, + detail="Mongo DB name not configured. Add it to MONGO_URI (…/dbname) or set MONGO_DB_NAME.", + ) + + try: + client = _mongo_client_cached() db = client[db_name] movie = db["movies"].find_one({"_id": oid}, {"video_file": 1}) + except PyMongoError as e: + raise HTTPException( + status_code=502, + detail=( + "Failed to query MongoDB (TLS/connection issue). " + "On some hosts you may need a CA bundle; try installing certifi or set MONGO_TLS_CA_FILE. " + f"Original error: {type(e).__name__}: {e}" + ), + ) video_file = (movie or {}).get("video_file") or "" if not isinstance(video_file, str) or not video_file.strip(): @@ -289,3 +339,13 @@ def healthz(): "mongo_db_name": _mongo_db_name() or None, } + +@app.on_event("shutdown") +def _shutdown(): + # Close the cached Mongo client cleanly. + try: + client = _mongo_client_cached() + client.close() + except Exception: + pass + -- 2.54.0 From 872d56213e9d99709ec768cc842c29a89e66752d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 22 Dec 2025 09:06:53 +0000 Subject: [PATCH 5/5] Checkpoint before follow-up message Co-authored-by: mathieumoran1 --- .../__pycache__/server.cpython-312.pyc | Bin 14907 -> 15555 bytes python_video_server/server.py | 15 +++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/python_video_server/__pycache__/server.cpython-312.pyc b/python_video_server/__pycache__/server.cpython-312.pyc index a0f8ad139d09e71732d6e8f06f5a680c81a137ae..f5a9b6b23df2b07db7948c4440f24f208594d09f 100644 GIT binary patch delta 2344 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 diff --git a/python_video_server/server.py b/python_video_server/server.py index a57016c..fee364f 100644 --- a/python_video_server/server.py +++ b/python_video_server/server.py @@ -1,6 +1,7 @@ import mimetypes import os import posixpath +import ssl from functools import lru_cache from datetime import datetime, timezone from pathlib import Path @@ -172,6 +173,19 @@ def _mongo_client() -> MongoClient: # If certifi isn't available, fall back to system trust. pass + # Some hosts / middleboxes break TLS 1.3 handshakes to Atlas and produce + # `TLSV1_ALERT_INTERNAL_ERROR`. Forcing TLS 1.2 can fix it. + if _env_str("MONGO_TLS_FORCE_TLS12", "0") in ("1", "true", "yes", "on"): + cafile = kwargs.pop("tlsCAFile", None) + ctx = ssl.create_default_context(cafile=cafile) + try: + ctx.minimum_version = ssl.TLSVersion.TLSv1_2 + ctx.maximum_version = ssl.TLSVersion.TLSv1_2 + except Exception: + # Older Python/OpenSSL may not support TLSVersion fields. + pass + kwargs["ssl_context"] = ctx + # Optional escape hatch (NOT recommended): allow invalid certs for debugging only. if _env_str("MONGO_TLS_INSECURE", "0") in ("1", "true", "yes", "on"): kwargs["tlsAllowInvalidCertificates"] = True @@ -337,6 +351,7 @@ def healthz(): "time": datetime.now(timezone.utc).isoformat(), "video_files_dir": str(_video_files_dir()), "mongo_db_name": _mongo_db_name() or None, + "openssl": ssl.OPENSSL_VERSION, } -- 2.54.0