!q6)`@7L7~>q-K^egMA>1^
z#t!c5b0w1pzh9{_Iz3wPIoz&L!BQJGZTBwUDSkWFi1p{E07mx#?^uwK-mF4l|8JaH
zUh@q}K%+Y?g(JY{?LAXjC+4ofk{rk_A(OsHm$*iUh
zyPJMBCLZ^88AD|Iljl?BPTQm&_M>EWnr;9Cd5UmFU5-ARb=72lWfmXaZ_;M$zv$`T
zkQ=QkyeBU1{yXa4_ys)=9`km59iA{(ZNfc4Qswcq1M1QxIJ_@#FY3!u=F8?}YC8Sj
zY1-;*9m4`iQ*E|rv*mf*6R^I5C7YfZoBYTUEkB|=S
zGv7n*&-u2Q-+ZS%{C<158hhp~HIJ9wC)pDU)Jd1IXR~#+h6cz(s74%3H;@{Q)FzLz
z@7)O8QoLuT_i1WX%$ko;SE
zTRp|iM&k0IITKiZJe3Meh>M<*gO>S}4K>CN5h0UzesQnJW`##3GoW8K4Q9HBbtHwp$G*m=Z`Rk)ocA$G1{WyM;e
ze{L=1-@y(k=sd{iC7tzgB@ub0t&}p(__CK`d@Ebe@X?~rJC6tWLM_x5_&e4>*)B`G
z=y|wXQF$FFnt@aZBpPW#85@QB-uANVqu1rmHOOu8Jq?3`wV_Oye5m?`5Wz~ZyS`0Guq
zW|ifi=MA4?$G;WiJ8=x29J2L{X}h@^d+Lzv&W%|r76-f*%6iUBYoAo
zg<3btTeL;2@q8`#YSR#e1C#L*dmEQDBi>E_ME+5dgf#$@-PBak36*|j|yA4HiK7Yjt(
z-fkq6VReF;&4Ch3BdP_qg__&vH3CaG0zuTYdXP|0Z;g-?%?G?BHvIl_^Em%8@1!y%
zcP_48C4;w5n`RDwm%3-itB?G!__xFes%!PW)~MmrO{x2$i&lI0c&!rq8jC-FX2WAN
zYmZ^iU^E+Z(^7o_@e_audk%!(1_mgaTU;R0Dpq2oDt&*UP@b+F#T#_?3P@LHxNrFy
zo&SMk3*gqQ9nSs|$S;s}Zw=ct>QhJ+R*=xo(BwVq+ZW06Tu!EI<)vtJ?OQcFT4dT7
z;J*G-;qL?xQG_vY76MY|;_{sYc)Om6P;vchHe?RF`hZMUlmw%p79+pCDtlbT7x~8p
z6?)6=I38VGn4{tc%9@@W}MzYG(1Y9)r6f4R%#JQ>yx9Rv2U<-5waVExLE7>No#uvJgN
z(()Pl5YFQf(F-rY=>Urbu`Es+#9ZHy-2i%=+NCxONg*SX^f~B@z`99;35TK9)~F
z{*OI!X8~OgX^8NsPQv^~S3hL!t`7d?BF1`wBZ)KyU8G7FFP)^CuA@Ijdxabr-3=94
zdb2=$^6S@QyC64}oTTd!?%%11N@U94g~ziCxT=L}z7$yH@tD<3y@>0jAYhyaaW9bl
zC8OC#x|dQE70OWaKWF+L?T@&AOWHi*chN==$Dn(;2(nneq`Q#9p3yo7Qc*`83AO0o=O?~Ft-^X!bimBRE>TjtoL_};KN?4!X@L4h
zM(JAZc;i)}7Cd%B#vLG;ME?#`mXI1!h_Gml_#+Yb_MwH4`!1ypbJs;+Q$EUY;v)MO
z1VG8kq>>A*cm&2zM^H2f`WJeckYd`ge0Wg5341q&hB$*88%235QBx3~)LByqGC<`A
za-Wi&Sb9cV$bU^hMFfHScw`5(OZac43%VQ{vYO12Y
zs4=+=+{Aeq)e2l5SYlN^s%XeUAKw1-QZ=LYHa(h}?r-%WTj)>G`vW0SY+0T9HGG7?
zPgM)AZ0QaEN+GP4YyK%1SGA=jqn5Ir>lJx&n&XugOoC2J@e}=t%Y{F>d%(~$m2uLbJrLqR`h?0ZDtfA(GUXp{w=h^K;+38B{Xf5$|3-7OiXI|q9
z6OUV|)o8Ci21N{9QpKIYOjD@kSZM5nYUIq0C5wxDSDatF+ThcSfX3!Ku;QVeE928u
zq1~>psBuP!lZwTA!Se&k}OOmX+_}th&w&LVq<9t85MPh5R?Jz?-tGFKak^opP
zknIvqX+mzr#E9gE!wbg+bBiukijCVG#lL*IjuuK}c)S`;JCD}+8{k2jj~v8so?Rb#
zqZ9wJTT&j1J6N@EW?z*8w}D*V%}vI|Ym6yfw#f(XdKhN9{wW~m0V9Vf@(pm61;!z3
z&C=yum1pcaT*YTgPqH?P9nR0zcFx;pyzwtix~AxReR_rlSvG#F{KZny31}Js_Hc%M
zLRuPLZzu+j5A!fOt@Y3q)b?Bw^m;0=IVaVf?_U+?T9Iw@0ek-Xj>}DsII%!{;5Cug
zUE6~=@BS^M;$_b~S$aZzO^arx)`bPTL7$Um?9t`ufBc+c$((QNIO1=0d4Qx=+SP|y
z#Q#ZNoPe}cPuwV2m0@y1s(s9BgX_&9S=1idbUlZ$aV2!hJI4*~y8Ss*z5|q9VRuO0
zzX1wcfs>F_z~hn4ryWlmr=~UZB3IqrUBiaCXYepi{lJ|tJR*-+uU>B
zX&a}?@HoMlpRHX1O+n8^d+~tgfnhGnyvhh!Lt%Z_^EP~!mrdH!D_HwQBd@!)2T84z!{f_jGS*kpikPi5#)Wdf
z`wY@KIjoq>&GmTWAIqitS)^Tl>^feukb~-PIcInoHw4>l{!%g&N&+DOgg6Iq3tA2x
zczhjb$kN;nET*gLl91x}U(63SHzy3v{feR)mq}Z3n49^I`*BAo0*^T0~f!t(>$J&(S0jeiGQU3;oXjTEzTS^qMiUx
zjiFl>3mtboDSs2raQmLw1L@z%j(4O9j}@9%=R-cqc}FK-Ylo`tGnko?Q%U8dGOzy5
zQ+fMkFc{^lCI+gd?cw8u%4giL&yJIonTNB&nBW5RRo{iL;^a$>3cj(x?{L+mz;ly9
zQ0Z#sW_)(>FMq`{E?1l6Q$xWzD5P~Ac&f3grX+hiJ0$LhoX`N4`OmTsh5KG*p?gc#
z3C-S`j$YmsP-lD=B~;lXjijx0_F(GjFTt2SyKhzN;Ks819oMNI)Lrd3I~!tloS0}E
zX*y^S!2Mnx&z1DEy#?5GJwvJA9l;Oysv;Bm9ONF*fiI4yzGdk5!LTyo3sc>sv&Ltp
zg5d*Km~M89IrSoU6(XyPSzI=76}p#F(M{sEo23qJp(P9?FVOr-rg;n9$ZqVn!d%?s
zMlk>ookXjOVZN_g6<^poP`_wy6$FOouqP3#N?MrRA8dL3?%j~A&q>$hu-y0M%~v>m
zfo`Rqs1u>KO-o75wqwRjLWSThD|3UJgKNdC2*x@4o}|?=_9QeGDCco;huqwp(b03IM8
z`-CtI`jF>|-x=0Pco6RMSNJ2NT+Ms)jA@zgD^8Qa@#3N?#*$gR-n4Cu5D_coy$tU9
z5MJv?x`a}-Ki96ZyoKEh-EGaZxA2~3M_54P*|}st07idJ2&?{MwDtVFXgOBn8qMC!
zX+1C<7GA$u3*h!bDw@d^S0N&~z*DvR!t?lc=g!eoRVZIuv5xi4JBxA66JgM(e`O@O
z5Z^nvX2CyA0ddM2IEc?L+6?@DD$wjA1MG+a>H$pqxV@679fdo_5CAP{LaM_`_V;cfT1^4r2B&|t-;POeGi%u5cGEM
zWGGtZNt(BDQX+V3{%2H_-CRtU&I88eOMtc3%?lTf7kw7(A3)~6w8TlW?jutWPTdhL
zp)Y59KqucZ-BcC5H9P*XThkTPav_FSsF1FAWI}m>@IqX@ERCXSm`7BF+bpihZ^z}w
zyxz4|)|52=UJ6hkuq=ef8U~gnUXL71m!VsUG?x#slL2+tL(4oV^Y2Qy1N%A|vS}
zlYygyMtNy%hFiSC{aJ`Pvm*<}&dVJ_wcH4dEV+~b+#*S^Ww9X?=*=y}wX#yU-!%OP
zRCp0KXnN^==lBYQvz3Y>1d9(5;Rou_0qP)K>Liu-*_4g_oSj@UJA?-HBUC*Y6d*P`
z^OOkNvN8*tr`hmIILTBavX?PD)^=DSQ&bfbLT7=J3YPIdt&%QT0{n_1E_?%DDG1#Ua=@|A*kiAebS}L
zs(|<)VNr6so~Pt58w#Uzlk{{o0WNEB;mQrk?Pi1QUIBnYR`RWWhJ|8*JFXq~rX=Hg
zoxHQe;q7gcFjT)fGK+=|hV
z&JY)R@hN~q>kUQcZZnH&|<^x2}%U7)t9(oQ1bT@LwpdNi30
z*wqK45y6(uD7J9eMxiv0O|*JQ)GzyP7Lofe%rvku
zTsnMG;r@V%<8+iNFA11VFw_3b$AKIM0wvb*d4#ONctIyHD>_#|9*s7g&>NP;L`h5N67G
zF^9GAEVDRmSz2^aLbPv!Gp!sQVw+`r$n*INl+6GzS0wPFyI*U4gNiVa}ZO*
zp`sn$TgNTUavj~Z|2A?Q{uKCv*)kOi4JNbAsdfLp^KRrtotZvUfKQv}g}DRuq~!zQ
zxw@KEnVJLkn~&HaH;q0A^$xyXWD-md)|>^9?|75djnLsC8C-sRr>YkWSLN$3ezF@)
zCt&v2h#a=#cNDU*|B;3=tdNo_EL}8Ff3IV283#+z&1p%jFVaPCsBrWsLOH|%kmYk?
zf3a@$xO;Ts{|tE)N6+$RRFU9|ZgfOZOu@*kOlQk$e8d+wxGHAM@=6mnkuwcdON@L0
zVz2KOzs48zRX7x>1ciLdz1tcsVAz>b%_t)GvvTQZh_m-~fja9=eR8U>uN`Ez{%40)
ze2$vzBX%wpDJ;!?3=wyR;ZOwFY753fEHBIBf39(eh`uO_JKK)U{c1nJdQ%K+O{=R?
zYx3Ix*f^U=1$fLgc?-MQ#lfLrU}vs8-EMjW-NLZ0e`3y`eV}7vd>@q@b
z-oTj&H-Dj``V>G)j@Ik;D0w}|sd!;v1d>5wsuK-u3l=O`IZ*k@uG7N^(HX{BWLU*A
z>(DNEmfAnNjnG-5M8qaM`G=w-KPY8!f|_RO{k0tG5LI4R@CHEr?z$9Tdcwmj+tsh6
z1{rch2ptUAu@ADCab+HUSarnD``h3PJpOhlJU*@7zMFgCjncyJf02ufikB0xBbAgn
zr2d!-y6~Bc+Vz;>8;H$dZ9E9*M^r;pfXOdXQXuv#LDfSj#^4qhN=VMs4JR?W6txdAUlT_BQFYiL*RY)!Qu~I
zP42Y%t9W5GAZ^LKlqNwSmnjhp21r-j&Kz0PL9+5)U2+5x+=T)A)51m@NglKN6iM#=
zx2%E^sYXtP)L#9t6SZi=wzLG$e@MCz)H+-VYCYPaBK9mM
zAP3d2n9l4I(-)8Rr5{+fZsziJaNywR3&KM
z49Le_ef?~^l@`m($+~e+OX*-yPUrnBdbys{d
zd5Zo&IukNkoW0kI{(_wJV$JPFo}xH|^IGhiCnh@mBcReg8anYa#?HU|SwjICyw1od
zQF8@hBjf0J?Ol*BKEjp?QlUO3Y1mjLJA#(NNx`2M_Z#3~3%`i!j|Un}
z_M>Tdyo!t}z(^1>n5c2>t7E_GT+~Boo)WwrSmONfqRmdV#hrqI5ApAFD@1(vZ#fuy
znv2fW`cN3m)um>9mR)+Y@m?KDZVjHBQ^N3W_v?t~-5iWy*478AaZqZ;XRT>=`)E1-
z2IjGl(tzF0osy=%eebE`OY!ELh#(*;^@~9#X(}W++fA4a)3dg&LHq0~X*C+a)0TW0
za?fCFMMPxWf&^I3M0F-4AYYXk1%eJM9d4>d8tQP5Zt
zd692J$AygQl_#d<`T&@P(<2KSOpE4i>u{CDQ-nK6vW9w@)~}SpImv9F4GtjGBr{4t
zv@$TMj4{fX7!38VPftHp32OjhPUJ>tcPj{?;3Gf*6fFybRXG}6VPdA<{j!CkY=h(H
zGK95Ox`F`UbD4|6@7&E$$g$zk0$5nST)j^+!10!v0qY>;;JUZ9g}We@dduUKDL9+3
z=%+B-Imj}jmBwGF#pcQbhFr44QXjhYND!)OS38$%`j&BO@CCTZcM9fTy&e=BR(9Cy
zf_Y`A6#yMggXUXs`q#kw6B@d&g^N{1YstE)<|2SC(|{QWXy_zxg&BY>8Tr3xvAgn+
ztc}1mLn*@|f7!Q4%#|jyPq}LTY}*L(U9Bc}d|OGhg`_oVPytBShVSN2&w&>&+i=O-
zTO$!&yMJT|MoWwQm*ru8L9hjDZ6#ualHq8KqNVf)RgON8vSz@D^p#Qydg6zCx56|k
z0^Hmx&znL*ez@&p^uF`2*t_!HFYbZBbjGGq{V$z(x7CQ3xn1(Ol!K8#_h-IBEp`zm
z#T*Pj7$?fm3Fg(^MY9ZpE}%kUYatc8{&}94K|*1u_gm^)Sg-I$)Me$f!eRW-@-J-t
z(Q{4l68=8a0EOTZ0CgLsRDAFOFElSmN&VNZp>1)%luk-f>=_%b{Xm<}wn%KwDz)mm
z5;HfsV40s>>=HV@%?VY=0S6v98TFJK+Ae&OrvvjpDyjH2O;tYusAZv4jwLQJZ$!}P
z#@L2M{Mqql?T|ttSa+hj~4~1J+e#boA%r6a$l>GmjWD2A8}h$JDZl+2|l53}khb
zh=S#qSiMxTqs>&>5#t2RQ_ua#?UWLQ0USG{zKff3sO7)WI4Doawpe{v(CLMygE(uC
zD;f`h?&c(b=A|g9KY<7}9fx%A*{izoM7xvk;I@NKGV2Y9CMCxwi@Z4uH@k6%S0p
z*uT?0s}RD#D#CCJ9F1dow`Xb0`0W`V`4X@?)Q*wX51BM>0UX>0K0A+B`;#x>>=dkm
zYKyN@A|vgG{~aiT^Um*f%P#+Tt=@99_MWDeXj{yv@$(V|?7Q&b+z2cRO9|Ht?6%a)
zx$^Ipo%w@EB@eEgWF|Rw9Bv3uxv--DL0d9!m3i`{sdrc1mMO(|AA1Q)vs^v8^qJS16ng?0(o+|rDQ}Nj^#%Z=9)D_zg-kbri
zZ+x{!y9(iY%m7ql+y&xF0H5gd=b7dJFKSB_1;kvzob$x^3SxB_jK=@T3bv?Cre~uV
zejJ+WrM(^B_&zkC7Djh@*4e?R>A{8hs3NvKHs!VFn*)X_x*-YuhR?)paF+y4xh625
zI6~z;xMme4QkbqWC*xl)z0CaWn
z=c6%P&LOG=tGDS@{xKs7a_X=lL-;XhJ-XfeK)`GA98x5=1{E7f+J!Box$4HBE|9#C9n164@odp_{S9$m{DgZ15mkkb%+^L2f}A4_yXeZ
zf>|-_T+tOB2+AtWU!W529AE>vx*Rm>0Ed!ovVr^J`@61+fII*w0@xWc!UoMtXGrsc
z@^_rge{S@SK4aecO+>M=B*$IjEX1#`mNy0RnEfAm%mQLM0f!;nD)q6E`&O1Nm&?m)v{CX@Gri&Iz}G20+&eR4(l4ZGZ(!r7lW}
z8j9UNn{3LRasglYXWHP5J&BvIaCL)bLsEaqr(`4X12FgQ&3lPUb;9lk(Pe<)P#MC>
zAHukktKFmL<#f$)HxDh(^|^{zrG$*!b$6ad5r>1?h(f9o2e!`f^2@T!`c!Y+S^9nW
z5ZoFNi2(w1Kl~`1F781ldjA<~oHP8a>2ui?(9q**2$65JVmW9o0&?ruUt7RrB1Wmf
zsS6ME!Xh`iT0sv7vctlEw04}kSOKy_PtPwj0y6fT+}L1PL?Pv|U$akPW*JHaB(?5J
zFrCaxCh{c5O{hM9T#}<$XPnR7yC;?0TRhjJC4Ts{&@rGq8oZgswtEWZ6iSx$l9n&{
z->7Q$_MM&3|iGUF(oYpR&X$NfHEisy~@J>RP)iW_L}4LL=cu4
zkq_IdbFrX+jG);@3PEY4d8x{bOuInHc7RB%J_F`to3J-{i2RwoQ9Lllr9wUd{DUeg
z8AKitDNrYe1H?$%`CQ3|3ij)OV&f4g&CS1m3RL}YX4xjM+)1JeyTc&39Y+SP?iiK4
z9m0KuP?>}#PD9$#6%1&F0fnqjVCGoDJQp2bV=C8_i35s8`l+ZxZovcp$A_gWsLKKW
zn6U7_EYlJ3HMnQDrgQrTEZ7}T`9K(C0nI1iTABbV`ciuwc3z2%f-Ds@dc~(^PDJCD
zhQJ`7oFuh`zm2I`y5a)(h(sFq-A)n^fn*T7`{2nS$ST6lxbr(K(BV{>$6DL(HZtwe
zfo=F?wbKWl&ldNpL=M|yVc>Z1lz@643BNX2j_f|!%5i>!
z7rRIt0E6el0&(iD0B{M^tot+P6^ny)09R}>bG1+4YW~vTb&5CV^XX1l(w5wh{>CXHO5R&hfA-FZw!*G&;YOy~eRc+8Y?tW_C{M8M
z?De@kiklV;S_R&QbRxX}Fx+10G2K+miI>dk-^(PGrE;AK^5M09;t*azQS>n5@7E=<
zzJkZveI68>oC1gBQF~>no~%3D;92VB%n0yQ_>E{7#1d3mppD8(?tSt`IpZ&Q-;Z~B
zXGZC(6AQ-^|Lq?yiyNCs!m91IwiZALf0Q!{&}$#aGHAMZm$wu704}A-1zO#3kjH7<
zHV9_ghn4^|1qi6#mjgrNUMC#>=n73*nM7fseO{Xz<{;^G_-8-6bojWx99Xvr#?@k$PxnI7k=8)31dc&^l_Jgx#Q(}gf30=~>
z%yEzI{(eyc$!-60F>a~Y)hDa5M%bbTEkB=8(>eLO$U7G&huWDZhhI3P5LRpKDM6h^
zxzRJ8EeO8bJ?B^!zk2_Jxq$suTC$}mj71HT(CpoVAk*drd4Df(dY+t=Zi;#vPo`M21{S_7#Ku^xMuoxiKdlX{y;Fkxi#xE_T=2Y>-o5heBI=P
zCL8R{$9|gG9dy5Eqz5P3IJO{+=h<@YOlZCT#x4n~KkXZhnY
zb_cR5dv-|*X4ucg{xvS9y+#V2&2pjjvH6`DN+wR{6X)Wac51P8T)Gs(SGCD}cV?9Y
z>O7jWr2;mtBsJ}k0){-wUh{aZ$_zSR{JIvk4D@@t&=|^5As$_MK9MH7*PvE}Te9KsQT^&_c@l*M$oaWyoQz%O&emuKK9uc+Mb!%VV3jz@}2{W)Bt;aNne@(4Vp=C58@0
z0&vpjl`4N$;tH5J97<)0mlc+OyB8#IQq=#V5g3ODvL4P>hy<#@si)H@INMfSq#4_(
zYvTKR^_jqH$)Rt?Kb&`h^G_QkG72ZV8T-A9why%&t52UA>~@ET&z%lCZk>$ME`C$o
zJ)(EIHG5FN@4uzda4UhMAb5J3CWfsGFJ-0a;(O+dHlJaZwQ?Sb>8yJwuv^7LDceSD=?c;l-;v&(O
ze^sZZijX~yLTkp7+)a_jlm7n1Y+
zD*^}9gND|V&TX^kjGbqzVU>=@pRg#)1ch3&Z(bJnXRNt>xNZFPT1Kx
z{ofNzjq-lapKJMjwg#$Ks?N89)*HN}#
z!j4Q%9+X60Oo=s2m-`K~Tui)(SP^54&`lygi7t7usL=H0_#
zR{PIMF|9aOm;Tb8j1NA?!1DXNP~=s_&7Q{uP2O85MfgcK-Ig4N%xCMMRU&=OG_!1C$9tA{Pi`yz*neZp
zuJUcdl$X|&>@e_Fi{95$AkgT}^yuZ^pZO+h=`UJgCRFnYDP`+2aoLXqg79;xK8Eq$2{`OyT~9T4Tt`*MxWx(zi@*eY(rW^+}ADUspBR>3Wo1G&Z6
zoXq>9k7y45c^zz(7MBN__uBF$0qLC-i5S?bElJ~kWpIYQ54lGp2>ZoNHpaV{hLa4J
z@oAaO$2Nio_ey+IDxM9FILUi;eK9?6AZq7PJ9(n?7%IG`YcMCeS(M6s)^jg&dF3T)
z_^fq0%)VatWWs^yY=`U=+!9kj&2Hex=(f};>?G+{c6rp34;w)59Yl;IQG{v7?&pT2
zTvr_JdePW(d9=&S(s=xE%O8Cgm$HDi(o})$S(-NjfjKy#^wpp6nbvQ`2+nwQ7iE@p
z4>rBdHBhiXC^L^v?mak~B-t-FV3FAtat;pU@`h(sQ
zV+&xcZO3J9Nhfis%<&xOd7rr;37E{s9(P$9Va?IAs5zKjNaKc7FKP*un>fg_ZAqWL
zdEC&@Y3$kc&}$+6;r+9a&BME_lgRh{{#iI2^6l~-rL>4E@=>xbndwBE<{!MH5p
zEQ<9`UQKMeXo*j)eq0Ybjc+&{fNSeeV>g|AE~^?EFMrMaETP}u^&n~Volz8&arg{;
zS}V5<`MrONd#2K1`0*5%1H9;&MVi6FAOuUTsD&-+YAD
z?&t=wa~hNkDvq-yh%*0GHCK
zCA0e1$gNIV!_axG`;FWpjOj3V{N&Z~_*ZJXg2D9JDcz%w057h6
zn^SY1a~Fy1usx|>4z+YEaG8@)AWTMdbVegIcGNJB%bbQ&EtWC#o_E#E-eDZ$CI#0K_~e^h6^4{sXA
za8$(s8mn#wGjf}%IJYnNAmLP-0c#1hV>!^-rFN*lQw@3lCzr4?9XcHS_J3lGPOE}u
zsKvfOk3Ne0x7vMd#=<<3d0_Z=h(Sh^YAoelF+{<~=Rg^-cu@KIx~){=1Ot@x30yV>
zuFVTBCR}UYmdt+q*@j;{hDa?SLOAgaM3*HOoYFPF=M`O0_~$oY#`t#OK?KLS0Qfsm
z>?4XW{eL?r6!ecyqIgwym)#O2V?QgG`uewxB#EatzRj@tN?&e06c`+YtMFB|~s4q{Jf(DGm27z^uky%D@biTgSdo}Vd
z@iEaa@sE`=hb{3w*cwi^Ck0%$*C5aSG5CiB8|ia-M=QamwYkzv2%CO+W^YJKW5w~K
z`@PCd#ZpiN>yz9D(d|LTHN)$mTr6=&cc00-VOnd`r8^>vipm#_zHI7!@2
z7r^EDW@$e}Xq;86(ruVCO7S}3?tCU6a(tWJhJPooBI#&8^@&G+ayOv#5nepPMcR1s7UY0%jx~p5*c*rph;n&Eaz?5c1*sY?MZgc*MmDHQs^zy
znsFN^mV`7`^RJ^&rN>u^ah_w#O5wkgTXdFhP=W|$C3%?WqizKb9g845`)%~|?Fxy(
z(Z_pUwWqC#7+!-fVIgnf`i2R&fgYkj!ec)C&0@N;s*zewnfAJzQWk>}Yb!qX8g$*^
zKlR)0_1W*?fxUs2LG=j8+;rWa@0&d219rifjYYX9T<)I4U9)79M2PLqvu(jv?+TjM
z{1Ei@EMl@SLF=;K9fMjsMqFs$vQPm6VSg@}eB9n_vgN1X9`Y|17D~@o~
z?N@8c?3;y(xmG
zzW1!`8<&WiDKyfm26wuT?@`w{uF7JkSQF^{gB5)K_c)(aBSTVOzmjt7!9TH;$cf=x
zjHPiUw+OX~j1VjT#PR_sC|dzm-=S2838RWI$JuW0DAdl}R}qNVOKuDE>T>CT`>(`D
zzUOsjEi8>5-!Ng?H0d|y1XHqc=cmcmp2mQHIQxJZ$HI*hU3QXL=jEHAmJmTNmfWqU
zhQrAtmOu4P-|~w-pW2iIifF<{2%0Pb6WQhaW1xSlaVzu*XjGGAo8F3!P=m##ebcm5
zR1}*N(`4?PYQr@+BhGx;s7(9bKhdf+>+`e|YMBGQSTur6d7wvtd<%z{-Q~tD^xyN~
zMk~d{*tE)+R(?OZ6%^a0Wn=Dpg=&Z~33-|oaI@D%4b*lXy@ZX&)48`{F@1%1{g-ZO
z#mV}2VmEKQJ-;7nwOsxB88q?{HV|BDIN->jNIwDNbANZ9e}&&|EjAD1LP;azblJlL
z$&wNfm3RcLfIza!8%#kCS+NBHd!w#bY?t-epX%~rsfaT=sGNSoT^je_q6{%66hzZE
zd?Ns{Sq3Mahr7(JT;${SeLci9JoPzs!3HZ*s8pbfTo>^}m
z*a#%bikGc#s@%VqvM6#uN!9za-5=a0!6x&*{md>K+f{Viy~01-nGMFf6MBt@_)sW#
zE0Yg|Rwy)%2-JsarpH4SNjyC~Qsv6Ds5j@3y;LMd_)qmAI|Pz)i>m)2vW{Px&(qU!
z0vJ@&ZqGpk28p4<7~;T#Sd|j{$Lq>~HJpTsfrOxGsEv`+4oQBnw$xe3ZD0+ics(}z
zZ^BUTIDfqC+oeAQQ{exD>32ca34(u>-fvtpZ2)&QtM2}(+Cq8W
zgJj+A?sFLW3WwkIG@1ezrvuQ=SDtJPBA8OvZ7D`JPWE;gBXaWX2n`?w1b-KID?NCb
zXrl=r`U;d!?=kdChNYgXpD;%qFMWva>cQd5wSCicK=4Ij=l{_(7Sm!UiM6N!)gf4Q
zxbF$B^G`%IkAM;j&D~F*0eJ=k&sUE!F?^IS$P&-kJb|1DA-?rb*1OB+r~GC
zxirQ9h>j{jp}~c+m0JR8v7^7vtOcax4Lkq+q3^RRp!ek|coj3H4d<26U}Alry?!rs
zK(F2^o-vbtH9_0eH$2Sm&eP9|Aol*#N|vG&Y_W^wdYLqE%3mp)iLr~i9j^ot@i(K(M+HAly`(NQ)9B`9{S`zeZFRuWWR8_V5;Q#6EO5CB|qkd!y
z8PwRbWXZLJP*=##jk{O6Su2GQl8Bj9_H9&%8l#LQl6_z1J>RMO
zKJQ=f&M;3i`kn7NpR<2{Ju*r#@%Geag3(>Pwfy>I{|V5N`Hm&zdNKThh8Rw4iTl@A
z-AFdH`#34XBw|MDb2870mGu#v|BxV6dP&%sU~t5_!tb+{Do1qUeg7hk+S
z_o6T%EqbsmyqBmVrElQGIPY2Qc_n-+cfWmB%HL
z8zKH&LCYD8;;lt@1vZA4gYDFB(uz|fQ0%-0^|*5hHoJj&=2%!!|58<0#Fvw#>Dt#G
zJ3RAf7>nEQ2)?~O$gL4e3yK0D2$2u~Bwdmq-SSV*vf$?+7>C`zdCi@4Bk3Ar?>m{j
zioW{sQCylMyY+!uq0M(f#7IHrgOF_{S%5u?2heDdT|~k2D5m;RKW@VR@1!bjTXtGC>DJZ>Xz$r?>*HWrnw2UxzU^GA$7P
z?%yKpM#g>w%j)s2WXsUuH)}B;f%+B59w^GzusS0+l^-qm3+6LLxWk7H0cJg~i63~<
zVBkEi7ab1#7E2znS|XTt(?l0f|F7mZcj}8
zW?JWb?V+w^Yx;hj)>xM#GR_I<@n5+?&_SSL!~6l>|Y`z~kiqf?X7
z9Ac;cGCJXvSCyaT-&D-<;k1dqW7%utAcxzGE%i`fKKaf)bj+iKR_IVQ;ue>xF1*{Z
z5{+~cP+45fur`>;+o^9J+J5K?t><|;=~ywbw83HLu!6n_-E~V7J=Ip#kH40hvE&XwZ1uWF!M!wE0#g9JyPhN%B#rf^p2K^BW)F(DudM<+iTi*#zQ!$pN
zix-;R-i+{iZM~IXEMji|o2l~N=5Xt7-arxmT7JawVTb_EJC
z?Psrt@@PRp@L9uU@IOC;i05wpHYbsPrAv4s*@L#9?+y7@HS1D;hrGVLIuiJ;3BOS$
zSCGhMHI*Mvi!D5rGJ|P)VsEAy#i~-ZI#7|^qqt
zoVB<_^46%XS-Q!~$J^1J&tI-*?375V*b=j3MKia*ZMMIYO`!^9pt)}
zK<7CkA%*$dQOPyfL-7P%94r^0!<4YmW
zx@BuouKg3a!S9|{9qtT&WQDyS{r%L36{E~6OJDefpxTNh|LObb#4dUC&KzFw(6c^$
z{zC^PIaX3HhVElyVPa}a5t;eX;8<9(Y;2C6XM36Pe?cPNvyLVDa+o)PpJ>6xMBZR(ce>8ism
zX=r$lIkj2wR}d8_-v?zoCgfCMj|q14EV|L%7#;K@DI$t5%k_!CyDmjNth0#UVQ5&9
z_G7q=wb09%l&y|KKZZa3rPuD*mo*YFI;OE^ZR+8`0KjN7LQUgX$;zkDRUcE5zbVr7
zMm|cRLgAU8U%woso5CHHsb&oaU4F{Zu-nxfvRK#I2$QQn*K&uOFh;PnFs@3ID?IqN
zP2NtAR^H3&A5cH}`>#8<$IQ2TAn6k;BuXTA|ELZ3N8&Iy^0K
zkk?54lDS;qR`l+LQ%@0KS3(rgEiYT#)dKT!J93tEG#M7(s<;v!bup%@1fF7tgB-vE
zu!lz{)H2t4=G}lE4m5q0{{vb7dy(h5gO;jDd@rZ*q=e&eDDN|Z9tI29d5UxSk-b?I
z>s~hp!6=1jHW<9Tb=s+)L#AsJ}ZmVwbB6t
z+1=?2!2RWn*;wm;0`4yn29pD&)jQTgycoUV`ml)o=-A@!Wyx^i2EHPzALqns&
zARproMowIETi_3wjbyRg^wMCDEP@Os-x~OIz4y_o-FA`@#HZrF5Tj6EwRJy4d$+GK
zyU^1)wVkxSIz+Mvc-IrA?24mB{1Expg@mh=?cL<<4|)0VY*uasiEIHdpiMj0?Q#jg
zGetyL$BnqvPmC~Dz5bxmn`i`uEKTf4`XXV3U!OvLO{yrVia1KWupTbFi(M(c$SJ_Z
zA;87Xp3?2P{_c6x`&`mh8I^C5z$K&|&{tMpoJ~!}jrqp)TwsKV*Je3%1I8pJX*iQC
zLeqy~hOlRMxr(3OE%1?>9yc0LgFWo@VtQ$j0WNsHcVr1S@)mdb20@=+7Ah{3Ze88r
zqnX>cM;Z0#=&EU7cg>oQDEhs#WaO6~z%fy+{LMsgIPgtaa>I^q;B8D#e*E!aw}J?&
zrG~p&M^(~dZpNTOD_{L3-kLGvUc-GzxXgVbnV_*+aYyQ`9nZ#lmQeinGd=_@e#j&(
zMyRJyS9FhV*fxnmqLu~X>!tH*=W$`Td;RO!84yUuaSn6DypVZOjg?2HrmF-!ep!@*
z>M&(wVZ|xJncGIF*y$=s|BGd3AI|pjmyhvf&E0IsY_9^ENv!?Aky|CzJN(>2>mPgV
z;soo#J5bUJqdBpKmL`k6-tBMp#UvdS7QP|4zS`vO^l5*@Os~cfB?fa~iB1cKnkp#o
zl=TLWj@seZjHU{pY`YaivO&(ksAz~Y2D5LvnJGzcYBl+;uz7lPf8N-bnaEj)a~_&c
zbES$nQK{NEOTY^G&5;v@@TlwRL**ktHv!Of+c9n81_#)?<6QJea)*WA{y3w&lGqz^
z-~XNE{k@&JyR;%?lvduo1+?
zgq)kVDcf)N*>Gic;~H{=>IKGS)LD-V_{F%_3e#hT#hQlXvMN@EuoK^Lm;TVaO2w|n^7CUaI_
zKa3}>kN8fM4m4_|Ls$8Bpwedwm-XpX;KI7OhW80HCPyPPBiQPv3Up(c`<_UuT-92%
zfDEQRMs~B`b5=ql8eV_ZhbjLWsZWmXP
z;cQu;8+J4R$vZh!h;CM2K%ThIDV7|K<_Q5Poi`;I6k@cLs4Aw)2KCG3DpUNuLacG*
z2TRTWg3ZOTA9IF=hygQ`E#=#=DQdY0P(T#?au!sDz5g+qs{LwQ-x69ogK2q-C)#$@24l7lk
z4w}^U7|{?O%PD?XDq637OyZAR$)+dN;qpJ-A;vWK?kB?xlkGcQ&D|Y39%$RIw()r|
z%0Ipb=*7hgC2WfO~J^i9*L5N-JHCS>!qS$@F}hn5i|3=b8os;cs$QGEZkHmbK2Bet@WKilJtkr{>+MbM@uuvM?$zxqdY$pw
z?w|ouOVR{loEBVO5HAQXU?DOK*|iTVW?Y2qq8bKqPc;ewrv%{?#Y)3V`+D28(ZzYm
zvLayO)$~el2X~o@Xm;%sul8|p64^}~QD&w0`0DT{UyR{;_LAyDH$RzsO1rkOpgk67
zkY7i`(uT~8MOz~}`ZO3C`RiyEKDYcZ1_me$)IWG5ujxdAJ5w-hu1mQ~6E!F{5`q@)
zK5|{k%uUA}Rcjt4xGO+~|E6K8U|Vx!?Lt|sO+Pt|*bM*T_zcN^WrrBN)(yWo6_zxh
zaDb=)xT9g+MdowT1E<8!afnOY_^z
zL+PI)ww8gnO%Co(pVoqQMi5NV;`&%_?7eHkIN$?9IW|TPKB}4cCNU|Fi>1yA?
z$-3-aFpgV*DVz0n_*|R_9VS-ckF-bHOnts^H*+pdYdK{qPnROBPZ5)9
zw$Q5-%czWVJ}B+LTWS$onc(mc-gH`+Hd@@tvB=0dmO->Qm{THC?`(Oa!#1#f@UrpR
z@cZ+PS;fVxAKor^z5Ten+?LzEqdKR4;h&R0x?O^H3bZg#SdcEjM?sC{q=))~qKFo2
zh+IhZVV1B;D%vP`X^+v7VG>UQ-zlZs5BtxBQIJfuw5YP7foFavT$ClrE=6od{A5U9
zs$R4n_yRR<^kC-rbrMt-W(b)=IP%v?)T8QzRXFIQ>FMZTo(1&LNn{i^P~jnfeg-Wo
zR8Xiz_GZ^Cs87#u7>793tERT#tY|8`kHS(45)*kkmKjww6G#(I;BnxSuM{r~oA<3h
z()KV}iBUq2VFTNAiu=1>37F9icdJ=E@vxD`-$^AAU$iLm2(YrD2ky%#|IZidTQ&y9
zh$ee^o&*nh+cCb11wM@vX%&t;pEuoXW@^fPWVzy-9V#tk1Z4!({N_iSBIf+&4`Fa)
zO}=XDNsRG~81(Z-vk)WE(6|M@L3{b%|2yZWbR<5aY4im{t1{1^{Y7+w<5;Wy;+hr3
zGx)jfjMZR0*Nl}6NQr26SOEFW5sa8$M}cq*SF%T?Ma3P`NL!v;0~j%gbVZg>7IOvzy+oHkWPIBCNxiS<`_EXt`8VjT_6_!Cz+a|ube!xIF^hqkd
z&o7Z`C7}7!Tq>-}r>Ll2LPF{XJZ$29%D^fR7VjfF03eqoo9*(6+BL0vf{EY-I2OKy
z`0*}cc5gP7XIkMJaK?&Ng-_YwrrUVM@x^{n%ysPkLT^hb_{(|6cP6hadB9+%)eQF_
wb^G(WvS>Ty)21?zOHg_L|G&1CcNt%4m%D2!l)`@quxHO{y+3u!we7?I2e^BLcmMzZ
literal 0
HcmV?d00001
diff --git a/packages/gui/src/background.js b/packages/gui/src/background.js
index 5be8601..b7d5593 100644
--- a/packages/gui/src/background.js
+++ b/packages/gui/src/background.js
@@ -410,7 +410,7 @@ function registerShowHideShortcut (showHideShortcut) {
function initApp () {
if (isMac) {
app.whenReady().then(() => {
- app.dock.setIcon(path.join(__dirname, '../extra/icons/512x512.png'))
+ app.dock.setIcon(path.join(__dirname, '../extra/icons/512x512-2.png'))
})
}
From 1dc589531415fd52dc0144a6ace69065a12d1eed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E8=89=AF?= <841369634@qq.com>
Date: Thu, 15 May 2025 17:47:53 +0800
Subject: [PATCH 10/13] =?UTF-8?q?bugfix:=20=E9=81=BF=E5=85=8D=20=E2=80=9C?=
=?UTF-8?q?=E7=AB=AF=E5=8F=A3=E8=A2=AB=E5=8D=A0=E7=94=A8=E2=80=9D=20?=
=?UTF-8?q?=E7=9A=84=E6=8F=90=E7=A4=BA=E6=A1=86=E8=BF=9E=E7=BB=AD=E5=BC=B9?=
=?UTF-8?q?=E4=B8=A4=E4=B8=AA=E7=9A=84=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/gui/src/bridge/error/front.js | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/packages/gui/src/bridge/error/front.js b/packages/gui/src/bridge/error/front.js
index ab1efbd..2b11216 100644
--- a/packages/gui/src/bridge/error/front.js
+++ b/packages/gui/src/bridge/error/front.js
@@ -1,3 +1,5 @@
+let latestConfirmTime = null
+
function install (app, api) {
api.ipc.on('error.core', (event, message) => {
console.error('view on error', message)
@@ -13,11 +15,20 @@ function install (app, api) {
function handleServerStartError (message, err, app, api) {
if (message.value === 'EADDRINUSE') {
+ // 避免重复弹窗
+ const now = Date.now()
+ if (latestConfirmTime != null && now - latestConfirmTime < 1000) {
+ if (now - latestConfirmTime > 5000) {
+ latestConfirmTime = null
+ }
+ return
+ }
+ latestConfirmTime = now
+
app.$confirm({
title: '端口被占用,代理服务启动失败',
content: '是否要杀掉占用进程?您也可以点击取消,然后前往加速服务->基本设置中修改代理端口',
onOk () {
- // TODO 杀掉进程
api.config.get().then((config) => {
console.log('config:', config)
api.shell.killByPort({ port: config.server.port }).then((ret) => {
From 71094c475879f569df980eece19176b00e05e7ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E8=89=AF?= <841369634@qq.com>
Date: Fri, 16 May 2025 18:10:05 +0800
Subject: [PATCH 11/13] =?UTF-8?q?bugfix:=20DoT=E7=9A=84DNS=EF=BC=8C?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=BA=86SNI=E4=BD=86=E6=9C=AA=E7=94=9F?=
=?UTF-8?q?=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/mitmproxy/package.json | 1 -
packages/mitmproxy/src/lib/dns/base.js | 2 +-
packages/mitmproxy/src/lib/dns/https.js | 2 +-
packages/mitmproxy/src/lib/dns/tls.js | 5 +-
.../src/lib/dns/util/dns-over-tls.js | 81 +++++++++++++
.../mitmproxy/test/dnsTest-abroad-doh-sni.mjs | 2 +-
.../mitmproxy/test/dnsTest-abroad-dot-sni.mjs | 107 ++++++++++++++++++
pnpm-lock.yaml | 10 --
8 files changed, 195 insertions(+), 15 deletions(-)
create mode 100644 packages/mitmproxy/src/lib/dns/util/dns-over-tls.js
create mode 100644 packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs
diff --git a/packages/mitmproxy/package.json b/packages/mitmproxy/package.json
index b2d53a3..9367be8 100644
--- a/packages/mitmproxy/package.json
+++ b/packages/mitmproxy/package.json
@@ -18,7 +18,6 @@
"axios": "^1.7.7",
"baidu-aip-sdk": "^4.16.16",
"dns-over-http": "^0.2.0",
- "dns-over-tls": "^0.0.9",
"is-browser": "^2.1.0",
"json5": "^2.2.3",
"lodash": "^4.17.21",
diff --git a/packages/mitmproxy/src/lib/dns/base.js b/packages/mitmproxy/src/lib/dns/base.js
index a71e2e2..7632fa1 100644
--- a/packages/mitmproxy/src/lib/dns/base.js
+++ b/packages/mitmproxy/src/lib/dns/base.js
@@ -159,7 +159,7 @@ module.exports = class BaseDNS {
return new Promise((resolve, reject) => {
// 设置超时任务
let isOver = false
- const timeout = 6000
+ const timeout = 8000
const timeoutId = setTimeout(() => {
if (!isOver) {
reject(new Error('DNS查询超时'))
diff --git a/packages/mitmproxy/src/lib/dns/https.js b/packages/mitmproxy/src/lib/dns/https.js
index d5e2d76..07f2639 100644
--- a/packages/mitmproxy/src/lib/dns/https.js
+++ b/packages/mitmproxy/src/lib/dns/https.js
@@ -9,7 +9,7 @@ const dohQueryAsync = promisify(doh.query)
function createAgent (dnsServer) {
return new (dnsServer.startsWith('https:') ? HttpsAgent : Agent)({
keepAlive: true,
- timeout: 20000,
+ timeout: 4000,
})
}
diff --git a/packages/mitmproxy/src/lib/dns/tls.js b/packages/mitmproxy/src/lib/dns/tls.js
index dfb7024..0194b4a 100644
--- a/packages/mitmproxy/src/lib/dns/tls.js
+++ b/packages/mitmproxy/src/lib/dns/tls.js
@@ -1,4 +1,4 @@
-const dnstls = require('dns-over-tls')
+const dnstls = require('./util/dns-over-tls')
const BaseDNS = require('./base')
const defaultPort = 853
@@ -16,10 +16,13 @@ module.exports = class DNSOverTLS extends BaseDNS {
host: this.dnsServer,
port: this.dnsServerPort,
servername: this.dnsServerName || this.dnsServer,
+ rejectUnauthorized: !this.dnsServerName,
name: hostname,
klass: 'IN',
type,
+
+ timeout: 4000,
}
return dnstls.query(options)
diff --git a/packages/mitmproxy/src/lib/dns/util/dns-over-tls.js b/packages/mitmproxy/src/lib/dns/util/dns-over-tls.js
new file mode 100644
index 0000000..8194f00
--- /dev/null
+++ b/packages/mitmproxy/src/lib/dns/util/dns-over-tls.js
@@ -0,0 +1,81 @@
+/**
+ * 由于组件 `dns-over-tls@0.0.9` 不支持 `rejectUnauthorized` 和 `timeout` 两个参数,所以将源码复制过来,并简化了代码。
+ */
+const dnsPacket = require('dns-packet')
+const tls_1 = require('node:tls')
+const randi = require('random-int')
+
+const TWO_BYTES = 2
+
+function getDnsQuery ({ type, name, klass, id }) {
+ return {
+ id,
+ type: 'query',
+ flags: dnsPacket.RECURSION_DESIRED,
+ questions: [{ class: klass, name, type }],
+ }
+}
+
+function query ({ host, servername, type, name, klass, port, rejectUnauthorized, timeout }) {
+ return new Promise((resolve, reject) => {
+ if (!host || !servername || !name) {
+ throw new Error('At least host, servername and name must be set.')
+ }
+
+ let response = Buffer.alloc(0)
+ let packetLength = 0
+ const dnsQuery = getDnsQuery({ id: randi(0x0, 0xFFFF), type, name, klass })
+ const dnsQueryBuf = dnsPacket.streamEncode(dnsQuery)
+ const socket = tls_1.connect({ host, port, servername, rejectUnauthorized, timeout })
+
+ // 超时处理
+ let isFinished = false
+ let interval
+ if (timeout > 0) {
+ interval = setInterval(() => {
+ if (!isFinished) {
+ socket.destroy((...args) => {
+ console.info('socket destory callback args:', args)
+ })
+
+ reject(new Error('DNS查询超时'))
+ }
+ }, timeout)
+ }
+
+ socket.on('secureConnect', () => socket.write(dnsQueryBuf))
+ socket.on('data', (data) => {
+ if (timeout) {
+ isFinished = true
+ clearInterval(interval)
+ }
+
+ if (response.length === 0) {
+ packetLength = data.readUInt16BE(0)
+ if (packetLength < 12) {
+ reject(new Error('Below DNS minimum packet length (DNS Header is 12 bytes)'))
+ }
+ response = Buffer.from(data)
+ } else {
+ response = Buffer.concat([response, data])
+ }
+
+ if (response.length === packetLength + TWO_BYTES) {
+ socket.destroy()
+ resolve(dnsPacket.streamDecode(response))
+ } else {
+ reject(new Error('响应长度不正确'))
+ }
+ })
+ socket.on('error', (err) => {
+ if (timeout) {
+ isFinished = true
+ clearInterval(interval)
+ }
+ reject(err)
+ })
+ })
+}
+
+exports.query = query
+exports.default = { query }
diff --git a/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs b/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs
index 9c98873..39fd2be 100644
--- a/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs
+++ b/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs
@@ -37,7 +37,7 @@ const servers = [
const hostname1 = 'github.com'
const sni = 'baidu.com'
-console.log(`\n--------------- 测试DoH的SNI功能:共 ${servers.length} 个DoH服务 ---------------\n`)
+console.log(`\n--------------- 测试DoH的SNI功能:共 ${servers.length} 个服务,${hostnames.length} 个域名,SNI: ${sni || '无'} ---------------\n`)
let n = 0
let success = 0
diff --git a/packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs b/packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs
new file mode 100644
index 0000000..c6cfddb
--- /dev/null
+++ b/packages/mitmproxy/test/dnsTest-abroad-dot-sni.mjs
@@ -0,0 +1,107 @@
+import DNSOverTLS from "../src/lib/dns/tls.js";
+
+// 境外DNS的DoT配置sni测试
+const servers = [
+ // 'dot.360.cn',
+
+ '1.1.1.1', // 可直连,无需SNI(有时候可以,有时候不行)
+ 'one.one.one.one',
+ 'cloudflare-dns.com',
+ 'security.cloudflare-dns.com',
+ 'family.cloudflare-dns.com',
+ '1dot1dot1dot1.cloudflare-dns.com',
+
+ 'dot.sb',
+ '185.222.222.222',
+ '45.11.45.11',
+
+ 'dns.adguard.com',
+ 'dns.adguard-dns.com',
+ 'dns-family.adguard.com',
+ 'family.adguard-dns.com',
+ 'dns-unfiltered.adguard.com',
+ 'unfiltered.adguard-dns.com',
+ 'dns.bebasid.com',
+ 'unfiltered.dns.bebasid.com',
+ 'antivirus.bebasid.com',
+ 'internetsehat.bebasid.com',
+ 'family-adblock.bebasid.com',
+ 'oisd.dns.bebasid.com',
+ 'hagezi.dns.bebasid.com',
+ 'dns.cfiec.net',
+ 'dns.opendns.com',
+ 'familyshield.opendns.com',
+ 'sandbox.opendns.com',
+ 'family-filter-dns.cleanbrowsing.org',
+ 'adult-filter-dns.cleanbrowsing.org',
+ 'security-filter-dns.cleanbrowsing.org',
+ 'p0.freedns.controld.com',
+ 'p1.freedns.controld.com',
+ 'p2.freedns.controld.com',
+ 'p3.freedns.controld.com',
+ 'dns.decloudus.com',
+ 'getdnsapi.net',
+ 'dnsovertls.sinodun.com',
+ 'dnsovertls1.sinodun.com',
+ 'dns.de.futuredns.eu.org',
+ 'dns.us.futuredns.eu.org',
+ 'unicast.censurfridns.dk',
+]
+
+const hostnames = [
+ 'github.com',
+ 'mvnrepository.com',
+]
+const sni = 'baidu.com'
+// const sni = ''
+
+console.log(`\n--------------- 测试DoT的SNI功能:共 ${servers.length} 个服务,${hostnames.length} 个域名,SNI: ${sni || '无'} ---------------\n`)
+
+let n = 0
+let success = 0
+let error = 0
+const arr = []
+
+function count (isSuccess, hostname, idx, dns, result, cost) {
+ if (isSuccess) {
+ success++
+ const ipList = []
+ for (const answer of result.answers) {
+ ipList[ipList.length] = answer.data;
+ }
+ arr[idx] = `${dns.dnsServer} : ${hostname} -> [ ${ipList.join(', ')} ] , cost: ${cost} ms`;
+ } else {
+ error++
+ }
+
+ n++
+
+ if (n === servers.length * hostnames.length) {
+ console.info(`\n\n=============================================================================\n全部测完:总计:${servers.length * hostnames.length}, 成功:${success},失败:${error}`);
+ for (const item of arr) {
+ if (item) {
+ console.info(item);
+ }
+ }
+ console.info('=============================================================================\n\n')
+ }
+}
+
+let x = 0;
+for (let i = 0; i < servers.length; i++) {
+ for (const hostname of hostnames) {
+ const dns = new DNSOverTLS(`dns-${i}-${hostname}`, null, null, servers[i], null, sni)
+ const start = Date.now()
+ const idx = x;
+ dns._doDnsQuery(hostname)
+ .then((result) => {
+ console.info(`===> ${dns.dnsServer}: ${hostname} ->`, result.answers, '\n\n')
+ count(true, hostname, idx, dns, result, Date.now() - start)
+ })
+ .catch((e) => {
+ console.error(`===> ${dns.dnsServer}: ${hostname} 失败:`, e, '\n\n')
+ count(false, hostname)
+ })
+ x++;
+ }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e46e3cc..6f06d8c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -169,9 +169,6 @@ importers:
dns-over-http:
specifier: ^0.2.0
version: 0.2.0
- dns-over-tls:
- specifier: ^0.0.9
- version: 0.0.9
is-browser:
specifier: ^2.1.0
version: 2.1.0
@@ -3045,9 +3042,6 @@ packages:
dns-over-http@0.2.0:
resolution: {integrity: sha512-K+SyN2L3ljxJ2MFtOv/vRS+3/YEMLvOuH7MrmO5ejaubi4w02/DLqzoK1kBGKlQrT9ND57pbapeDf+ue8AElEA==}
- dns-over-tls@0.0.9:
- resolution: {integrity: sha512-IdI/Qgku2KQPLtUBsC6HDdK6bWIZADQMOYWtqJzRivV5Z+EDKIVAaC+tWE6lJ9vn11qj5L39ZaIaTj/14Lzgkw==}
-
dns-packet@4.2.0:
resolution: {integrity: sha512-bn1AKpfkFbm0MIioOMHZ5qJzl2uypdBwI4nYNsqvhjsegBhcKJUlCrMPWLx6JEezRjxZmxhtIz/FkBEur2l8Cw==}
engines: {node: '>=4'}
@@ -10553,10 +10547,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- dns-over-tls@0.0.9:
- dependencies:
- dns-packet: 5.6.1
-
dns-packet@4.2.0:
dependencies:
ip: 1.1.9
From 867909cbf5e782e8e9d698758c971de65b4fad7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E8=89=AF?= <841369634@qq.com>
Date: Tue, 20 May 2025 10:19:44 +0800
Subject: [PATCH 12/13] =?UTF-8?q?feature:=20proxy=E3=80=81redirect?=
=?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=99=A8=E9=85=8D=E7=BD=AE=EF=BC=8C=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=E5=9F=9F=E5=90=8D=E6=AD=A3=E5=88=99=E5=8C=B9=E9=85=8D?=
=?UTF-8?q?=E5=8D=A0=E4=BD=8D=E7=AC=A6=E6=9B=BF=E6=8D=A2=E3=80=82=EF=BC=88?=
=?UTF-8?q?=E4=B9=8B=E5=89=8D=E4=BB=85=E6=94=AF=E6=8C=81path=E5=8C=B9?=
=?UTF-8?q?=E9=85=8D=E6=9B=BF=E6=8D=A2=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/lib/interceptor/impl/req/baiduOcr.js | 2 +-
.../src/lib/interceptor/impl/req/proxy.js | 40 +++++++++++--------
.../src/lib/interceptor/impl/req/redirect.js | 4 +-
packages/mitmproxy/src/options.js | 4 +-
packages/mitmproxy/src/utils/util.match.js | 21 +++++++++-
5 files changed, 49 insertions(+), 22 deletions(-)
diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js b/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js
index 063b811..d7bf308 100644
--- a/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js
+++ b/packages/mitmproxy/src/lib/interceptor/impl/req/baiduOcr.js
@@ -111,7 +111,7 @@ function checkIsLimitConfig (id, api) {
module.exports = {
name: 'baiduOcr',
priority: 131,
- requestIntercept (context, interceptOpt, req, res, ssl, next, matched) {
+ requestIntercept (context, interceptOpt, req, res, ssl, next) {
const { rOptions, log } = context
const headers = {
diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js b/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js
index a6d59a4..1e265c7 100644
--- a/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js
+++ b/packages/mitmproxy/src/lib/interceptor/impl/req/proxy.js
@@ -1,21 +1,29 @@
const url = require('node:url')
const lodash = require('lodash')
+function replacePlaceholder0 (url, matched, pre) {
+ if (matched) {
+ for (let i = 0; i < matched.length; i++) {
+ url = url.replace(`\${${pre}[${i}]}`, matched[i] || '')
+ }
+ if (matched.groups) {
+ for (const key in matched.groups) {
+ url = url.replace(`\${${key}}`, matched.groups[key] || '')
+ }
+ }
+ }
+ return url
+}
+
// 替换占位符
-function replacePlaceholder (url, rOptions, matched) {
+function replacePlaceholder (url, rOptions, pathMatched, hostnameMatched) {
if (url.includes('${')) {
// eslint-disable-next-line no-template-curly-in-string
url = url.replace('${host}', rOptions.hostname)
- if (matched && url.includes('${')) {
- for (let i = 0; i < matched.length; i++) {
- url = url.replace(`\${m[${i}]}`, matched[i] == null ? '' : matched[i])
- }
- if (matched.groups) {
- for (const key in matched.groups) {
- url = url.replace(`\${${key}}`, matched.groups[key] == null ? '' : matched.groups[key])
- }
- }
+ if (url.includes('${')) {
+ url = replacePlaceholder0(url, pathMatched, 'p')
+ url = replacePlaceholder0(url, hostnameMatched, 'h')
}
// 移除多余的占位符
@@ -27,7 +35,7 @@ function replacePlaceholder (url, rOptions, matched) {
return url
}
-function buildTargetUrl (rOptions, urlConf, interceptOpt, matched) {
+function buildTargetUrl (rOptions, urlConf, interceptOpt, matched, hostnameMatched) {
let targetUrl
if (interceptOpt && interceptOpt.replace) {
const regexp = new RegExp(interceptOpt.replace)
@@ -45,7 +53,7 @@ function buildTargetUrl (rOptions, urlConf, interceptOpt, matched) {
}
// 替换占位符
- targetUrl = replacePlaceholder(targetUrl, rOptions, matched)
+ targetUrl = replacePlaceholder(targetUrl, rOptions, matched, hostnameMatched)
// 拼接协议
targetUrl = targetUrl.indexOf('http:') === 0 || targetUrl.indexOf('https:') === 0 ? targetUrl : `${rOptions.protocol}//${targetUrl}`
@@ -53,9 +61,9 @@ function buildTargetUrl (rOptions, urlConf, interceptOpt, matched) {
return targetUrl
}
-function doProxy (proxyConf, rOptions, req, interceptOpt, matched) {
+function doProxy (proxyConf, rOptions, req, interceptOpt, matched, hostnameMatched) {
// 获取代理目标地址
- const proxyTarget = buildTargetUrl(rOptions, proxyConf, interceptOpt, matched)
+ const proxyTarget = buildTargetUrl(rOptions, proxyConf, interceptOpt, matched, hostnameMatched)
// 替换rOptions的属性
// eslint-disable-next-line node/no-deprecated-api
@@ -81,7 +89,7 @@ module.exports = {
replacePlaceholder,
buildTargetUrl,
doProxy,
- requestIntercept (context, interceptOpt, req, res, ssl, next, matched) {
+ requestIntercept (context, interceptOpt, req, res, ssl, next, matched, hostnameMatched) {
const { rOptions, log, RequestCounter } = context
const originHostname = rOptions.hostname
@@ -112,7 +120,7 @@ module.exports = {
}
// 替换 rOptions 中的地址,并返回代理目标地址
- const proxyTarget = doProxy(proxyConf, rOptions, req, interceptOpt, matched)
+ const proxyTarget = doProxy(proxyConf, rOptions, req, interceptOpt, matched, hostnameMatched)
if (context.requestCount) {
log.info('proxy choice:', JSON.stringify(context.requestCount))
diff --git a/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js b/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js
index 5a800cc..85f0a68 100644
--- a/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js
+++ b/packages/mitmproxy/src/lib/interceptor/impl/req/redirect.js
@@ -3,11 +3,11 @@ const proxyApi = require('./proxy')
module.exports = {
name: 'redirect',
priority: 105,
- requestIntercept (context, interceptOpt, req, res, ssl, next, matched) {
+ requestIntercept (context, interceptOpt, req, res, ssl, next, matched, hostnameMatched) {
const { rOptions, log } = context
// 获取重定向目标地址
- const redirect = proxyApi.buildTargetUrl(rOptions, interceptOpt.redirect, interceptOpt, matched)
+ const redirect = proxyApi.buildTargetUrl(rOptions, interceptOpt.redirect, interceptOpt, matched, hostnameMatched)
const headers = {
'Location': redirect,
diff --git a/packages/mitmproxy/src/options.js b/packages/mitmproxy/src/options.js
index 6504dee..293a83a 100644
--- a/packages/mitmproxy/src/options.js
+++ b/packages/mitmproxy/src/options.js
@@ -187,12 +187,12 @@ module.exports = (serverConfig) => {
if (impl.requestIntercept) {
// req拦截器
interceptor.requestIntercept = (context, req, res, ssl, next) => {
- return impl.requestIntercept(context, interceptOpt, req, res, ssl, next, matched)
+ return impl.requestIntercept(context, interceptOpt, req, res, ssl, next, matched, interceptOpts.matched)
}
} else if (impl.responseIntercept) {
// res拦截器
interceptor.responseIntercept = (context, req, res, proxyReq, proxyRes, ssl, next) => {
- return impl.responseIntercept(context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next, matched)
+ return impl.responseIntercept(context, interceptOpt, req, res, proxyReq, proxyRes, ssl, next, matched, interceptOpts.matched)
}
}
diff --git a/packages/mitmproxy/src/utils/util.match.js b/packages/mitmproxy/src/utils/util.match.js
index 46bf246..387f9a2 100644
--- a/packages/mitmproxy/src/utils/util.match.js
+++ b/packages/mitmproxy/src/utils/util.match.js
@@ -150,10 +150,29 @@ function matchHostnameAll (hostMap, hostname, action) {
// }
// 正则表达式匹配
- if (hostname.match(regexp)) {
+ const matched = hostname.match(regexp)
+ if (matched) {
value = hostMap[regexp]
log.debug(`matchHostname-one: ${action}: '${hostname}' -> { "${regexp}": ${JSON.stringify(value)} }`)
values = merge(values, value)
+
+ // 设置matched
+ if (matched.length > 1) {
+ if (values.matched) {
+ // 合并array
+ matched.shift()
+ values.matched = [...values.matched, ...matched] // 拼接上多个matched
+
+ // 合并groups
+ if (matched.groups) {
+ values.matched.groups = merge(values.matched.groups, matched.groups)
+ } else {
+ values.matched.groups = matched.groups
+ }
+ } else {
+ values.matched = matched
+ }
+ }
}
}
From 495f65c92b73cf99c39e8fbca4a76a519a66c3ae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=8E=8B=E8=89=AF?= <841369634@qq.com>
Date: Tue, 26 Aug 2025 14:31:27 +0800
Subject: [PATCH 13/13] =?UTF-8?q?optimize:=20=E6=97=A5=E5=BF=97=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E3=80=81DNS=E7=9B=B8=E5=85=B3=E5=8D=95=E6=B5=8B?=
=?UTF-8?q?=E8=B0=83=E6=95=B4=E3=80=81=E9=83=A8=E5=88=86=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E5=B0=8F=E8=B0=83=E6=95=B4=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/core/src/utils/util.version.js | 2 +-
packages/gui/src/bridge/error/front.js | 3 -
packages/gui/src/view/pages/server.vue | 2 +-
packages/mitmproxy/src/lib/dns/base.js | 22 ++-
packages/mitmproxy/src/lib/dns/index.js | 4 +-
packages/mitmproxy/src/lib/dns/tcp.js | 2 +-
packages/mitmproxy/src/lib/dns/udp.js | 2 +-
.../interceptor/impl/res/responseReplace.js | 6 +-
.../mitmproxy/src/lib/speed/SpeedTester.js | 8 +-
packages/mitmproxy/src/options.js | 3 +-
packages/mitmproxy/src/utils/util.match.js | 13 +-
.../mitmproxy/test/dnsTest-abroad-doh-sni.mjs | 57 ++++---
packages/mitmproxy/test/dnsTest-abroad.mjs | 149 ++++++++++++++++++
packages/mitmproxy/test/dnsTest.mjs | 94 ++++-------
14 files changed, 249 insertions(+), 118 deletions(-)
create mode 100644 packages/mitmproxy/test/dnsTest-abroad.mjs
diff --git a/packages/core/src/utils/util.version.js b/packages/core/src/utils/util.version.js
index 12a0b8f..265ef34 100644
--- a/packages/core/src/utils/util.version.js
+++ b/packages/core/src/utils/util.version.js
@@ -14,7 +14,7 @@ function parseVersion (version) {
* @param log 日志对象
* @returns {number} 比较线上版本号是否为更新的版本,大于0=是|0=相等|小于0=否|-999=出现异常,比较结果未知
*/
-export function isNewVersion (onlineVersion, currentVersion, log = console) {
+export function isNewVersion (onlineVersion, currentVersion, log = null) {
if (onlineVersion === currentVersion) {
return 0
}
diff --git a/packages/gui/src/bridge/error/front.js b/packages/gui/src/bridge/error/front.js
index 2b11216..ef5d6e3 100644
--- a/packages/gui/src/bridge/error/front.js
+++ b/packages/gui/src/bridge/error/front.js
@@ -18,9 +18,6 @@ function handleServerStartError (message, err, app, api) {
// 避免重复弹窗
const now = Date.now()
if (latestConfirmTime != null && now - latestConfirmTime < 1000) {
- if (now - latestConfirmTime > 5000) {
- latestConfirmTime = null
- }
return
}
latestConfirmTime = now
diff --git a/packages/gui/src/view/pages/server.vue b/packages/gui/src/view/pages/server.vue
index 36d712c..96b2a5f 100644
--- a/packages/gui/src/view/pages/server.vue
+++ b/packages/gui/src/view/pages/server.vue
@@ -313,7 +313,7 @@ export default {
- 这里配置的域名不会通过代理
+ 配置为不代理
的域名不会通过代理
diff --git a/packages/mitmproxy/src/lib/dns/base.js b/packages/mitmproxy/src/lib/dns/base.js
index 7632fa1..298b73c 100644
--- a/packages/mitmproxy/src/lib/dns/base.js
+++ b/packages/mitmproxy/src/lib/dns/base.js
@@ -126,10 +126,18 @@ module.exports = class BaseDNS {
async _lookup (hostname) {
const start = Date.now()
+
+ let response
try {
// 执行DNS查询
log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query start: ${hostname}`)
- const response = await this._doDnsQuery(hostname)
+ response = await this._doDnsQuery(hostname, 'A', start)
+ } catch {
+ // 异常日志在 _doDnsQuery已经打印过,这里就不再打印了
+ return []
+ }
+
+ try {
const cost = Date.now() - start
log.debug(`[DNS-over-${this.dnsType} '${this.dnsName}'] query end: ${hostname}, cost: ${cost} ms, response:`, response)
@@ -147,21 +155,23 @@ module.exports = class BaseDNS {
return ret
} catch (e) {
- log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS query error, hostname: ${hostname}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}, cost: ${Date.now() - start} ms, error:`, e)
+ log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] 解读响应失败,response:`, response, ', error:', e)
return []
}
}
- _doDnsQuery (hostname, type) {
+ _doDnsQuery (hostname, type = 'A', start) {
if (start == null) {
start = Date.now()
}
+
return new Promise((resolve, reject) => {
// 设置超时任务
let isOver = false
const timeout = 8000
const timeoutId = setTimeout(() => {
if (!isOver) {
+ log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询超时, hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms`)
reject(new Error('DNS查询超时'))
}
}, timeout)
@@ -176,11 +186,17 @@ module.exports = class BaseDNS {
.catch((e) => {
isOver = true
clearTimeout(timeoutId)
+ if (e.message === 'DNS查询超时') {
+ log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询超时. hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms`)
+ } else {
+ log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询错误, hostname: ${hostname}, sni: ${this.dnsServerName || '无'}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms, error:`, e)
+ }
reject(e)
})
} catch (e) {
isOver = true
clearTimeout(timeoutId)
+ log.error(`[DNS-over-${this.dnsType} '${this.dnsName}'] DNS查询异常, hostname: ${hostname}, type: ${type}${this.dnsServer ? `, dnsServer: ${this.dnsServer}` : ''}${this.dnsServerPort ? `:${this.dnsServerPort}` : ''}, cost: ${Date.now() - start} ms, error:`, e)
reject(e)
}
})
diff --git a/packages/mitmproxy/src/lib/dns/index.js b/packages/mitmproxy/src/lib/dns/index.js
index ba0436a..7a36d6f 100644
--- a/packages/mitmproxy/src/lib/dns/index.js
+++ b/packages/mitmproxy/src/lib/dns/index.js
@@ -28,7 +28,7 @@ module.exports = {
if (type == null) {
if (server.startsWith('https://') || server.startsWith('http://')) {
type = 'https'
- } else if (server.startsWith('tls://')) {
+ } else if (server.startsWith('tls://') || server.startsWith('dot://')) {
type = 'tls'
} else if (server.startsWith('tcp://')) {
type = 'tcp'
@@ -65,7 +65,7 @@ module.exports = {
if (type === 'tls' || type === 'dot' || type === 'dns-over-tls') {
// 基于 tls
dnsMap[provider] = new DNSOverTLS(provider, conf.cacheSize, preSetIpList, server, port, conf.sni || conf.servername)
- } else if (type === 'tcp' || type === 'dns-over-tcp') {
+ } else if (type === 'tcp') {
// 基于 tcp
dnsMap[provider] = new DNSOverTCP(provider, conf.cacheSize, preSetIpList, server, port)
} else {
diff --git a/packages/mitmproxy/src/lib/dns/tcp.js b/packages/mitmproxy/src/lib/dns/tcp.js
index c431964..f3e5f5c 100644
--- a/packages/mitmproxy/src/lib/dns/tcp.js
+++ b/packages/mitmproxy/src/lib/dns/tcp.js
@@ -4,7 +4,7 @@ const dnsPacket = require('dns-packet')
const randi = require('random-int')
const BaseDNS = require('./base')
-const defaultPort = 53 // UDP类型的DNS服务默认端口号
+const defaultPort = 53 // TCP类型的DNS服务默认端口号
module.exports = class DNSOverTCP extends BaseDNS {
constructor (dnsName, cacheSize, preSetIpList, dnsServer, dnsServerPort) {
diff --git a/packages/mitmproxy/src/lib/dns/udp.js b/packages/mitmproxy/src/lib/dns/udp.js
index 81b2201..cd97a7d 100644
--- a/packages/mitmproxy/src/lib/dns/udp.js
+++ b/packages/mitmproxy/src/lib/dns/udp.js
@@ -55,7 +55,7 @@ module.exports = class DNSOverUDP extends BaseDNS {
// 设置超时任务
timeoutId = setTimeout(() => {
if (!isOver) {
- reject(new Error('查询超时'))
+ reject(new Error('DNS查询超时'))
udpClient.close()
}
}, timeout)
diff --git a/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js b/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js
index 559c8be..05b9f4c 100644
--- a/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js
+++ b/packages/mitmproxy/src/lib/interceptor/impl/res/responseReplace.js
@@ -92,9 +92,9 @@ module.exports = {
// 如果未手动配置需要缓存,则不允许使用缓存
const maxAge = cacheReq.getMaxAge(interceptOpt)
if (maxAge == null || maxAge <= 0) {
- replaceHeaders['cache-control'] = '[remove]'
- replaceHeaders['last-modified'] = '[remove]'
- replaceHeaders.expires = '[remove]'
+ replaceHeaders['cache-control'] = REMOVE
+ replaceHeaders['last-modified'] = REMOVE
+ replaceHeaders.expires = REMOVE
}
actions += `${actions ? ',' : ''}download:${filename}`
diff --git a/packages/mitmproxy/src/lib/speed/SpeedTester.js b/packages/mitmproxy/src/lib/speed/SpeedTester.js
index c6bbb6d..cbbeaf0 100644
--- a/packages/mitmproxy/src/lib/speed/SpeedTester.js
+++ b/packages/mitmproxy/src/lib/speed/SpeedTester.js
@@ -133,7 +133,7 @@ class SpeedTester {
async doTest (item, aliveList) {
try {
const ret = await this.testOne(item)
- item.title = `${ret.by}测速成功:${item.host}`
+ item.title = `${ret.by}测速成功:${ret.target}`
log.info(`[speed] test success: ${this.hostname} ➜ ${item.host}:${this.port} from DNS '${item.dns}'`)
_.merge(item, ret)
aliveList.push({ ...ret, ...item })
@@ -175,7 +175,7 @@ class SpeedTester {
clearTimeout(timeoutId)
const connectionTime = Date.now()
- resolve({ status: 'success', by: 'TCP', time: connectionTime - startTime })
+ resolve({ status: 'success', by: 'TCP', target: `${host}:${this.port}`, time: connectionTime - startTime })
client.end()
})
client.on('error', (e) => {
@@ -249,7 +249,7 @@ class SpeedTester {
// } else {
// // 计算平均延迟
// const avg = times.reduce((a, b) => a + b, 0) / times.length
- // resolve({ status: 'success', by: 'PING', time: Math.round(avg) })
+ // resolve({ status: 'success', by: 'PING', target: host, time: Math.round(avg) })
// }
// })
// })
@@ -272,7 +272,7 @@ class SpeedTester {
// reject(new Error(`TCP测速失败:${e.message};PING测速失败:${e2.message};`))
// })
- reject(new Error(`TCP测速失败:${e.message}`))
+ reject(new Error(`TCP测速失败:${item.host}:${this.port} ${e.message}`))
})
})
}
diff --git a/packages/mitmproxy/src/options.js b/packages/mitmproxy/src/options.js
index 293a83a..b9c5f12 100644
--- a/packages/mitmproxy/src/options.js
+++ b/packages/mitmproxy/src/options.js
@@ -1,7 +1,6 @@
const fs = require('node:fs')
const path = require('node:path')
const lodash = require('lodash')
-const jsonApi = require('./json')
const dnsUtil = require('./lib/dns')
const interceptorImpls = require('./lib/interceptor')
const scriptInterceptor = require('./lib/interceptor/impl/res/script')
@@ -110,7 +109,7 @@ module.exports = (serverConfig) => {
// 配置了白名单的域名,将跳过代理
const inWhiteList = !!matchUtil.matchHostname(whiteList, hostname, 'in whiteList')
if (inWhiteList) {
- log.info(`为白名单域名,不拦截: ${hostname}, headers:`, jsonApi.stringify2(req.headers))
+ log.info(`为白名单域名,不拦截: ${hostname}`)
return false // 不拦截
}
diff --git a/packages/mitmproxy/src/utils/util.match.js b/packages/mitmproxy/src/utils/util.match.js
index 387f9a2..e990699 100644
--- a/packages/mitmproxy/src/utils/util.match.js
+++ b/packages/mitmproxy/src/utils/util.match.js
@@ -1,5 +1,6 @@
const lodash = require('lodash')
const log = require('./util.log.server')
+const mergeApi = require('@docmirror/dev-sidecar/src/merge')
function isMatched (url, regexp) {
if (regexp === '.*' || regexp === '*' || regexp === 'true' || regexp === true) {
@@ -113,16 +114,6 @@ function merge (oldObj, newObj) {
}
})
}
-function deleteNullItems (target) {
- lodash.forEach(target, (item, key) => {
- if (item == null || item === '[delete]') {
- delete target[key]
- }
- if (lodash.isObject(item)) {
- deleteNullItems(item)
- }
- })
-}
function matchHostnameAll (hostMap, hostname, action) {
// log.debug('matchHostname-all:', action, hostMap)
@@ -197,7 +188,7 @@ function matchHostnameAll (hostMap, hostname, action) {
}
if (!lodash.isEmpty(values)) {
- deleteNullItems(values)
+ mergeApi.deleteNullItems(values)
log.info(`matchHostname-all: ${action}: '${hostname}':`, JSON.stringify(values))
return values
} else {
diff --git a/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs b/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs
index 39fd2be..163bb1c 100644
--- a/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs
+++ b/packages/mitmproxy/test/dnsTest-abroad-doh-sni.mjs
@@ -6,8 +6,6 @@ const servers = [
'https://max.rethinkdns.com/dns-query',
'https://sky.rethinkdns.com/dns-query',
'https://doh.opendns.com/dns-query',
- 'https://1.1.1.1/dns-query',
- 'https://dns.cloudflare.com/dns-query',
'https://cloudflare-dns.com/dns-query',
'https://dns.google/dns-query',
'https://dns.bebasid.com/unfiltered',
@@ -30,12 +28,15 @@ const servers = [
'https://jp.tiarap.org/dns-query',
'https://dns.adguard.com/dns-query',
'https://rubyfish.cn/dns-query',
- 'https://i.233py.com/dns-query'
-
+ 'https://i.233py.com/dns-query',
]
-const hostname1 = 'github.com'
+const hostnames = [
+ 'github.com',
+ 'mvnrepository.com',
+]
const sni = 'baidu.com'
+// const sni = ''
console.log(`\n--------------- 测试DoH的SNI功能:共 ${servers.length} 个服务,${hostnames.length} 个域名,SNI: ${sni || '无'} ---------------\n`)
@@ -44,15 +45,22 @@ let success = 0
let error = 0
const arr = []
-function count (isSuccess, i, doh, result) {
- n++
+function count (isSuccess, hostname, idx, dns, result, cost) {
if (isSuccess) {
success++
- arr[i] = `${doh.dnsServer} : ${hostname1} -> ${result.answers[0].data}`;
- } else error++
+ const ipList = []
+ for (const answer of result.answers) {
+ ipList[ipList.length] = answer.data;
+ }
+ arr[idx] = `${dns.dnsServer} : ${hostname} -> [ ${ipList.join(', ')} ] , cost: ${cost} ms`;
+ } else {
+ error++
+ }
- if (n === servers.length) {
- console.info(`\n\n=============================================================================\n全部测完:总计:${servers.length}, 成功:${success},失败:${error}`);
+ n++
+
+ if (n === servers.length * hostnames.length) {
+ console.info(`\n\n=============================================================================\n全部测完:总计:${servers.length * hostnames.length}, 成功:${success},失败:${error}`);
for (const item of arr) {
if (item) {
console.info(item);
@@ -62,16 +70,21 @@ function count (isSuccess, i, doh, result) {
}
}
+let x = 0;
for (let i = 0; i < servers.length; i++) {
- const n = i;
- const doh = new DNSOverHTTPS(`dns${i}`, null, null, servers[i], sni)
- doh._doDnsQuery(hostname1)
- .then((result) => {
- // console.info(`===> test testDoH '${doh.dnsServer}': ${hostname1} ->`, result.answers, '\n\n')
- count(true, n, doh, result)
- })
- .catch((e) => {
- // console.error(`===> test testDoH '${doh.dnsServer}': ${hostname1} 失败:`, e, '\n\n')
- count(false)
- })
+ for (const hostname of hostnames) {
+ const dns = new DNSOverHTTPS(`dns-${i}-${hostname}`, null, null, servers[i], sni)
+ const start = Date.now()
+ const idx = x;
+ dns._doDnsQuery(hostname)
+ .then((result) => {
+ console.info(`===> ${dns.dnsServer}: ${hostname} ->`, result.answers, '\n\n')
+ count(true, hostname, idx, dns, result, Date.now() - start)
+ })
+ .catch((e) => {
+ console.error(`===> ${dns.dnsServer}: ${hostname} 失败:`, e, '\n\n')
+ count(false, hostname)
+ })
+ x++;
+ }
}
diff --git a/packages/mitmproxy/test/dnsTest-abroad.mjs b/packages/mitmproxy/test/dnsTest-abroad.mjs
new file mode 100644
index 0000000..7e8a70e
--- /dev/null
+++ b/packages/mitmproxy/test/dnsTest-abroad.mjs
@@ -0,0 +1,149 @@
+import assert from 'node:assert'
+import dns from '../src/lib/dns/index.js'
+import matchUtil from '../src/utils/util.match.js'
+
+const presetIp = '100.100.100.100'
+const preSetIpList = matchUtil.domainMapRegexply({
+ 'xxx.com': [
+ presetIp
+ ]
+})
+
+// 境外DNS测试
+const dnsProviders = dns.initDNS({
+ // udp
+ cloudflareUdp: {
+ server: 'udp://1.1.1.1',
+ },
+ quad9Udp: {
+ server: 'udp://9.9.9.9',
+ },
+
+ // tcp
+ cloudflareTcp: {
+ server: 'tcp://1.1.1.1',
+ },
+ quad9Tcp: {
+ server: 'tcp://9.9.9.9',
+ },
+
+ // https
+ cloudflare: {
+ server: 'https://1.1.1.1/dns-query',
+ },
+ quad9: {
+ server: 'https://9.9.9.9/dns-query',
+ forSNI: true,
+ },
+ rubyfish: {
+ server: 'https://rubyfish.cn/dns-query',
+ },
+ py233: {
+ server: ' https://i.233py.com/dns-query',
+ },
+
+ // tls
+ cloudflareTLS: {
+ type: 'tls',
+ server: '1.1.1.1',
+ servername: 'cloudflare-dns.com',
+ },
+ quad9TLS: {
+ server: 'tls://9.9.9.9',
+ servername: 'dns.quad9.net',
+ },
+}, preSetIpList)
+
+
+const hasPresetHostname = 'xxx.com'
+const noPresetHostname = 'yyy.com'
+
+const hostname1 = 'github.com'
+const hostname2 = 'api.github.com'
+const hostname3 = 'hk.docmirror.cn'
+const hostname4 = 'github.docmirror.cn'
+const hostname5 = 'gh.docmirror.top'
+const hostname6 = 'gh2.docmirror.top'
+
+let ip
+
+
+console.log('\n--------------- test ForSNI ---------------\n')
+console.log(`===> test ForSNI: ${dnsProviders.ForSNI.dnsName}`, '\n\n')
+assert.strictEqual(dnsProviders.ForSNI, dnsProviders.quad9)
+
+
+console.log('\n--------------- test PreSet ---------------\n')
+ip = await dnsProviders.PreSet.lookup(hasPresetHostname)
+console.log(`===> test PreSet: ${hasPresetHostname} ->`, ip, '\n\n')
+console.log('\n\n')
+assert.strictEqual(ip, presetIp) // 预设过IP,等于预设的IP
+
+ip = await dnsProviders.PreSet.lookup(noPresetHostname)
+console.log(`===> test PreSet: ${noPresetHostname} ->`, ip, '\n\n')
+console.log('\n\n')
+assert.strictEqual(ip, noPresetHostname) // 未预设IP,等于域名自己
+
+
+console.log('\n--------------- test udp ---------------\n')
+ip = await dnsProviders.cloudflareUdp.lookup(hasPresetHostname)
+assert.strictEqual(ip, presetIp) // test preset
+console.log('\n\n')
+
+assert.strictEqual(dnsProviders.cloudflareUdp.dnsType, 'UDP')
+ip = await dnsProviders.cloudflareUdp.lookup(hostname1)
+console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n')
+
+assert.strictEqual(dnsProviders.quad9Udp.dnsType, 'UDP')
+ip = await dnsProviders.quad9Udp.lookup(hostname1)
+console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n')
+
+
+console.log('\n--------------- test tcp ---------------\n')
+ip = await dnsProviders.cloudflareTcp.lookup(hasPresetHostname)
+assert.strictEqual(ip, presetIp) // test preset
+console.log('\n\n')
+
+assert.strictEqual(dnsProviders.cloudflareTcp.dnsType, 'TCP')
+ip = await dnsProviders.cloudflareTcp.lookup(hostname1)
+console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n')
+
+assert.strictEqual(dnsProviders.quad9Tcp.dnsType, 'TCP')
+ip = await dnsProviders.quad9Tcp.lookup(hostname1)
+console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n')
+
+
+console.log('\n--------------- test https ---------------\n')
+ip = await dnsProviders.cloudflare.lookup(hasPresetHostname)
+assert.strictEqual(ip, presetIp) // test preset
+console.log('\n\n')
+
+assert.strictEqual(dnsProviders.cloudflare.dnsType, 'HTTPS')
+ip = await dnsProviders.cloudflare.lookup(hostname1)
+console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n')
+
+assert.strictEqual(dnsProviders.quad9.dnsType, 'HTTPS')
+ip = await dnsProviders.quad9.lookup(hostname1)
+console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n')
+
+assert.strictEqual(dnsProviders.rubyfish.dnsType, 'HTTPS')
+ip = await dnsProviders.rubyfish.lookup(hostname1)
+console.log(`===> test rubyfish: ${hostname1} ->`, ip, '\n\n')
+
+assert.strictEqual(dnsProviders.py233.dnsType, 'HTTPS')
+ip = await dnsProviders.py233.lookup(hostname1)
+console.log(`===> test py233: ${hostname1} ->`, ip, '\n\n')
+
+
+console.log('\n--------------- test TLS ---------------\n')
+ip = await dnsProviders.cloudflareTLS.lookup(hasPresetHostname)
+assert.strictEqual(ip, presetIp) // test preset
+console.log('\n\n')
+
+assert.strictEqual(dnsProviders.cloudflareTLS.dnsType, 'TLS')
+ip = await dnsProviders.cloudflareTLS.lookup(hostname1)
+console.log(`===> test cloudflareTLS: ${hostname1} ->`, ip, '\n\n')
+
+assert.strictEqual(dnsProviders.quad9TLS.dnsType, 'TLS')
+ip = await dnsProviders.quad9TLS.lookup(hostname1)
+console.log(`===> test quad9TLS: ${hostname1} ->`, ip, '\n\n')
diff --git a/packages/mitmproxy/test/dnsTest.mjs b/packages/mitmproxy/test/dnsTest.mjs
index 75a36b0..65df397 100644
--- a/packages/mitmproxy/test/dnsTest.mjs
+++ b/packages/mitmproxy/test/dnsTest.mjs
@@ -9,17 +9,9 @@ const preSetIpList = matchUtil.domainMapRegexply({
]
})
+// 常用DNS测试
const dnsProviders = dns.initDNS({
// https
- cloudflare: {
- type: 'https',
- server: 'https://1.1.1.1/dns-query',
- cacheSize: 1000,
- },
- quad9: {
- server: 'https://9.9.9.9/dns-query',
- cacheSize: 1000,
- },
aliyun: {
type: 'https',
server: 'https://dns.alidns.com/dns-query',
@@ -33,28 +25,10 @@ const dnsProviders = dns.initDNS({
safe360: {
server: 'https://doh.360.cn/dns-query',
cacheSize: 1000,
- },
- rubyfish: {
- server: 'https://rubyfish.cn/dns-query',
- cacheSize: 1000,
- },
- py233: {
- server: ' https://i.233py.com/dns-query',
- cacheSize: 1000,
+ forSNI: true,
},
// tls
- cloudflareTLS: {
- type: 'tls',
- server: '1.1.1.1',
- servername: 'cloudflare-dns.com',
- cacheSize: 1000,
- },
- quad9TLS: {
- server: 'tls://9.9.9.9',
- servername: 'dns.quad9.net',
- cacheSize: 1000,
- },
aliyunTLS: {
server: 'tls://223.5.5.5:853',
cacheSize: 1000,
@@ -93,7 +67,9 @@ const dnsProviders = dns.initDNS({
}, preSetIpList)
-const presetHostname = 'xxx.com'
+const hasPresetHostname = 'xxx.com'
+const noPresetHostname = 'yyy.com'
+
const hostname1 = 'github.com'
const hostname2 = 'api.github.com'
const hostname3 = 'hk.docmirror.cn'
@@ -104,26 +80,36 @@ const hostname6 = 'gh2.docmirror.top'
let ip
+console.log('\n--------------- test ForSNI ---------------\n')
+console.log(`===> test ForSNI: ${dnsProviders.ForSNI.dnsName}`, '\n\n')
+assert.strictEqual(dnsProviders.ForSNI, dnsProviders.safe360)
+
+const dnsProviders2 = dns.initDNS({
+ aliyun: {
+ server: 'udp://223.5.5.5',
+ },
+}, {})
+console.log(`===> test ForSNI2: ${dnsProviders2.ForSNI.dnsName}`, '\n\n')
+assert.strictEqual(dnsProviders2.ForSNI, dnsProviders2.PreSet) // 未配置forSNI的DNS时,默认使用PreSet作为ForSNI
+
+
console.log('\n--------------- test PreSet ---------------\n')
-ip = await dnsProviders.PreSet.lookup(presetHostname)
-console.log('===> test PreSet:', ip, '\n\n')
+ip = await dnsProviders.PreSet.lookup(hasPresetHostname)
+console.log(`===> test PreSet: ${hasPresetHostname} ->`, ip, '\n\n')
console.log('\n\n')
-assert.strictEqual(ip, presetIp) // test preset
+assert.strictEqual(ip, presetIp) // 预设过IP,等于预设的IP
+
+ip = await dnsProviders.PreSet.lookup(noPresetHostname)
+console.log(`===> test PreSet: ${noPresetHostname} ->`, ip, '\n\n')
+console.log('\n\n')
+assert.strictEqual(ip, noPresetHostname) // 未预设IP,等于域名自己
console.log('\n--------------- test https ---------------\n')
-ip = await dnsProviders.cloudflare.lookup(presetHostname)
+ip = await dnsProviders.aliyun.lookup(hasPresetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
-assert.strictEqual(dnsProviders.cloudflare.dnsType, 'HTTPS')
-// ip = await dnsProviders.cloudflare.lookup(hostname1)
-// console.log(`===> test cloudflare: ${hostname1} ->`, ip, '\n\n')
-
-assert.strictEqual(dnsProviders.quad9.dnsType, 'HTTPS')
-// ip = await dnsProviders.quad9.lookup(hostname1)
-// console.log(`===> test quad9: ${hostname1} ->`, ip, '\n\n')
-
assert.strictEqual(dnsProviders.aliyun.dnsType, 'HTTPS')
ip = await dnsProviders.aliyun.lookup(hostname1)
console.log(`===> test aliyun: ${hostname1} ->`, ip, '\n\n')
@@ -136,28 +122,12 @@ assert.strictEqual(dnsProviders.safe360.dnsType, 'HTTPS')
ip = await dnsProviders.safe360.lookup(hostname1)
console.log(`===> test safe360: ${hostname1} ->`, ip, '\n\n')
-assert.strictEqual(dnsProviders.rubyfish.dnsType, 'HTTPS')
-// ip = await dnsProviders.rubyfish.lookup(hostname1)
-// console.log(`===> test rubyfish: ${hostname1} ->`, ip, '\n\n')
-
-assert.strictEqual(dnsProviders.py233.dnsType, 'HTTPS')
-// ip = await dnsProviders.py233.lookup(hostname1)
-// console.log(`===> test py233: ${hostname1} ->`, ip, '\n\n')
-
console.log('\n--------------- test TLS ---------------\n')
-ip = await dnsProviders.cloudflareTLS.lookup(presetHostname)
+ip = await dnsProviders.aliyunTLS.lookup(hasPresetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
-assert.strictEqual(dnsProviders.cloudflareTLS.dnsType, 'TLS')
-// ip = await dnsProviders.cloudflareTLS.lookup(hostname1)
-// console.log(`===> test cloudflareTLS: ${hostname1} ->`, ip, '\n\n')
-
-assert.strictEqual(dnsProviders.quad9TLS.dnsType, 'TLS')
-// ip = await dnsProviders.quad9TLS.lookup(hostname1)
-// console.log(`===> test quad9TLS: ${hostname1} ->`, ip, '\n\n')
-
assert.strictEqual(dnsProviders.aliyunTLS.dnsType, 'TLS')
ip = await dnsProviders.aliyunTLS.lookup(hostname1)
console.log(`===> test aliyunTLS: ${hostname1} ->`, ip, '\n\n')
@@ -172,7 +142,7 @@ console.log(`===> test safe360TLS: ${hostname1} ->`, ip, '\n\n')
console.log('\n--------------- test TCP ---------------\n')
-ip = await dnsProviders.googleTCP.lookup(presetHostname)
+ip = await dnsProviders.googleTCP.lookup(hasPresetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
@@ -186,7 +156,7 @@ console.log(`===> test aliyunTCP: ${hostname1} ->`, ip, '\n\n')
console.log('\n--------------- test UDP ---------------\n')
-ip = await dnsProviders.googleUDP.lookup(presetHostname)
+ip = await dnsProviders.googleUDP.lookup(hasPresetHostname)
assert.strictEqual(ip, presetIp) // test preset
console.log('\n\n')
@@ -200,17 +170,13 @@ console.log(`===> test aliyunUDP: ${hostname1} ->`, ip, '\n\n')
dnsProviders.aliyunUDP.lookup(hostname1).then(ip0 => {
console.log(`===> test aliyunUDP: ${hostname1} ->`, ip0, '\n\n')
- assert.strictEqual(ip0, ip)
})
dnsProviders.aliyunUDP.lookup(hostname2).then(ip0 => {
console.log(`===> test aliyunUDP: ${hostname2} ->`, ip0, '\n\n')
- assert.notStrictEqual(ip0, ip)
})
dnsProviders.aliyunUDP.lookup('baidu.com').then(ip0 => {
console.log('===> test aliyunUDP: baidu.com ->', ip0, '\n\n')
- assert.notStrictEqual(ip0, ip)
})
dnsProviders.aliyunUDP.lookup('gitee.com').then(ip0 => {
console.log('===> test aliyunUDP: gitee.com ->', ip0, '\n\n')
- assert.notStrictEqual(ip0, ip)
})