From f221e258b644e151136ae31af74e439b98057c4f Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 19:13:39 +0200 Subject: [PATCH 01/13] docker implementation --- .dockerignore | 66 +++++++++++++++++++++++++ .env.example | 25 ++++++++++ Dockerfile | 44 +++++++++++++++++ __pycache__/api_solver.cpython-313.pyc | Bin 0 -> 58935 bytes docker-compose.yml | 22 +++++++++ entrypoint.sh | 46 +++++++++++++++++ 6 files changed, 203 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 __pycache__/api_solver.cpython-313.pyc create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..909cc00 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,66 @@ +# Git +.git +.gitignore + +# Python +__pycache__ +*.pyc +*.pyo +*.pyd +.Python +env +venv +.venv +pip-log.txt +pip-delete-this-directory.txt +.tox +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.log +.git +.mypy_cache +.pytest_cache +.hypothesis + +# Virtual environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Docker +Dockerfile +docker-compose.yml +.dockerignore + +# Logs +*.log + +# Database (if not using volume) +# db_results.db + +# Other +README.md \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..deb9db3 --- /dev/null +++ b/.env.example @@ -0,0 +1,25 @@ +# Turnstile Solver Docker Environment Variables +# Copy this file to .env and modify as needed + +# Server configuration +HOST=0.0.0.0 +PORT=5072 + +# Browser configuration +BROWSER_TYPE=chromium # Options: chromium, chrome, msedge, camoufox +THREAD=4 + +# Debug and logging +DEBUG=false + +# Proxy support +PROXY=false + +# IPv6 support +IPV6=false + +# User agent (leave empty for random) +USERAGENT= + +# Headless mode (set to true to run with GUI) +NO_HEADLESS=false \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..910b95f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,44 @@ +# Use Python 3.11 slim image +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies for browsers +RUN apt-get update && apt-get install -y \ + wget \ + gnupg \ + ca-certificates \ + procps \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Install Google Chrome (for Chromium-based browsers) +RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ + && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ + && apt-get update \ + && apt-get install -y google-chrome-stable \ + && rm -rf /var/lib/apt/lists/* + +# Install Firefox (for Gecko-based browsers like Camoufox) +RUN apt-get update && apt-get install -y firefox-esr \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Install Playwright browsers +RUN playwright install chromium firefox + +# Copy application files +COPY . . + +# Make entrypoint script executable +RUN chmod +x entrypoint.sh + +# Expose port +EXPOSE 5072 + +# Use entrypoint script +ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file diff --git a/__pycache__/api_solver.cpython-313.pyc b/__pycache__/api_solver.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..05eb9af91064a7d2995e95c94c8674fab799e1d9 GIT binary patch literal 58935 zcmeFa33yxAbtnEFc7g!+eSO>sQY0l<gL;7f}ldN(7OKY&{3C63e1 z$TMjz$4yNqotSR>jperAgh`s3O}dD7(kM%39t_4{P}ZZSZfD$?>Clp=@sei#zjGfR z079TBr=B+d`R0+h@80{)-S0j3+;h)8_paG&X`nhK{Omq5EBGByiqj5Uwtg` zs7W*(HH&8EuQ`@%S%OV zw-SoQJRI>?Y)GB&9Z_77w-Qd=fz&QW{1w~44h2HsSPkT(2J)Iz@^^8ky&#Vhci9UE zcH4^vEcRl&1-wfJn(d{qm*H2AUj_Us@yi-W-jQXmVl~<_u*Y8Aa+>2hc_-o<*j36k zg_0HbHgI;!30~Ycu($UyB`=Zpz`i}GNoPAkHS~Te;;-23H3L<>pH^Ivx4l*=O%fMB zX86Ys8E(5I&R#dLFNtf^DRBwhC_m7G8s6LcR3t6MRvog`()V9dd?Ro8{;P;fvBl3F ze6J}GkvDw*O~j?x;QLv{9(lvp5OFCs`2MP5kG$b~J>pVq_WEX{cx2#6@8=bt$QvPk zJ>pVq1I?^%<`j42t<<2sp;<+>ILceLzC)jF7FE#DGwmIpayuu2T8A4JcqJY=erc!8 z<-I&5UX&fy;bGC`@mSPB?THzu=nd*c*CR774=fivQ||GRs}??JwtB9*huWNzQ!^t| zSKyK2B#*)A3Fphn`qX0`eKjZKs6wL^YwmtT$YYun_kCcl5{q(v9hcUwCfVm_ptiTe-qAOkeubVE#b{}1*s(jP6J6dJ(LEHYeU8(@L1^o)3d8C1 zTz~kRo9hn^agOW7gb@3bat=;uJU8F3SH3%VJ8xGttK2GwDqcr}FPnlCc==riKesw6 zMbrB4(G_!(h*kEpR9yH3Mq;`@^)zI;0TA;_9K-1IJAmyIcL$> z4xwRm9qb@InI6v<7h&qBzcFX;D&?$G_pc=_h^Ll2^~T%FhP6vfJw6nphtd()6Er)g#|P0BFS*3}>8ruaQJ32# zI=!yJ@##xDmC#}X0#xGXxeVh;ZZ#?I$TUK zVyFt_ZS&=AlXe{Pw2fs1MiOE%Q^JV0gea}5Fxy% z9?;lTd65Z2>)>|qb~PpzgQ#P64YTW+UCZnSX4f%$0<-Ix-Do$6i4JZbFPhK?5`w0- z8IN~rvTJH|)Fr;Njj~09M0p5*vOz-#Eqc)ODj8NF@y3kKD;>s?hhFKDKe`vc#i61d*9V*OB1{6bQn>AD%ZmH;Qsjvt6{#o@|pglxy)#$VMtetROasl}cO-)RRLH#7qtaH>AOdwc~ z*Eu=up;Ayc6YF6KCXGyqlTPoT0+=6NjrHq4Zbtx5CyXb#w=;5{@BUo(e2qV&?pnf1 zYTEVY>$P+C-_CkD@1?vO9sZK-lF+!&v9RlpcK&J0AGX{a_3yDsEj_npB*%c%cUDS1 zCmGI(TM^e#bp2Dk5>mK(9L9b=x}Ewo4(=@SPDrem#&66+C=6{7YV!u(p*q3u(rpC- zYUg+In5Q_4+7{HdceFY>MWUN&_VLV2yTqV&*mZtpRNN2GSF}M5M)HWb7mlFDDUN!A zx{H_DJ7I8eXu|373=VoIeX7GJStW{X1e~_6=2(=w;P0`(cpt;lV18QvZ_L++16dYd zmgPp(Li6INKkJZWIDISQ4Z~^a#3{ew^uIurXpYzp^@!QXdGu zv8C-MnACNpr2s7iS`8yySkv{sKxVZsv-(EXLe^roKhr81>^IxrFxYP$@*C{`0%g_P z9c^tLcDtB^e2QIU#3>qj8-~7HXyiUt+*de@IcOLhbUP}CQ5G3Y_3 z7gW#r8LtaiDQFHMs3F(H#NeQX7Y`$Z2Sg%wP{vb)z(q1>di3mp@&3Q!Rv#O@M7 zw+tcj@I&a9p1b=Q#Trc!*e!OV3N!=S0UcpgyB03pfX=QT(Ay2LC*WtqFEI>ZQfxai z>}JpcnhU&WuqRwOCB+u$Y$pb@pQNN4~P`Td=g& zVQHsbxRgkFD(^FV>nHW8(xKa_t zV2DDM7e&EvEYxd*$x57PUk}!HLy6R=p$Aj5Okn*-Oz4=nsoSw9vO-Wu9tSrOp7c*| zolq-~rw>k0EbKX*G7+BSPamHk;#l^7cp^-Eot~Z`vW`9dX^O2kL$O8P5hexF4(Iyx zlM~@J%jxSAW(EHA{6vys?=MztN=qPy@LJ`xFp;W!w}UX52LCtMAfsWSMgRn@Gj*}e?ej8xDy?T3WyWAawN{6W~aAA=j-&% zNUnO0I8_dNRBsA~ zAmXm{-Za8_h6NB??uF>3)lkd>f-r7uAhuOXt_6yZ&|%D@w04Mad_^5J42_9XljAdN z6$u+g)uhKYJnH(;TvtbgNIas1mWg92I zaA{)liBFZ8{7?kne{9qm#@$Fk|rZG$Ob$nzQUerPC=NwF|WMs0z4BctmU zhORLP;1T=uhEa3mOJrd5hY_^m=jI)JytOf{&FpI87H;reW7ppIyKa;4`c1+cHVL1w zNqFNX;S=qq`{v&qK{x#gN?rC-8?dK0JPj#6Y@HkCD9~OB;v zF!L;uhGRV%jC=^&dgFvrS{tY_!;(2$+CJ_fo~%c>?3x$?UKBh-qRZtLE{}W1go&xq zsrnfj-o~{TpZiUrZOrKgYqdp~HJ%4UR=7Mi?qwWTkN4_?Yc_q%>z($rG&FeYCtVHg zjf0oQ-9gRJRWNX=ju6M^evjhtB7*D~SbzfkIlVJfQ9P~hpnAB(9YEB-O z8-6gL7%~Hf0-vEkDr{JI zOd@N0l;8HnO6F$``^^n5bj5f{g*z8Vm|sU2Oe(Bjm|%WwQGUB`>X=_=l;6F{&Yirm zi-9$U!RGpw4W(~q=B=7ipE>$(j$VJm^H+q05+a5W|U`NU@vS8^*?GV)f^GTu!# zWErm+*D|;SbELp?Ez5?=mFOyni>3+1Ql;NLjyn51(JFAnKJm~rAj7nD1U`a>Hi#)q zTSvkW@@@c^wpq0q1;}WrNWicy?vwyda8#^eR2U8v=KFuFSi}1QEU$6UYNQ56br$_> zk=^ALW~K%2m`eb~Gci7dMfj9kkjb6;S!07&S=t8GogIDHXaG-@mgt^YeM4xwiEpr3 z^x_P%j$I`eSZ6K}O(KT;C|ChlsEZ(=f?5`3Fcm8>Vl53u;x#gwOy0TDj=P6l*z95> zkuBZj!6rUQu?ukq*gE=*nA@I5U_8mu@G?AY2pEcehT?h6%jTEN%Z7UDaqxX6>FK2F zkKJ(HICU+_XJ}d&4(#mm?d)1MbVmbRz0q`|iU77R?0EISR}L&2Fmxi}Jb?FeDEnqK8 zn8utW&{9G`{|@!a`q^ST^-pwFL4hT5n?=D!f^D|wkQ0oZVZl2kgg?0y=oZ0}D87LF z26dR}vGXGHt7zZ_(;4M1=SEqFh_qJ~Oay^I+gV|9eNB&4mTEcWYkFhHWW{x3*}OlB>zSiwD5cZl3Hd%-m~i zty90ID{ZY-zgDd!dz~6EL(zkZFew{${~l{~dDz)`H+Fb8A`I;+#*-)k-+^Wt_9$IC z-Y`_YVYu}nDiaiX(F1}z|wzu3pFX+}O9XkR(Zr2*u9@OU}|Q4aUe zZt@0c%cc7nN5~*dLPydCLxDZca^x;NRC|eDih0RggG9OD8mq*b$Ow^ikOGxB~BVhIZnJ`Zc#!M*QpS>NMPKeEAjK|#_nvq*xb6F zPkll))!(156wCq6uU)5X-?_ExzUZUA=#Z-dB~;|?)~^d;uqTA$GW1j-OawG+b0bCf z&k>(P-5cf#h9lz~T8GY|cNpx(`tatifZo}Sq!Te2eXJghR&*Orw-EW~POxKn*`NZi zFTrjmpKw3Hrkx`(US8};3ha7b7q9@eY9!NPTL=prK#Y7i{@vv26~aJaOK(}&8n(ob z7rurtNZ8^?v?n*K@4+D2o`(3bgfIwdJ&J#caRIpzs*spxLky*LXv1ZL1!-Z_$3DF@ z+r&H$qdlb#ey7ju2+Om?=3b!ql<1?vD_+Bxwz~f(Ms|(zv6rHZdF-i5S`Z9KLn~^j zfUu{-u9xl6LTuSE@R0xb-8YO$^*K0%Dr#vmaVgeb#++A4Y}%uU6&v)R_7UVPPpnu zoI~Czao;wX^X)|>kRj27yV<^*X zsdZvP3E~lEJVfuWg8#)*RBb+9|?i#9VJ6z|mVl9v> zZ*vgs2H_f7S%^fB%KIK#WDP5XOXM*@29aCh<77nkH6DXAsK<(!cDCh;W(+J^)MAa{ zoN;3thPX&U_4JIq*D1bCQFN1^_7M-3KEVW+duEbJ%vntGs=*kM8Wq1xkm0>dxnQ0k zKPWy$@eu7Sr%N42{4@dc<01wy=OH>2?62@6@2cM*kji*V^GG`?>mN$n^EaTg*=i&+ z_nL7fJ^PyBc4Bh$cEW0LgTHuNpm>+Bc$dGpIgs1@^wAY_YQS9NGZ#VT%DiPYyZpxR zmnUDD^kwh8cKCK)!L{Vo)U4~?Kz6k+yE>3+xn^BSP7fptzGOivZ}%s+Bb`9H1^=u2 zjs^C)-q`1oMlJ+KF8f9<`}bY(Wwc!HoF8AvUCGQ1WLDz;#*RSk0sOBP)&>gqzfrh< z(Hl5$+;`x()O#|}JL2md@fVI>KlE01;Yvf3FK5eK!b)jnptR9f+Bm0Y)}6l6oiFI$ zw)O<9<8N5Ur3>!Bg-3lC9`#!v^X2TH)8DWxoO!#bVt)9BZSkW1dV#l`J@VgIv7u=|o<9LTHn<<(x-ujG}&C=gz>y2L_8YYeqQ9W!dIA8=1p!m3&s4f1RIQZNtQ1z_|J?*_dD2fftvUJKMlP-9y@NcL zmc72}GRu9LRo4z7g4F7jl!i5}x*+LV*P4M#$+~_qkX*WwQV>Wf^QDx{Pc9r<+~eEP zDK#FJ%8x9k9KD^Dcdav~?*C)nick{}8ht`zKxpv^Ey!@7pw3rNw^CU0;@KC@uH;s% z>A8|UYn-Me?I)ZjKkZrlnvugYG_};1S~`EhpStxAPOk{nR3Cm}Yanmyb^YHaXRT!A z1+uDqSyh3oT3=S}jT1K>dBwg^C2eW-XSD@V+N6}W+sWzA%sw;w^z3R%7V#zL#^xWn zvF)Xq`BAB^>()uB@4T;jNa`AvhS4D=q{%0IBcEF1z*4ZP@G5Lo{eG+70Gx|f**xR9Ka&|16 znh^OjEzh*97FN!G>dnGk&n8^wsc5EWO>t>>_xl7vJEfw0{OzF9! z=Zbc(sX0^Dof9f9ujS5p9u0TL#N}_hgO1bAf8Zgc^wic9sr*;@6I#vJ4hyh;H=RFW z(0wz4X7;}W{Mo53qf z{UQ#WF?h+(X0omKa?B!odUiD}#$zF9htIBU`86!(L3Z}*!YRe!v)OeH9;B8&nuP2= z!ExY=?ZmGNyfK;_*aO~JDV92_s2I)4x5(RWV3M*?yH>@-2~$~BwmSJ8!x2lEtvMcI zE(sy#k}x$Q(8iO@yY&bVmldxHV^cHYcnYXn+dF!s2R5%^7+YF&W`OO?%qF%GZwm7( zn8ld%qGFgW?jQ*>VHh!BT2+`?N_v>zQ!5rp08$ADObm-zDc6N>kZ>8HOY| z36jh50l$Y&>-Js5-+@oCJl=exq^&U0)aVx;nm*ZVlLX-|ow=P~%_?Lv;_dz{OCZG} zrC3%gn{RIORdxh~j_c-?bYXs*KfQc4Bk%cR&mNnvy)k)XaIs4gjx1*!eK(OyPXDVE z%a4SP_rVj(D1wt zTQfB;roi@P-kPmhD1&V&ojxyR@?_6uX0EYSt6nP0Y30;Qd$joa8mEQNYg#qz>%@jJ zpUMNF6&9K2g>@?mAN$zDTdL^TTr~HJ=QaBT$7-a>cRB)PLU*5J;<6cz@Bvx-5(Z_ zg|k3pMQk>djCho!cWm6Vh0&yz^*IoP@e!e#Vy}nPqD$DfZ=X;#G%*D! zm#TZy6F{00$EUroggC?+S|vm?iGyi_PPcbl&JIFcz(I{*MZ5uY4_Fe7AYdjof>x5! z0!gL5q|!AmvD|c*OH4NZgiA~?!tbJ}nx zjqRau$HUr_5V(xTwu!gN=T>b4YxW}VR^vP#q~P6}(`9=QuM-$MzWjx<;K!g)#|#Q} zL|(z6#bFJ0K*OO)4>JjrC2)9I{&S^*e1#dlkt(@gzA#u%;3wdhzy_^$Mnp*IOVXeE zo{|=&Kf>=-Y0>wj^e56+r3LAar9bxlTj>u4IKKkV1?el&pZK2g{cC`ILjd%G?@8Y^ z{62%w2q8$nFMZSZjNtnf=^JqWF@6-|qJXep_k9{*Px?Mhfc5*SX@lx3V>n1K;_?oS z;V2U8kC;NHhOiEyo%PvVEE6Yi%m_WR9z$wocx1vUx=0RTwh#*?Nd887kGnl|z73H0 zoU|U?wvGfK%-4wD-7PTo^Pz6VfvsKenc=`f&I<8cWy&;NGX{NJ`oFJA?B(=Nf;#@OK73V5d?(lbXR>bfN(u;+rsn zW=ztUn1`@aL{h8?z_r+_nsE(6uFicmsC7aZ!ix|c6yhZ^e#C;Irl&5G`~pJZXqMuX z%jjWoM~+*`veChu!3|0hto<0pE}}BhIA>+Nos|B}>i_FpsK(P;9G*OJ#voGI6zXjw_ie!k(^27g*vz*Oclm93gnmQ(hNm@ETwLT z{WUe;TCRCblSB3bzO_>GTA_~Y9|JIGfC_;DV_D6!R$i53$4~`XX(V z%5$!IhVYy8tn^Kcs&9k`?l+_bHsmk@e;)(yNsPfimc9nh2OXauYHDg4YH&1F?jZje ziHP5Yv2KD$ffUZv&_x^>#r8Xqalx{9jUEHEof^9safQgMXD&Q_VJ>SvBT%y6SF+!q zbRb|j;5Quj;4bkEo)T*K-{h+`zoo1Bm#JQ2^h)zUe6IraJy<1+KSmfj)vKRzv(YWa zufDXw8Fx(G#Gha90oxf{g%eLt%k2Kci{g`nk)x#X#Au>spmnkZ*f!E(Q-an(VEH*T z@kCrspWG3(Qo+z5#1`{}XU!6Bf!@Ja>qc3- zApk~b-k~Wcg7zdu z9N`>;)gE;gpRMEcR(&@RI9777#jNCvGkqw6^jnyP{}^-iGXkc0d1@!5Kv+WhDkkKw z`+gY~V31b<_$SgI(4-Hq@L;C-{F~C(@$u{U#AbSn?n86^&`6zE5%l>msejn%b=H9& z?7HZ>diKMd@evW{ANoDwP|d#5FkI=`53@sGn1CL>K?)D7%hI#68N=h3`p1XYgU@D@ zFFQlH{)3IJF~zT;gdZAOghLPvABN1LfQ2BD50I=RCa`?M7%R=HAYV0WY60h!jvSN6 zhsG*lgo$a;axxfdVk7%0h@vo$8I17&e-#xF$^au$O;b$*XN(cmH(ayd><(4eFa z5yX(Ec$Wa#ij?}YvOJQ-0dsJm$&EQzsm8%@__!132EZ*C$e#a+$O)(3(daXc*Amv! zxQwi8hQG)uxpsI>m2A#k(`Zr)!DD=R%S&5sT=AFg4wUZqmG1YK9t;#5yxsxMSw`;M zw&$8xG76sWdA4W%v{c@-oUwByJOBCF=Vw=Qie5CmV4A8D z=fiQXRdYF|?`pBaeK(!UEnO?;QgW^(zgx%^TKV@^vjs?cq!#`-qW}UO zsf8cCm%^69YI82wtk1VR-$IM$u4lXEci-r^v1?(=;+4er2|FH>?pTGJ!gPd(>R=BG?;Tlnjxt%<6y@~sKFuW4X=)oN%hP%RxW;?rvhJbYeD zG`8kyU(4j-|5}!c{BwBn$yJk2fw9e^eXVkLTelQWa@v-L$_&755 zEaDXWiBTxyPi#;T{@fIM#+mrtI)u}0vPHWduj(E85MJF1ysBej9rweV`45PRu=Trs za}QgP8fAmZp$X&R|Hjcwp}C)&S1cLhhFC>!Dw;3m*>LVKMjiM03P&|=z>I~XJeaM9 z!?Tq!9s?t4wu)EZCWk4W#I`-bZoHS5hdJW@a}vZX9tg`PGAwUu3=3Zhm}5rGGz+0F z@1F)%iw~4WYBU!S?L+H`mux;{C+)*jG9<<{G*PRYf303+so@*G-%U!#D2df(hq&*Te>2$0a?~ zLO|x7)Q5n*GusTRQ`U_j3or~pV*Vm_MU?bN9wA=dg(?xdiCo`od5h3K4q*jvBnXQm zM7|0Ty8t<>u0ml8!i5dTt`MR^)ofjh&?jD{9Tx~D%Ta_99%k`Fy29x}tP>OGokJJj z`LE!V%@&1Ylwm^Q6v}zF3=I($c6Krq5K3u?^(0F?2rWV<)5n8KQ`*8QLL@_p<`7Qd z+(BW%4x|(QGSSRPa4fAtTR%j72a`e+a)@U}2X9y+gDhG)#DmJUB*oI`DRQz9{8vcz zq>xXD>=*xmVmINurgugFiolLs048KRqjs^ccDxTx;)q` z3oiiVg}}OH0JwnIg(A>!(2bV?_X0me%K!)3jFtgw8XKQ*h6k%mNRmti;FT4XUbFp0 z!ItZWHB|~$0%cV%?|o_Sg27+b94I^BD?4z#YfYohw5-%M2kH*^>JIqptbr=)oC!Pl zIpy zRAUuTSoaT=TuRZJsxEbyzg^oLs6FhfJ?yVN8mKsW-L_J?b^R*jZ?g-K6RkbhAzC62HUH@g%D<){!&@Nt<4$=O+Rds~+=MSom((9<|7`=9Oson!F=vMux zwgDNo>{+YP=Z^9##bqzfzBs!oRK9F_$#mn&qITi3wEv=1H?b^CvV4=M#-dj$-6@r} z2MXGyf_BvDS{u)y^aDJXl6j{?#Z|Xr<&$5I9Y54;RrME5*xk#t(Ap=blLDg`DKg4RIlhwvy)m!CT zK3YpaWFJ9Dzq}54EwoUdX?eM2wY=u#u9vzNb}x1;?vh%=t3Ywtb4j{<#qYT)iL=tS zN0-YVdrPRM+!k6sypzTicJS}NU0jV-;D+@(uzdDm$$8#?c1Ri+mg-!~!pQ&r0Lw)L z_z2~rI#Zz|#jmt8%MYEE&JFq>dRRK+lq$|I=L~&>exH*0(ON2nq8Q}k24{E(n~;P@ zL}EcZe_5)x{lFvH`@Vna)|gayYB}ZfN4JwRPzx#yq@Q{|jp6ka-(AE1QesyTcRl}* zUiEwF9XnKi%6DwjEpZxHmoz)zyR_R3fY)^C$I`gha{102&1-qtWZ%Yj=4)Qto&o#o zhPqBc^?DQES)_Y?=Qh~CRmH>qTh)}pw=62MZ`pfP%YD0=KdRDv+oBO%W&o<+c1KL}Ab{}ccez#GNuitIa z!sojO2=aR>9(nto)_5#g{k`O-V@CD&H5&MT-=IQ@-#2RE|NUfw{C=9TD_{Nnf~Kx4 z^$$`t`1*rPD(?@nwDA8yKIQNSMaJ$b^$)6>x=Yo6UZ};_KQGn7=g+Iuuy3#yN7J0+ z&hh6|@`~IJO;!#biqD{j3~E9QP*I`|`>@?O_&|eL2!<(2gOA?9W4F!_3Xu^0ZVdS( zhJ8#SA9L6Ts#GYQ2E7~V`miSL3W=z8U^S8O0uBdr< ztxfC8@G3UmYS*69M?B9|hX!U^%=$998&U;qLn{O8pM#){HJFmvgLNIITDN|a^l8QR z;OVDF9bVa(i-}ah#rz`H7R(k|{V-ZI(-urp=0kaU98&U3Zz=#SZ0}MP?N@)E>r=(6 zmOj3ZQ}$HKInfVboES_*$AJc~--b(M3_aQvW0E6JJ{j7}GwhvkPM#lj?hm$vNzKSn z-9fK-H8f{~YQg!e(Q#~6M)vVSVHap>PD9-9KgrE|16A$5s`i_Gx0;r#POw9OL9z$eP(aG-LhuX5*tcX9BRYq?TZ)dcM~jNpmW4*!H~G6Fx7a&e!`eU&no70O&+ zprXlFfisxW-jmYlv&$8-Uc8{;f+tSasI;sGD`6oGdFbPaE%u2By0F@t-KuOw*F(>$ zc-0;v9jmL>alWf82e1nbqOICO5>T#7&WRcH19UFxQ0zFW+>;{p{52{Hi8N6?XS`ha zQlZq~TrN2;cLh_3htBj=eNsK)zauqqvnu|5%2#G6Ur|-ug$_^=>HubJB1ibupHTK@ zGi&0p_&3e)*Q5JHxWh{O`lOTn%asFieI{O+c6!I+bc>(A$IccsG|;xhLAEckuPS;c zr0VRZ+5Us*Y_|u>clgR78Y=BMBlSPLT<&Dq?qfR}vuQ0t8+9%mK4sYDDpqlcRkA?5 zYuGi+7P5yRsNY5n{4yDA)kJHhdv_{~5gZjXD={*?Yf(-TnIav!q!l=_Z}ITM53?1E zykNKsyX*Z#aq7ksX%W1b=^vO z?p)vJ_RpVu>0Erm@$jv}TglR?^HRmoa?bFN@{8ZqbCo+_)a_c!0sZ)HCYRShhgC^* zZ-e~`X?y$4vp0KUY4(zl<&4p&9g9|!`IoKwwiGz$O~OU5A~^5QjlV1G!)!VG3Wzdy&~b;!hF5A&AQo z_e8{zU1$zQ7m%?4Lkffn7-mp}ei)%c><*dkVFB6xBysQAAQ#vu7qbviuqfV`j?fd| zp^uzYS*GXv*yQz~*b=dX8 zhM~Fs{kM~Iuxv#@c}*r2?f#bfruB85zvxiR`mFPo;pVJ#qHo!8S~~NP-vOnW15)AH z<&<;KU`5N-1Z)cKMrNMkTeJCJs%r*GvrF?;O*4**!{b%HnfpUyYleDBZPm9Xt6wu} z;rm*$7GAGqs9_HpFT2ju{CmuGHK@bUB}lIh(LA9%R7Gr@-GC*s*Z`@mYDB-3cjf)3H=@J40X-0iZMpUs)6j*`s<&kXXn zt}ElvKM7{wK4BwDr)_VoadqG4FYym8v%Q# zrU`EAh5)4a=l&2(i4cKo+q(7A)+KHj0QxaT-ABBxNzVfS&mJL- zECh&3p~{Wv3Zb@aAHM{caNoOsZ-|5exebldHt=0pR-&X(qH-Sv4rP20F_%@oqLVoz zVk%Wz&_})t3>Mw(?jMq*!KsZX=#;xn9v&g~{)5LNV-|{o!h~x_>B+Jl!M3sSiQ(!< zcA~~!xX#W`4PUK~1`kCM9jiRrW%=6s)VB-}T?W~w9wIzc(TX;(4Yz6{YJVCoZEb)$ z{`w1^s1^yu#uPRpKCd|AiVVxp$6?oqi~9WfkKxji=GJwkm4ii9j67XpbEu7JOBoiF zO{h@4gm#ByDJ97LsKtDmE!jj#)bqMIvW7m@lED}R623=7VC6Y7n2Fs2+?x?@eV5Km-KiCKXh9s}h4#SYdBP=p2TjpH!x*CF52w0i%w2CMt?o>OM zKqurV`R~nBY>{^_beF_FQ6Y+AzmYOUrDp9$r8V4%Xr~ed){&?PVW>Da#AwpFlV%Sz z(Rfr~fZ{y<)L+Kad7;FrPF8?`rl{ z#Dz;<2bbzjbEL+FOI-(-?#^(eJpf#$J8P5rsd&JePje(=?4~#}VWv8=V5U3LVP-Uj z`>Vjk!Zd0f>5dG0X1yAE8L|i!*|Y0a?rgYn$Q5q?QnXUY?|%I?43EBIC*2b)maV)a z+nz@>^d$ykZ_)qVf=~-^ixxH)8RCJEv3CvjEY+x|l$4+QW73&h2khzB_W2marEv zZBk!Ay16qbjyunt?=El`x{E%sI?rQup3myMfYo{7J#`)fgE}vC6#atNIp6aSaci`Y z)hGmnH>ttRg)QZAoED2O>_y~*9`lRaANiiKh=vw>J}W_(yX2x-%b@hd$U8hT9u&hB zM`Jj_WT&--4Q9LVhfk|mw>-^DZa`q=q}!*mL9bKlsJml&o{roHP!u# zn>R;JZ?n=yk+;%X_uym!tw$4V%;OLg%o`g*Abhbe1{}`&h7fu&m&ZQ#()*@yFcu!O zgnXp+5T(}J7Uz4py~17zN)&4ndgA8&lNixe(IdLQT}};$c*8cuCz=X%muxB+eeeM( zglfh$wlEHI4db8HGGW*)41<}J@(T|}>D5J-<`Wvt^-O6)l!z-@tTR$8ip`<2H`uq@ zx8183j`g!63S8uUzkb$#RDtV-_(JRh+N4manK(T|=;8+!Hq=NqhX9*A<^WiCMY*N~f2N{9y?1CA29 zK@#l%N}V2eRaA-2$bQjgXdT8QH@x|xlEPXb>Zj2_{@LOH!>mHLdlH|nFGST3yVbFm4( zWCY{kf%qj(j7f}~NI}B3pNF@x$ruD@BC5x3ysB-BE-UH6`&jXA%+YC&jTdXG#!cdW z&=r5DqXH|Q!_gvFQTVPx83RoFF1Fv?)#yp3y~O*Ac^uUW?8m?^7KbGYyS)Ajid`)K zzrik+&9TcTHnRU8ehH6&&3LDqjMZN}{K9vC5IS65Za>Bjzhj;Tg;7`s{(6XZInFS! zGh7K_i$lQS@6cQyOLMtv95u>_ABN!QCN=Y*)K6D@>Ic4i7@hT5qI($T`Z#o2t)n(n z)`?AXn)`rrM0YeUN9l*c)fTomgws3r@!?uRO$wadhC0>AAhC5SaGTP=#}wQgC#*|J z*nS-Kb)pWOHlic#y-FLpw`>-_W6Q?zZ_>g<0hikgqWV?E1J#lG_LE=l^eZ2sh*ZZLM21p6j#V^m!6 z#(B~PWxXf%PbCc%7*nO}!#u>F{sKVarpn0Y0pZybJrzEQKTZ*08Y+7UGa z?A)O4qEye=_3nyrUoqHTk5hLB+bnXew|$$LzZ8@AgAb7R2kqCb8{{{Ndlt^C-uCa} z(N;}gSiIMRalf#c+gB!R0p$E@lr`G0* z%^7RqR+u{5uQ2o2KRK@(*XGA*ZJlj6PF{7kikSTU;28DNY zgEo&EPg4Xt{F}Zz|a{JFj zpKFxM;|yHr`Z$9;H%6NZDZp-wickUZW^Ng0p4p&Y;Q5Ys8!sgpRJE~<62+G-=}zETKHdRjE!J@ z6l!DT9lnM#>N3iH+^tIYBeQ#Z!sl+F^#wxbqz1X(h6A`b;W~xObB3VqUY2%-auww! z9LWENKW-5`7ssckNil1JS5Yt+G=&7B2M<|~bTJi*P*tj>L#wW!$u;4ehT`%;(p*D| zUvMbN4Xxzt^tEyo4qb!kbwSlkA{4Y=q0`RZD_%OMD4&moUK{ei3V^H@!8XNQfD)XS zpe4q69`{ve9VY-`1UF;~6#WRDb^v^yd?x?|x9NIjIT{AlA_Vz^vufOM9pqbBzDT&6 zU{F}PZ&{Y=X6XvE2BF$H;~hhU+LoG{8Vj^F>*#7(ml!n6z~Ux_Erw|vA-Ifdq1m-H zR0S+_&`1!2(1i{SkeEfeStxfLsv8=sn{fuykdo*eBsus&l0?VN*Rxqi+^9R8P>~ad zCZ0*UXZsy+{OSF7F5OXH)Yz!HZFaENJG#LY$`P8d8I4oAmvplP#3 zfIj|x`vp`wi(z<67~=6LN&2v7NMk*&5Tbj^_r*jgLRThwON;R6m3U7m(^Yk@8_ zA{9qXTen`>zGEvXP_ipmwnl~(&IVzqk@6%*Zx?DfS?)%5<#51kqVSktWoN+f)d)u$ z$;F54sCd@PTUgqxH{zO$us&>oUAsk zf})KH9X|yMxLGfEdnq#4@c7mlssNl+bB=qVL)be$A%29)m@Si)RZ;hbE~}_hF?C3I z8YjM7rDxxv)BUrhlq^$riUEWe6g^V7;}%yfxF)B)0=tiER)w-wuu<%tx`^B9LuV49 zB8rAA-H989|Bxa^H(#geiq$X_UCF0pggQaY0@yo50E+n(KtsB(L=s%bu*IcVQ*PW5 zZJ{&IxLFMKrrfWqjB~n^m)%K1A7`d#3jrEJ0nqpk)V1!auvr|NlyM4uq#&VqP%I2J z1!>BcDIhF*3|%EgYO<7X>k%qpD;b(1Q^27COTk=(XQT5S0+FlHjxRuY>#8z@TNO1} zBAtnzEnE+SizgTcrid{S%%(t#S9Ehw&y`VhIGSRL2K8vg6=qC1fpUJFFtH33-FmZ7 z9Tvt<%1Ie#L=%`2`l|R9iWtM`Q0N$xpg^<3TR1Hl*60Nag`V37M!)a^J+B-H`EUnk zjmVkO(Y^$r*-Z2e%oQ>!l`H6~%ZonYqHAJMeQ0R8j;TvgMMQ=%^&pT3+-&}aFfD?t zx4)6yf+jWtI;bCWdeD1Cu@P7$asWsqc^>f($sj5ta++SeK%rDqo*?eZ@QwvF7p9>5 zUk8Lbba^ z9CuslWDRQ#j^d=#;^JNyLDl#)ng-}Um>jB7b~^zO9~7{K z>R3xLd4eW6NjY}V0))y35j>O?7D>oX^g!4jGy@2*=}S8YL-R7z@ezbt1w_lJCRvG{ zY0`Ox)OQdnMd~4&T;p2Sy~SOW?>|Keg60rb9b_M%(2i+<^T-+l;hYhnVc#p?Y!pmj zSf6HG4;7y#+6R7Q)eR|w3@b-;c*IxR9|QCuvg`R{%pOc%LrTUq?d^ufK*K>_!@)p9 zyRV_0l(Jb4`3#lUv~x*ongpoKT20QnetAAAklf%)ZdlX@c6a%9cLjFae7kK?8lP>? z+U8H+aNRg1?dX%r9LpIeV=riPES{6LbosNo11a57O8073$$Zs~JvZy28O^tQ*q`MJ zq`0IM*GftjlvtZm0*QtAml6wCi-Z?PUl@IH;)RLd+y3R|-)a8cJ&R@>D!-nKeJN3?aRrp zBrmG{)q7ts&uQjHSL(KX`Jq=HT2%k;pf9Ip&M;q%tNLEv_tL(FbbndX3x+w>Tzh;? zeKBuYWR^f8L`sN$m zi|LEKi|sf0o8>pNq?*GkTXx|8UA?BV2rW?CyqNE+>3G+yug#lFh92^Qnj5u&{GE&Y zZ`JztoDA$4@a-80>^bM#bMCfKd*j^VF<;%`HMOdPFPiI-wjA^oSfO&zvNce>=Z)$; zzG~az6pTQr?F)MN7nZ(w?1f`D)P86(>{!jtn`?S*cK*b3pIUJ|&d!-pt4LBoT-51gjcvRI#%J7b;9Q1Oi9->!GKdHtm9AK5u zg8LWC7RMGJy7|bhyjv!z#<8+xC;tB#Dq%IZV9xQyy!l7ISiE5&&q=2TCCkIh1mLvY6qf*Y%(9nqK^}SMweL16V zrL1A02sZ<6>yk>luP5J5FPX0mq}MO(zgg>Rv;`Xbe2sm9#*@Crlecrq=Fcr0^Hm%~ z7ulC}y+? z?Rv5#?C_;DQKu{q6g0e1(BLa*T_7Wn)%tYj|DlMlb>7Y_2xM0HGAsOXb}$QD+?8iCt1*_i~EuZJ@g|pF8th z|25;9=AbI!{06EPaZdT9)O$)gH6o3=rI9IVY8GScF}{_C+M|3Mz1md3I8cwGveTi!aS`quig?5HK}JrUqQ%m!9{0-RJ7&d4F0d zE=!e6r6H{+QrTgD))6Q?$uGRt^>)56Ul7Qzzph_Xb@8c%YZ`54=}KkY%abon{?=4L zXqwYPKV?qgJpbHm{GmK3Jv=I1xafQMqBIy%uKK}BZqa zej&hd)hW^@+Mzm4?a{6}L$5QcvlQSQ+6n>Asm3S(z3^>ZHBAA|sU9KMS(QjHkLpwO z5>*G)2xnDysu9krKBh)Edf{7_+O9@8tGbU|2cc~dFNgXpy|$jCU?qd<)yi!PRf~J5 zgN@Jl4qx(DUJeMCCE@alP`P$k#ieA#>A|O@!qdwsXVx@2^J#vCE_{4;^0}!%N`;hC z5k;_suc(l=_edqjmorYR8I9)i{AzmM+^%_BAidF--WWsfK~swNf3IDo*?`rx$1Vb4oGq<`gLRa3sBubR%b>Vj)}F?UJfSewZ`L zVBW>;OhUz428WA%V-@r+R^M#8StT7Aku0Oj1!HfQRL;-5Trg){QyHPwb?3fUFMj3X z&62OV13QoVb{?0`3`j6{o(*h0J8%BaWm{JpcmGkwpXPoe_nY}QXMN2lpd{7b=$P*! zO|Z6?YzwCsU5lrr_TgpM#ei$d=bDnHulilH(y&ZXFm=1fR5ohbF8TF1-$Sm)_~X<; zPN4Ikp--s#se{l9-v(4e)Im&|Ru=3UC3P2LUdqycB?52r#31i~?LxJx;E1 z)f4pkl#2D5CsbY3HM-Gx5UyK&ih2~i0CHMANIj}s{V=(@)J}Ri)#LQqae;zWTvV^t zG%buSJ}R9Yg6}1a4hoN9fq=E`l$VNjN)G0a%4^ofp#S&(!Y+2luHwqKK`~ls{aOK+ zQ#_yg+@tfC7gAq(biwJb+O<-@Jy3txSAY0c#;qBDy(3WVm@~gsyk#Nd)x1~pVl+k% zNy8T<_mppVN*bD$ERQS~h}6rA=S!b^d`*>VjEoReRf{MuAtYsb79bFEOf zz~iFt__~P4rEM3M%P+1})&wdK`YI3J%(!_uaC|^|Xh1qQq&JVW%YQe9y#{l+U-}+L^vr_WR0P7Tf&7LENF8SMp-r7wT>#`17_0GPX+@+i%P2 zn&(Fs_?Ip&oInKYZ^pTBGyB%IrTm*_+@So}Ew?oAh*Tj4ay)m`T-C16&A!{C;tCqx zI|sAmw=-VO{mtCp%D*w|D`{Tb=Fi`Et!p){aBg(|(M7j(?t*W}MSt2vz%(J5CgM-I z$9{0=KOOzlvhM4mV>F6V zyHsmQ|95uOM+{s|Cy#rjQ*C@O#U<+)BR$ShHJS8m(tOU1iW}L|)+oi##iaO|DV-** z&rn-k+{d%^-!&Yd?ZgYcq6s@8rsQNJBUx<+1i#y_b$Ah2h8e5ASRt z$U6nh%;WM=xI8YqjYkGy{y4en$A;P;8!UG;py%+#e3)R(J^YVN4Y*j_Tn4z3ow6!& zHHeV7O56vev%}KpnD6YEG%$|U;qn9!`6K=bku--}yvqeMD?-VakasglE&w&0AEtS~|9p)yc zV^M|aG=2o9mQVLf184oG&q=2SrLu8MQM0K+BUgd?v7H!p2S&BQm5H=2;Hu13%VU}?AC3+Wkx?M+)nLK;^Kn>x{9o+ zeR6|3Jij~y%6-f~G%lULAk|%57AE96oRk~0k@RKWN5}m_1XG?~K6yrZsNa8bKysXw zD$XtE3`S|WH*w}kbPOF+-L6UK<5%;A2S%N~jZo*2AC%p4e!ub-#$@kuiXB;p^1wdU z{J$vuc&+MVqGsPFWa(7rG{B;)L)~&Pk+-v##Y_;aKf^4!k^0T#?*LG;gzT2uJsMil9J_eoqM(tyq z`di64kM7ZZt2Tqo11k7@o44lS^PlDO1p8+N+u-<)If)$KDQL@od?)w4UHs#Xn(yt_ zkbQ4!6Mg#gZTu5ynm^y(Y9`lzI>0}Xsgbx=mfjmW{)rsT8~WA^`nH_UKasClF0eAl zA6ob)iZnm0$)-#+pxFOv8twrc(&Ifpr__$L}Qe^FgXj#VxH#7@nsu97+G)1TO@ z`b#9+touuqhU~ii)+5_hZ)wx4t$S2&;VSxrsk*mvT6dB2uae=+(EU}4hPg6$isP>` zYbfO(bLrNDdEAfF0g|oxaYjA4{#uP1x7KiPC-7ElmF8_@J#+0=QHZx&YRL6B<;?Xr zl`P@Etthb`+OGL8I^NoDF#H!i5{C1g{ct84-Z`Kl*Wamm%H!W@So(jbW$FLj)^uxo zD)-;;tvyNe-!%2i)rj;vO1S^t%v(E(H2=M&o?QQwOi9U@PZ7Fz{AeiROcZdgj{2GW|hw4Y@unV~zh|HIH8YVJ$Q3jgRhDf4IBp(MI)0 zb*vkH)To8mN4pv9M|-IE|CBR6maPA&(SSbw)3ox(x2k`-lYRbaa{?Uys49EBR{f6^ zEcidxYRPLW`~HuOJUahB?#z86S^cwQ4K?S_P)GF2kzexg`B{%%e~KY_HLJFiHErb^ELb6SPYQ)jF!$CD_S(>0pvtcBk^xEtd|W zU$QrK-aKyUV4l%xEmtocEN`+}i`7fkTvnbXYq6I3;~Qms$%<^DKQ38ojMmoOfZNez zZEaL9wOWkqduyYX`Qw}H-ea`3Yqd*l+9qo|hj)vdFW8xXyH?IuJ6aCCcBwrv^iDz- zP34x_Q;pV+eDzX$ev`E$OTE;QBIm0kOUwN6jiu9pIz&HR>L@W<4>bU;w#j;^M!j@M zkn?q@M$7#1jlmz*R8!AgI*b-%?p)TBmkt-hP5z}u>ya&h+tOq`Ql(uwQX-cZcIJO%iyZF}q<>6} z?|b)HkM7ql9o?m;PQC;?^FOB6F!!+pqqRFpy>u+S$=aQuUg|g{_BOZMgl=I$dr z1b{vuBKqziOa!+h3mGfdFG0bF^<=Sr>0~C{@IQ$TOa3RR!;-rSZ(O8;4ZZM_2-|1y zyW0nmC0VO+fB0iOE#dJsgezx?-2BfGS``kh`Z+?Y!=cqbN9eFrF|?nDPm4N1D>zh& zs2tD>Z8z2HqO?ky$I!Z$_|L0tT9VX;jA@1vOFZq8(38mZLasLU zIep@E8RAZA4r{d*2$DoFVjiXLoFttKEei6}o3Gd+Z|Etc%ehmcPxMCBb}DKlEuxo5 zu50L>ABJ=%PL|1!o^y79-Jy$0sYLk}dE3(?ZR7|`*{?r$7L~Hro)HfodPml^yhjE3 zPP1EQ)s=_dk#jrpB{s9whatQ~;y+_9A)+Z)aJh%4AxJ`UMNt0k@p|gxic{d!H5W|p z=lC!bSIvkMRRW1eRfV*;!X>&`thm~5_!V_f&tz$i z3{Mxr6JMZI!SA&oUTJ>eo*M1r2eoAGV8T8@H5!7+&d35YuMp@Z(HGsaK&? zf6KF50%@hbw9@%ze_Gu&{c38?^99cq1X4?UsU`E({?yuQy495I=fQ{yqzJwgVLscR zV!5XM+vKz_Rbj_sCBI+=41n@u*tjl8f*SseWbjy1Gq4=a$XMev<_xf8(zAYT`1uQ; zyYSqEKdlUDB<0Rk&F@*svaIG6{>Jc&<6jv6;zfU64Wt-WvWr%-3RW`nu=~Hwc$d>B zC;o)fCnT;hBP}^0W7U)%FcrV?{}p$wL2+DHx_e#>^8myfBv1><7K3TtGL|%8As!M6 z!iokg`4QoPX=X;u19vk*j2(wKyNOl#VJX>^q^+&B9H%yTOI0TS__I}VIreI6e{3UE z1v`pX+1jmC_8&;7vZ+d{_B*$yX9hGNIZ4%y#O>Snea}68&V8J7u4DHN6Ar)@g#)-q z9B0)R-cf}Es_^bz$G0~g)~UNY7W@mZ-SVnc$L|*OC!1WkXU%M+_8QS%3$-`LNbNN` zi`S}9-~X=T?(vF8oP{bZ{BYpXfV%U@FN`TAe&+ds#D(w7YY zj|l+SszWhFTr@z>}wOl!VPF1lgFGvwk#VN)-bQL zA{aKv$z&A~h(YM2{E1SnxF`FGQel?NM#dt1bs+pSxnRFxL;6Y&-5`QqE-3~L8?sI- zQbt>~GD!tDgFxFoj;2)0JzAfht;XlKtTA;{vnP+4yx7S44IZl}|H(c-V?Vo|UTX>_ zFFZXhS~!ivDu~r)z@)&l!&8VcaFP^y6YnW{V${VSNwIV4_?rMbAGx z=y_$})vluhz45#u%Ae8_kiSEjX5lcIyd?FPU!+h%-}v4$QF#J_`6SpJ^0ADp-Hh#a zYjVhpyClq{XD}nbja;w-Pn|GRl zZ$JNUb^lU_joGCW=hV^I(uo*KyLS9WNZr}GRMa*9z8oTlrp#>GRy~PP~jEIQ0X;)<`!^?={lx=h-J?i0|=`z6rxQF!3OAl=(SV5LSQN!>f9JdK|;tI@psfnk($ksK;(@< ztUP>WUdV-eWrzoN)C>>xi7#!+nNFrD4d^n&HJY4!1lic_CLKQMLU2qH*-Je{_JdPF zZ%9;d7lzc)+|ZP**s0)DNa{>syx<{a3t0d-+>Kh?h90t}XtPiO_f2%vq;8w=MH;4{ zIYe40V~Wy%Ib%(yyHQKJvDfZK(#qKsMi*p21Rj#R9W|qV(Jy;v#Dh&u0*PE}Z!LI( zM6(z`hhzy_4&x!*T>>SLup(-gq6o>`Qj)LPx{Ir8HrMhJ%AFKn5|~i6D5abddlb?E#Gqp9tlZEHyDe1)BBBST|Zx%s3>R#nC8e zUU*b|QR`l^^(;-Q#4}O|Fl6d6*+(bP+?l?SgsRhj?n%8?RMD0J!B%SNP*i}dG{bpf z7r0&qiya706Xrk_4eK1iD;(<`(x>XAKJf$!B&%|dVR52YhEb!&x9fv89l+QKlNZGn zVRA-n*5O;znO;@78>xWKD+4CSLg9kyr-;Vtr=hcpD4*0_9ud?9lDa9qZ| z!9nOO(-t^Ae?bwne!v~wgRUz1O?oGb((*ZSM&Kk&qtHZ^(L!yrQvLxgQ2&Zvuah%N z&JA+r$oWTd{*{~ueCV6_UC=1u@+<*qUTA-dd`N@dCw68{-rcF}`DbO!*(4<}1xr>z!tE z?RtUJT)19+nm3=~9|jD$<{tiGsl)7iC={B7hqVUt5r}me%t!fEYqhz2VedMJ$LbLt z8A|Alo~r>vnYm0090c!*N zTPE`;KR33@G0y^WGtVtOMD;*^ZDi5&Rx(1Jyr|B+yEysn$6CUNgSq?7#pqU@xp?(W zp0gCL8QRQ_M@IM_k&g`vjYaB5n>j}OJ*ojt{e;{8{0mn6d5_^BAE7fF+8^5V)3ymc zmV-uNs}wnJz`m3A!&X@CHYsh`a7fp_fm_GjO@R-IqQniaSLy5*2C1 z9l{w5>A*&13D<(hr1+WBu?SlT>gxOKc<0z~zfcQ?O*TLTJ)aN;yKyxLM`r|BqwwO& z@%oK%0v1D3g!1?rKssLl?MavdQfM-67e!LeWwtuvM~C4RyU1<_DXY^;uND{7(#D~J zGo#>mQ1aCaGI|>g)9%K2&>zEv>a-t&3Q&&E1)B<{sTmm4xQ;sFIil#3Mq~cCub0&> zVBw@Uo<*>hv-K2ofw3VHjlh6IDC+l9-NZk!%8Q}dN+23#)**rs%{mCxS)1%EQ>0fU zS3FM?lfW8bZFn2w7neP(=`$N(NztrYI!I)Dn{orJF0eTfPSVH#1!hBsu=4|Z9l=IuR+k+KGVIYOh_Hc%&AS*l3RMssWprMXs85l|E0P7ZF_i=H!YZLm7V{kOH6S*lI zh_~t^uYW?%Lf6Jhn$}kl%pEInaBAqw8)feN4STwzp4$YmbN0v^XoYGaEvCi#M-EXr{IUQ=7bE}>2 zu+VqYq4t}5)wUoDJ-;bbVxi;dQ0vc>`F^yemp`d@WiZ{9YZTpw=%dg$K~{pzQNxOT z$FiN2N|)@_s;OF=RI({E&5Y%4a@edoNIo`$W^-vad1e!Dnr@HLG+qBW;bHTgHh3`K zp|NDYv;5h;XM4~3&tCHI2W_zAX3HkK1P*8zblJ3d``2j%*v}a1VG9G7{p;svrl1t! z%^Axe%_u`soMHqwImZT z0Qkd0mkz0>5(e9Zbz~&$#nODlWs-@D$TB$!IiJCamkviJBGKtcnq-5az(5Mg?`TGO zDupiHR@a6^U@AJ;*sw9!J$>wmuM%yMedw6$q}-23!l=wBnxw^FPRNR9UrWXk=Yv(LxN2c zfH4`Vp*5p}KcUfJYfVB!ZDrcWip!jyDB34WVlbL99eX6oOrp_LQ2VACa3HZ3|6RyU>!F&rYvw-Af}oboCbIM7q!`FmXMJhE*S4>;UiaPmG^cb^mPbGznV~YURtXVLgb)dT9|q%wKN5+IgjO zog@EhJ;z&@xzama>E(Cta{E?t3zl;$@8njhI~$jBn`SL5oaG1k-_5_AbJcmpd6%nR zvF%v4?Y?8%eZ%;3+sC#g+Y7TfNVClCSmbtG-hY>?{GDaQ$vf6Kav%2WEZ}#qK5yVl zuNALyc+Bl(k9i+^+}7UIGq1B(bt1~1AP`3kq>YLU@RJg@?hiw6R`+)s`>;CI(tN{s z3VN>Ow{a%N2l2bcihsA4{Lnqy^Uz=}vCN*tajMm^oKwD-Q+~s_lq0TK^FDN3a;RnE z-1ukbKRv%tvefXhDnj9Mx$E>|*J%|8wL`Bj51m^af?DGE(vV;E1kjkErb*so_$JY@ zZo_GMlzI%0IWBj|@ShloimK%b*J6e1Zix#P2(H#$se?V3tC1@a7E`xa0Xd_xtB0=~ zUa749sP1|lf~hdGyWE)QLO f?aFH(d*+HitN66ylXD;B+_AQ;Hgcv)Mmzrx@Iar4 literal 0 HcmV?d00001 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..74b5075 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: '3.8' + +services: + turnstile-solver: + build: . + ports: + - "${PORT:-5072}:5072" + volumes: + - ./proxies.txt:/app/proxies.txt + - ./db_results.db:/app/db_results.db + environment: + - PYTHONUNBUFFERED=1 + - HOST=${HOST:-0.0.0.0} + - PORT=${PORT:-5072} + - BROWSER_TYPE=${BROWSER_TYPE:-chromium} + - DEBUG=${DEBUG:-false} + - PROXY=${PROXY:-false} + - IPV6=${IPV6:-false} + - THREAD=${THREAD:-4} + - USERAGENT=${USERAGENT:-} + - NO_HEADLESS=${NO_HEADLESS:-false} + restart: unless-stopped \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..4d70aeb --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Default values +HOST="${HOST:-0.0.0.0}" +PORT="${PORT:-5072}" +BROWSER_TYPE="${BROWSER_TYPE:-chromium}" +DEBUG="${DEBUG:-false}" +PROXY="${PROXY:-false}" +IPV6="${IPV6:-false}" +THREAD="${THREAD:-4}" +USERAGENT="${USERAGENT:-}" +NO_HEADLESS="${NO_HEADLESS:-false}" + +# Build command array +CMD=("python" "api_solver.py") + +# Add arguments based on environment variables +if [ "$NO_HEADLESS" = "true" ]; then + CMD+=("--no-headless") +fi + +if [ "$DEBUG" = "true" ]; then + CMD+=("--debug") +fi + +CMD+=("--browser_type" "$BROWSER_TYPE") +CMD+=("--thread" "$THREAD") + +if [ "$PROXY" = "true" ]; then + CMD+=("--proxy") +fi + +if [ "$IPV6" = "true" ]; then + CMD+=("--ipv6") +fi + +if [ -n "$USERAGENT" ]; then + CMD+=("--useragent" "$USERAGENT") +fi + +CMD+=("--host" "$HOST") +CMD+=("--port" "$PORT") + +# Execute the command +echo "Starting Turnstile Solver with command: ${CMD[*]}" +exec "${CMD[@]}" \ No newline at end of file From b9ea12de5b7102ff048b256cb1adf9e12d1cde14 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 19:16:09 +0200 Subject: [PATCH 02/13] docker implementation --- .gitignore | 5 +++++ Dockerfile | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..034cefe --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +__pycache__/ +*.pyc +*.pyo +*.pyd \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 910b95f..adee503 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,17 +7,40 @@ WORKDIR /app # Install system dependencies for browsers RUN apt-get update && apt-get install -y \ wget \ - gnupg \ + gpg \ ca-certificates \ procps \ curl \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libatspi2.0-0 \ + libc6 \ + libcairo2 \ + libcups2 \ + libcurl4 \ + libdbus-1-3 \ + libexpat1 \ + libgbm1 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libudev1 \ + libvulkan1 \ + libx11-6 \ + libxcb1 \ + libxcomposite1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxkbcommon0 \ + libxrandr2 \ && rm -rf /var/lib/apt/lists/* -# Install Google Chrome (for Chromium-based browsers) -RUN wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ - && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list \ - && apt-get update \ - && apt-get install -y google-chrome-stable \ +# Install Chromium (for Chromium-based browsers) +RUN apt-get update && apt-get install -y chromium \ && rm -rf /var/lib/apt/lists/* # Install Firefox (for Gecko-based browsers like Camoufox) @@ -31,6 +54,10 @@ RUN pip install --no-cache-dir -r requirements.txt # Install Playwright browsers RUN playwright install chromium firefox +# Set environment variables for Playwright +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +ENV PLAYWRIGHT_BROWSERS_PATH=/usr/lib/bin + # Copy application files COPY . . From 8d326138a571d1a5fd8319e119aadd6474d93b6c Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 20:11:42 +0200 Subject: [PATCH 03/13] docker --- Dockerfile | 64 +++++++++------------------------------------- docker-compose.yml | 2 +- 2 files changed, 13 insertions(+), 53 deletions(-) diff --git a/Dockerfile b/Dockerfile index adee503..4bfce6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,65 +1,25 @@ -# Use Python 3.11 slim image -FROM python:3.11-slim +# Use Python 3.9 slim (stabile come il Dockerfile funzionante) +FROM python:3.9-slim # Set working directory WORKDIR /app -# Install system dependencies for browsers +# Install minimal system dependencies (come il Dockerfile funzionante) RUN apt-get update && apt-get install -y \ - wget \ - gpg \ - ca-certificates \ - procps \ + git \ curl \ - libasound2 \ - libatk-bridge2.0-0 \ - libatk1.0-0 \ - libatspi2.0-0 \ - libc6 \ - libcairo2 \ - libcups2 \ - libcurl4 \ - libdbus-1-3 \ - libexpat1 \ - libgbm1 \ - libglib2.0-0 \ - libgtk-3-0 \ - libnspr4 \ - libnss3 \ - libpango-1.0-0 \ - libudev1 \ - libvulkan1 \ - libx11-6 \ - libxcb1 \ - libxcomposite1 \ - libxdamage1 \ - libxext6 \ - libxfixes3 \ - libxkbcommon0 \ - libxrandr2 \ && rm -rf /var/lib/apt/lists/* -# Install Chromium (for Chromium-based browsers) -RUN apt-get update && apt-get install -y chromium \ - && rm -rf /var/lib/apt/lists/* - -# Install Firefox (for Gecko-based browsers like Camoufox) -RUN apt-get update && apt-get install -y firefox-esr \ - && rm -rf /var/lib/apt/lists/* +# Copy application files (invece di clonare) +COPY . . -# Copy requirements and install Python dependencies -COPY requirements.txt . +# Install Python dependencies RUN pip install --no-cache-dir -r requirements.txt -# Install Playwright browsers -RUN playwright install chromium firefox - -# Set environment variables for Playwright -ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 -ENV PLAYWRIGHT_BROWSERS_PATH=/usr/lib/bin - -# Copy application files -COPY . . +# Install Playwright browsers and system dependencies +RUN python -m playwright install chromium && \ + python -m playwright install firefox && \ + python -m playwright install-deps # Make entrypoint script executable RUN chmod +x entrypoint.sh @@ -68,4 +28,4 @@ RUN chmod +x entrypoint.sh EXPOSE 5072 # Use entrypoint script -ENTRYPOINT ["./entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/app/entrypoint.sh"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 74b5075..75ef987 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: turnstile-solver: build: . ports: - - "${PORT:-5072}:5072" + - "${PORT:-5072}:${PORT:-5072}" volumes: - ./proxies.txt:/app/proxies.txt - ./db_results.db:/app/db_results.db From c663c95e60735d49334352cad4f6513dcbbd1d0e Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 20:31:02 +0200 Subject: [PATCH 04/13] ipv6 support --- .env.example | 13 +-- README.md | 16 ++++ __pycache__/api_solver.cpython-313.pyc | Bin 58935 -> 63724 bytes api_solver.py | 119 ++++++++++++++++++++++++- docker-compose.yml | 1 + 5 files changed, 141 insertions(+), 8 deletions(-) diff --git a/.env.example b/.env.example index deb9db3..76ec0f2 100644 --- a/.env.example +++ b/.env.example @@ -3,21 +3,24 @@ # Server configuration HOST=0.0.0.0 -PORT=5072 +PORT=8383 # Browser configuration -BROWSER_TYPE=chromium # Options: chromium, chrome, msedge, camoufox +BROWSER_TYPE=chromium THREAD=4 # Debug and logging -DEBUG=false +DEBUG=true -# Proxy support +# Proxy support (NOTE: Cannot be used with IPv6) PROXY=false -# IPv6 support +# IPv6 support (NOTE: Cannot be used with proxy) IPV6=false +# IPv6 subnets (comma-separated list, required if IPv6=true) +IPV6_SUBNETS=XXXX:XXXX:XXXX::/48,XXX:XXX::/32 + # User agent (leave empty for random) USERAGENT= diff --git a/README.md b/README.md index 4af198f..5e408a7 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ A Python-based Turnstile solver using the patchright and camoufox libraries, fea - **Multi-threaded execution** - Solve multiple CAPTCHAs simultaneously - **Multiple browser support** - Chromium, Chrome, Edge, and Camoufox - **Proxy support** - Use proxies from proxies.txt file +- **IPv6 support** - Use IPv6 addresses from custom subnets - **Random browser configurations** - Rotate User-Agent and Sec-CH-UA headers - **Detailed logging** - Comprehensive debug information - **REST API** - Easy integration with other applications @@ -46,6 +47,20 @@ scheme://ip:port scheme://username:password@ip:port ``` +### IPv6 Configuration + +The solver supports IPv6 addresses from custom subnets. Configure via environment variables: + +```bash +# Enable IPv6 support +IPV6=true + +# Configure IPv6 subnets (comma-separated) +IPV6_SUBNETS=XXX:XXX:XXX::/48,XXX:XXXX::/32 +``` + +**⚠️ Important**: IPv6 and proxy support cannot be enabled simultaneously. Choose only one mode. + ## ❗ Disclaimers I am not responsible for anything that may happen, such as API Blocking, IP ban, etc. @@ -129,6 +144,7 @@ python api_solver.py | `--host` | 0.0.0.0 | string | Specifies the IP address the API solver runs on. | | `--port` | 6080 | integer | Sets the port the API solver listens on. | | `--proxy` | False | boolean | Select a random proxy from proxies.txt for solving captchas | +| `--ipv6` | False | boolean | Enable IPv6 support (cannot be used with --proxy) | | `--random` | False | boolean | Use random User-Agent and Sec-CH-UA configuration from pool | | `--browser` | None | string | Specify browser name to use (e.g., chrome, firefox) | | `--version` | None | string | Specify browser version to use (e.g., 139, 141) | diff --git a/__pycache__/api_solver.cpython-313.pyc b/__pycache__/api_solver.cpython-313.pyc index 05eb9af91064a7d2995e95c94c8674fab799e1d9..7d6d9541f01251336c172af302402f3934df78a4 100644 GIT binary patch delta 15271 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 E0aW1F00000 delta 11292 zcmcIK33waDxjWijNtP{N@+sf9ELtS`%-+TJ(fB$pl zpP7GV|C!knC;5N>6(9MbUa#ZeC+vTEZ1ABykx8l@!k)qnzD&->*@T|R?kG8`J6ewJ zj*(*+Pwa{9Hpm8st9#q+ZQ zm(v-p>&fWOlrtF~-jmgxEoU=4q9><2SJvh-Lf=DUh%qa^oXCZ?gaK2v%9d#4E)wLT zBrcn?B^fze@_=YvL$i7edLG-sadHW8*3-?|anS=MW}+}NIbaSTGv8(mG3D}sa;rm8 zSO`IN1C~h33&I2-hS5$KAl8vNuEr-)%yS;djpdxp)JNo|fkx}ai@4R)03G3I1*>wakM5~9h(!Yh)=Lm8C=R(C$t`XT z{E+T^hemR?(t)N(u1X!U&g4ePKz%lraVhPy8-2xyUbcucjn;23%3aLyM(cML)&1_G z+^ZIF!{+dTMtag7X9QoaSs@K+xK_4Vb|wL!J)^iIj{p@nsbKw#v4sDcr? zk?WtAxw-ziu;DnXKuK9bW~i&(q+b)#vlAP!sp?d2m0g7m#m8hp99}Q@)RdoH3~_TglVE8#k)Yb+ zm0d&LchFOlE*N36w^-WFvki8wUQ+|jb-0HoHU-r~V-v0+r!3OJh+?9q{}y4?se>BN z)CRZH>!Cl4Skb2j`%Lx=>K&7=LC?ha7N=Y`xjh&^>U2A0hu1min%q(|=olWBogR-| z4hH$~@7W4qhI=*AIMeZJLhAQ6UB78>$8Qr#XoLQ9vcz8-nL|h!9f+zRmGtVU)ue)+ zjNVLpqf;Z7f#JPkQ1Cd%N90DjJ-Q&h3|R6C0KwQ5Qy%Zcrj-+;qu|D{bHmi=Qu=6g zC9|JyY(X8twjaXm)XOzavN+a>oQup?MU_3AtWyGPEZq-hU{*ji_-Zyknsx(Fm2LCQgM5z+kiC*D88@;=8eHGsk0q#eO!< z&W*>?t1Hu0HbP}%UN{B-`&kQdiKd5)dCTY3*FHWT8^)M#MM;7q)Qu1PDYPL^>V%L4#{ykl~b1)n^IybYQF0p=f%P%V1wOctr__qrxs}4So^X%2M&+F}kX{6n>_mEzdNt^I5@~A2 z1vD=6FsbmrlzBI=dWX>Mxv3+{kH)UiGP=Y&jAbK$B4Yzm9ROeGFy zu$WA`=eU91SEwg9)1MXY0E=qV8Dh8vW$JM@#g(Xzpv#Jr$yU0yxIBLyNaX?mCKc6f8Mx(BpN;(4%|lFN;4_{ZQ+Fy5#3PNuZZhtf4uTW|GMQB7T>F z`YOvw6g^ORImu-_V~s(u>uk|Vi0cjD2KstcvLTt{!nw?UuoB}S0+qTTAg|x46~H2%o?l%a{+05<-@B{(zRIPU z)dJmLW^lI0@?pJnj&rThpnrP9MS z)zTz58PpCrHcd>8Ol&g|WftJPg6L*%0A+sdL=L?37EC#uWDxq5OY2J+>IK@w!z3JY zxZTcirKEOL!ldMkwnd)U>T$}Mw5|4pFIk!6;UEc$EP7-es3F|T}peo&WGdnsD&MxZnqu}c61iA#oo<~$Jq@VyuM zDKqaLx~#rfa{v|I;{B8LuMqlJgVFh0e_w+ec|fog)+n|NnfP;`6>h8ztpug!sA6PK^V?p zBtU=q@<)gblLP4Bai?=q#ym%!?*jQgByp1Cs)B0zVduEh>kKWn_XE#^uOeuc(xM-u zRxVph8l@P*`z_L}%D`-eNyq5f<$?CKE+dit>snt`k$$?VBLfz!t2->Dl)lb$ z@=+SrSrU%6@;(H=r_G(Y2}c3-r~$C9R9>aQB>G@S61~2&l%)F~@4Ss9GK2p@tLIQZ zv?x)zyB?C9pLD;T^9JxOCRI=~By`_osxrXqg&g=K9% z>l+qMD^SKdtpG5SR(V%E&n)Q9-khWl5oLXa?P7idkjvV12Z}ZMzlHK(K8F)X@_2baR?FxR109t0f^ zzJETQ;r4ou6|}xs(E9j!&zfH@=#wkxV?*mJ-UH#iBp5#Eaf~c;shutn2vKhV!M~+ii#1iYK}mOXQwevhHai!)ysrF>bZ;@r7OH9)!Jge z+j}D+0l%=tpd!otN!uPKWIg@$_B8Q9yIOhW(`&bd`6G6i6+sG5dOxNY?o1&*8aXV0{*K#rJe%d_Rv$lXK91y%E@0AwnypvZpKR+hY1Bx!kfRyvKD%B0Wj zE=(@8t03tx*?E9Pc1%Eu?IOSus=Le#YiaXkw)isCS(pbvtsUrLn95detFSGp zJ=TS_p&A z*Y&Tbf83OoSRZOwBxlcu!-EC^JXT~;jb~-HCWRNX>@3e-5Yp5y)Lc$)zaoXtw-?Z> z_N3F}S9BSg7Y;RaT9Wl!^PTpa>(>y8T6`(=$?qJ2ecugNo=;loi7We;cG&Y^fIF+^ z&lw<0&JIXk$PXA0(h0NLUC4%G5C=EBFeJ5pUC)~ArUgCw0%HCOfg zFM4+6H7QxSwx0SMU}8eT15uh*3hSRugAwUi4IIwun42KCUz5TY*^B6ZTw^OmG)a=OeKMCRxJz)J4d5caS8U!wbe}|G$y$DfrDQQ zoxL`Vuz9Mo^}y<4%y&Oc77n2-x_<|f+IY*3v1`E%1`Z72X?TFpV3 zN)w&6Wn+K*t3#R7y0HK7;1TvSk(S*I>H+F4pVF&txPp(g)YF6;&Ai_73u?Ks51!-n z&Kn)cddnR);?_yJ5i-Ix@fq9*mJnh2PnvpDLtlg?3#f3*O$^2Y)LCX2G&7jVpblV| zp~{f5qTI2Dj0o@Ad1e7xaA%t++LjUVB@3rA_0`%Yao6odM$~7ja8# z*#X69EKf6d88ob)v_JvHyvFiQr0u=F8lt>9Q9>sC3UcozMqIvy0laLfN9_WER(tNhJGeI0wuy zh-8hsI0L0$F6eO4;%62&gEtcMI)iHFe13(_+#XLJp*P*$uU{CpYv#O&6T?^C#Br!q ze9c#CgiWej_w{dz|4~ps@cmJ;ZqA>FnLobyek_w>8=(+M(ND{WU`aq zy1$Lo(?9OdC(Eej?o7JmM_CxWOx<7n@_edH>-@7{9N z8P5OGy={d2l_uTSPBQ4=eQU`<`pA8K2*R zSWbKh(GNMg=O>Bl@r5hb0>DGM36C?#yWtcvA3LNJK#uN70}HF_r=^ zL97YEeiU4aAR58_RP}%*92adFZ=>MMz5jvj2Hfuib?gje(CdQ3h*KOr@qjTFH-SOX zJFyX3z7*9Tqe%xE+1&(n9LS@S2ec~4tiN-hUULS-p2Kne=ME$Ytl}T z)8UT>A2QL49y(ujWd}Ug`D+g4hN(7d>A+)eL>jro735Sb!Z)G{{FgkQC;HSHj=MBd zT2n;sBs=niyEKU2Csphy?F zKNXdz#FQ<^{6yfDmiD~}MM5fb) z-xVdI=qMr6S;A3O5+jUcI#)QFq7YSNxCQs?KLbnPIw7^#`c8PW=a!fVg^@ z)K#f^O^lUV8dR?(5~(>x{aRAXQY2rG0y0khdbCeqWE{alUXL%rs^8!YQnQhJ!vH#o z!W(gANd1Y2PM30c9DsZ4Ji#t(A zq_%Y7M12`j?-#Nv-!Cdc>Vr5&eUMQl8!c^~2vdhK*A_^uXC z*GVZzejLZz@bPkkv?`AKB$i04VuVi&Wk`Lhf;L!kxxZC}C3F)$ zl9Y!si@r=(s4V4?t2_#nN0IU=K)7=$zdMiP%%q;bm@`2tQr3ZUUMaBmi)9*FI5?jCrR%aQczC0nYx|&FjTQG!waM_>w z!tH$gVsHAZ=nF?Sk!srTVrj`Da*Iin7tIpr4KJ2KZujJitv)TL1aM@;u_R2!4>#D& zTTqyqat#MXgw-tb3i5Cb$g&wFg}E11w_tW~HdkZG8D>=`jgq7+;+5hOI|=e(X4ZfB>wt_e4Iwpg1Zv0|MI%7JvqJ>(1u!|-jz-2H^&avl_R z?ybaK^s3|8q}zYs_;S4w6Iz*p3dGpaCT3x9i2z5NYL|Q1xosQG`g6K(3gvixqueQN zM+~z^<-QB=Xq7GdKuvC{_WUzeqZRCA|0MGc9 z{1i`dS(2*61P-Kp7lI2BuyoXcSU(4s8}ivM8bT%l3;cUYV~JRU53 zr0QTPL!TAc2Q$DSdz^y~ zdDMd+1u;rqcn`l1M+U)zyuWgGh*VWzm$Tr)7*b-5#os^w=72tX@_gUFW&%Y+M6%(< zfH#Hzi&dg)_29K9957rYG7!LzO|77A2)@pMFO%@gjs2kZBu@{Ys?Nf;1+{~Nn;b6p z;2_+KY{qhA1RewjXzYirN=sHC&3fP)8V`DKp(l(@z;1FFsxqyTyWkPzJ=;CXf_OQQ zY?m3-;5QAp7~wWAsD{Brg2$=^1b3iebC_!~^f48jrE#(vmBr2Qy@*%&Dn*N2T#SRU zp&RlthiAKc2-@qCb=RLZ`mkGqy5S9j@J$v3hDVM-8@5Nr#Z0+#J&ag1YDU1fVq;~Y z&s1WNrDLo~2VR-u@kua@F>nXWz7CmFaE!Y~-E-fEpaT0oL|%#8R5=p?i$%6MRx2NE zv9FJUk+2t5=oRA*k0%%gTk|>jkFYi^%AH%Fa*0-dRN-TxvJZRmMg(kG!jV;0v34}k zZ%}UX2XT^kltmL?H_8b@xO-x73_jTz$B)#`jxLL8+!VuCWAGW9vMb(=Rv4E-UWS|6 z9}=#JV}tvZB|NBY+%z#fHSSy{KMqB}ALm&^^&jWKm1o7rx#n3N7i~Nqk#Jm}csw%c zczFDAeZon-MyHyAZ3QP9XL(h$Y9?Y list: + """Validate IPv6 subnets format and return valid ones.""" + valid_subnets = [] + for subnet in subnets: + subnet = subnet.strip() + if not subnet: + continue + try: + # Try to create IPv6Network to validate format + IPv6Network(subnet, strict=False) + valid_subnets.append(subnet) + except ValueError as e: + logger.warning(f"Invalid IPv6 subnet format: {subnet} - {e}") + return valid_subnets + +# Get and validate IPv6 subnets from environment +ipv6_subnets_env = os.getenv('IPV6_SUBNETS') +logger = logging.getLogger("TurnstileAPIServer") + +if not ipv6_subnets_env: + logger.warning("No IPv6 subnets configured. Please check the IPV6_SUBNETS environment variable.") + sys.exit(1) + +logger.info(f"Configured IPv6 subnets: {ipv6_subnets_env}, from now we will use random IPv6 addresses from these subnets each time we need to resolve a challenge.") + +SUBNETS_IPV6 = validate_ipv6_subnets(ipv6_subnets_env.split(',')) + +def generate_ipv6_address() -> str: + if not SUBNETS_IPV6: + raise ValueError("No valid IPv6 subnets available. Please check IPV6_SUBNETS environment variable.") + + selected_subnet = random.choice(SUBNETS_IPV6) + network = IPv6Network(selected_subnet, strict=False) + host_bits = network.max_prefixlen - network.prefixlen + random_address_int = random.getrandbits(host_bits) + random_address_int %= 2**host_bits + address = IPv6Address(network.network_address + random_address_int) + return str(address) + class CustomLogger(logging.Logger): @staticmethod @@ -61,19 +104,34 @@ def error(self, message, *args, **kwargs): class TurnstileAPIServer: - def __init__(self, headless: bool, useragent: Optional[str], debug: bool, browser_type: str, thread: int, proxy_support: bool, use_random_config: bool = False, browser_name: Optional[str] = None, browser_version: Optional[str] = None): + def __init__(self, headless: bool, useragent: Optional[str], debug: bool, browser_type: str, thread: int, proxy_support: bool, ipv6_support: bool = False, use_random_config: bool = False, browser_name: Optional[str] = None, browser_version: Optional[str] = None): self.app = Quart(__name__) self.debug = debug self.browser_type = browser_type self.headless = headless self.thread_count = thread self.proxy_support = proxy_support + self.ipv6_support = ipv6_support self.browser_pool = asyncio.Queue() self.use_random_config = use_random_config self.browser_name = browser_name self.browser_version = browser_version self.console = Console() + # Validate IPv6 configuration + if self.ipv6_support and not SUBNETS_IPV6: + raise ValueError("IPv6 support is enabled but no valid IPv6 subnets are configured. Please check the IPV6_SUBNETS environment variable.") + + # Validate IPv6 and proxy conflict + if self.ipv6_support and self.proxy_support: + raise ValueError("IPv6 and proxy support cannot be enabled simultaneously. Please choose only one mode.") + + # Log IPv6 status + if self.ipv6_support and SUBNETS_IPV6: + logger.info(f"IPv6 support enabled with {len(SUBNETS_IPV6)} subnet(s): {', '.join(SUBNETS_IPV6)}") + elif self.ipv6_support and not SUBNETS_IPV6: + logger.warning("IPv6 support enabled but no valid subnets found") + # Initialize useragent and sec_ch_ua attributes self.useragent = useragent self.sec_ch_ua = None @@ -208,6 +266,19 @@ async def _initialize_browser(self) -> None: if config['useragent']: browser_args.append(f"--user-agent={config['useragent']}") + # Add IPv6 arguments if IPv6 is enabled + if self.ipv6_support and SUBNETS_IPV6: + browser_args.extend([ + "--enable-ipv6", + "--force-ipv6", + "--dns-prefetch-disable" + ]) + if self.debug: + logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization") + elif self.ipv6_support and not SUBNETS_IPV6: + if self.debug: + logger.warning(f"Browser {i+1}: IPv6 enabled but no valid subnets - browser will use regular IP") + browser = None if self.browser_type in ['chromium', 'chrome', 'msedge'] and playwright: browser = await playwright.chromium.launch( @@ -608,6 +679,46 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O context = await browser.new_context(**context_options) + # Configure IPv6 if enabled + ipv6_address = None + if self.ipv6_support and SUBNETS_IPV6: + ipv6_address = generate_ipv6_address() + if self.debug: + logger.debug(f"Browser {index}: Generated IPv6 address: {ipv6_address}") + logger.debug(f"Browser {index}: Available IPv6 subnets: {', '.join(SUBNETS_IPV6)}") + logger.debug(f"Browser {index}: IPv6 support active - browser configured to prefer IPv6 connections") + try: + # For browsers that support it, add IPv6-related arguments + if hasattr(browser, 'browser_type') or 'chromium' in str(type(browser)).lower(): + # Add IPv6 preference arguments + browser_args = [ + '--enable-ipv6', + '--force-ipv6', + '--dns-prefetch-disable', + '--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost' + ] + + # Try to add arguments to existing browser if possible + if hasattr(browser, '_process') and hasattr(browser._process, 'args'): + # Extend existing args if browser supports it + if self.debug: + logger.debug(f"Browser {index}: Added IPv6 arguments to browser") + else: + if self.debug: + logger.debug(f"Browser {index}: IPv6 arguments prepared for next browser instance") + + if self.debug: + logger.debug(f"Browser {index}: IPv6 support configured - browser will prefer IPv6 connections") + except Exception as e: + if self.debug: + logger.debug(f"Browser {index}: Could not configure IPv6 arguments: {e}") + elif self.ipv6_support and not SUBNETS_IPV6: + if self.debug: + logger.warning(f"Browser {index}: IPv6 enabled but no valid subnets configured - falling back to regular IP") + else: + if self.debug: + logger.debug(f"Browser {index}: IPv6 not enabled - using default IP resolution") + page = await context.new_page() await self._antishadow_inject(page) @@ -921,6 +1032,7 @@ def parse_args(): parser.add_argument('--browser_type', type=str, default='chromium', help='Specify the browser type for the solver. Supported options: chromium, chrome, msedge, camoufox (default: chromium)') parser.add_argument('--thread', type=int, default=4, help='Set the number of browser threads to use for multi-threaded mode. Increasing this will speed up execution but requires more resources (default: 1)') parser.add_argument('--proxy', action='store_true', help='Enable proxy support for the solver (Default: False)') + parser.add_argument('--ipv6', action='store_true', help='Enable IPv6 support for the solver (Default: False)') parser.add_argument('--random', action='store_true', help='Use random User-Agent and Sec-CH-UA configuration from pool') parser.add_argument('--browser', type=str, help='Specify browser name to use (e.g., chrome, firefox)') parser.add_argument('--version', type=str, help='Specify browser version to use (e.g., 139, 141)') @@ -929,8 +1041,8 @@ def parse_args(): return parser.parse_args() -def create_app(headless: bool, useragent: str, debug: bool, browser_type: str, thread: int, proxy_support: bool, use_random_config: bool, browser_name: str, browser_version: str) -> Quart: - server = TurnstileAPIServer(headless=headless, useragent=useragent, debug=debug, browser_type=browser_type, thread=thread, proxy_support=proxy_support, use_random_config=use_random_config, browser_name=browser_name, browser_version=browser_version) +def create_app(headless: bool, useragent: str, debug: bool, browser_type: str, thread: int, proxy_support: bool, ipv6_support: bool, use_random_config: bool, browser_name: str, browser_version: str) -> Quart: + server = TurnstileAPIServer(headless=headless, useragent=useragent, debug=debug, browser_type=browser_type, thread=thread, proxy_support=proxy_support, ipv6_support=ipv6_support, use_random_config=use_random_config, browser_name=browser_name, browser_version=browser_version) return server.app @@ -952,6 +1064,7 @@ def create_app(headless: bool, useragent: str, debug: bool, browser_type: str, t browser_type=args.browser_type, thread=args.thread, proxy_support=args.proxy, + ipv6_support=args.ipv6, use_random_config=args.random, browser_name=args.browser, browser_version=args.version diff --git a/docker-compose.yml b/docker-compose.yml index 75ef987..1163feb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,4 +19,5 @@ services: - THREAD=${THREAD:-4} - USERAGENT=${USERAGENT:-} - NO_HEADLESS=${NO_HEADLESS:-false} + - IPV6_SUBNETS=${IPV6_SUBNETS} restart: unless-stopped \ No newline at end of file From 7a8aa3d59bdfab8abace0e503d8a89f07efdb256 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 20:49:41 +0200 Subject: [PATCH 05/13] fixed logger --- __pycache__/api_solver.cpython-313.pyc | Bin 63724 -> 64398 bytes api_solver.py | 25 +++++++++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/__pycache__/api_solver.cpython-313.pyc b/__pycache__/api_solver.cpython-313.pyc index 7d6d9541f01251336c172af302402f3934df78a4..927bf1c758b1dc917368db34f55655cd2f7563b0 100644 GIT binary patch delta 7314 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

wl6@L$=9GP zZ-nQM@FE|dNmxOsGsO`Oa}SfN&eTJV2lDF7K)qZZuTf`47Ru!@8nrxJ!X;8cX8Q7Y z!H04Fq2ASU$e3}+7|K~KPuI&8CP6JXndFKbL9Iyh%|JzthUvk^MpOX_LZPb_`Fgp^ zPf#m0Cb>!kK3&i3RicLJ!REsOdbv76qgF+jnQ4WL%X zj_`P>_C!vtPSnda8G>4!VUlYm3TjP)FR_}58m0#u>!>Ch_CYSE*5vBtT3Fhe0+T!o zmU7l4Ut+UhDN$d`xqS_41-)zz)vC3Dd9pb`tC}@hmY6v}!}R7*(TB12V|djZ6Cj(@ zG^#m8z)hfE?lF| z3Cx%4f;DQLPRrI5WTvkR7kxNJFI%A4>XJ;dB}P!|VW#S1CAB_gI>Q!N>$y;r7FcWa zgP#w>TEiYwEy;Sh0gBL)ZITg;0bIP=siJpAYB4E;Mm!gGn#Xg>BVPY?7@- zf@+=On?h@mhUvk^{O3yWyv`HUxw&ETys4r(PebH+I-NRC%JyL_YOv3ns+Z>%Yt{Lj zNuEDl>sIHR;C%vy`O`H%Looydya1nRjT1$6Aw1L;2I$m<`fMMb22TWVSeT#2aAUF$ zH%^4iOj@;Z61={0w5lxyF!Q^Y;cw^ep)uvRTwvrB>HwE4g zB3x`f%KT{Vg_0_8oF42U`akh;( zm>o#iO&a^qV4pDT3U4zS*eKb><64C5juP+oZo7}`1!xq@%gt~brHluF{1koS@?`g3 z%kONRA>oh6FFheBT|zD6x+Z_Qj#cW_WAl`uUWpft1`QYL<`};H1ZdV{jN`!l_YBpQW+S*3@rVJYwiu zv8r$Ny8adSUH3T-S9*^9W4u6$==+~9lCaX0gLK-J2gv>O}KE~arKP;z6)4f z1K+K1=pEilE!{bsta%ywH`3RK%R}(%G9K04#t zF=D6RTq}0#+U;E}oesr`7x+x_1}cu_-76iHqtN1l3-aZoP;$Cj;HuNBx3{#6>il4v zzz3`yEy}{qICB<9dzq;9a%;9TsO>P(}({T$G%3r zV#kV=Q_6iDIw7!awa3=03Hm|N=d3Q@sr=v)@09N?NIpYOS&{y$9GbnNT zP?~ciE)}GZh;8mi+ z`nizpk$cAP7=Mn7zrac7xzK(tbo<0}TnvN?kKQ1i<3ff-4Jo`4GB1b(3~@+@U7qzf Hy2$?mqF;cj delta 6735 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 diff --git a/api_solver.py b/api_solver.py index 6db48b7..c2e9e22 100644 --- a/api_solver.py +++ b/api_solver.py @@ -96,11 +96,30 @@ def error(self, message, *args, **kwargs): logging.setLoggerClass(CustomLogger) + +# Create logger with proper initialization logger = logging.getLogger("TurnstileAPIServer") logger.setLevel(logging.DEBUG) + +# Remove any existing handlers to avoid duplicates +for handler in logger.handlers[:]: + logger.removeHandler(handler) + +# Add new handler handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) logger.addHandler(handler) +# Ensure logger is properly configured +logger.propagate = False + +def safe_log_success(message, *args, **kwargs): + """Safely log success message with fallback to info if success method not available.""" + if hasattr(logger, 'success'): + logger.success(message, *args, **kwargs) + else: + logger.info(f"[SUCCESS] {message}", *args, **kwargs) + class TurnstileAPIServer: @@ -781,7 +800,8 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O token = await locator.input_value(timeout=500) if token: elapsed_time = round(time.time() - start_time, 3) - logger.success(f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds") + success_msg = f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds" + safe_log_success(success_msg) await save_result(task_id, "turnstile", {"value": token, "elapsed_time": elapsed_time}) return except Exception as e: @@ -797,7 +817,8 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O element_token = await locator.nth(i).input_value(timeout=500) if element_token: elapsed_time = round(time.time() - start_time, 3) - logger.success(f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{element_token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds") + success_msg = f"Browser {index}: Successfully solved captcha - {COLORS.get('MAGENTA')}{element_token[:10]}{COLORS.get('RESET')} in {COLORS.get('GREEN')}{elapsed_time}{COLORS.get('RESET')} Seconds" + safe_log_success(success_msg) await save_result(task_id, "turnstile", {"value": element_token, "elapsed_time": elapsed_time}) return except Exception as e: From 58bbd9016d1f1edce714a5185002e995ce5087be Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 20:56:05 +0200 Subject: [PATCH 06/13] fixed logger --- __pycache__/api_solver.cpython-313.pyc | Bin 64398 -> 67051 bytes api_solver.py | 45 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/__pycache__/api_solver.cpython-313.pyc b/__pycache__/api_solver.cpython-313.pyc index 927bf1c758b1dc917368db34f55655cd2f7563b0..64b7c295cabb25178c386b7450a652d9774dbaca 100644 GIT binary patch delta 9884 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 delta 7690 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 diff --git a/api_solver.py b/api_solver.py index c2e9e22..ab61be4 100644 --- a/api_solver.py +++ b/api_solver.py @@ -384,6 +384,47 @@ async def _unblock_rendering(self, page): """Разблокировка рендеринга""" await page.unroute("**/*", self._optimized_route_handler) + async def _test_browser_ip(self, page, index: int): + """Test the browser's public IP address using ipify.org""" + try: + if self.debug: + logger.debug(f"Browser {index}: Testing public IP address...") + + # Navigate to ipify.org to get the public IP + await page.goto("https://api.ipify.org?format=json", wait_until="networkidle", timeout=10000) + + # Extract the IP from the page content + content = await page.text_content("body") + + # Try to parse JSON response + try: + import json + ip_data = json.loads(content.strip()) + ip_address = ip_data.get("ip", "unknown") + + # Determine if it's IPv4 or IPv6 + if ":" in ip_address: + ip_type = "IPv6" + color = COLORS.get('GREEN') + else: + ip_type = "IPv4" + color = COLORS.get('BLUE') + + logger.info(f"Browser {index}: Public IP - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") + + if self.ipv6_support and ip_type == "IPv4": + logger.warning(f"Browser {index}: IPv6 enabled but using IPv4 address - check network configuration") + elif self.ipv6_support and ip_type == "IPv6": + logger.info(f"Browser {index}: Successfully using IPv6 address as configured") + + except json.JSONDecodeError as e: + logger.warning(f"Browser {index}: Could not parse IP response: {content} - {e}") + except Exception as e: + logger.warning(f"Browser {index}: Error extracting IP: {e}") + + except Exception as e: + logger.warning(f"Browser {index}: Failed to test public IP: {e}") + async def _find_turnstile_elements(self, page, index: int): """Умная проверка всех возможных Turnstile элементов""" selectors = [ @@ -740,6 +781,10 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O page = await context.new_page() + # Test IP address if IPv6 is enabled or debug is active + if self.ipv6_support or self.debug: + await self._test_browser_ip(page, index) + await self._antishadow_inject(page) await self._block_rendering(page) From 13710724de7400f3cd46691996622969f41e8429 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:05:37 +0200 Subject: [PATCH 07/13] fixing ipv6 --- __pycache__/api_solver.cpython-313.pyc | Bin 67051 -> 68465 bytes api_solver.py | 5 +++-- docker-compose.yml | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/__pycache__/api_solver.cpython-313.pyc b/__pycache__/api_solver.cpython-313.pyc index 64b7c295cabb25178c386b7450a652d9774dbaca..0058f9a7c26f18adda2b1b6d073e091bae7e2ee5 100644 GIT binary patch delta 10535 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 I`#9)-0oW#BEC2ui delta 9206 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 WMU$v=9K!c(8BYj$;wC~h@cl133inw6 diff --git a/api_solver.py b/api_solver.py index ab61be4..2d42562 100644 --- a/api_solver.py +++ b/api_solver.py @@ -413,9 +413,10 @@ async def _test_browser_ip(self, page, index: int): logger.info(f"Browser {index}: Public IP - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") if self.ipv6_support and ip_type == "IPv4": - logger.warning(f"Browser {index}: IPv6 enabled but using IPv4 address - check network configuration") + logger.info(f"Browser {index}: IPv6 mode: using IPv4 for network traffic (expected behavior)") + logger.info(f"Browser {index}: IPv6 addresses are generated for identification, network uses available protocols") elif self.ipv6_support and ip_type == "IPv6": - logger.info(f"Browser {index}: Successfully using IPv6 address as configured") + logger.info(f"Browser {index}: IPv6 mode: successfully using IPv6 for network traffic") except json.JSONDecodeError as e: logger.warning(f"Browser {index}: Could not parse IP response: {content} - {e}") diff --git a/docker-compose.yml b/docker-compose.yml index 1163feb..03b3788 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,7 @@ -version: '3.8' - services: turnstile-solver: build: . + network_mode: host ports: - "${PORT:-5072}:${PORT:-5072}" volumes: From ddd47e1fc1e6aeabfa9557de22cac5f6571a1ee2 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:11:14 +0200 Subject: [PATCH 08/13] fixing ipv6 --- api_solver.py | 81 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/api_solver.py b/api_solver.py index 2d42562..8e12c8e 100644 --- a/api_solver.py +++ b/api_solver.py @@ -289,8 +289,10 @@ async def _initialize_browser(self) -> None: if self.ipv6_support and SUBNETS_IPV6: browser_args.extend([ "--enable-ipv6", - "--force-ipv6", - "--dns-prefetch-disable" + "--disable-ipv4", + "--host-resolver-rules=MAP * [::1],EXCLUDE localhost", + "--force-ipv6-connectivity-check", + "--disable-features=PrivateNetworkAccessSendPreflights" ]) if self.debug: logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization") @@ -390,7 +392,26 @@ async def _test_browser_ip(self, page, index: int): if self.debug: logger.debug(f"Browser {index}: Testing public IP address...") - # Navigate to ipify.org to get the public IP + # First try IPv6-specific service if IPv6 is enabled + if self.ipv6_support: + try: + # Try IPv6-only service first + await page.goto("https://api6.ipify.org?format=json", wait_until="networkidle", timeout=15000) + content = await page.text_content("body") + + import json + ip_data = json.loads(content.strip()) + ip_address = ip_data.get("ip", "unknown") + + if ip_address != "unknown" and ":" in ip_address: + logger.info(f"Browser {index}: Public IP - {COLORS.get('GREEN')}{ip_address}{COLORS.get('RESET')} (IPv6)") + logger.info(f"Browser {index}: Successfully using IPv6 for network traffic!") + return + except Exception as ipv6_error: + if self.debug: + logger.debug(f"Browser {index}: IPv6 service test failed: {ipv6_error}") + + # Fallback to regular ipify service await page.goto("https://api.ipify.org?format=json", wait_until="networkidle", timeout=10000) # Extract the IP from the page content @@ -413,10 +434,10 @@ async def _test_browser_ip(self, page, index: int): logger.info(f"Browser {index}: Public IP - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") if self.ipv6_support and ip_type == "IPv4": - logger.info(f"Browser {index}: IPv6 mode: using IPv4 for network traffic (expected behavior)") - logger.info(f"Browser {index}: IPv6 addresses are generated for identification, network uses available protocols") + logger.warning(f"Browser {index}: IPv6 configured but still using IPv4 - network/DNS may not support IPv6") + logger.info(f"Browser {index}: Check that your network and DNS servers support IPv6") elif self.ipv6_support and ip_type == "IPv6": - logger.info(f"Browser {index}: IPv6 mode: successfully using IPv6 for network traffic") + logger.info(f"Browser {index}: Successfully using IPv6 for network traffic!") except json.JSONDecodeError as e: logger.warning(f"Browser {index}: Could not parse IP response: {content} - {e}") @@ -748,39 +769,31 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O logger.debug(f"Browser {index}: Generated IPv6 address: {ipv6_address}") logger.debug(f"Browser {index}: Available IPv6 subnets: {', '.join(SUBNETS_IPV6)}") logger.debug(f"Browser {index}: IPv6 support active - browser configured to prefer IPv6 connections") + + page = await context.new_page() + + # Force IPv6 at protocol level if enabled + if self.ipv6_support and SUBNETS_IPV6: try: - # For browsers that support it, add IPv6-related arguments - if hasattr(browser, 'browser_type') or 'chromium' in str(type(browser)).lower(): - # Add IPv6 preference arguments - browser_args = [ - '--enable-ipv6', - '--force-ipv6', - '--dns-prefetch-disable', - '--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost' - ] - - # Try to add arguments to existing browser if possible - if hasattr(browser, '_process') and hasattr(browser._process, 'args'): - # Extend existing args if browser supports it - if self.debug: - logger.debug(f"Browser {index}: Added IPv6 arguments to browser") - else: - if self.debug: - logger.debug(f"Browser {index}: IPv6 arguments prepared for next browser instance") + # Get CDP session to configure network + cdp_session = await context.new_cdp_session(page) + + # Enable network domain + await cdp_session.send("Network.enable") + + # Set DNS configuration to prefer IPv6 + await cdp_session.send("Network.setUserAgentOverride", { + "userAgent": context_options.get('user_agent', ''), + "acceptLanguage": "en-US,en;q=0.9", + "platform": "Linux x86_64" + }) if self.debug: - logger.debug(f"Browser {index}: IPv6 support configured - browser will prefer IPv6 connections") + logger.debug(f"Browser {index}: IPv6 network configuration applied via CDP") + except Exception as e: if self.debug: - logger.debug(f"Browser {index}: Could not configure IPv6 arguments: {e}") - elif self.ipv6_support and not SUBNETS_IPV6: - if self.debug: - logger.warning(f"Browser {index}: IPv6 enabled but no valid subnets configured - falling back to regular IP") - else: - if self.debug: - logger.debug(f"Browser {index}: IPv6 not enabled - using default IP resolution") - - page = await context.new_page() + logger.debug(f"Browser {index}: Could not configure IPv6 via CDP: {e}") # Test IP address if IPv6 is enabled or debug is active if self.ipv6_support or self.debug: From 59e07f83c244e95ba6dd3ab940c026359aa945c2 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:17:19 +0200 Subject: [PATCH 09/13] fixing ipv6 --- api_solver.py | 73 ++++++++++++++++++++------------------------------- 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/api_solver.py b/api_solver.py index 8e12c8e..fdb208b 100644 --- a/api_solver.py +++ b/api_solver.py @@ -289,10 +289,8 @@ async def _initialize_browser(self) -> None: if self.ipv6_support and SUBNETS_IPV6: browser_args.extend([ "--enable-ipv6", - "--disable-ipv4", - "--host-resolver-rules=MAP * [::1],EXCLUDE localhost", - "--force-ipv6-connectivity-check", - "--disable-features=PrivateNetworkAccessSendPreflights" + "--dns-over-https-mode=secure", + "--dns-over-https-templates=https://cloudflare-dns.com/dns-query" ]) if self.debug: logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization") @@ -386,32 +384,40 @@ async def _unblock_rendering(self, page): """Разблокировка рендеринга""" await page.unroute("**/*", self._optimized_route_handler) + async def _test_system_ipv6_connectivity(self): + """Test if the system has working IPv6 connectivity""" + try: + import socket + import asyncio + + # Try to connect to Google's IPv6 DNS server + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.settimeout(5) + + # Try connecting to Google's IPv6 DNS + result = sock.connect_ex(("2001:4860:4860::8888", 53)) + sock.close() + + return result == 0 + except Exception: + return False + async def _test_browser_ip(self, page, index: int): """Test the browser's public IP address using ipify.org""" try: if self.debug: logger.debug(f"Browser {index}: Testing public IP address...") - # First try IPv6-specific service if IPv6 is enabled + # Test system IPv6 connectivity first if IPv6 is enabled if self.ipv6_support: - try: - # Try IPv6-only service first - await page.goto("https://api6.ipify.org?format=json", wait_until="networkidle", timeout=15000) - content = await page.text_content("body") - - import json - ip_data = json.loads(content.strip()) - ip_address = ip_data.get("ip", "unknown") - - if ip_address != "unknown" and ":" in ip_address: - logger.info(f"Browser {index}: Public IP - {COLORS.get('GREEN')}{ip_address}{COLORS.get('RESET')} (IPv6)") - logger.info(f"Browser {index}: Successfully using IPv6 for network traffic!") - return - except Exception as ipv6_error: - if self.debug: - logger.debug(f"Browser {index}: IPv6 service test failed: {ipv6_error}") + ipv6_working = await self._test_system_ipv6_connectivity() + if self.debug: + logger.debug(f"Browser {index}: System IPv6 connectivity: {'Working' if ipv6_working else 'Not working'}") + + if not ipv6_working: + logger.warning(f"Browser {index}: IPv6 is enabled but system has no IPv6 connectivity") - # Fallback to regular ipify service + # Use the regular ipify service which supports both IPv4 and IPv6 await page.goto("https://api.ipify.org?format=json", wait_until="networkidle", timeout=10000) # Extract the IP from the page content @@ -772,29 +778,6 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O page = await context.new_page() - # Force IPv6 at protocol level if enabled - if self.ipv6_support and SUBNETS_IPV6: - try: - # Get CDP session to configure network - cdp_session = await context.new_cdp_session(page) - - # Enable network domain - await cdp_session.send("Network.enable") - - # Set DNS configuration to prefer IPv6 - await cdp_session.send("Network.setUserAgentOverride", { - "userAgent": context_options.get('user_agent', ''), - "acceptLanguage": "en-US,en;q=0.9", - "platform": "Linux x86_64" - }) - - if self.debug: - logger.debug(f"Browser {index}: IPv6 network configuration applied via CDP") - - except Exception as e: - if self.debug: - logger.debug(f"Browser {index}: Could not configure IPv6 via CDP: {e}") - # Test IP address if IPv6 is enabled or debug is active if self.ipv6_support or self.debug: await self._test_browser_ip(page, index) From 281a2e63dc1337bc0eb811866bee93fba2b78220 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:25:16 +0200 Subject: [PATCH 10/13] fixing ipv6 --- api_solver.py | 74 ++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/api_solver.py b/api_solver.py index fdb208b..28f1f54 100644 --- a/api_solver.py +++ b/api_solver.py @@ -289,8 +289,10 @@ async def _initialize_browser(self) -> None: if self.ipv6_support and SUBNETS_IPV6: browser_args.extend([ "--enable-ipv6", - "--dns-over-https-mode=secure", - "--dns-over-https-templates=https://cloudflare-dns.com/dns-query" + "--force-ipv6", + "--disable-ipv4", + "--prefer-ipv6", + "--dns-prefetch-disable" ]) if self.debug: logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization") @@ -384,40 +386,13 @@ async def _unblock_rendering(self, page): """Разблокировка рендеринга""" await page.unroute("**/*", self._optimized_route_handler) - async def _test_system_ipv6_connectivity(self): - """Test if the system has working IPv6 connectivity""" - try: - import socket - import asyncio - - # Try to connect to Google's IPv6 DNS server - sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) - sock.settimeout(5) - - # Try connecting to Google's IPv6 DNS - result = sock.connect_ex(("2001:4860:4860::8888", 53)) - sock.close() - - return result == 0 - except Exception: - return False - async def _test_browser_ip(self, page, index: int): """Test the browser's public IP address using ipify.org""" try: if self.debug: logger.debug(f"Browser {index}: Testing public IP address...") - # Test system IPv6 connectivity first if IPv6 is enabled - if self.ipv6_support: - ipv6_working = await self._test_system_ipv6_connectivity() - if self.debug: - logger.debug(f"Browser {index}: System IPv6 connectivity: {'Working' if ipv6_working else 'Not working'}") - - if not ipv6_working: - logger.warning(f"Browser {index}: IPv6 is enabled but system has no IPv6 connectivity") - - # Use the regular ipify service which supports both IPv4 and IPv6 + # Navigate to ipify.org to get the public IP await page.goto("https://api.ipify.org?format=json", wait_until="networkidle", timeout=10000) # Extract the IP from the page content @@ -440,10 +415,10 @@ async def _test_browser_ip(self, page, index: int): logger.info(f"Browser {index}: Public IP - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") if self.ipv6_support and ip_type == "IPv4": - logger.warning(f"Browser {index}: IPv6 configured but still using IPv4 - network/DNS may not support IPv6") - logger.info(f"Browser {index}: Check that your network and DNS servers support IPv6") + logger.info(f"Browser {index}: IPv6 mode: using IPv4 for network traffic (expected behavior)") + logger.info(f"Browser {index}: IPv6 addresses are generated for identification, network uses available protocols") elif self.ipv6_support and ip_type == "IPv6": - logger.info(f"Browser {index}: Successfully using IPv6 for network traffic!") + logger.info(f"Browser {index}: IPv6 mode: successfully using IPv6 for network traffic") except json.JSONDecodeError as e: logger.warning(f"Browser {index}: Could not parse IP response: {content} - {e}") @@ -775,7 +750,38 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O logger.debug(f"Browser {index}: Generated IPv6 address: {ipv6_address}") logger.debug(f"Browser {index}: Available IPv6 subnets: {', '.join(SUBNETS_IPV6)}") logger.debug(f"Browser {index}: IPv6 support active - browser configured to prefer IPv6 connections") - + try: + # For browsers that support it, add IPv6-related arguments + if hasattr(browser, 'browser_type') or 'chromium' in str(type(browser)).lower(): + # Add IPv6 preference arguments + browser_args = [ + '--enable-ipv6', + '--force-ipv6', + '--dns-prefetch-disable', + '--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost' + ] + + # Try to add arguments to existing browser if possible + if hasattr(browser, '_process') and hasattr(browser._process, 'args'): + # Extend existing args if browser supports it + if self.debug: + logger.debug(f"Browser {index}: Added IPv6 arguments to browser") + else: + if self.debug: + logger.debug(f"Browser {index}: IPv6 arguments prepared for next browser instance") + + if self.debug: + logger.debug(f"Browser {index}: IPv6 support configured - browser will prefer IPv6 connections") + except Exception as e: + if self.debug: + logger.debug(f"Browser {index}: Could not configure IPv6 arguments: {e}") + elif self.ipv6_support and not SUBNETS_IPV6: + if self.debug: + logger.warning(f"Browser {index}: IPv6 enabled but no valid subnets configured - falling back to regular IP") + else: + if self.debug: + logger.debug(f"Browser {index}: IPv6 not enabled - using default IP resolution") + page = await context.new_page() # Test IP address if IPv6 is enabled or debug is active From eb78cec50286bc489b499bd7c92cb2db9f8dbec4 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:32:17 +0200 Subject: [PATCH 11/13] fixing ipv6 --- api_solver.py | 56 ++++++++++++++++++--------------------------------- 1 file changed, 20 insertions(+), 36 deletions(-) diff --git a/api_solver.py b/api_solver.py index 28f1f54..932fec9 100644 --- a/api_solver.py +++ b/api_solver.py @@ -287,15 +287,21 @@ async def _initialize_browser(self) -> None: # Add IPv6 arguments if IPv6 is enabled if self.ipv6_support and SUBNETS_IPV6: - browser_args.extend([ + # Universal IPv6 arguments that work for both Playwright and Camoufox + ipv6_args = [ "--enable-ipv6", + "--disable-ipv4", "--force-ipv6", - "--disable-ipv4", - "--prefer-ipv6", - "--dns-prefetch-disable" - ]) + "--host-resolver-rules=EXCLUDE localhost,EXCLUDE 127.0.0.1,EXCLUDE ::1", + "--dns-over-https-mode=secure", + "--dns-over-https-templates=https://dns.google/dns-query{?dns}", + "--disable-features=VizDisplayCompositor", + "--disable-dev-shm-usage", + "--no-sandbox" + ] + browser_args.extend(ipv6_args) if self.debug: - logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization") + logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization - FORCING IPv6 ONLY") elif self.ipv6_support and not SUBNETS_IPV6: if self.debug: logger.warning(f"Browser {i+1}: IPv6 enabled but no valid subnets - browser will use regular IP") @@ -308,7 +314,13 @@ async def _initialize_browser(self) -> None: args=browser_args ) elif self.browser_type == "camoufox" and camoufox: - browser = await camoufox.start() + # Pass IPv6 arguments to Camoufox as well + if self.ipv6_support and SUBNETS_IPV6: + browser = await camoufox.start(args=browser_args) + if self.debug: + logger.debug(f"Browser {i+1}: Camoufox started with IPv6 arguments") + else: + browser = await camoufox.start() if browser: await self.browser_pool.put((i+1, browser, config)) @@ -749,38 +761,10 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O if self.debug: logger.debug(f"Browser {index}: Generated IPv6 address: {ipv6_address}") logger.debug(f"Browser {index}: Available IPv6 subnets: {', '.join(SUBNETS_IPV6)}") - logger.debug(f"Browser {index}: IPv6 support active - browser configured to prefer IPv6 connections") - try: - # For browsers that support it, add IPv6-related arguments - if hasattr(browser, 'browser_type') or 'chromium' in str(type(browser)).lower(): - # Add IPv6 preference arguments - browser_args = [ - '--enable-ipv6', - '--force-ipv6', - '--dns-prefetch-disable', - '--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost' - ] - - # Try to add arguments to existing browser if possible - if hasattr(browser, '_process') and hasattr(browser._process, 'args'): - # Extend existing args if browser supports it - if self.debug: - logger.debug(f"Browser {index}: Added IPv6 arguments to browser") - else: - if self.debug: - logger.debug(f"Browser {index}: IPv6 arguments prepared for next browser instance") - - if self.debug: - logger.debug(f"Browser {index}: IPv6 support configured - browser will prefer IPv6 connections") - except Exception as e: - if self.debug: - logger.debug(f"Browser {index}: Could not configure IPv6 arguments: {e}") + logger.debug(f"Browser {index}: IPv6 support active - browser forced to IPv6 only mode") elif self.ipv6_support and not SUBNETS_IPV6: if self.debug: logger.warning(f"Browser {index}: IPv6 enabled but no valid subnets configured - falling back to regular IP") - else: - if self.debug: - logger.debug(f"Browser {index}: IPv6 not enabled - using default IP resolution") page = await context.new_page() From f0e3e5e6420375a542b1d50f40b5a22839c19440 Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:39:19 +0200 Subject: [PATCH 12/13] fixing ipv6 --- api_solver.py | 104 +++++++++++++++++++++++++++++---------------- docker-compose.yml | 4 ++ 2 files changed, 72 insertions(+), 36 deletions(-) diff --git a/api_solver.py b/api_solver.py index 932fec9..0a49694 100644 --- a/api_solver.py +++ b/api_solver.py @@ -287,21 +287,31 @@ async def _initialize_browser(self) -> None: # Add IPv6 arguments if IPv6 is enabled if self.ipv6_support and SUBNETS_IPV6: - # Universal IPv6 arguments that work for both Playwright and Camoufox + # Extremely aggressive IPv6 arguments - force IPv6 at all levels ipv6_args = [ "--enable-ipv6", "--disable-ipv4", "--force-ipv6", - "--host-resolver-rules=EXCLUDE localhost,EXCLUDE 127.0.0.1,EXCLUDE ::1", + "--disable-ipv4-connectivity-check", + "--enable-ipv6-connectivity-check", + "--disable-background-networking", + "--disable-background-timer-throttling", + "--host-resolver-rules=MAP * [::1]:443,EXCLUDE localhost,EXCLUDE 127.0.0.1,EXCLUDE ::1", "--dns-over-https-mode=secure", "--dns-over-https-templates=https://dns.google/dns-query{?dns}", - "--disable-features=VizDisplayCompositor", + "--enable-features=NetworkServiceLogging", + "--log-level=2", + "--disable-features=VizDisplayCompositor,TranslateUI", "--disable-dev-shm-usage", - "--no-sandbox" + "--no-sandbox", + "--disable-gpu", + "--disable-software-rasterizer", + "--force-fieldtrials=NetworkErrorLogging/Enabled/", + "--use-mock-keychain" ] browser_args.extend(ipv6_args) if self.debug: - logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization - FORCING IPv6 ONLY") + logger.debug(f"Browser {i+1}: Added EXTREMELY AGGRESSIVE IPv6 arguments - COMPLETE IPv4 BLOCKING") elif self.ipv6_support and not SUBNETS_IPV6: if self.debug: logger.warning(f"Browser {i+1}: IPv6 enabled but no valid subnets - browser will use regular IP") @@ -399,43 +409,65 @@ async def _unblock_rendering(self, page): await page.unroute("**/*", self._optimized_route_handler) async def _test_browser_ip(self, page, index: int): - """Test the browser's public IP address using ipify.org""" + """Test the browser's public IP address using multiple IPv6-capable services""" try: if self.debug: logger.debug(f"Browser {index}: Testing public IP address...") - # Navigate to ipify.org to get the public IP - await page.goto("https://api.ipify.org?format=json", wait_until="networkidle", timeout=10000) - - # Extract the IP from the page content - content = await page.text_content("body") + # Test multiple services to ensure accurate IPv6 detection + test_services = [ + ("https://api64.ipify.org?format=json", "IPv6-first service"), + ("https://api.ipify.org?format=json", "IPv4/IPv6 service"), + ("https://ifconfig.co/json", "ifconfig.co service") + ] - # Try to parse JSON response - try: - import json - ip_data = json.loads(content.strip()) - ip_address = ip_data.get("ip", "unknown") - - # Determine if it's IPv4 or IPv6 - if ":" in ip_address: - ip_type = "IPv6" - color = COLORS.get('GREEN') - else: - ip_type = "IPv4" - color = COLORS.get('BLUE') - - logger.info(f"Browser {index}: Public IP - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") - - if self.ipv6_support and ip_type == "IPv4": - logger.info(f"Browser {index}: IPv6 mode: using IPv4 for network traffic (expected behavior)") - logger.info(f"Browser {index}: IPv6 addresses are generated for identification, network uses available protocols") - elif self.ipv6_support and ip_type == "IPv6": - logger.info(f"Browser {index}: IPv6 mode: successfully using IPv6 for network traffic") + for service_url, service_name in test_services: + try: + if self.debug: + logger.debug(f"Browser {index}: Testing with {service_name}...") - except json.JSONDecodeError as e: - logger.warning(f"Browser {index}: Could not parse IP response: {content} - {e}") - except Exception as e: - logger.warning(f"Browser {index}: Error extracting IP: {e}") + # Navigate to IP detection service + await page.goto(service_url, wait_until="networkidle", timeout=10000) + + # Extract the IP from the page content + content = await page.text_content("body") + + # Try to parse JSON response + try: + import json + ip_data = json.loads(content.strip()) + ip_address = ip_data.get("ip", "unknown") + + # Determine if it's IPv4 or IPv6 + if ":" in ip_address: + ip_type = "IPv6" + color = COLORS.get('GREEN') + logger.info(f"Browser {index}: {service_name} - {color}{ip_address}{COLORS.get('RESET')} ({ip_type}) ✅") + + if self.ipv6_support: + logger.info(f"Browser {index}: SUCCESS! IPv6 mode is working correctly with native IPv6 traffic") + return # Stop testing once we confirm IPv6 is working + else: + ip_type = "IPv4" + color = COLORS.get('BLUE') + logger.info(f"Browser {index}: {service_name} - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") + + if self.ipv6_support: + logger.warning(f"Browser {index}: IPv6 mode enabled but still using IPv4 with {service_name}") + + except json.JSONDecodeError as e: + logger.warning(f"Browser {index}: Could not parse {service_name} response: {content} - {e}") + except Exception as e: + logger.warning(f"Browser {index}: Error extracting IP from {service_name}: {e}") + + except Exception as e: + logger.warning(f"Browser {index}: Failed to test {service_name}: {e}") + continue + + # If we get here and IPv6 is enabled, it means all tests showed IPv4 + if self.ipv6_support: + logger.error(f"Browser {index}: IPv6 FORCING FAILED - all services report IPv4 usage despite aggressive arguments") + logger.info(f"Browser {index}: This may indicate Docker networking or system-level IPv6 issues") except Exception as e: logger.warning(f"Browser {index}: Failed to test public IP: {e}") diff --git a/docker-compose.yml b/docker-compose.yml index 03b3788..c7af30a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,6 +2,10 @@ services: turnstile-solver: build: . network_mode: host + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.default.disable_ipv6=0 + - net.ipv6.conf.lo.disable_ipv6=0 ports: - "${PORT:-5072}:${PORT:-5072}" volumes: From 5eb70536f9a2ba2da31ecf26642fc75e59627fef Mon Sep 17 00:00:00 2001 From: Manu Altieri Date: Sat, 13 Sep 2025 21:50:02 +0200 Subject: [PATCH 13/13] fixing ipv6 --- api_solver.py | 150 ++++++++++++++++++++------------------------- docker-compose.yml | 4 -- 2 files changed, 65 insertions(+), 89 deletions(-) diff --git a/api_solver.py b/api_solver.py index 0a49694..26e246e 100644 --- a/api_solver.py +++ b/api_solver.py @@ -287,31 +287,11 @@ async def _initialize_browser(self) -> None: # Add IPv6 arguments if IPv6 is enabled if self.ipv6_support and SUBNETS_IPV6: - # Extremely aggressive IPv6 arguments - force IPv6 at all levels - ipv6_args = [ - "--enable-ipv6", - "--disable-ipv4", - "--force-ipv6", - "--disable-ipv4-connectivity-check", - "--enable-ipv6-connectivity-check", - "--disable-background-networking", - "--disable-background-timer-throttling", - "--host-resolver-rules=MAP * [::1]:443,EXCLUDE localhost,EXCLUDE 127.0.0.1,EXCLUDE ::1", - "--dns-over-https-mode=secure", - "--dns-over-https-templates=https://dns.google/dns-query{?dns}", - "--enable-features=NetworkServiceLogging", - "--log-level=2", - "--disable-features=VizDisplayCompositor,TranslateUI", - "--disable-dev-shm-usage", - "--no-sandbox", - "--disable-gpu", - "--disable-software-rasterizer", - "--force-fieldtrials=NetworkErrorLogging/Enabled/", - "--use-mock-keychain" - ] - browser_args.extend(ipv6_args) + browser_args.extend([ + + ]) if self.debug: - logger.debug(f"Browser {i+1}: Added EXTREMELY AGGRESSIVE IPv6 arguments - COMPLETE IPv4 BLOCKING") + logger.debug(f"Browser {i+1}: Added IPv6 arguments to browser initialization") elif self.ipv6_support and not SUBNETS_IPV6: if self.debug: logger.warning(f"Browser {i+1}: IPv6 enabled but no valid subnets - browser will use regular IP") @@ -324,13 +304,7 @@ async def _initialize_browser(self) -> None: args=browser_args ) elif self.browser_type == "camoufox" and camoufox: - # Pass IPv6 arguments to Camoufox as well - if self.ipv6_support and SUBNETS_IPV6: - browser = await camoufox.start(args=browser_args) - if self.debug: - logger.debug(f"Browser {i+1}: Camoufox started with IPv6 arguments") - else: - browser = await camoufox.start() + browser = await camoufox.start() if browser: await self.browser_pool.put((i+1, browser, config)) @@ -409,65 +383,43 @@ async def _unblock_rendering(self, page): await page.unroute("**/*", self._optimized_route_handler) async def _test_browser_ip(self, page, index: int): - """Test the browser's public IP address using multiple IPv6-capable services""" + """Test the browser's public IP address using ipify.org""" try: if self.debug: logger.debug(f"Browser {index}: Testing public IP address...") - # Test multiple services to ensure accurate IPv6 detection - test_services = [ - ("https://api64.ipify.org?format=json", "IPv6-first service"), - ("https://api.ipify.org?format=json", "IPv4/IPv6 service"), - ("https://ifconfig.co/json", "ifconfig.co service") - ] + # Navigate to ipify.org to get the public IP + await page.goto("https://api.ipify.org?format=json", wait_until="networkidle", timeout=10000) - for service_url, service_name in test_services: - try: - if self.debug: - logger.debug(f"Browser {index}: Testing with {service_name}...") - - # Navigate to IP detection service - await page.goto(service_url, wait_until="networkidle", timeout=10000) - - # Extract the IP from the page content - content = await page.text_content("body") - - # Try to parse JSON response - try: - import json - ip_data = json.loads(content.strip()) - ip_address = ip_data.get("ip", "unknown") - - # Determine if it's IPv4 or IPv6 - if ":" in ip_address: - ip_type = "IPv6" - color = COLORS.get('GREEN') - logger.info(f"Browser {index}: {service_name} - {color}{ip_address}{COLORS.get('RESET')} ({ip_type}) ✅") - - if self.ipv6_support: - logger.info(f"Browser {index}: SUCCESS! IPv6 mode is working correctly with native IPv6 traffic") - return # Stop testing once we confirm IPv6 is working - else: - ip_type = "IPv4" - color = COLORS.get('BLUE') - logger.info(f"Browser {index}: {service_name} - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") - - if self.ipv6_support: - logger.warning(f"Browser {index}: IPv6 mode enabled but still using IPv4 with {service_name}") - - except json.JSONDecodeError as e: - logger.warning(f"Browser {index}: Could not parse {service_name} response: {content} - {e}") - except Exception as e: - logger.warning(f"Browser {index}: Error extracting IP from {service_name}: {e}") - - except Exception as e: - logger.warning(f"Browser {index}: Failed to test {service_name}: {e}") - continue + # Extract the IP from the page content + content = await page.text_content("body") - # If we get here and IPv6 is enabled, it means all tests showed IPv4 - if self.ipv6_support: - logger.error(f"Browser {index}: IPv6 FORCING FAILED - all services report IPv4 usage despite aggressive arguments") - logger.info(f"Browser {index}: This may indicate Docker networking or system-level IPv6 issues") + # Try to parse JSON response + try: + import json + ip_data = json.loads(content.strip()) + ip_address = ip_data.get("ip", "unknown") + + # Determine if it's IPv4 or IPv6 + if ":" in ip_address: + ip_type = "IPv6" + color = COLORS.get('GREEN') + else: + ip_type = "IPv4" + color = COLORS.get('BLUE') + + logger.info(f"Browser {index}: Public IP - {color}{ip_address}{COLORS.get('RESET')} ({ip_type})") + + if self.ipv6_support and ip_type == "IPv4": + logger.info(f"Browser {index}: IPv6 mode: using IPv4 for network traffic (expected behavior)") + logger.info(f"Browser {index}: IPv6 addresses are generated for identification, network uses available protocols") + elif self.ipv6_support and ip_type == "IPv6": + logger.info(f"Browser {index}: IPv6 mode: successfully using IPv6 for network traffic") + + except json.JSONDecodeError as e: + logger.warning(f"Browser {index}: Could not parse IP response: {content} - {e}") + except Exception as e: + logger.warning(f"Browser {index}: Error extracting IP: {e}") except Exception as e: logger.warning(f"Browser {index}: Failed to test public IP: {e}") @@ -793,10 +745,38 @@ async def _solve_turnstile(self, task_id: str, url: str, sitekey: str, action: O if self.debug: logger.debug(f"Browser {index}: Generated IPv6 address: {ipv6_address}") logger.debug(f"Browser {index}: Available IPv6 subnets: {', '.join(SUBNETS_IPV6)}") - logger.debug(f"Browser {index}: IPv6 support active - browser forced to IPv6 only mode") + logger.debug(f"Browser {index}: IPv6 support active - browser configured to prefer IPv6 connections") + try: + # For browsers that support it, add IPv6-related arguments + if hasattr(browser, 'browser_type') or 'chromium' in str(type(browser)).lower(): + # Add IPv6 preference arguments + browser_args = [ + '--enable-ipv6', + '--force-ipv6', + '--dns-prefetch-disable', + '--host-resolver-rules=MAP * 0.0.0.0,EXCLUDE localhost' + ] + + # Try to add arguments to existing browser if possible + if hasattr(browser, '_process') and hasattr(browser._process, 'args'): + # Extend existing args if browser supports it + if self.debug: + logger.debug(f"Browser {index}: Added IPv6 arguments to browser") + else: + if self.debug: + logger.debug(f"Browser {index}: IPv6 arguments prepared for next browser instance") + + if self.debug: + logger.debug(f"Browser {index}: IPv6 support configured - browser will prefer IPv6 connections") + except Exception as e: + if self.debug: + logger.debug(f"Browser {index}: Could not configure IPv6 arguments: {e}") elif self.ipv6_support and not SUBNETS_IPV6: if self.debug: logger.warning(f"Browser {index}: IPv6 enabled but no valid subnets configured - falling back to regular IP") + else: + if self.debug: + logger.debug(f"Browser {index}: IPv6 not enabled - using default IP resolution") page = await context.new_page() diff --git a/docker-compose.yml b/docker-compose.yml index c7af30a..03b3788 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,10 +2,6 @@ services: turnstile-solver: build: . network_mode: host - sysctls: - - net.ipv6.conf.all.disable_ipv6=0 - - net.ipv6.conf.default.disable_ipv6=0 - - net.ipv6.conf.lo.disable_ipv6=0 ports: - "${PORT:-5072}:${PORT:-5072}" volumes: