From 778f2953b92f3333c76a5c8ccaf1f8333907c8df Mon Sep 17 00:00:00 2001 From: Christian Seibold Date: Sun, 20 May 2018 15:24:39 -0500 Subject: [PATCH] Delete core and lib directories during uninstallation, Add imachug's ZeroNet Cmd Lib under Tools directory --- Output/ZeroNetInstaller.exe | Bin 10423691 -> 10423695 bytes Tools/ZeroNet-cmd-lib/.gitignore | 2 + Tools/ZeroNet-cmd-lib/lib/__init__.py | 0 Tools/ZeroNet-cmd-lib/lib/args.py | 2 + Tools/ZeroNet-cmd-lib/lib/callable.py | 193 ++++++++ Tools/ZeroNet-cmd-lib/lib/config.py | 139 ++++++ Tools/ZeroNet-cmd-lib/requirements.txt | 2 + Tools/ZeroNet-cmd-lib/zerohello.py | 222 ++++++++++ Tools/ZeroNet-cmd-lib/zeroname.py | 109 +++++ Tools/ZeroNet-cmd-lib/zeronet.py | 418 ++++++++++++++++++ Tools/ZeroNet-cmd-lib/zeronet_lib/__init__.py | 0 .../ZeroNet-cmd-lib/zeronet_lib/addresses.py | 2 + Tools/ZeroNet-cmd-lib/zeronet_lib/instance.py | 26 ++ Tools/ZeroNet-cmd-lib/zeronet_lib/site.py | 50 +++ Tools/ZeroNet-cmd-lib/zeronet_lib/user.py | 11 + .../zeronet_lib/zerowebsocket.py | 46 ++ ZeroNetInstaller.iss | 6 +- 17 files changed, 1227 insertions(+), 1 deletion(-) create mode 100644 Tools/ZeroNet-cmd-lib/.gitignore create mode 100644 Tools/ZeroNet-cmd-lib/lib/__init__.py create mode 100644 Tools/ZeroNet-cmd-lib/lib/args.py create mode 100644 Tools/ZeroNet-cmd-lib/lib/callable.py create mode 100644 Tools/ZeroNet-cmd-lib/lib/config.py create mode 100644 Tools/ZeroNet-cmd-lib/requirements.txt create mode 100644 Tools/ZeroNet-cmd-lib/zerohello.py create mode 100644 Tools/ZeroNet-cmd-lib/zeroname.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet_lib/__init__.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet_lib/addresses.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet_lib/instance.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet_lib/site.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet_lib/user.py create mode 100644 Tools/ZeroNet-cmd-lib/zeronet_lib/zerowebsocket.py diff --git a/Output/ZeroNetInstaller.exe b/Output/ZeroNetInstaller.exe index c5b8d75a94f526daaa3d4c06b8f5995aa40202dc..604c28e26a379d886ab05f1522e5d9ed666b8237 100644 GIT binary patch delta 13113 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 delta 13113 zcmWmLV{@Jj69(Yewrw=Foiw&>H8$=fjcuDxY^$-8#%6=YM#J}dKO8gr2X=O6&fSEZ zcc>UZP$~EfQ4Dec7`${A7#J7^*s~0xv)Xnh7}y3lm`b`3WCYO+H~<3h{~8Ja4S)f_ z0^k7f00aOc011E$Kmniv&;aNF3;-qo3xEy40pJ4g0Qdj`03m<~Knx%OkOIg6;Mh`Cx8pU4d4Or0{8&@00Dp?KnNfV z5CMn+!~o&|34kO(3Lp)T0muU60N(-f00n>|Knb7>Pywg{)Bx%L4S*&<3!n|q0q6qs z0Qvv}fFZyLU<@z;m;%fI<^T(TCBO<`4X^>&0_*_x00)30;0M47;0$m9xB}b&?f?&f zC%_Bf4e$Z@0{j5}fB--s00amE1Oq|W9+RnaeW0U$o;0_Wc9@_<9GU#8#qt1Gs^GV zuY%Q^&{6+hQ02z{jFeCn`?B-6yGko|0WFH6uyNKAkqxPB)nJ6W$_J|M-?iSi8S4tE zykQvVe5z?1CeX%e3c0kW0djhltFLOGC@#Na)rm5V$eZeFb?Yf{RqJ(UM3qLg+Y>i? zD7T=DiD((E5<+u`x29CljZOe7t9V^LOT4%5EzBf3wd9>eu!G4odiWV?;pXVIu%#)u zne{hZ`3$xTv|z3qp-2%4NT5qDKBKFuA?vsrB8c+bdcd&e2+M^(z|1>e!>S|&& zEgw}nDNG4HmXR5$j`6*UN0L!8Mf9RZhrw!EVQ%L=65EE(tnYBAH`6>D=H9GnA?#;pTKc%#9&O zsbRX1eY+{A*(O)r$I)x!`mp0Ig<(_@5XusNhB}L%tYAef5 zD%BInv|=4o8w&LCq!ImhAsY(8*h)EBk=Cdi*+V>k`(Hn?_s!M95ub0pjqFvJJf4%V zF~o+8A0kOP4P$yl;UPxYkOO7SHLrAKkS_=+5Y}eXRV9NZQ#bO|>{;b!fBaKM0fn#> z^Y3`^hl*HC`%yu`wy6)89MQiaC()ak$p6*0h6?c(R*Uk1*zI%~fLlXuYRf&=w{}#_A-!8G`Gr81CWg30M$^uawi+a3XT zlZU?p92j=iE9%dwIztSjaL=%%JW)Z%`Z-Z&yMiuO3hRrf`XQ`ZKW;75`hQ$Ft%7&x zN5&3F7B7jeH%{R-o<~tlr?AxBCH}W$pA!8Kt3rx$v4W~3>;@x(>ua4w>e)mPL`yOE z2A>uk!!sRgvmJ{pujZ?*qNcI#=yHW^lrH3KxR#m)H99#@fA<7$6oKkxyI`Rx`MF?e z1z!251BED1Xb9$LeXn`Sd;jQoy7bx42i@&@pZn2uPDs_o{idSN0PL znR};K!uv!XlO5JhOCjzkU5-}kKR8~}NijG!J+;$tuV`-ZEspe%CiS8*P$+E6&!Do( zJZE#&L75B_8jOeF7by$Z(Q85l5#u47e^DO+q|gyD9=6{W8fO0+ea&xnt+-{9nfW-s zcx5?y-KO|u>P1K!q<4+NM8;P%#{Hisc;7$?`}-D;^<}d;u2-1IIrAdXHw={MhRcm_ z1|J-+CGk?lIr*W|RAAk;{6_+%b|9S*7w4!udHi1X{q_~}&U~SY(qMMPZg75|n zaAvGiadGe@C`9;&^*aCp_+1a0M{AiQVg-?C|N_N2w~Z6*a4#!& z8*ubz_+SccLn*(gHo`!EQeI`N^k)TM1R}2%<93iL2kML07|85_V_3rH9R`eqAq$>U z*772>X|P~BS=qcj*@5nJ#3>+4l_7WVvK~{+_QG0%IB1Ia&{|JVyiLX1($KNJ89VYRa!BB(d#)?siBK_ zIqKo>ZcK>J*vh$aT?=@>uG_yxuYYt#6u=_gu!60uDSDWs`GOP+1w#+mi{dWH!MabH z#3c1mWAAHQrnk7Uk)iqsdtOv>sn7#y(D0Om;hS}j8Mxn|%SZzUR0`~nFJvup`h=65MtbcUx{~n|ARO@ZH7k_}o@Y(USzbz?RWm;&>sU#**2U?JX}^&jxyVOj zX{ez}&~#v(H-HQfQ2M?{Qazip(+ya4gI2n!6u-dq4<+Q<=o@FhsW}7}taqt9cBpSGholqmU^EI#-zyv6G<{ z&q~ynkY9h(##r(?ULV0Gn-Eq*tl^7aj3L}H@KJLUkoy-P2f^uVO}8g z*_`M$UN@F!J>6ozzXU=be=A~)?>4dZ6N4p*_58ytiZl`@R-))ZjNdJposA(ErW58) zq?OcyyWbI-U*mr{on4!`G3D+3gs_J>lG~NIxm^mP^*dZug3jz}T{2FvPMFj*j6kkE z>ruFKIpewDDHY$wC@N1@U>My}NepRSLWaF6${S-?VsZC<>sQCK{ciL$y~s}z;dRA( zsYab`MoXyls=X?ZruieM${17a`{c;0&BjmR7}>yN8~B`Km-4hr@4~k^;X@_#%Ull% z;V?JQU!-t1Qw&lj&dj^!h=EyI%g$oDa3vkol^!=t_B8E@8h)LJt&#vI1pekx$9FrJ zzu_&<y(jO!Ex8z(-8E7_XAFyQrglqpgk_tL&se|kfNIh z=nX*_71z;e7^R>IUlEK2Px(kxDB*lt`$y9Dq+k4V1?qmB3XY~RG`T4S0^{Uf>L2D3 z2ua{D00-R!TH3uSKtqK8Ti`S5puYCN*4=A{!!Ue7IjoTe+5W3p`_HQo5c#0|x?$VLiAV!*SiG6Tki zlXPA6u9YxTL)UL=bc3u6kzNUgRUWr;ki6pL3T@c!IcmCq)0Tifg*9UuE#{W>m zdphO6q(rWM9@yccc7`K`M=E7B6ybbN>8~uC((yUVt|^~A!&3o=gB=LLpg)G`Jb6pM|U^;i%{X9khREiAIg{ zz8D(7Aeob5p26}#0m0J0(y3`4XeFpZ$z_c`Co)7Evl|)8;`|?68bzw`9+e4dPqnES zj2E#5K3omyoaS|ppPDeQ>$ZNt1hLrXNqp)3#k)@k(+i8I>gu} zV}Sy0e@ZK4ps+;aMexFU9?0h6Gd{L=Khi;c=NF@fVVrCZ0i&C72&;|D!#{~8w;K>F zDVq=JD%(uWayu(9K{HV>){(5SYLR#~`G~%=x}IXl^pa*gyB>=&%*Yj9Sm#FRny&;%@$rz&BFf3PvVEz0*FG6d*04dGbg*bHoc># z_?IfLo1_|T0#`HBDwpR0&MNYe=*(x7)Uxiy2+Ed;Kx5F(XuS>gtTop2(Bp8wnT8Ds zYRnu-oPJ?Ue=Xu1*GTdwl}G*P+4=2)!uthRz+aUY*5jSA%7Uqi#PsLA2*7GJfcdCsWML z4UOAq^Fh`VLe-(dfj{bXOWi`WkczsNJ_o7EU&f;*Z&G9JD(T}?K}X2fVV?Mb zlv(V(tvxE^tUJ6d(ZtDFyc`n^{1Nfbu$S^{D?z3FORUReNUn;B=vUIyH**>F! z+Hy68I?#P+owL*{f;NO;VYmC^o(?l8%^Nz)NkE2HHP0z-*sH};MG2jWMkQC=FhbzR zu0%q(9LvvpHc{&!!BuzH%>?{*C!Oo#7M=|!sEKCMQlSTzGmZfQ8S8K#i$8jhip~W? ziGBD|A=4qLws2VE-I?n_KR!9SlZANSm%T)=Izf~_ISEt-5K%qO6vgnBMdsmo%wRhY z&ii7(7}l08aGE2av`=IA$O(^2o>Bibn=QJpAA?Cm9Lu&8-b?Ie8ROVGkM{oqmsUww zv=tjt=eW1MGrKW$=G!3@t~o9!TWZq)mr6F;f_LeMkD2i(W8Vujln&?1rvEiTD&2$a z*9h8owsFegA`2B)jIdF_6p@E1nv;NxP5Tw9kCgGJ)x`@kU`{u%wPY@gcl_NPvoVCx zwMX+k=NI}ipXkpxil85bbIE+;RcvF$=h{6LZWO0V!L_lFL;R`boCJqEbtL`XI+2-0 z<8k&(+jFk(?r@lsFcom=D`m?{LfU?qVjm!ZTUzqthyIb+p?J~;3>~^$Fs?{z8`rL* zS={f8Sp&>P1ug$f%eS=|CpelfK9m_0=4YGVt8#oBupe#7LFk>EesK0{M$Zlw>1JNM z<-|VzI9l?&E4QSG2kOkr_7WxziSeHKCgAt%4(t_ihDfLqae5qBC6a@G*WU0?pwU2f zk|Xb6#cMx2tk~|{@a^ilt_#)W9wrT7eOjvEcA*azG}-1Qn;8alXQgx5+&oQ!c%1$n8SsrSGTXW6M$MZ~a-WP>t(rkVg5a`ivu#{4b1WZQMe# z(yO>vZcq4jjuHHquER-PNchblM@iAyX{T8Ph|i8oB~_>;Xuw85>Xx1uI>L>5{;|F4r$WXSjMfDT(@cNukg!e8P^TtaQU9$IS}c}*E+|P*beC(7 z^oL8G?TMCK*|0bb)zwafp#_d33z=S+B)yaRdlt1h;)l}yv_gP!HdfXW^4Oynzm#}w&O_VTB2wT2}2a50N`F0XlJmzzt{Kj1&>vnmx zkp}`|dxDCJ>jd)msV!*w5|#qaoS^PSZ%;Ndzdkpvl6aZ;dZ?~ICRw^Z9CHWVjb)T4{wOSGngG|KGXTWw24(aOt zi&mdWHjGN;=Ge0l+!DA=b()YsLzmQKi5`x-{V^@(#7~uap88Ho)*3&k7cOWnn4;V= zkk~_eu#j4;x>;D0B>WW?>Ny$0&qZhL_}{KveP^7rh}xggZ3Ro*vZiu4i=f$&X^5;l zy@{B%uU2=W6DNH&MG$St8s>KYY7z;L>g}j6d2xK!GvRsxgG;%4n0H?lVDMD`JyYE8LLi-AX$!-3_;a$tF< zR|SHcL5Z@0@8t$jr9OkI*^!_MU6j7!eJd&f1q~r~H2J8^J*ZSIUESwb_xuK!h=CC( zkY`lNT#SLhj#pdhg6h)Jo|#@flU>Yqgjd0f{m0=zM0OemNy&yA#i~zW>E3fzz zE3R{UJ;RkgMTh_$*Dr0;gfo{%i!yo*E%sAMLl6vA)R=o~?+ElFE^Ui7?IuT!z-M4+ z*IM4~omNT*80etifM80-q$xt0I_B+nr_nO!jY_G6)MpO0+Y0gnSN|$7A};p@T_&Ei zGJ(oaHO~l_zImS4SZsu?OR+nk7XS2XF?Ua8+G2lw01=8!^M9B*_8sspn5(dlBeA;sdv?*M`zE4{|I6dZn?Ws%p4^v(0A9jz?^Xac zc@j>eTOthc?U{pjNIXf4hkSDKdG*JcRgMB*t;;Q;%Y#Y0+M}Yth|Q{RrF;yG!le}f z!@Yv(eP@E1F@V#O$07+;I|vC?(E2RtdPi`q|ob(HKJRfHtF@@S9~O#Vry7uEv_W@ z;(%fg&0cG96JBb&pg~q8T1Hd(caX2-FW>rtzf5q)JVi`hP$P2v2iVs*#6R-?sh8*xS;r@IGDUO7qHBjwq47qd>!&mT$az06qCKql^I z%BW!amP_E!IUo6`zj%_VaK2#GTskv`xB{}CfDWfXeR1J^AxkoleZ{laZxE9|ME^!w zP}rAP8BS1Iaig$eoh=_efv1bb6yPOhAnN3?)wK<+F38A^!7h#Kc)RLrlr)*SX;7P} z>e7IIZa*Ek>flp;8%Bys3xxvbqu+6aTgw!(;$fW_z=TO-&Wy|rOBby)q&DZBc?DvO z)ISxjzw#AOxb|!B360xabVb+mPzOg~rl+*Hw6ON+biP{H44^Xb+up37mX>{Yp(5;t zO@v#|GW9`K>iIz=1l8_5m)yUrF$V$lA8RQ!uRuso_&e|Ox6u_8a0Q@!H?{WF*X?~C zkQ*QC9gy~n4VLu2Ta^j*+!=HhaP}BE zT_$N%O(yJ?71M+NE0>Hnpu+7$m-t*FL;BZCbJ+`~2T#*(XO!o#nZJ$VPZVK7Ro2-W zuq%M*>R{Z0@2OpYww>!|K3K*(&RUm?d212Dx?t`vQgusLc3Z7FjS3obKdj*2 zO7!AH{|*_5+ch1}ny01uU*aS=ZCl7@394aci@Mj{;^-?> zti+XX4e<;P=V^XR4P+=85^-lZxY_tiJxZKUYk9xSCV!DWOSFOFy`((M|8i#9K;7*} zD>Wrvt&a_g&>vXok0eTV;-CCc8Kz@_t@M{li-_WGoKHvxDh(}T-v{~Uc5Eo;Xh`30 zE9T`WkHtDW=p96Eqn=SdGQ`}y|AT2c|J0<_c@cu7R!|h_4SZ(Q?aB*QGHhsUl<#r@f6PsKeI2&$mgcDR&XvX`rhi%2R`(H~dK4I61kMF>gjzSasqSaClbuG}>e z0p6_X3~B@}gUJ81Rq9?CBmd;})(DQq7<6zV}=T$fv@;apSl)1bcEN z+-%BTkNWEv&>|kjbj$Z zZzd%cu-HpJl9FSqFJV=&*H%i^?T{`W?80EHnmcB=G{b*%V_(9hFDu((9h0QWS(kCXqV zbGaNoY`L`c477>+#%ogi>pux*+u`RGj&HQ0uw@#VtPhE$^(fsBQMy|yWh}B$(cr>V zndgQ3);WI`#LfhV>58X=Cf;mx$@9x7Yz_Lphe~`iKpZC@^=I}Wa}qvo9d$G&?c3j> zjK+CnOk9Yv8E6v)_Y(Pb852$is~z@ngy_{O@RaT$kg{nLo&0F^b6^)NPqX{gSY9A?fc+S~wcAU^TzyQA69Ln5 zgt$eiNp~3b61D0+pSCwfHf1Y&>$i89_r~n|CBLHGIac^{K_iL}?zj6@tYxiogrfd! zE>sbL6*kDP=61OCZmUd@!MusKmo@D&FGlv$&7u?}h;ANU##|@MkUgcnD#V=EkoTo_ z0|tH4BPGS;1~XPBUhQb7kJ)so_vHWVN>m!;{5x3hiVs6lEoJ@5!LwRC{P4u^w%4t< zbNJxZ2@L$f#vlbI4WoAMlg3Y~t}AO{AsTlYz$uh~CrAAemD5oF zSn~M`b&SvtKHWazy{p=lm1G>cVwuab7r&W6Ev;|y$$@X)61?QQUW9{vfhpl#ELp;w z*NqJBXh-b(S<0WVK>aX5b@B_TroIVY-kST8o0OVssWI%~?~r{mv7Byv;tqXz0m|HE za#coiwR2W>Wx%qLJ>5MO+u8?carGj47ZJ(dEPCm&$~qrr%4bNG z{T;|TKgo9e5)15Cu;w4XDix)Q;B)>PYGDKk=!?aQa(s?rSfuvweZMtYMwfjTlb4~^ z&6V+a*oTqJUXJ=I_|nNlR#s8^?^V+N->TAJ&g@S)DjM-h3Co=1n8f)W=qR)`(U?Do)(L2(kY-!2347t)0@4B!9J0*78eN zR|d2K0+}b>2gXw8f3h;hbnP4vN@@b4cDoap2}CEDrw;bc@;@z|0$FswW^v zjUjv5WYh}!vL@NjLC>h!p5>j~7BEH-f>OiT!c%DxX%P<3D>PXfetduKIuHc**&3O* zNs_#xtyiQw|Kud%(L}$O3Kr1Z2R}9zKSC|+A9nj^x`XN~T$Yb=SGcos?Rt`~>CEGD zQC?f!a1LaAx_HWHCqzX^E)~D@oHuKiR(H&4m_7t?UHctF%GERld8olz+KI z`%nLd6J3=tyx&1y$bi@Nxg%)?sv(8x2a3H&%aB>G&|#fXKt*)B?oWZ7?Rf}T5rwJx z5~&HH6)l7@E!bdYN3bYk`9m;HQNSuQee8DOYYcGd6uyNVeAHpY!aUa)UEhQIF%{5D zMez5+c_(lriG=p#$4EyZ!Kb?rqSS48PDhs~NAR_o!0N_$1q|@FqBBpj==Srr=gB+7 zO9=9+l1?b};}Jl~lHh`1;%bf*FcahTX|>SR4;Z@0^tEIgO-6JV1N~g2N-o877to_K z5e3_O^jP59?PvtPfMY3-|H1{{z1*XNgg6k{p&hTq8t`B=>vWR5S+Z|3_wHw-)#f+S zH>m+N&Q2wWXCUX_3C}5ig*sM&B7{mNfM7Wd2dH%~!zgEGB|Xr7T0GQyanc?uJ^p+JC{oF_12vKWn;@s&%r;H?0>I*eR+O z{itc^@16+ZNd5$c8W=R!93q=sYZWkw2Cb`}Xq>0vgpSa2|Kko!f%(2PF#H%GvOJ`5 znWaq3DB_Ai)34!}JB4qJfJ{Rl|KxWSq#2e}DB)thpl+!{<;hEmSnJl@yoeH4Ai;J` zxR9pyivLT>=#^FeZYtx6N+%hrM6*owWZ=O+GWal#%pMuUw=Ic0_RFkIR>sHp`DGp% zLTEvg5luvBETuliYdfcI<&B3WJWW~n9c4Wuia*9M;iFKAq1WugNi$cDT7@R0mI&+Q z{6hqqqGClt%8kQ2zSfbkLc5WA*I~ff6UI7?!w9R1{;J2ARd4%)_R(Vil=Z;&{VMD@ z|Ild;YEuPd6(|b!5u`}Cx@BbHQJd!&O&FB0=f^o`>h`ufRLd$XX{y=50$N7 z78X2fK%o1+*y3HxP&fS2h7Dz4As{@}+(??WIy8G#IE|6mYU1Mh zDtVe&y3`;214P?9TP>WSm2R`)T!Uy_CbRjvpb%TKa~|+IjEvBh?mb_jgA z5A743AJ|;JS^iE}-uAU|Mvn+e4Zy$8&Yh(1Y5Xn#yDJtiB-A3VMkyK90omK#2vLxj z0D?TIOH;4OvG9+tJ!a;HRDbl}LfXi{ISvLp#pnEK?SI56C`~@$s)bd0!c}@?j!a)- zh_1v@jc$vo%yfj7pW$AT<)|D$wLRi4tJJ!Go!8!T0AUY(W$16(q=Q?he6C@FrzftpxvZ4bXF^siUz{E4srZH@pul<4ZBQG$g9rpn zQhEPP1ndh>!pl6m-{k%++by%6l1XPt=YVgcYV<-z(%$*{_~wf6XVJ$D?NXmAtMCgp zwotSMt7a`riK&(JkhSY}7wh-p{83zyB zMB?rPKYNRPC8o}By2_Tx^TSec*=_5b;IMpVlv8ST2AV^vV(D=5i9(-1nQenEM;rbh z2n@BtL!N?UH$jpfWO%w1Cme6n|5$nv_Ha&AAy8a|re1ROCrx7YNho{fr$B3EAjP4f z*!^x^-Y91EKPJ;^*yoa;F9Ut^QW@5w=V9_o-2}-BUetA`K^fmQ!IQ2ju zo8Y(CU7)l@sfK5$l*ma-*|sraDowt4X*!~MUiuv_w6r{ymOWR>eemf-ep`hFoAU!b zK_H17AB!60OQ@*9olP)8O|V7~oFaNMc--$J=G49w?rs)4&B`G30XNY7tGaGkUz;O~ zt}y?%+<`3(Qo^}JzF)?lF8k<@b2BN6_%WmSg)<9b*WcLfn#9Q1;~G%93Hvv?BHOl@ zKarj%9QlSi46?0#jtl+eYwlZE_0f&PDyz4{a50j+Ia4Zh`ARD2k_MsNeCaYO1N6Yt zNs5`HWRQlh(Iy)>YCs?+DX~w{zIlGQo&7e5D@rxf*T6)2Erw-1^lzV9y0qr*mY;;& zKb8LQ?S3HWahn;fO??<&Ss5rSF>9Nb!w&z9wuyVzn+N zht4-}#5>y@k|(5auSa)C!O&1>gl4vgxLpybJutV=2O}RqsL4i z7pfx)wINjNxA<)P)W$oxP_0pNYh4!Z^9Ie7$19i;%5-DQPm=B0&gu;|C-0)O7&vwj zl~v~wwQgdOmCi&YFAROhl;0O-T5gjWF4x0B{viosYNL&4=?AByJydw-*W6ORSn@*0JVwj8UTXTor`!`SLGKLrjWFBR z!S4zwCw#lK1B0zhTii1VB?A>%woDgdZtBPxG^k)TWL{MJZG4UZ2PNF(JGP#Fv`#a{ zU>H)$etB%+bo5|^TFPTQK>yDIL{2G|JC&th9_c}Y1SvsRHGMq3eHhmsb9;``vE zIH^1!TzxURxQArYrD6;0ez4TNae;2W%RB9(5`|9q^^+%UUNXrtuTNuf6IeJ~e-54&xrfDfO^6yKP?A+mWlj%9ig=9K31 z#CH+S!Ak`6eRZ%9gd>{iqTwo7X%Fx{7G7&Ik4ZJiZ%wUp%r;;pk;4AtG)=s<#+Bm= zBb{*fmJ>Nlm7QUM2kYUK-7K-)jTDcQjnK~LCU*`3{XkmeupCyibgP!_Tv6m_WNRJ0 zLfM+4D182pm2}=dxqs$%yjE;u_>oFqd{=|t#PRO8koVo}CiO_mp1vK!HPS8mu^N1V#-4HZQ}GLCid22CQ7`3`o@f&dk9vqpBIzVxW22xRm95uFv%uNJC!gs;$i$$BW;~gF@sT){Y?>Uh zBRZ;s-c{eZO8IKax1q3o;ZtyvwADpI?UK+CB(e8oi5N{;C|B)797Tm}vVw^+>zV9! z@SpYyW0c%H7;(5WWV8TPWF+B8FlCF$+ppUpna+IXy3t`PDkq)v7)F}4%*`UnPtXp7 z7J6Ly^4}{XKY=+J3~Qpc`G1&9cJ~ 6] + raise Callable.Error("Unknown command '%s'. Allowed commands are: %s" % (cmd, ", ".join(all_commands))) + + self.checkCall(cmd, handler, args) + + try: + return self.callArgs(handler, args) + except Callable.SubCommand as e: + if len(args) == 0: + raise Callable.Error("'%s' command is not a command but has subcommands." % cmd) + + if len(tuple(e)) == 0: + # Remove first argument and call it + return self.call("%s %s" % (cmd, args[0]), args[1:]) + else: + # Remove first argument and call given command + return self.call(tuple(e)[0], args[1:]) + except Callable.Redirect as e: + if len(tuple(e)) == 0: + # Remove first argument and call it (as SubCommand) + if len(args) == 0: + raise Callable.Error("'%s' command is not a command but has subcommands." % cmd) + return self.call("%s %s" % (cmd, args[0]), args[1:]) + elif len(tuple(e)) == 1: + # Call given value + return self.call(tuple(e)[0], args) + else: + # Call given value and arguments + return self.call(tuple(e)[0], tuple(e)[1]) + + def checkCall(self, cmd, func, argv): + import inspect + + expected_args = inspect.getargspec(func).args[1:] # Split "self" + defaults = inspect.getargspec(func).defaults or tuple() + + if self.checkArgs(cmd, func, argv): + return True + + if len(defaults) > 0: + default_args = reversed(zip(reversed(expected_args), reversed(defaults))) + default_args = map(lambda arg: "%s=%s" % arg, default_args) + expected_args = expected_args[:-len(default_args)] + default_args + + raise Callable.Error("Allowed arguments: %s" % ", ".join(expected_args)) + + def checkArgs(self, cmd, func, argv): + args, kwargs = self.parseArgs(argv) + + import inspect + + expected_args = inspect.getargspec(func).args[1:] # Split "self" + varargs = inspect.getargspec(func).varargs + keywords = inspect.getargspec(func).keywords + defaults = inspect.getargspec(func).defaults or tuple() + + resulting_args = dict() + if varargs is not None: + resulting_args[varargs] = [] + if keywords is not None: + resulting_args[keywords] = {} + + # Positional arguments + for cnt, value in enumerate(args): + if cnt < len(expected_args): + # Passed just as argument + resulting_args[expected_args[cnt]] = value + else: + # Passed to *args + if varargs is None: + raise Callable.Error("Too many positional arguments passed to '%s': expected at most %s, got %s." % (cmd, len(expected_args), len(args))) + else: + resulting_args[varargs].append(value) + + # Named arguments + handled_kwargs = [] + for name, value in kwargs.iteritems(): + if name in handled_kwargs: + raise Callable.Error("'%s' was passed to '%s' as named argument several times." % (name, cmd)) + + handled_kwargs.append(name) + + if name in expected_args: + # Passed just as argument + if name in resulting_args: + raise Callable.Error("'%s' was passed to '%s' as both positional argument and named." % (name, cmd)) + + resulting_args[name] = value + else: + # Passed to **kwargs + if keywords is None: + raise Callable.Error("Unknown named argument '%s' passed to '%s'." % (name, cmd)) + else: + resulting_args[keywords][name] = value + + # Defaults + if len(defaults) > 0: + for cnt, name in enumerate(expected_args[-len(defaults):]): + if name not in resulting_args: + resulting_args[name] = defaults[cnt] + + # Check that all the arguments were passed + for name in expected_args: + if name not in resulting_args: + raise Callable.Error("Argument '%s' was not passed to '%s'." % (name, cmd)) + + return True + + def parseArgs(self, argv): + args = [] + kwargs = {} + + kwname = None + + for arg in argv: + if arg.startswith("--"): + if kwname is not None: + kwargs[kwname] = True + + kwname = arg[2:] + else: + if kwname is None: + args.append(arg) + else: + kwargs[kwname] = arg + kwname = None + + if kwname is not None: + kwargs[kwname] = True + + return args, kwargs + + def callArgs(self, handler, argv): + args, kwargs = self.parseArgs(argv) + return handler(*args, **kwargs) + + def action(self, *args, **kwargs): + raise Callable.SubCommand + +class WithHelp(Callable): + def actionHelp(self, *cmd): + if cmd in [[], [""], tuple(), ("",)]: + # Print info about the class + print inspect.cleandoc(self.__doc__) + return + + try: + handler = getattr(self, "action" + "".join(map(lambda part: part[0].upper() + part[1:], cmd))) + if handler.__doc__ is not None: + print inspect.cleandoc(handler.__doc__) + return + except AttributeError: + pass + + if cmd == ["help"] or cmd == ("help",): + # Unable to find info on topic 'help' - no __doc__ in 'help' method or no 'help' method, use default help + print inspect.cleandoc(self.__doc__) + return + + print "Unknown topic '%s'" % " ".join(cmd) + +Callable.WithHelp = WithHelp \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/lib/config.py b/Tools/ZeroNet-cmd-lib/lib/config.py new file mode 100644 index 0000000..1dcc352 --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/lib/config.py @@ -0,0 +1,139 @@ +import os, json + +def recursiveDir(obj, prefix=""): + result = [] + for name in obj: + absolute = "%s.%s" % (prefix, name) if prefix != "" else name + if isinstance(obj[name], dict): + result += recursiveDir(obj[name], absolute) + else: + result.append(absolute) + return result + +class Config(object): + class AttributeError(AttributeError): + pass + + def __init__(self, path): + self.__dict__["path"] = path + + # Read single value + def __getattr__(self, name): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + return config[name] + except (KeyError, AttributeError): + raise Config.AttributeError("No '%s' config variable" % name) + except IOError: + raise Config.AttributeError("No config file and therefore no '%s' attribute" % name) + def __getitem__(self, name): + return self.__getattr__(name) + + # Read value recursively + def get(self, name, default=None): + name = name.split(".") + + try: + val = self[name[0]] + for part in name[1:]: + val = val[part] + + return val + except (KeyError, AttributeError, Config.AttributeError): + return default + + # Write single value + def __setattr__(self, name, value): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + except IOError: + config = dict() + + config[name] = value + + with open(self.path, "w") as f: + f.write(json.dumps(config)) + def __setitem__(self, name, value): + self.__setattr__(name, value) + + # Write value recursively + def set(self, name, value): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + except IOError: + config = dict() + + current = config + for part in name.split(".")[:-1]: + try: + current = current[part] + except KeyError: + current[part] = dict() + current = current[part] + + current[name.split(".")[-1]] = value + + with open(self.path, "w") as f: + f.write(json.dumps(config)) + + # Delete variable + def __delattr__(self, name): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + except IOError: + config = dict() + + del config[name] + + with open(self.path, "w") as f: + f.write(json.dumps(config)) + def __delitem__(self, name, value): + self.__delattr__(name, value) + + # Delete variable recursively + def remove(self, name): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + except IOError: + config = dict() + + current = config + for part in name.split(".")[:-1]: + try: + current = current[part] + except KeyError: + current[part] = dict() + current = current[part] + + del current[name.split(".")[-1]] + + with open(self.path, "w") as f: + f.write(json.dumps(config)) + + # Get data list + def __dir__(self): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + return dir(config) + except IOError: + return [] + + # Get data list recursively + def list(self): + try: + with open(self.path, "r") as f: + config = json.loads(f.read()) + except IOError: + return [] + + return recursiveDir(config) + +current_dir = os.path.dirname(os.path.abspath(__file__)) +config_json = os.path.abspath(os.path.join(current_dir, "../config.json")) +config = Config(config_json) \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/requirements.txt b/Tools/ZeroNet-cmd-lib/requirements.txt new file mode 100644 index 0000000..06671a1 --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/requirements.txt @@ -0,0 +1,2 @@ +psutil +websocket \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zerohello.py b/Tools/ZeroNet-cmd-lib/zerohello.py new file mode 100644 index 0000000..6623746 --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zerohello.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python + +import sys, datetime +from lib.callable import Callable +from lib.args import argv +from lib.config import config +import zeronet_lib.site as Site +import zeronet_lib.addresses as Addresses +from zeronet_lib.zerowebsocket import ZeroWebSocket + +class ZeroHello(Callable.WithHelp): + """ + Commands: + help Print this help + site Edit or get some info about site + feed Show feed + + Use 'help ' or 'help ' for more info + """ + + def action(self, *args, **kwargs): + if len(args) == 0: + if len(kwargs) == 0: + raise Callable.Redirect("help") + elif len(kwargs) == 1 and "help" in kwargs: + raise Callable.SubCommand("help") + else: + sys.stderr.write("Why are you passing named arguments to this command? Try 'help' instead.\n") + return 2 + else: + raise Callable.SubCommand + + def actionSite(self, *args, **kwargs): + """ + Edit or get some info about site + + Subcommands: + site pause Pause site + site resume Resume site + site favorites Manage favorite sites + """ + + raise Callable.SubCommand + + def actionSitePause(self, address): + """ + Pause site + + Usage: + site pause
Stop seeding
+ """ + + try: + with self.connect(self.getAddress()) as ws: + ws.send("sitePause", address=address) + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + def actionSiteResume(self, address): + """ + Resume site + + Usage: + site resume
Resume seeding
+ """ + + try: + with self.connect(self.getAddress()) as ws: + ws.send("siteResume", address=address) + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + def actionSiteFavorites(self, *args, **kwargs): + """ + Manage favorite sites + + Subcommands: + site favorites list Get all favorite sites + site favorites add Add site to favorites + site favorites remove Remove site from favorites + """ + + raise Callable.SubCommand + + def actionSiteFavoritesList(self): + """ + Get all favorite sites + + Usage: + site favorites list Print newline-separated favorites + """ + + try: + with self.connect(self.getAddress()) as ws: + settings = ws.send("userGetSettings") + favorites = settings.get("favorite_sites", {}).keys() + print "\n".join(favorites) + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + def actionSiteFavoritesAdd(self, address): + """ + Add site to favorites + + Usage: + site favorites add Add
to favorite sites +
+ """ + + try: + with self.connect(self.getAddress()) as ws: + settings = ws.send("userGetSettings") + + if "favorite_sites" not in settings: + settings["favorite_sites"] = {} + settings["favorite_sites"][address] = True + + ws.send("userSetSettings", settings) + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + def actionSiteFavoritesRemove(self, address): + """ + Remove site from favorites + + Usage: + site favorites remove Remove
from favorite sites +
+ """ + + try: + with self.connect(self.getAddress()) as ws: + settings = ws.send("userGetSettings") + + if "favorite_sites" not in settings: + settings["favorite_sites"] = {} + + try: + del settings["favorite_sites"][address] + except KeyError: + sys.stderr.write("%s is not in favorites.\n" % address) + return 1 + + ws.send("userSetSettings", settings) + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + def actionFeed(self, reverse=False, raw=None): + """ + Show feed + + Usage: + feed Display newsfeed + feed --reverse Show new posts first + feed --raw Use machine-readable format and separate items by + """ + + try: + with self.connect(self.getAddress()) as ws: + feed = ws.send("feedQuery", 30, 3) + + if "rows" in feed: + rows = feed["rows"] + else: + rows = feed + + if reverse: + rows.sort(key=lambda row: -row["date_added"]) + else: + rows.sort(key=lambda row: row["date_added"]) + + for row in rows: + title, body = row["title"], row["body"] + url, site = row["url"], row["site"] + feed_type, feed_name = row["type"], row["feed_name"] + date_added = row["date_added"] + + if raw is None: + date_added = datetime.datetime.fromtimestamp(date_added) + + print "%s on %s" % (feed_name.encode("utf-8"), date_added.strftime("%Y-%m-%d %H:%M:%S")) + print "<%s>" % title.encode("utf-8") + + if body: + print body.encode("utf-8") + + print "" + else: + print "%s\t%s\t%s\t%s\t%s\t%s" % tuple(map(lambda s: s.encode("utf-8"), [title, url, site, feed_type, feed_name]) + [date_added]) + print body.encode("utf-8") + print "---" if raw is True else raw + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + def getDataDirectory(self): + return config.get("data_directory", "%s/data" % config["root_directory"]) + def getAddress(self): + return config.get("homepage", Addresses.ZeroHello) + + def connect(self, site): + wrapper_key = Site.getWrapperkey(self.getDataDirectory(), site) + + address = config.get("server.address", "127.0.0.1") + port = config.get("server.port", "43110") + secure = config.get("server.secure", False) + + return ZeroWebSocket(wrapper_key, "%s:%s" % (address, port), secure) + +try: + sys.exit(ZeroHello(argv)) +except config.AttributeError as e: + sys.stderr.write("%s\n" % e) + sys.exit(1) +except Callable.Error as e: + sys.stderr.write("%s\n" % e) + sys.exit(2) \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeroname.py b/Tools/ZeroNet-cmd-lib/zeroname.py new file mode 100644 index 0000000..e3951ce --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeroname.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +import sys, os, signal as os_signal +import sqlite3, json +from lib.callable import Callable +from lib.args import argv +from lib.config import config +import zeronet_lib.site as Site +import zeronet_lib.addresses as Addresses + +class ZeroName(Callable.WithHelp): + """ + Commands: + help Print this help + list Print all domains + resolve Get address of a site by its domain + lookup Get site domain(s) by address + alias Find aliases of a domain + + Use 'help ' or 'help ' for more info + """ + + def action(self, *args, **kwargs): + if len(args) == 0: + if len(kwargs) == 0: + raise Callable.Redirect("help") + elif len(kwargs) == 1 and "help" in kwargs: + raise Callable.SubCommand("help") + else: + sys.stderr.write("Why are you passing named arguments to this command? Try 'help' instead.\n") + return 2 + else: + raise Callable.SubCommand + + def actionList(self): + """ + Print all domains + + Usage: + list Print newline-separated list of domains + """ + + try: + print "\n".join(Site.getDomains("%s/%s/data/names.json" % (self.getDataDirectory(), self.getAddress()))) + except KeyError as e: + sys.stderr.write("%s\n" % e[0]) + return 1 + + def actionResolve(self, domain): + """ + Get address of a site by its domain + + Usage: + resolve Print address of the site + """ + + try: + print Site.findByDomain("%s/%s/data/names.json" % (self.getDataDirectory(), self.getAddress()), domain) + except KeyError as e: + sys.stderr.write("%s\n" % e[0]) + return 1 + + def actionLookup(self, address): + """ + Get site domain(s) by address + + Usage: + lookup
Print domain(s) of the site + """ + + try: + print "\n".join(Site.getDomains("%s/%s/data/names.json" % (self.getDataDirectory(), self.getAddress()), address)) + except KeyError as e: + sys.stderr.write("%s\n" % e[0]) + return 1 + + def actionAlias(self, domain): + """ + Find aliases of a domain + + Usage: + alias Print all domains linking to the same domain as + """ + + try: + address = Site.findByDomain("%s/%s/data/names.json" % (self.getDataDirectory(), self.getAddress()), domain) + except KeyError as e: + sys.stderr.write("%s\n" % e[0]) + return 1 + + try: + print "\n".join(Site.getDomains("%s/%s/data/names.json" % (self.getDataDirectory(), self.getAddress()), address)) + except KeyError as e: + sys.stderr.write("%s\n" % e[0]) + return 1 + + def getDataDirectory(self): + return config.get("data_directory", "%s/data" % config["root_directory"]) + def getAddress(self): + return config.get("zeroname.registry", Addresses.ZeroName) + +try: + sys.exit(ZeroName(argv)) +except config.AttributeError as e: + sys.stderr.write("%s\n" % e) + sys.exit(1) +except Callable.Error as e: + sys.stderr.write("%s\n" % e) + sys.exit(2) \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeronet.py b/Tools/ZeroNet-cmd-lib/zeronet.py new file mode 100644 index 0000000..312362c --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeronet.py @@ -0,0 +1,418 @@ +#!/usr/bin/env python + +import sys, os, signal as os_signal +import sqlite3, json +from lib.callable import Callable +from lib.args import argv +from lib.config import config +import zeronet_lib.site as Site +import zeronet_lib.user as User +import zeronet_lib.instance as Instance +import zeronet_lib.addresses as Addresses +from zeronet_lib.zerowebsocket import ZeroWebSocket + +class ZeroNet(Callable.WithHelp): + """ + Commands: + help Print this help + config Get or set config values + wrapperkey Return wrapper key of a site or find a site by wrapper key + socket Send request to ZeroWebSocket + account Configure accounts + certs Configure certificates + instance Get info about ZeroNet instance + sql Run sql query on a database + + Use 'help ' or 'help ' for more info + """ + + def action(self, *args, **kwargs): + if len(args) == 0: + if len(kwargs) == 0: + raise Callable.Redirect("help") + elif len(kwargs) == 1 and "help" in kwargs: + raise Callable.SubCommand("help") + else: + sys.stderr.write("Why are you passing named arguments to this command? Try 'help' instead.\n") + return 2 + else: + raise Callable.SubCommand + + def actionConfig(self, *args, **kwargs): + """ + Get or set config values + + Subcommands: + config list Print list of all saved values as newline-separated values + config set Set config value + config get Get config value + config remove Remove config value + + Config variables: + Name Default Comment + server.address 127.0.0.1 The address which will be used for communication with ZeroNet + server.port 43110 ZeroNet port + server.secure False Sets whether ws/http or wss/https protocol should be used + account.current (the first account) The account chosen by 'account choose' command + homepage (ZeroHello address) ZeroNet homepage + root_directory Path to ZeroNet root directory (the one having 'src') + data_directory (root_directory)/data Path to data directory + zeroname.registry (ZeroName address) Domain name registry site + """ + + raise Callable.SubCommand + + def actionConfigList(self, prefix=""): + """ + Print list of all saved values as newline-separated values + + Usage: + config list Print all values + config list Print all values beginning with + """ + + print "\n".join(filter(lambda name: name.startswith(prefix), config.list())) + + def actionConfigSet(self, name, value): + """ + Set config value + + Usage: + config set Set config variable to . can be dot-separated. + """ + + config.set(name, value) + + def actionConfigGet(self, name): + """ + Get config value + + Usage: + config get Print config variable . can be dot-separated. + """ + + print config.get(name) + + def actionConfigRemove(self, name): + """ + Remove config variable + + Usage: + config remove Remove config variable . All the following 'config get' will be rejected. + can be dot-separated. + """ + + config.remove(name) + + + def actionWrapperkey(self, search, reverse=False): + """ + Return wrapper key of a site or find a site by wrapper key + + Usage: + wrapperkey
Print wrapper key of a site by address + wrapperkey --reverse Print site address by wrapper key + """ + + try: + if reverse == False: + print Site.getWrapperkey(self.getDataDirectory(), search) + else: + print Site.findByWrapperkey(self.getDataDirectory(), search) + except KeyError as e: + sys.stderr.write("%s\n" % e[0]) + return 1 + + def actionSocket(self, site, cmd, *args, **kwargs): + """ + Send request to ZeroWebSocket + + Usage: + socket Send command without arguments to site + socket 1 2 3 Send command with arguments 1, 2 and 3 + socket --1 2 Send command with arguments 1=2 and 3=True + --3 + """ + + if len(args) > 0 and len(kwargs) > 0: + sys.stderr.write("ZeroWebSocket doesn't accept requests with both positional arguments and named arguments used.\n") + return 2 + + try: + with self.connect(site) as ws: + print unicode(ws.send(cmd, *args, **kwargs)).encode("utf-8") + return 0 + except ZeroWebSocket.Error as e: + sys.stderr.write("%s\n" % "\n".join(e)) + return 1 + + + def actionAccount(self, *args, **kwargs): + """ + Configure accounts + + Subcommands: + account list Get list of addresses + account master Get master_seed of account + account choose Choose account for actions + """ + + raise Callable.SubCommand + + def actionAccountList(self): + """ + Get list of addresses + + Usage: + account list Print newline-separated list of addresses + """ + + print "\n".join(User.getUsers(self.getDataDirectory())) + + def actionAccountMaster(self): + """ + Get master_seed of account + + Usage: + account master Print master_seed of current account + """ + + address = self.getCurrentAccount() + + try: + print User.getUser(self.getDataDirectory(), address)["master_seed"] + except KeyError: + sys.stderr.write("No account %s\n" % address) + return 1 + + def getCurrentAccount(self): + address = config.get("account.current", None) + + if address is None: + address = User.getUsers(self.getDataDirectory())[0] + config.set("account.current", address) + + return address + def getCurrentUser(self): + return User.getUser(self.getDataDirectory(), self.getCurrentAccount()) + + def actionAccountChoose(self, address): + """ + Choose account for actions + + Usage: + account choose
Use
account for all actions + """ + + try: + User.getUser(self.getDataDirectory(), address) + except KeyError: + sys.stderr.write("No account %s\n" % address) + return 1 + + config.set("account.current", address) + + def actionCerts(self, *args, **kwargs): + """ + Configure certificates + + Subcommands: + certs list Get list of certs + certs address Get auth_address of a cert + certs privatekey Get auth_privatekey of a cert + certs username Get user name of a cert + """ + + raise Callable.SubCommand + + def actionCertsList(self): + """ + Get list of certs + + Usage: + certs list Print newline-separated names of auth certs + """ + + print "\n".join(self.getCurrentUser()["certs"].keys()) + + def actionCertsAddress(self, cert): + """ + Get auth_address of a cert + + Usage: + certs address Print auth_address of a certificate + """ + + certs = self.getCurrentUser()["certs"] + + if cert in certs: + print certs[cert]["auth_address"] + else: + sys.stderr.write("No cert %s\n" % cert) + return 1 + + def actionCertsPrivatekey(self, cert): + """ + Get auth_privatekey of a cert + + Usage: + certs privatekey Print auth_privatekey of a certificate + """ + + certs = self.getCurrentUser()["certs"] + + if cert in certs: + print certs[cert]["auth_privatekey"] + else: + sys.stderr.write("No cert %s\n" % cert) + return 1 + + def actionCertsUsername(self, cert): + """ + Get user name of a cert + + Usage: + certs username Print auth_user_name of a certificate + """ + + certs = self.getCurrentUser()["certs"] + + if cert in certs: + print certs[cert]["auth_user_name"] + else: + sys.stderr.write("No cert %s\n" % cert) + return 1 + + + def actionInstance(self, *args, **kwargs): + """ + Get info about ZeroNet instance + + Subcommands: + instance running Check whether ZeroNet instance is running + instance pid Get PID of ZeroNet instance + instance shutdown Shutdown ZeroNet instance + instance start Start ZeroNet instance + """ + + raise Callable.SubCommand + + def actionInstanceRunning(self): + """ + Check whether ZeroNet instance is running + + Usage: + instance running Return 0 if running, 1 otherwise + """ + + return 1 if Instance.isRunning(self.getDataDirectory()) else 0 + + def actionInstancePid(self): + """ + Get PID of ZeroNet instance + + Usage: + instance pid Return 0 and print the PID if running, return 1 otherwise + """ + + pid = Instance.getPid(self.getDataDirectory()) + if pid is None: + return 1 + else: + print pid + return 0 + + def actionInstanceShutdown(self, force=False, signal=None): + """ + Shutdown ZeroNet instance + + Usage: + instance shutdown Call ZeroWebSocket for shutdown + instance shutdown --force Kill ZeroNet process + instance shutdown Send signal to ZeroNet process + """ + + if not force and signal is None: + try: + with self.connect(config.get("homepage", Addresses.ZeroHello)) as ws: + try: + ws.send("serverShutdown") + except ZeroWebSocket.Error as e: + pass + except KeyError as e: + sys.stderr.write("Could not get wrapper key of ZeroHello. Try 'instance shutdown --force'.\n") + return 1 + else: + if signal is None: + signal = os_signal.SIGINT + + pid = Instance.getPid(self.getDataDirectory()) + if pid is None: + sys.stderr.write("Could not find ZeroNet process.\n") + return 1 + + os.kill(pid, signal) + + def actionInstanceStart(self): + Instance.start(config["root_directory"]) + + def getDataDirectory(self): + return config.get("data_directory", "%s/data" % config["root_directory"]) + + def actionSql(self, query, site=None): + """ + Run sql query on a database + + Usage: + sql Run on content.db + sql --site Run on 's database + """ + + if site is None: + path = "%s/content.db" % self.getDataDirectory() + else: + try: + with open("%s/%s/content.json" % (self.getDataDirectory(), site), "r") as f: + pass + except IOError as e: + sys.stderr.write("No site %s\n" % site) + return 1 + + try: + with open("%s/%s/dbschema.json" % (self.getDataDirectory(), site), "r") as f: + dbschema = json.loads(f.read()) + path = "%s/%s/%s" % (self.getDataDirectory(), site, dbschema["db_file"]) + except IOError as e: + sys.stderr.write("No database in site %s\n" % site) + return 1 + except (ValueError, KeyError) as e: + sys.stderr.write("Malformed dbschema.json\n") + return 1 + + try: + rows = Site.sqlQuery(path, query) + except sqlite3.OperationalError as e: + sys.stderr.write("%s\n" % e) + return 1 + + for row in rows: + print "\t".join(map(str, row)) + + + def connect(self, site): + wrapper_key = Site.getWrapperkey(self.getDataDirectory(), site) + + address = config.get("server.address", "127.0.0.1") + port = config.get("server.port", "43110") + secure = config.get("server.secure", False) + + return ZeroWebSocket(wrapper_key, "%s:%s" % (address, port), secure) + +try: + sys.exit(ZeroNet(argv)) +except config.AttributeError as e: + sys.stderr.write("%s\n" % e) + sys.exit(1) +except Callable.Error as e: + sys.stderr.write("%s\n" % e) + sys.exit(2) \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeronet_lib/__init__.py b/Tools/ZeroNet-cmd-lib/zeronet_lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tools/ZeroNet-cmd-lib/zeronet_lib/addresses.py b/Tools/ZeroNet-cmd-lib/zeronet_lib/addresses.py new file mode 100644 index 0000000..32788cc --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeronet_lib/addresses.py @@ -0,0 +1,2 @@ +ZeroHello = "1HeLLo4uzjaLetFx6NH3PMwFP3qbRbTf3D" +ZeroName = "1Name2NXVi1RDPDgf5617UoW7xA6YrhM9F" \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeronet_lib/instance.py b/Tools/ZeroNet-cmd-lib/zeronet_lib/instance.py new file mode 100644 index 0000000..285e44a --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeronet_lib/instance.py @@ -0,0 +1,26 @@ +import psutil, os, sys, subprocess + +def isRunning(data_directory): + try: + with open("%s/lock.pid" % data_directory, "w") as f: + f.write("0") + return True + except IOError: + return False + + +def getPid(data_directory): + lock_file = os.path.realpath("%s/lock.pid" % data_directory).encode("utf-8") + + for proc in psutil.process_iter(): + try: + if lock_file in (x.path for x in proc.open_files()): + return proc.pid + except psutil.Error as e: + pass + + return None + +def start(root_directory): + with open(os.devnull, "w") as null: + subprocess.Popen([sys.executable, "%s/start.py" % root_directory], stdout=null, stderr=null) \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeronet_lib/site.py b/Tools/ZeroNet-cmd-lib/zeronet_lib/site.py new file mode 100644 index 0000000..c4d99ca --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeronet_lib/site.py @@ -0,0 +1,50 @@ +import json, sqlite3 + +def getWrapperkey(data_directory, address): + with open("%s/sites.json" % data_directory) as f: + sites = json.loads(f.read()) + if address in sites: + return sites[address]["wrapper_key"] + else: + raise KeyError("No site %s" % address) + +def findByWrapperkey(data_directory, wrapper_key): + with open("%s/sites.json" % data_directory) as f: + sites = json.loads(f.read()) + + for address, site in sites.iteritems(): + if site["wrapper_key"] == wrapper_key: + return address + + raise KeyError("No wrapper key %s" % wrapper_key) + +def sqlQuery(path, query): + conn = sqlite3.connect(path) + cursor = conn.cursor() + return cursor.execute(query) + + +def getDomains(path, address=None): + with open(path, "r") as f: + names = json.loads(f.read()) + + if address is None: + domains = names.keys() + else: + domains = [] + for domain, result in names.iteritems(): + if result == address: + domains.append(domain) + + if len(domains) == 0: + raise KeyError("%s has no domains" % address) + else: + return domains + +def findByDomain(path, domain): + with open(path, "r") as f: + names = json.loads(f.read()) + if domain.lower() in names: + return names[domain.lower()] + else: + raise KeyError("No domain %s" % domain) \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeronet_lib/user.py b/Tools/ZeroNet-cmd-lib/zeronet_lib/user.py new file mode 100644 index 0000000..da87351 --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeronet_lib/user.py @@ -0,0 +1,11 @@ +import json + +def getUsersJson(data_directory): + with open("%s/users.json" % data_directory) as f: + return json.loads(f.read()) + +def getUsers(data_directory): + return getUsersJson(data_directory).keys() + +def getUser(data_directory, address): + return getUsersJson(data_directory)[address] \ No newline at end of file diff --git a/Tools/ZeroNet-cmd-lib/zeronet_lib/zerowebsocket.py b/Tools/ZeroNet-cmd-lib/zeronet_lib/zerowebsocket.py new file mode 100644 index 0000000..2ce7e65 --- /dev/null +++ b/Tools/ZeroNet-cmd-lib/zeronet_lib/zerowebsocket.py @@ -0,0 +1,46 @@ +import socket, websocket, sys, re, json + +class ZeroWebSocket(object): + def __init__(self, wrapper_key, address="127.0.0.1:43110", secure=False): + try: + self.ws = websocket.create_connection("%s://%s/Websocket?wrapper_key=%s" % ("wss" if secure else "ws", address, wrapper_key)) + except socket.error as e: + raise ZeroWebSocket.Error("Cannot open socket.") + + self.next_id = 1000000 + + def __enter__(self): + return self + def __exit__(self, exc_type, exc_value, traceback): + self.ws.close() + + def send(self, cmd, *args, **kwargs): + data = None + if len(args) == 0: + data = dict(cmd=cmd, params=kwargs, id=self.next_id) + elif len(kwargs) == 0: + data = dict(cmd=cmd, params=args, id=self.next_id) + else: + raise TypeError("Only args/kwargs alone are allowed in call to ZeroWebSocket") + + self.ws.send(json.dumps(data)) + + while True: + try: + response = json.loads(self.ws.recv()) + except websocket.WebSocketConnectionClosedException: + raise ZeroWebSocket.Error("Connection terminated.") + + if response["cmd"] == "response" and response["to"] == self.next_id: + self.next_id += 1 + + if response["result"] is not None and "error" in response["result"]: + raise ZeroWebSocket.Error(response["result"]["error"]) + else: + return response["result"] + elif response["cmd"] == "error": + self.next_id += 1 + raise ZeroWebSocket.Error(*map(lambda x: re.sub(r"<[^<]+?>", "", x), response["params"].split("
"))) + + class Error(Exception): + pass \ No newline at end of file diff --git a/ZeroNetInstaller.iss b/ZeroNetInstaller.iss index da34ef6..7fec254 100644 --- a/ZeroNetInstaller.iss +++ b/ZeroNetInstaller.iss @@ -35,7 +35,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ ;Name: "p2pmessagesplugin"; Description: "P2P Messages"; GroupDescription: "Install Additional Plugins" [Components] -Name: "main"; Description: "Main Files"; Types: full compact custom; Flags: fixed +Name: "main"; Description: "ZeroNet Core"; Types: full compact custom; Flags: fixed Name: "plugins"; Description: "Plugins"; Types: full custom Name: "plugins\p2pmessages"; Description: "P2P Messages Plugin (imachug) - Beta"; Types: full @@ -63,6 +63,10 @@ Root: "HKCU"; Subkey: "SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFla ValueType: String; ValueName: "{app}\{#MyAppExeName}"; ValueData: "RUNASADMIN"; \ Flags: uninsdeletekeyifempty uninsdeletevalue; MinVersion: 0,6.1 +[UninstallDelete] +Type: filesandordirs; Name: "{app}\core" +Type: filesandordirs; Name: "{app}\lib" + [Code] procedure SetElevationBit(Filename: string); var