From c20215350c9b764c2df760bd5b7ab830243230b6 Mon Sep 17 00:00:00 2001 From: sanbo Date: Fri, 30 Aug 2019 16:00:43 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E5=87=BA=E9=94=99=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/warpper/gradle-wrapper.properties | 6 - gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 164 +++++++++++++++++++++++ gradlew.bat | 90 +++++++++++++ 5 files changed, 255 insertions(+), 7 deletions(-) delete mode 100644 gradle/warpper/gradle-wrapper.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradlew create mode 100644 gradlew.bat diff --git a/gradle/warpper/gradle-wrapper.properties b/gradle/warpper/gradle-wrapper.properties deleted file mode 100644 index 04e285f..0000000 --- a/gradle/warpper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Dec 28 10:00:20 PST 2015 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..f6b961fd5a86aa5fbfe90f707c3138408be7c718 GIT binary patch literal 54329 zcmagFV|ZrKvM!pAZQHhO+qP}9lTNj?q^^Y^VFp)SH8qbSJ)2BQ2giqr}t zFG7D6)c?v~^Z#E_K}1nTQbJ9gQ9<%vVRAxVj)8FwL5_iTdUB>&m3fhE=kRWl;g`&m z!W5kh{WsV%fO*%je&j+Lv4xxK~zsEYQls$Q-p&dwID|A)!7uWtJF-=Tm1{V@#x*+kUI$=%KUuf2ka zjiZ{oiL1MXE2EjciJM!jrjFNwCh`~hL>iemrqwqnX?T*MX;U>>8yRcZb{Oy+VKZos zLiFKYPw=LcaaQt8tj=eoo3-@bG_342HQ%?jpgAE?KCLEHC+DmjxAfJ%Og^$dpC8Xw zAcp-)tfJm}BPNq_+6m4gBgBm3+CvmL>4|$2N$^Bz7W(}fz1?U-u;nE`+9`KCLuqg} zwNstNM!J4Uw|78&Y9~9>MLf56to!@qGkJw5Thx%zkzj%Ek9Nn1QA@8NBXbwyWC>9H z#EPwjMNYPigE>*Ofz)HfTF&%PFj$U6mCe-AFw$U%-L?~-+nSXHHKkdgC5KJRTF}`G zE_HNdrE}S0zf4j{r_f-V2imSqW?}3w-4=f@o@-q+cZgaAbZ((hn))@|eWWhcT2pLpTpL!;_5*vM=sRL8 zqU##{U#lJKuyqW^X$ETU5ETeEVzhU|1m1750#f}38_5N9)B_2|v@1hUu=Kt7-@dhA zq_`OMgW01n`%1dB*}C)qxC8q;?zPeF_r;>}%JYmlER_1CUbKa07+=TV45~symC*g8 zW-8(gag#cAOuM0B1xG8eTp5HGVLE}+gYTmK=`XVVV*U!>H`~j4+ROIQ+NkN$LY>h4 zqpwdeE_@AX@PL};e5vTn`Ro(EjHVf$;^oiA%@IBQq>R7_D>m2D4OwwEepkg}R_k*M zM-o;+P27087eb+%*+6vWFCo9UEGw>t&WI17Pe7QVuoAoGHdJ(TEQNlJOqnjZ8adCb zI`}op16D@v7UOEo%8E-~m?c8FL1utPYlg@m$q@q7%mQ4?OK1h%ODjTjFvqd!C z-PI?8qX8{a@6d&Lb_X+hKxCImb*3GFemm?W_du5_&EqRq!+H?5#xiX#w$eLti-?E$;Dhu`{R(o>LzM4CjO>ICf z&DMfES#FW7npnbcuqREgjPQM#gs6h>`av_oEWwOJZ2i2|D|0~pYd#WazE2Bbsa}X@ zu;(9fi~%!VcjK6)?_wMAW-YXJAR{QHxrD5g(ou9mR6LPSA4BRG1QSZT6A?kelP_g- zH(JQjLc!`H4N=oLw=f3{+WmPA*s8QEeEUf6Vg}@!xwnsnR0bl~^2GSa5vb!Yl&4!> zWb|KQUsC$lT=3A|7vM9+d;mq=@L%uWKwXiO9}a~gP4s_4Yohc!fKEgV7WbVo>2ITbE*i`a|V!^p@~^<={#?Gz57 zyPWeM2@p>D*FW#W5Q`1`#5NW62XduP1XNO(bhg&cX`-LYZa|m-**bu|>}S;3)eP8_ zpNTnTfm8 ze+7wDH3KJ95p)5tlwk`S7mbD`SqHnYD*6`;gpp8VdHDz%RR_~I_Ar>5)vE-Pgu7^Y z|9Px+>pi3!DV%E%4N;ii0U3VBd2ZJNUY1YC^-e+{DYq+l@cGtmu(H#Oh%ibUBOd?C z{y5jW3v=0eV0r@qMLgv1JjZC|cZ9l9Q)k1lLgm))UR@#FrJd>w^`+iy$c9F@ic-|q zVHe@S2UAnc5VY_U4253QJxm&Ip!XKP8WNcnx9^cQ;KH6PlW8%pSihSH2(@{2m_o+m zr((MvBja2ctg0d0&U5XTD;5?d?h%JcRJp{_1BQW1xu&BrA3(a4Fh9hon-ly$pyeHq zG&;6q?m%NJ36K1Sq_=fdP(4f{Hop;_G_(i?sPzvB zDM}>*(uOsY0I1j^{$yn3#U(;B*g4cy$-1DTOkh3P!LQ;lJlP%jY8}Nya=h8$XD~%Y zbV&HJ%eCD9nui-0cw!+n`V~p6VCRqh5fRX z8`GbdZ@73r7~myQLBW%db;+BI?c-a>Y)m-FW~M=1^|<21_Sh9RT3iGbO{o-hpN%d6 z7%++#WekoBOP^d0$$|5npPe>u3PLvX_gjH2x(?{&z{jJ2tAOWTznPxv-pAv<*V7r$ z6&glt>7CAClWz6FEi3bToz-soY^{ScrjwVPV51=>n->c(NJngMj6TyHty`bfkF1hc zkJS%A@cL~QV0-aK4>Id!9dh7>0IV;1J9(myDO+gv76L3NLMUm9XyPauvNu$S<)-|F zZS}(kK_WnB)Cl`U?jsdYfAV4nrgzIF@+%1U8$poW&h^c6>kCx3;||fS1_7JvQT~CV zQ8Js+!p)3oW>Df(-}uqC`Tcd%E7GdJ0p}kYj5j8NKMp(KUs9u7?jQ94C)}0rba($~ zqyBx$(1ae^HEDG`Zc@-rXk1cqc7v0wibOR4qpgRDt#>-*8N3P;uKV0CgJE2SP>#8h z=+;i_CGlv+B^+$5a}SicVaSeaNn29K`C&=}`=#Nj&WJP9Xhz4mVa<+yP6hkrq1vo= z1rX4qg8dc4pmEvq%NAkpMK>mf2g?tg_1k2%v}<3`$6~Wlq@ItJ*PhHPoEh1Yi>v57 z4k0JMO)*=S`tKvR5gb-(VTEo>5Y>DZJZzgR+j6{Y`kd|jCVrg!>2hVjz({kZR z`dLlKhoqT!aI8=S+fVp(5*Dn6RrbpyO~0+?fy;bm$0jmTN|t5i6rxqr4=O}dY+ROd zo9Et|x}!u*xi~>-y>!M^+f&jc;IAsGiM_^}+4|pHRn{LThFFpD{bZ|TA*wcGm}XV^ zr*C6~@^5X-*R%FrHIgo-hJTBcyQ|3QEj+cSqp#>&t`ZzB?cXM6S(lRQw$I2?m5=wd z78ki`R?%;o%VUhXH?Z#(uwAn9$m`npJ=cA+lHGk@T7qq_M6Zoy1Lm9E0UUysN)I_x zW__OAqvku^>`J&CB=ie@yNWsaFmem}#L3T(x?a`oZ+$;3O-icj2(5z72Hnj=9Z0w% z<2#q-R=>hig*(t0^v)eGq2DHC%GymE-_j1WwBVGoU=GORGjtaqr0BNigOCqyt;O(S zKG+DoBsZU~okF<7ahjS}bzwXxbAxFfQAk&O@>LsZMsZ`?N?|CDWM(vOm%B3CBPC3o z%2t@%H$fwur}SSnckUm0-k)mOtht`?nwsDz=2#v=RBPGg39i#%odKq{K^;bTD!6A9 zskz$}t)sU^=a#jLZP@I=bPo?f-L}wpMs{Tc!m7-bi!Ldqj3EA~V;4(dltJmTXqH0r z%HAWKGutEc9vOo3P6Q;JdC^YTnby->VZ6&X8f{obffZ??1(cm&L2h7q)*w**+sE6dG*;(H|_Q!WxU{g)CeoT z(KY&bv!Usc|m+Fqfmk;h&RNF|LWuNZ!+DdX*L=s-=_iH=@i` z?Z+Okq^cFO4}_n|G*!)Wl_i%qiMBaH8(WuXtgI7EO=M>=i_+;MDjf3aY~6S9w0K zUuDO7O5Ta6+k40~xh~)D{=L&?Y0?c$s9cw*Ufe18)zzk%#ZY>Tr^|e%8KPb0ht`b( zuP@8#Ox@nQIqz9}AbW0RzE`Cf>39bOWz5N3qzS}ocxI=o$W|(nD~@EhW13Rj5nAp; zu2obEJa=kGC*#3=MkdkWy_%RKcN=?g$7!AZ8vBYKr$ePY(8aIQ&yRPlQ=mudv#q$q z4%WzAx=B{i)UdLFx4os?rZp6poShD7Vc&mSD@RdBJ=_m^&OlkEE1DFU@csgKcBifJ zz4N7+XEJhYzzO=86 z#%eBQZ$Nsf2+X0XPHUNmg#(sNt^NW1Y0|M(${e<0kW6f2q5M!2YE|hSEQ*X-%qo(V zHaFwyGZ0on=I{=fhe<=zo{=Og-_(to3?cvL4m6PymtNsdDINsBh8m>a%!5o3s(en) z=1I z6O+YNertC|OFNqd6P=$gMyvmfa`w~p9*gKDESFqNBy(~Zw3TFDYh}$iudn)9HxPBi zdokK@o~nu?%imcURr5Y~?6oo_JBe}t|pU5qjai|#JDyG=i^V~7+a{dEnO<(y>ahND#_X_fcEBNiZ)uc&%1HVtx8Ts z*H_Btvx^IhkfOB#{szN*n6;y05A>3eARDXslaE>tnLa>+`V&cgho?ED+&vv5KJszf zG4@G;7i;4_bVvZ>!mli3j7~tPgybF5|J6=Lt`u$D%X0l}#iY9nOXH@(%FFJLtzb%p zzHfABnSs;v-9(&nzbZytLiqqDIWzn>JQDk#JULcE5CyPq_m#4QV!}3421haQ+LcfO*>r;rg6K|r#5Sh|y@h1ao%Cl)t*u`4 zMTP!deC?aL7uTxm5^nUv#q2vS-5QbBKP|drbDXS%erB>fYM84Kpk^au99-BQBZR z7CDynflrIAi&ahza+kUryju5LR_}-Z27g)jqOc(!Lx9y)e z{cYc&_r947s9pteaa4}dc|!$$N9+M38sUr7h(%@Ehq`4HJtTpA>B8CLNO__@%(F5d z`SmX5jbux6i#qc}xOhumzbAELh*Mfr2SW99=WNOZRZgoCU4A2|4i|ZVFQt6qEhH#B zK_9G;&h*LO6tB`5dXRSBF0hq0tk{2q__aCKXYkP#9n^)@cq}`&Lo)1KM{W+>5mSed zKp~=}$p7>~nK@va`vN{mYzWN1(tE=u2BZhga5(VtPKk(*TvE&zmn5vSbjo zZLVobTl%;t@6;4SsZ>5+U-XEGUZGG;+~|V(pE&qqrp_f~{_1h@5ZrNETqe{bt9ioZ z#Qn~gWCH!t#Ha^n&fT2?{`}D@s4?9kXj;E;lWV9Zw8_4yM0Qg-6YSsKgvQ*fF{#Pq z{=(nyV>#*`RloBVCs;Lp*R1PBIQOY=EK4CQa*BD0MsYcg=opP?8;xYQDSAJBeJpw5 zPBc_Ft9?;<0?pBhCmOtWU*pN*;CkjJ_}qVic`}V@$TwFi15!mF1*m2wVX+>5p%(+R zQ~JUW*zWkalde{90@2v+oVlkxOZFihE&ZJ){c?hX3L2@R7jk*xjYtHi=}qb+4B(XJ z$gYcNudR~4Kz_WRq8eS((>ALWCO)&R-MXE+YxDn9V#X{_H@j616<|P(8h(7z?q*r+ zmpqR#7+g$cT@e&(%_|ipI&A%9+47%30TLY(yuf&*knx1wNx|%*H^;YB%ftt%5>QM= z^i;*6_KTSRzQm%qz*>cK&EISvF^ovbS4|R%)zKhTH_2K>jP3mBGn5{95&G9^a#4|K zv+!>fIsR8z{^x4)FIr*cYT@Q4Z{y}};rLHL+atCgHbfX*;+k&37DIgENn&=k(*lKD zG;uL-KAdLn*JQ?@r6Q!0V$xXP=J2i~;_+i3|F;_En;oAMG|I-RX#FwnmU&G}w`7R{ z788CrR-g1DW4h_`&$Z`ctN~{A)Hv_-Bl!%+pfif8wN32rMD zJDs$eVWBYQx1&2sCdB0!vU5~uf)=vy*{}t{2VBpcz<+~h0wb7F3?V^44*&83Z2#F` z32!rd4>uc63rQP$3lTH3zb-47IGR}f)8kZ4JvX#toIpXH`L%NnPDE~$QI1)0)|HS4 zVcITo$$oWWwCN@E-5h>N?Hua!N9CYb6f8vTFd>h3q5Jg-lCI6y%vu{Z_Uf z$MU{{^o~;nD_@m2|E{J)q;|BK7rx%`m``+OqZAqAVj-Dy+pD4-S3xK?($>wn5bi90CFAQ+ACd;&m6DQB8_o zjAq^=eUYc1o{#+p+ zn;K<)Pn*4u742P!;H^E3^Qu%2dM{2slouc$AN_3V^M7H_KY3H)#n7qd5_p~Za7zAj|s9{l)RdbV9e||_67`#Tu*c<8!I=zb@ z(MSvQ9;Wrkq6d)!9afh+G`!f$Ip!F<4ADdc*OY-y7BZMsau%y?EN6*hW4mOF%Q~bw z2==Z3^~?q<1GTeS>xGN-?CHZ7a#M4kDL zQxQr~1ZMzCSKFK5+32C%+C1kE#(2L=15AR!er7GKbp?Xd1qkkGipx5Q~FI-6zt< z*PTpeVI)Ngnnyaz5noIIgNZtb4bQdKG{Bs~&tf)?nM$a;7>r36djllw%hQxeCXeW^ z(i6@TEIuxD<2ulwLTt|&gZP%Ei+l!(%p5Yij6U(H#HMkqM8U$@OKB|5@vUiuY^d6X zW}fP3;Kps6051OEO(|JzmVU6SX(8q>*yf*x5QoxDK={PH^F?!VCzES_Qs>()_y|jg6LJlJWp;L zKM*g5DK7>W_*uv}{0WUB0>MHZ#oJZmO!b3MjEc}VhsLD~;E-qNNd?x7Q6~v zR=0$u>Zc2Xr}>x_5$-s#l!oz6I>W?lw;m9Ae{Tf9eMX;TI-Wf_mZ6sVrMnY#F}cDd z%CV*}fDsXUF7Vbw>PuDaGhu631+3|{xp<@Kl|%WxU+vuLlcrklMC!Aq+7n~I3cmQ! z`e3cA!XUEGdEPSu``&lZEKD1IKO(-VGvcnSc153m(i!8ohi`)N2n>U_BemYJ`uY>8B*Epj!oXRLV}XK}>D*^DHQ7?NY*&LJ9VSo`Ogi9J zGa;clWI8vIQqkngv2>xKd91K>?0`Sw;E&TMg&6dcd20|FcTsnUT7Yn{oI5V4@Ow~m zz#k~8TM!A9L7T!|colrC0P2WKZW7PNj_X4MfESbt<-soq*0LzShZ}fyUx!(xIIDwx zRHt^_GAWe0-Vm~bDZ(}XG%E+`XhKpPlMBo*5q_z$BGxYef8O!ToS8aT8pmjbPq)nV z%x*PF5ZuSHRJqJ!`5<4xC*xb2vC?7u1iljB_*iUGl6+yPyjn?F?GOF2_KW&gOkJ?w z3e^qc-te;zez`H$rsUCE0<@7PKGW?7sT1SPYWId|FJ8H`uEdNu4YJjre`8F*D}6Wh z|FQ`xf7yiphHIAkU&OYCn}w^ilY@o4larl?^M7&8YI;hzBIsX|i3UrLsx{QDKwCX< zy;a>yjfJ6!sz`NcVi+a!Fqk^VE^{6G53L?@Tif|j!3QZ0fk9QeUq8CWI;OmO-Hs+F zuZ4sHLA3{}LR2Qlyo+{d@?;`tpp6YB^BMoJt?&MHFY!JQwoa0nTSD+#Ku^4b{5SZVFwU9<~APYbaLO zu~Z)nS#dxI-5lmS-Bnw!(u15by(80LlC@|ynj{TzW)XcspC*}z0~8VRZq>#Z49G`I zgl|C#H&=}n-ajxfo{=pxPV(L*7g}gHET9b*s=cGV7VFa<;Htgjk>KyW@S!|z`lR1( zGSYkEl&@-bZ*d2WQ~hw3NpP=YNHF^XC{TMG$Gn+{b6pZn+5=<()>C!N^jncl0w6BJ zdHdnmSEGK5BlMeZD!v4t5m7ct7{k~$1Ie3GLFoHjAH*b?++s<|=yTF+^I&jT#zuMx z)MLhU+;LFk8bse|_{j+d*a=&cm2}M?*arjBPnfPgLwv)86D$6L zLJ0wPul7IenMvVAK$z^q5<^!)7aI|<&GGEbOr=E;UmGOIa}yO~EIr5xWU_(ol$&fa zR5E(2vB?S3EvJglTXdU#@qfDbCYs#82Yo^aZN6`{Ex#M)easBTe_J8utXu(fY1j|R z9o(sQbj$bKU{IjyhosYahY{63>}$9_+hWxB3j}VQkJ@2$D@vpeRSldU?&7I;qd2MF zSYmJ>zA(@N_iK}m*AMPIJG#Y&1KR)6`LJ83qg~`Do3v^B0>fU&wUx(qefuTgzFED{sJ65!iw{F2}1fQ3= ziFIP{kezQxmlx-!yo+sC4PEtG#K=5VM9YIN0z9~c4XTX?*4e@m;hFM!zVo>A`#566 z>f&3g94lJ{r)QJ5m7Xe3SLau_lOpL;A($wsjHR`;xTXgIiZ#o&vt~ zGR6KdU$FFbLfZCC3AEu$b`tj!9XgOGLSV=QPIYW zjI!hSP#?8pn0@ezuenOzoka8!8~jXTbiJ6+ZuItsWW03uzASFyn*zV2kIgPFR$Yzm zE<$cZlF>R8?Nr2_i?KiripBc+TGgJvG@vRTY2o?(_Di}D30!k&CT`>+7ry2!!iC*X z<@=U0_C#16=PN7bB39w+zPwDOHX}h20Ap);dx}kjXX0-QkRk=cr};GYsjSvyLZa-t zzHONWddi*)RDUH@RTAsGB_#&O+QJaaL+H<<9LLSE+nB@eGF1fALwjVOl8X_sdOYme z0lk!X=S(@25=TZHR7LlPp}fY~yNeThMIjD}pd9+q=j<_inh0$>mIzWVY+Z9p<{D^#0Xk+b_@eNSiR8;KzSZ#7lUsk~NGMcB8C2c=m2l5paHPq`q{S(kdA7Z1a zyfk2Y;w?^t`?@yC5Pz9&pzo}Hc#}mLgDmhKV|PJ3lKOY(Km@Fi2AV~CuET*YfUi}u zfInZnqDX(<#vaS<^fszuR=l)AbqG{}9{rnyx?PbZz3Pyu!eSJK`uwkJU!ORQXy4x83r!PNgOyD33}}L=>xX_93l6njNTuqL8J{l%*3FVn3MG4&Fv*`lBXZ z?=;kn6HTT^#SrPX-N)4EZiIZI!0ByXTWy;;J-Tht{jq1mjh`DSy7yGjHxIaY%*sTx zuy9#9CqE#qi>1misx=KRWm=qx4rk|}vd+LMY3M`ow8)}m$3Ggv&)Ri*ON+}<^P%T5 z_7JPVPfdM=Pv-oH<tecoE}(0O7|YZc*d8`Uv_M*3Rzv7$yZnJE6N_W=AQ3_BgU_TjA_T?a)U1csCmJ&YqMp-lJe`y6>N zt++Bi;ZMOD%%1c&-Q;bKsYg!SmS^#J@8UFY|G3!rtyaTFb!5@e(@l?1t(87ln8rG? z--$1)YC~vWnXiW3GXm`FNSyzu!m$qT=Eldf$sMl#PEfGmzQs^oUd=GIQfj(X=}dw+ zT*oa0*oS%@cLgvB&PKIQ=Ok?>x#c#dC#sQifgMwtAG^l3D9nIg(Zqi;D%807TtUUCL3_;kjyte#cAg?S%e4S2W>9^A(uy8Ss0Tc++ZTjJw1 z&Em2g!3lo@LlDyri(P^I8BPpn$RE7n*q9Q-c^>rfOMM6Pd5671I=ZBjAvpj8oIi$! zl0exNl(>NIiQpX~FRS9UgK|0l#s@#)p4?^?XAz}Gjb1?4Qe4?j&cL$C8u}n)?A@YC zfmbSM`Hl5pQFwv$CQBF=_$Sq zxsV?BHI5bGZTk?B6B&KLdIN-40S426X3j_|ceLla*M3}3gx3(_7MVY1++4mzhH#7# zD>2gTHy*%i$~}mqc#gK83288SKp@y3wz1L_e8fF$Rb}ex+`(h)j}%~Ld^3DUZkgez zOUNy^%>>HHE|-y$V@B}-M|_{h!vXpk01xaD%{l{oQ|~+^>rR*rv9iQen5t?{BHg|% zR`;S|KtUb!X<22RTBA4AAUM6#M?=w5VY-hEV)b`!y1^mPNEoy2K)a>OyA?Q~Q*&(O zRzQI~y_W=IPi?-OJX*&&8dvY0zWM2%yXdFI!D-n@6FsG)pEYdJbuA`g4yy;qrgR?G z8Mj7gv1oiWq)+_$GqqQ$(ZM@#|0j7})=#$S&hZwdoijFI4aCFLVI3tMH5fLreZ;KD zqA`)0l~D2tuIBYOy+LGw&hJ5OyE+@cnZ0L5+;yo2pIMdt@4$r^5Y!x7nHs{@>|W(MzJjATyWGNwZ^4j+EPU0RpAl-oTM@u{lx*i0^yyWPfHt6QwPvYpk9xFMWfBFt!+Gu6TlAmr zeQ#PX71vzN*_-xh&__N`IXv6`>CgV#eA_%e@7wjgkj8jlKzO~Ic6g$cT`^W{R{606 zCDP~+NVZ6DMO$jhL~#+!g*$T!XW63#(ngDn#Qwy71yj^gazS{e;3jGRM0HedGD@pt z?(ln3pCUA(ekqAvvnKy0G@?-|-dh=eS%4Civ&c}s%wF@0K5Bltaq^2Os1n6Z3%?-Q zAlC4goQ&vK6TpgtzkHVt*1!tBYt-`|5HLV1V7*#45Vb+GACuU+QB&hZ=N_flPy0TY zR^HIrdskB#<$aU;HY(K{a3(OQa$0<9qH(oa)lg@Uf>M5g2W0U5 zk!JSlhrw8quBx9A>RJ6}=;W&wt@2E$7J=9SVHsdC?K(L(KACb#z)@C$xXD8^!7|uv zZh$6fkq)aoD}^79VqdJ!Nz-8$IrU(_-&^cHBI;4 z^$B+1aPe|LG)C55LjP;jab{dTf$0~xbXS9!!QdcmDYLbL^jvxu2y*qnx2%jbL%rB z{aP85qBJe#(&O~Prk%IJARcdEypZ)vah%ZZ%;Zk{eW(U)Bx7VlzgOi8)x z`rh4l`@l_Ada7z&yUK>ZF;i6YLGwI*Sg#Fk#Qr0Jg&VLax(nNN$u-XJ5=MsP3|(lEdIOJ7|(x3iY;ea)5#BW*mDV%^=8qOeYO&gIdJVuLLN3cFaN=xZtFB=b zH{l)PZl_j^u+qx@89}gAQW7ofb+k)QwX=aegihossZq*+@PlCpb$rpp>Cbk9UJO<~ zDjlXQ_Ig#W0zdD3&*ei(FwlN#3b%FSR%&M^ywF@Fr>d~do@-kIS$e%wkIVfJ|Ohh=zc zF&Rnic^|>@R%v?@jO}a9;nY3Qrg_!xC=ZWUcYiA5R+|2nsM*$+c$TOs6pm!}Z}dfM zGeBhMGWw3$6KZXav^>YNA=r6Es>p<6HRYcZY)z{>yasbC81A*G-le8~QoV;rtKnkx z;+os8BvEe?0A6W*a#dOudsv3aWs?d% z0oNngyVMjavLjtjiG`!007#?62ClTqqU$@kIY`=x^$2e>iqIy1>o|@Tw@)P)B8_1$r#6>DB_5 zmaOaoE~^9TolgDgooKFuEFB#klSF%9-~d2~_|kQ0Y{Ek=HH5yq9s zDq#1S551c`kSiWPZbweN^A4kWiP#Qg6er1}HcKv{fxb1*BULboD0fwfaNM_<55>qM zETZ8TJDO4V)=aPp_eQjX%||Ud<>wkIzvDlpNjqW>I}W!-j7M^TNe5JIFh#-}zAV!$ICOju8Kx)N z0vLtzDdy*rQN!7r>Xz7rLw8J-(GzQlYYVH$WK#F`i_i^qVlzTNAh>gBWKV@XC$T-` z3|kj#iCquDhiO7NKum07i|<-NuVsX}Q}mIP$jBJDMfUiaWR3c|F_kWBMw0_Sr|6h4 zk`_r5=0&rCR^*tOy$A8K;@|NqwncjZ>Y-75vlpxq%Cl3EgH`}^^~=u zoll6xxY@a>0f%Ddpi;=cY}fyG!K2N-dEyXXmUP5u){4VnyS^T4?pjN@Ot4zjL(Puw z_U#wMH2Z#8Pts{olG5Dy0tZj;N@;fHheu>YKYQU=4Bk|wcD9MbA`3O4bj$hNRHwzb zSLcG0SLV%zywdbuwl(^E_!@&)TdXge4O{MRWk2RKOt@!8E{$BU-AH(@4{gxs=YAz9LIob|Hzto0}9cWoz6Tp2x0&xi#$ zHh$dwO&UCR1Ob2w00-2eG7d4=cN(Y>0R#$q8?||q@iTi+7-w-xR%uMr&StFIthC<# zvK(aPduwuNB}oJUV8+Zl)%cnfsHI%4`;x6XW^UF^e4s3Z@S<&EV8?56Wya;HNs0E> z`$0dgRdiUz9RO9Au3RmYq>K#G=X%*_dUbSJHP`lSfBaN8t-~@F>)BL1RT*9I851A3 z<-+Gb#_QRX>~av#Ni<#zLswtu-c6{jGHR>wflhKLzC4P@b%8&~u)fosoNjk4r#GvC zlU#UU9&0Hv;d%g72Wq?Ym<&&vtA3AB##L}=ZjiTR4hh7J)e>ei} zt*u+>h%MwN`%3}b4wYpV=QwbY!jwfIj#{me)TDOG`?tI!%l=AwL2G@9I~}?_dA5g6 zCKgK(;6Q0&P&K21Tx~k=o6jwV{dI_G+Ba*Zts|Tl6q1zeC?iYJTb{hel*x>^wb|2RkHkU$!+S4OU4ZOKPZjV>9OVsqNnv5jK8TRAE$A&^yRwK zj-MJ3Pl?)KA~fq#*K~W0l4$0=8GRx^9+?w z!QT8*-)w|S^B0)ZeY5gZPI2G(QtQf?DjuK(s^$rMA!C%P22vynZY4SuOE=wX2f8$R z)A}mzJi4WJnZ`!bHG1=$lwaxm!GOnRbR15F$nRC-M*H<*VfF|pQw(;tbSfp({>9^5 zw_M1-SJ9eGF~m(0dvp*P8uaA0Yw+EkP-SWqu zqal$hK8SmM7#Mrs0@OD+%_J%H*bMyZiWAZdsIBj#lkZ!l2c&IpLu(5^T0Ge5PHzR} zn;TXs$+IQ_&;O~u=Jz+XE0wbOy`=6>m9JVG} zJ~Kp1e5m?K3x@@>!D)piw^eMIHjD4RebtR`|IlckplP1;r21wTi8v((KqNqn%2CB< zifaQc&T}*M&0i|LW^LgdjIaX|o~I$`owHolRqeH_CFrqCUCleN130&vH}dK|^kC>) z-r2P~mApHotL4dRX$25lIcRh_*kJaxi^%ZN5-GAAMOxfB!6flLPY-p&QzL9TE%ho( zRwftE3sy5<*^)qYzKkL|rE>n@hyr;xPqncY6QJ8125!MWr`UCWuC~A#G1AqF1@V$kv>@NBvN&2ygy*{QvxolkRRb%Ui zsmKROR%{*g*WjUUod@@cS^4eF^}yQ1>;WlGwOli z+Y$(8I`0(^d|w>{eaf!_BBM;NpCoeem2>J}82*!em=}}ymoXk>QEfJ>G(3LNA2-46 z5PGvjr)Xh9>aSe>vEzM*>xp{tJyZox1ZRl}QjcvX2TEgNc^(_-hir@Es>NySoa1g^ zFow_twnHdx(j?Q_3q51t3XI7YlJ4_q&(0#)&a+RUy{IcBq?)eaWo*=H2UUVIqtp&lW9JTJiP&u zw8+4vo~_IJXZIJb_U^&=GI1nSD%e;P!c{kZALNCm5c%%oF+I3DrA63_@4)(v4(t~JiddILp7jmoy+>cD~ivwoctFfEL zP*#2Rx?_&bCpX26MBgp^4G>@h`Hxc(lnqyj!*t>9sOBcXN(hTwEDpn^X{x!!gPX?1 z*uM$}cYRwHXuf+gYTB}gDTcw{TXSOUU$S?8BeP&sc!Lc{{pEv}x#ELX>6*ipI1#>8 zKes$bHjiJ1OygZge_ak^Hz#k;=od1wZ=o71ba7oClBMq>Uk6hVq|ePPt)@FM5bW$I z;d2Or@wBjbTyZj|;+iHp%Bo!Vy(X3YM-}lasMItEV_QrP-Kk_J4C>)L&I3Xxj=E?| zsAF(IfVQ4w+dRRnJ>)}o^3_012YYgFWE)5TT=l2657*L8_u1KC>Y-R{7w^S&A^X^U}h20jpS zQsdeaA#WIE*<8KG*oXc~$izYilTc#z{5xhpXmdT-YUnGh9v4c#lrHG6X82F2-t35} zB`jo$HjKe~E*W$=g|j&P>70_cI`GnOQ;Jp*JK#CT zuEGCn{8A@bC)~0%wsEv?O^hSZF*iqjO~_h|>xv>PO+?525Nw2472(yqS>(#R)D7O( zg)Zrj9n9$}=~b00=Wjf?E418qP-@8%MQ%PBiCTX=$B)e5cHFDu$LnOeJ~NC;xmOk# z>z&TbsK>Qzk)!88lNI8fOE2$Uxso^j*1fz>6Ot49y@=po)j4hbTIcVR`ePHpuJSfp zxaD^Dn3X}Na3@<_Pc>a;-|^Pon(>|ytG_+U^8j_JxP=_d>L$Hj?|0lz>_qQ#a|$+( z(x=Lipuc8p4^}1EQhI|TubffZvB~lu$zz9ao%T?%ZLyV5S9}cLeT?c} z>yCN9<04NRi~1oR)CiBakoNhY9BPnv)kw%*iv8vdr&&VgLGIs(-FbJ?d_gfbL2={- zBk4lkdPk~7+jIxd4{M(-W1AC_WcN&Oza@jZoj zaE*9Y;g83#m(OhA!w~LNfUJNUuRz*H-=$s*z+q+;snKPRm9EptejugC-@7-a-}Tz0 z@KHra#Y@OXK+KsaSN9WiGf?&jlZ!V7L||%KHP;SLksMFfjkeIMf<1e~t?!G3{n)H8 zQAlFY#QwfKuj;l@<$YDATAk;%PtD%B(0<|8>rXU< zJ66rkAVW_~Dj!7JGdGGi4NFuE?7ZafdMxIh65Sz7yQoA7fBZCE@WwysB=+`kT^LFX zz8#FlSA5)6FG9(qL3~A24mpzL@@2D#>0J7mMS1T*9UJ zvOq!!a(%IYY69+h45CE?(&v9H4FCr>gK0>mK~F}5RdOuH2{4|}k@5XpsX7+LZo^Qa4sH5`eUj>iffoBVm+ zz4Mtf`h?NW$*q1yr|}E&eNl)J``SZvTf6Qr*&S%tVv_OBpbjnA0&Vz#(;QmGiq-k! zgS0br4I&+^2mgA15*~Cd00cXLYOLA#Ep}_)eED>m+K@JTPr_|lSN}(OzFXQSBc6fM z@f-%2;1@BzhZa*LFV z-LrLmkmB%<<&jEURBEW>soaZ*rSIJNwaV%-RSaCZi4X)qYy^PxZ=oL?6N-5OGOMD2 z;q_JK?zkwQ@b3~ln&sDtT5SpW9a0q+5Gm|fpVY2|zqlNYBR}E5+ahgdj!CvK$Tlk0 z9g$5N;aar=CqMsudQV>yb4l@hN(9Jcc=1(|OHsqH6|g=K-WBd8GxZ`AkT?OO z-z_Ued-??Z*R4~L7jwJ%-`s~FK|qNAJ;EmIVDVpk{Lr7T4l{}vL)|GuUuswe9c5F| zv*5%u01hlv08?00Vpwyk*Q&&fY8k6MjOfpZfKa@F-^6d=Zv|0@&4_544RP5(s|4VPVP-f>%u(J@23BHqo2=zJ#v9g=F!cP((h zpt0|(s++ej?|$;2PE%+kc6JMmJjDW)3BXvBK!h!E`8Y&*7hS{c_Z?4SFP&Y<3evqf z9-ke+bSj$%Pk{CJlJbWwlBg^mEC^@%Ou?o>*|O)rl&`KIbHrjcpqsc$Zqt0^^F-gU2O=BusO+(Op}!jNzLMc zT;0YT%$@ClS%V+6lMTfhuzzxomoat=1H?1$5Ei7&M|gxo`~{UiV5w64Np6xV zVK^nL$)#^tjhCpTQMspXI({TW^U5h&Wi1Jl8g?P1YCV4=%ZYyjSo#5$SX&`r&1PyC zzc;uzCd)VTIih|8eNqFNeBMe#j_FS6rq81b>5?aXg+E#&$m++Gz9<+2)h=K(xtn}F ziV{rmu+Y>A)qvF}ms}4X^Isy!M&1%$E!rTO~5(p+8{U6#hWu>(Ll1}eD64Xa>~73A*538wry?v$vW z>^O#FRdbj(k0Nr&)U`Tl(4PI*%IV~;ZcI2z&rmq=(k^}zGOYZF3b2~Klpzd2eZJl> zB=MOLwI1{$RxQ7Y4e30&yOx?BvAvDkTBvWPpl4V8B7o>4SJn*+h1Ms&fHso%XLN5j z-zEwT%dTefp~)J_C8;Q6i$t!dnlh-!%haR1X_NuYUuP-)`IGWjwzAvp!9@h`kPZhf zwLwFk{m3arCdx8rD~K2`42mIN4}m%OQ|f)4kf%pL?Af5Ul<3M2fv>;nlhEPR8b)u} zIV*2-wyyD%%) zl$G@KrC#cUwoL?YdQyf9WH)@gWB{jd5w4evI& zOFF)p_D8>;3-N1z6mES!OPe>B^<;9xsh)){Cw$Vs-ez5nXS95NOr3s$IU;>VZSzKn zBvub8_J~I%(DozZW@{)Vp37-zevxMRZ8$8iRfwHmYvyjOxIOAF2FUngKj289!(uxY zaClWm!%x&teKmr^ABrvZ(ikx{{I-lEzw5&4t3P0eX%M~>$wG0ZjA4Mb&op+0$#SO_ z--R`>X!aqFu^F|a!{Up-iF(K+alKB{MNMs>e(i@Tpy+7Z-dK%IEjQFO(G+2mOb@BO zP>WHlS#fSQm0et)bG8^ZDScGnh-qRKIFz zfUdnk=m){ej0i(VBd@RLtRq3Ep=>&2zZ2%&vvf?Iex01hx1X!8U+?>ER;yJlR-2q4 z;Y@hzhEC=d+Le%=esE>OQ!Q|E%6yG3V_2*uh&_nguPcZ{q?DNq8h_2ahaP6=pP-+x zK!(ve(yfoYC+n(_+chiJ6N(ZaN+XSZ{|H{TR1J_s8x4jpis-Z-rlRvRK#U%SMJ(`C z?T2 zF(NNfO_&W%2roEC2j#v*(nRgl1X)V-USp-H|CwFNs?n@&vpRcj@W@xCJwR6@T!jt377?XjZ06=`d*MFyTdyvW!`mQm~t3luzYzvh^F zM|V}rO>IlBjZc}9Z zd$&!tthvr>5)m;5;96LWiAV0?t)7suqdh0cZis`^Pyg@?t>Ms~7{nCU;z`Xl+raSr zXpp=W1oHB*98s!Tpw=R5C)O{{Inl>9l7M*kq%#w9a$6N~v?BY2GKOVRkXYCgg*d

<5G2M1WZP5 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 F{|7XC?P&l2 literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 44b68b4..0d8f19c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Thu Jul 05 14:53:57 CST 2018 +#Thu Aug 29 16:24:53 CST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f6d5974 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From ebef29310a5f3f37228e052dbd38c0bb778321af Mon Sep 17 00:00:00 2001 From: sanbo Date: Fri, 30 Aug 2019 19:12:15 +0800 Subject: [PATCH 2/3] update gitinore --- .gitignore | 5 ++ app/app.iml | 182 ---------------------------------------------------- 2 files changed, 5 insertions(+), 182 deletions(-) create mode 100644 .gitignore delete mode 100644 app/app.iml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95776c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.gradle/ +.idea/ +*.iml +build/ +local.properties diff --git a/app/app.iml b/app/app.iml deleted file mode 100644 index 16c1323..0000000 --- a/app/app.iml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 5a94b24ed576299de555d1f36603036848633ff4 Mon Sep 17 00:00:00 2001 From: sanbo Date: Thu, 5 Sep 2019 16:47:54 +0800 Subject: [PATCH 3/3] format code. and update some class name and method name. --- .travis.yml | 22 +- README.md | 5 - app/build.gradle | 15 + app/doc/MCCMNCs v3.xlsx | Bin 0 -> 97655 bytes app/src/main/AndroidManifest.xml | 21 +- ... AbstractAppCompatPreferenceActivity.java} | 3 +- .../Activity/ChangeFilterActivity.java | 30 +- .../Activity/HarDetailActivity.java | 15 +- .../Activity/JsonPreviewActivity.java | 50 +- .../Activity/MainActivity.java | 405 +- .../Activity/SettingsActivity.java | 16 +- .../Adapter/ContentFilterAdapter.java | 16 +- .../Adapter/PageFilterAdapter.java | 12 +- .../networkdiagnosis/Bean/PageBean.java | 14 +- .../Bean/ResponseFilterRule.java | 16 +- .../Fragment/BaseFragment.java | 6 +- .../Fragment/NetworkFragment.java | 18 +- .../Fragment/PreviewFragment.java | 134 +- .../Fragment/WebViewFragment.java | 65 +- .../darkal/networkdiagnosis/MyVpnService.java | 11 +- .../networkdiagnosis/SysApplication.java | 30 +- .../networkdiagnosis/Task/BaseTask.java | 18 +- .../darkal/networkdiagnosis/Task/DnsTask.java | 32 +- .../networkdiagnosis/Task/InfoTask.java | 24 +- .../networkdiagnosis/Task/PingTask.java | 27 +- .../Task/TraceRouteContainer.java | 27 +- .../Task/TraceRouteWithPing.java | 5 +- .../networkdiagnosis/Task/TraceTask.java | 45 +- ... => AbstractX509ExtendedTrustManager.java} | 2 +- .../Utils/DatatypeConverter.java | 302 +- .../networkdiagnosis/Utils/DeviceUtils.java | 22 +- .../networkdiagnosis/Utils/FileUtil.java | 8 +- .../Utils/NetInfo/NetBasicInfo.java | 24 +- .../Utils/NetInfo/SystemBasicInfo.java | 2 +- .../Utils/SharedPreferenceUtils.java | 3 +- .../networkdiagnosis/View/ClearTextView.java | 70 +- .../networkdiagnosis/View/LoadingDialog.java | 7 +- .../networkdiagnosis/View/ProgressWheel.java | 914 +- .../View/RecycleViewDivider.java | 7 +- .../com/google/zxing/QrCodeScanActivity.java | 28 +- .../google/zxing/camera/AutoFocusManager.java | 4 +- .../google/zxing/camera/CameraManager.java | 34 +- .../decoding/CaptureActivityHandler.java | 14 +- .../zxing/decoding/DecodeFormatManager.java | 8 +- .../google/zxing/decoding/DecodeHandler.java | 24 +- .../google/zxing/decoding/DecodeThread.java | 2 +- .../com/google/zxing/view/ViewfinderView.java | 57 +- .../AbstractLDNetAsyncTaskEx.java | 222 + .../LDNetDiagnoService/LDNetAsyncTaskEx.java | 216 - .../LDNetDiagnoListener.java | 28 +- .../LDNetDiagnoService.java | 948 +- .../netease/LDNetDiagnoService/LDNetPing.java | 211 +- .../LDNetDiagnoService/LDNetSocket.java | 367 +- .../LDNetDiagnoService/LDNetTraceRoute.java | 545 +- .../netease/LDNetDiagnoUtils/LDNetUtil.java | 503 +- .../netease/LDNetDiagnoUtils/LDPingParse.java | 150 +- .../net/lightbody/bmp/BrowserMobProxy.java | 107 +- .../lightbody/bmp/BrowserMobProxyServer.java | 196 +- .../net/lightbody/bmp/client/ClientUtil.java | 3 +- .../net/lightbody/bmp/core/har/HarCookie.java | 12 +- .../net/lightbody/bmp/core/har/HarEntry.java | 15 +- .../net/lightbody/bmp/core/har/HarLog.java | 28 +- .../bmp/core/har/HarNameValuePair.java | 21 +- .../net/lightbody/bmp/core/har/HarPage.java | 2 +- .../lightbody/bmp/core/har/HarRequest.java | 6 +- .../lightbody/bmp/core/har/HarResponse.java | 6 +- .../lightbody/bmp/core/har/HarTimings.java | 2 +- .../bmp/filters/AddHeadersFilter.java | 7 +- .../bmp/filters/AutoBasicAuthFilter.java | 7 +- .../bmp/filters/BlacklistFilter.java | 9 +- .../filters/BrowserMobHttpFilterChain.java | 16 +- .../filters/ClientRequestCaptureFilter.java | 26 +- .../bmp/filters/HarCaptureFilter.java | 71 +- .../filters/HttpConnectHarCaptureFilter.java | 115 +- .../bmp/filters/HttpsAwareFiltersAdapter.java | 11 +- .../bmp/filters/HttpsHostCaptureFilter.java | 8 +- .../HttpsOriginalHostCaptureFilter.java | 4 +- .../lightbody/bmp/filters/LatencyFilter.java | 7 +- .../bmp/filters/RegisterRequestFilter.java | 6 +- .../lightbody/bmp/filters/RequestFilter.java | 9 +- .../bmp/filters/RequestFilterAdapter.java | 12 +- .../filters/ResolvedHostnameCacheFilter.java | 26 +- .../lightbody/bmp/filters/ResponseFilter.java | 7 +- .../bmp/filters/ResponseFilterAdapter.java | 12 +- .../bmp/filters/RewriteUrlFilter.java | 14 +- .../filters/ServerResponseCaptureFilter.java | 46 +- .../bmp/filters/UnregisterRequestFilter.java | 6 +- .../bmp/filters/WhitelistFilter.java | 13 +- .../filters/support/HttpConnectTiming.java | 42 +- .../bmp/filters/util/HarCaptureUtil.java | 2 +- .../bmp/mitm/CertificateInfoGenerator.java | 2 +- .../mitm/KeyStoreFileCertificateSource.java | 8 +- .../bmp/mitm/PemFileCertificateSource.java | 2 + .../bmp/mitm/RootCertificateGenerator.java | 72 +- .../net/lightbody/bmp/mitm/TrustSource.java | 33 +- .../manager/ImpersonatingMitmManager.java | 101 +- .../BouncyCastleSecurityProviderTool.java | 260 +- .../tools/DefaultSecurityProviderTool.java | 4 +- .../bmp/mitm/tools/SecurityProviderTool.java | 3 +- .../InsecureExtendedTrustManager.java | 75 +- .../InsecureTrustManagerFactory.java | 11 +- .../bmp/mitm/util/EncryptionUtil.java | 5 +- .../lightbody/bmp/mitm/util/KeyStoreUtil.java | 5 +- .../bmp/mitm/util/MitmConstants.java | 2 +- .../net/lightbody/bmp/mitm/util/SslUtil.java | 26 +- .../lightbody/bmp/mitm/util/TrustUtil.java | 23 +- .../lightbody/bmp/proxy/BlacklistEntry.java | 6 +- .../net/lightbody/bmp/proxy/CaptureType.java | 1 - .../net/lightbody/bmp/proxy/RewriteRule.java | 16 +- .../net/lightbody/bmp/proxy/Whitelist.java | 9 +- ...er.java => AbstractBasicHostResolver.java} | 2 +- .../proxy/dns/AbstractHostNameRemapper.java | 2 +- .../bmp/proxy/dns/AdvancedHostResolver.java | 4 +- .../bmp/proxy/dns/ChainedHostResolver.java | 14 +- .../bmp/proxy/dns/DelegatingHostResolver.java | 2 +- .../bmp/proxy/dns/DnsJavaResolver.java | 11 +- .../bmp/util/BrowserMobHttpUtil.java | 38 +- .../bmp/util/BrowserMobProxyUtil.java | 4 +- .../bmp/util/ClasspathResourceUtil.java | 3 +- .../bmp/util/HttpMessageContents.java | 68 +- .../lightbody/bmp/util/HttpObjectUtil.java | 20 +- .../java/net/lightbody/bmp/util/HttpUtil.java | 5 +- .../org/littleshoot/proxy/mitm/Authority.java | 10 +- .../mitm/BouncyCastleSslEngineSource.java | 91 +- .../proxy/mitm/CertificateHelper.java | 107 +- .../mitm/CertificateSniffingMitmManager.java | 11 +- .../mitm/SubjectAlternativeNameHolder.java | 12 +- .../res/drawable-v21/ic_info_black_24dp.xml | 4 +- .../main/res/drawable-v21/ic_menu_camera.xml | 4 +- .../main/res/drawable-v21/ic_menu_gallery.xml | 4 +- .../main/res/drawable-v21/ic_menu_manage.xml | 4 +- .../main/res/drawable-v21/ic_menu_send.xml | 4 +- .../main/res/drawable-v21/ic_menu_share.xml | 4 +- .../res/drawable-v21/ic_menu_slideshow.xml | 4 +- .../main/res/drawable-v21/ic_menu_webview.xml | 2 +- .../ic_notifications_black_24dp.xml | 4 +- .../res/drawable-v21/ic_sync_black_24dp.xml | 4 +- app/src/main/res/drawable/jz_toast_bg.xml | 4 +- .../main/res/layout-v21/fragment_webview.xml | 32 +- .../res/layout/activity_change_filter.xml | 8 +- app/src/main/res/layout/activity_detail.xml | 4 +- app/src/main/res/layout/activity_json.xml | 10 +- app/src/main/res/layout/activity_main.xml | 52 +- app/src/main/res/layout/alert_code.xml | 9 +- app/src/main/res/layout/alert_edittext.xml | 23 +- app/src/main/res/layout/alert_filter.xml | 9 +- app/src/main/res/layout/alert_resp_filter.xml | 34 +- app/src/main/res/layout/alert_textview.xml | 23 +- app/src/main/res/layout/fragment_network.xml | 42 +- app/src/main/res/layout/fragment_preview.xml | 7 +- app/src/main/res/layout/fragment_webview.xml | 32 +- app/src/main/res/layout/item_cate.xml | 11 +- app/src/main/res/layout/item_filter.xml | 27 +- app/src/main/res/layout/item_pages.xml | 17 +- app/src/main/res/layout/item_preview.xml | 8 +- .../res/layout/layout_loading2_dialog.xml | 18 +- app/src/main/res/layout/nav_header_main.xml | 4 +- app/src/main/res/menu/json_menu.xml | 2 +- app/src/main/res/menu/main.xml | 17 +- app/src/main/res/values/attrs.xml | 2 +- app/src/main/res/values/drawables.xml | 2 +- app/src/main/res/values/zxing_ids.xml | 44 +- app/src/main/res/xml/admin.xml | 2 +- .../main/res/xml/network_security_config.xml | 8 +- app/src/main/res/xml/pref_data_sync.xml | 23 +- app/src/main/res/xml/searchable.xml | 4 +- .../net/sf/uadetector/config.properties | 3 +- .../main/resources/net/sf/uadetector/uas.xml | 52763 ++++++++-------- .../net/sf/uadetector/uasxmldata.dtd | 87 +- .../networkdiagnosis/ExampleUnitTest.java | 2 +- build.gradle | 15 +- gradle.properties | 22 +- 172 files changed, 31280 insertions(+), 29874 deletions(-) create mode 100644 app/doc/MCCMNCs v3.xlsx rename app/src/main/java/cn/darkal/networkdiagnosis/Activity/{AppCompatPreferenceActivity.java => AbstractAppCompatPreferenceActivity.java} (96%) rename app/src/main/java/cn/darkal/networkdiagnosis/Utils/{X509ExtendedTrustManager.java => AbstractX509ExtendedTrustManager.java} (98%) create mode 100644 app/src/main/java/com/netease/LDNetDiagnoService/AbstractLDNetAsyncTaskEx.java delete mode 100644 app/src/main/java/com/netease/LDNetDiagnoService/LDNetAsyncTaskEx.java rename app/src/main/java/net/lightbody/bmp/proxy/dns/{BasicHostResolver.java => AbstractBasicHostResolver.java} (97%) diff --git a/.travis.yml b/.travis.yml index bb298ee..e4328a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,14 +15,14 @@ android: - android-sdk-license-.+ - '.+' before_script: - - wget http://services.gradle.org/distributions/gradle-4.4-all.zip - - unzip gradle-4.4-all.zip - - export GRADLE_HOME=$PWD/gradle-4.4 - - export PATH=$GRADLE_HOME/bin:$PATH - - mkdir "$ANDROID_HOME/licenses" || true - - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" - - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" - - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a - - emulator -avd test -no-audio -no-window & - - android-wait-for-emulator - - adb shell input keyevent 82 & + - wget http://services.gradle.org/distributions/gradle-4.4-all.zip + - unzip gradle-4.4-all.zip + - export GRADLE_HOME=$PWD/gradle-4.4 + - export PATH=$GRADLE_HOME/bin:$PATH + - mkdir "$ANDROID_HOME/licenses" || true + - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" + - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" + - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a + - emulator -avd test -no-audio -no-window & + - android-wait-for-emulator + - adb shell input keyevent 82 & diff --git a/README.md b/README.md index 8460f7f..34a3b6f 100644 --- a/README.md +++ b/README.md @@ -59,11 +59,6 @@ schemaçš„å议格å¼ä¸ºï¼šjdhttpmonitor://webview?param={'url'='http://www.dark 1. 信任所有的æœåС噍è¯ä¹¦ä¸åšæ ¡éªŒ
~~2. å¼€å¯è¿”回包注入功能åŽï¼Œhttps返回的部分页é¢å­˜åœ¨ err_CONTENT_LENGTH_MISMATCH 错误
~~(看起æ¥ä¼¼ä¹Žæ˜¯è§£å†³äº†ï¼Œå¾…用户å馈) -#### 如果觉得工具好用的è¯è¯·å¤šå¤šstar以åŠPull requests
æ”¯æŒæˆ‘å–æ¯å’–啡请扫æä¸‹é¢çš„二维ç ï¼Œè°¢è°¢(ง •̀_•Ì)ง
-![image](http://h5.darkal.cn/har/guide/img/code.jpg)

- -#### 相关技术交æµå¯ä»¥åŠ å…¥QQ群:816839175
-![image](http://h5.darkal.cn/har/guide/img/qq.jpg)

### 致谢
AndroidHttpCapture基于Nettyã€browsermob-proxyæ¥å®žçŽ°æ ¸å¿ƒæŠ“åŒ…çš„åŠŸèƒ½
diff --git a/app/build.gradle b/app/build.gradle index 73adbe9..fa755a3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,11 +58,26 @@ android { } productFlavors { } + aaptOptions { + cruncherEnabled = false + useNewCruncher = false + } lintOptions { + checkReleaseBuilds false abortOnError false + warningsAsErrors false + disable "UnusedResources" + textOutput "stdout" + textReport true } } +tasks.withType(Javadoc) { + options.addStringOption('Xdoclint:none', '-quiet') + options.addStringOption('encoding', 'UTF-8') + options.addStringOption('charSet', 'UTF-8') +} + dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') // Bugly上报 diff --git a/app/doc/MCCMNCs v3.xlsx b/app/doc/MCCMNCs v3.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..abca7a75d11530e06d1d2c63a47a2920a731ea4c GIT binary patch literal 97655 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 YSBx~!qXF{-fzX4W5HM@g{^XqeKf$7r&;S4c literal 0 HcmV?d00001 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 857a7a5..48f1f0d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,28 +1,35 @@ - + - + - + @@ -104,10 +111,8 @@ - - + + diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/AppCompatPreferenceActivity.java b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/AbstractAppCompatPreferenceActivity.java similarity index 96% rename from app/src/main/java/cn/darkal/networkdiagnosis/Activity/AppCompatPreferenceActivity.java rename to app/src/main/java/cn/darkal/networkdiagnosis/Activity/AbstractAppCompatPreferenceActivity.java index a797260..ead622a 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/AppCompatPreferenceActivity.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/AbstractAppCompatPreferenceActivity.java @@ -16,7 +16,7 @@ * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls * to be used with AppCompat. */ -public abstract class AppCompatPreferenceActivity extends PreferenceActivity { +public abstract class AbstractAppCompatPreferenceActivity extends PreferenceActivity { private AppCompatDelegate mDelegate; @@ -96,6 +96,7 @@ protected void onDestroy() { getDelegate().onDestroy(); } + @Override public void invalidateOptionsMenu() { getDelegate().invalidateOptionsMenu(); } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/ChangeFilterActivity.java b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/ChangeFilterActivity.java index 5c75f26..24a9e1b 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/ChangeFilterActivity.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/ChangeFilterActivity.java @@ -1,11 +1,11 @@ package cn.darkal.networkdiagnosis.Activity; import android.content.DialogInterface; +import android.os.Bundle; import android.support.design.widget.FloatingActionButton; import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; -import android.os.Bundle; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; @@ -14,7 +14,6 @@ import android.widget.RelativeLayout; import java.util.ArrayList; -import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; @@ -22,7 +21,6 @@ import cn.darkal.networkdiagnosis.Bean.ResponseFilterRule; import cn.darkal.networkdiagnosis.R; import cn.darkal.networkdiagnosis.SysApplication; -import cn.darkal.networkdiagnosis.Utils.DeviceUtils; import cn.darkal.networkdiagnosis.Utils.SharedPreferenceUtils; public class ChangeFilterActivity extends AppCompatActivity { @@ -44,10 +42,10 @@ protected void onCreate(Bundle savedInstanceState) { ButterKnife.bind(this); setupActionBar(); - if(((SysApplication)getApplication()).ruleList == null){ - contentFilterAdapter = new ContentFilterAdapter(this,new ArrayList()); - }else{ - contentFilterAdapter = new ContentFilterAdapter(this,((SysApplication)getApplication()).ruleList); + if (((SysApplication) getApplication()).ruleList == null) { + contentFilterAdapter = new ContentFilterAdapter(this, new ArrayList()); + } else { + contentFilterAdapter = new ContentFilterAdapter(this, ((SysApplication) getApplication()).ruleList); } listView.setAdapter(contentFilterAdapter); @@ -72,19 +70,19 @@ private void setupActionBar() { } } - public void showDialog(final ResponseFilterRule responseFilterRule){ + public void showDialog(final ResponseFilterRule responseFilterRule) { AlertDialog.Builder builder = new AlertDialog.Builder(ChangeFilterActivity.this); View textEntryView = LayoutInflater.from(ChangeFilterActivity.this).inflate(R.layout.alert_resp_filter, null); final EditText urlEditText = (EditText) textEntryView.findViewById(R.id.et_origin_url); final EditText regexEditText = (EditText) textEntryView.findViewById(R.id.et_regex); final EditText contentEditText = (EditText) textEntryView.findViewById(R.id.et_replace_result); - if(responseFilterRule!=null){ + if (responseFilterRule != null) { urlEditText.setText(responseFilterRule.getUrl()); regexEditText.setText(responseFilterRule.getReplaceRegex()); contentEditText.setText(responseFilterRule.getReplaceContent()); builder.setTitle("修改注入项"); - }else{ + } else { builder.setTitle("新增注入项"); } @@ -93,13 +91,13 @@ public void showDialog(final ResponseFilterRule responseFilterRule){ builder.setPositiveButton("确认", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - if(responseFilterRule!=null){ + if (responseFilterRule != null) { responseFilterRule.setUrl(urlEditText.getText().toString()); responseFilterRule.setReplaceRegex(regexEditText.getText().toString()); responseFilterRule.setReplaceContent(contentEditText.getText().toString()); - }else { - if(urlEditText.getText().length()>0 && regexEditText.getText().length()>0 - && contentEditText.getText().length()>0) { + } else { + if (urlEditText.getText().length() > 0 && regexEditText.getText().length() > 0 + && contentEditText.getText().length() > 0) { ResponseFilterRule responseFilterRule = new ResponseFilterRule(); responseFilterRule.setUrl(urlEditText.getText().toString()); responseFilterRule.setReplaceRegex(regexEditText.getText().toString()); @@ -110,14 +108,14 @@ public void onClick(DialogInterface dialog, int which) { contentFilterAdapter.notifyDataSetChanged(); } }); - builder.setNegativeButton("å–æ¶ˆ",null); + builder.setNegativeButton("å–æ¶ˆ", null); builder.show(); } @Override protected void onStop() { SharedPreferenceUtils.save(getApplicationContext(), - "response_filter",((SysApplication) getApplication()).ruleList); + "response_filter", ((SysApplication) getApplication()).ruleList); super.onStop(); } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/HarDetailActivity.java b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/HarDetailActivity.java index ee96669..1652249 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/HarDetailActivity.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/HarDetailActivity.java @@ -5,7 +5,6 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.MenuItem; @@ -76,7 +75,7 @@ public void initHarLog(int pos) { addItem("Request Header"); for (HarNameValuePair pair : harRequest.getHeaders()) { // 䏿˜¾ç¤ºcookie - if (!pair.getName().equals("Cookie")) { + if (!"Cookie".equals(pair.getName())) { addItem(pair.getName(), pair.getDecodeValue()); } } @@ -89,14 +88,14 @@ public void initHarLog(int pos) { } if (harRequest.getPostData() != null) { - if(harRequest.getPostData().getText()!= null - && harRequest.getPostData().getText().length()>0) { + if (harRequest.getPostData().getText() != null + && harRequest.getPostData().getText().length() > 0) { addItem("Request Content"); addItem("PostData", harRequest.getPostData().getText()); } - if(harRequest.getPostData().getParams()!= null - && harRequest.getPostData().getParams().size()>0){ + if (harRequest.getPostData().getParams() != null + && harRequest.getPostData().getParams().size() > 0) { addItem("Request PostData"); for (HarPostDataParam pair : harRequest.getPostData().getParams()) { @@ -107,7 +106,7 @@ public void initHarLog(int pos) { addItem("Response Header"); for (HarNameValuePair pair : harResponse.getHeaders()) { - if (!pair.getName().equals("Cookie")) { + if (!"Cookie".equals(pair.getName())) { addItem(pair.getName(), pair.getDecodeValue()); } } @@ -145,7 +144,7 @@ public void addContentItem(String title, final String value, final int pos) { valueTextView.setText(value.substring(0, value.length() > 50 ? 50 : value.length())); } - if (title.equals("Content")) { + if ("Content".equals(title)) { view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/JsonPreviewActivity.java b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/JsonPreviewActivity.java index bda5e8c..b8d56cb 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/JsonPreviewActivity.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/JsonPreviewActivity.java @@ -6,7 +6,6 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AlertDialog; import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.view.Menu; import android.view.MenuItem; import android.widget.TextView; @@ -39,6 +38,7 @@ public class JsonPreviewActivity extends AppCompatActivity { private Handler mHandler = new Handler(); private String content; private int selectedEncode = 0; + private String[] encodeItem = new String[]{"UTF-8", "ISO-8859-1", "GBK"}; @Override protected void onCreate(Bundle savedInstanceState) { @@ -52,13 +52,13 @@ protected void onCreate(Bundle savedInstanceState) { setupActionBar(); try { - int pos = getIntent().getIntExtra("pos",-1); - if(pos > -1){ + int pos = getIntent().getIntExtra("pos", -1); + if (pos > -1) { HarLog harLog = ((SysApplication) getApplication()).proxy.getHar().getLog(); HarEntry harEntry = harLog.getEntries().get(pos); content = harEntry.getResponse().getContent().getText(); initViewDelay(content); - }else{ + } else { finish(); } } catch (Exception e) { @@ -120,8 +120,6 @@ public String jsonFormatter(String uglyJSONString) throws Exception { return gson.toJson(je); } - private String[] encodeItem = new String[]{"UTF-8", "ISO-8859-1", "GBK"}; - @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.json_menu, menu); @@ -131,9 +129,9 @@ public boolean onCreateOptionsMenu(Menu menu) { public boolean onMenuItemClick(MenuItem item) { DialogInterface.OnClickListener listener = new ButtonOnClick(); AlertDialog.Builder builder = new AlertDialog.Builder(JsonPreviewActivity.this); - builder.setNegativeButton("å–æ¶ˆ",null); + builder.setNegativeButton("å–æ¶ˆ", null); builder.setPositiveButton("确认", listener); - builder.setSingleChoiceItems(encodeItem,selectedEncode,listener); + builder.setSingleChoiceItems(encodeItem, selectedEncode, listener); builder.create().show(); return true; } @@ -143,6 +141,24 @@ public boolean onMenuItemClick(MenuItem item) { return super.onCreateOptionsMenu(menu); } + public void changeEncode(int pos) { + switch (pos) { + case 0: + initViewDelay(content); + break; + case 1: + initViewDelay(new String(content.getBytes(Charset.forName("ISO-8859-1")), Charset.forName("UTF-8"))); + break; + case 2: + initViewDelay(new String(content.getBytes(Charset.forName("GBK")), Charset.forName("UTF-8"))); + break; + default: + initViewDelay(content); + break; + } + + } + private class ButtonOnClick implements DialogInterface.OnClickListener { private int index = -1; // 表示选项的索引 @@ -160,22 +176,4 @@ public void onClick(DialogInterface dialog, int which) { } } } - - public void changeEncode(int pos){ - switch (pos){ - case 0: - initViewDelay(content); - break; - case 1: - initViewDelay(new String(content.getBytes(Charset.forName("ISO-8859-1")), Charset.forName("UTF-8"))); - break; - case 2: - initViewDelay(new String(content.getBytes(Charset.forName("GBK")), Charset.forName("UTF-8"))); - break; - default: - initViewDelay(content); - break; - } - - } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/MainActivity.java b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/MainActivity.java index b6ff32c..9138823 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/MainActivity.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/MainActivity.java @@ -75,8 +75,8 @@ import butterknife.ButterKnife; import cn.darkal.networkdiagnosis.Adapter.PageFilterAdapter; import cn.darkal.networkdiagnosis.Bean.PageBean; -import cn.darkal.networkdiagnosis.Fragment.BaseFragment; import cn.darkal.networkdiagnosis.Fragment.BackHandledInterface; +import cn.darkal.networkdiagnosis.Fragment.BaseFragment; import cn.darkal.networkdiagnosis.Fragment.NetworkFragment; import cn.darkal.networkdiagnosis.Fragment.PreviewFragment; import cn.darkal.networkdiagnosis.Fragment.WebViewFragment; @@ -101,51 +101,99 @@ public class MainActivity extends AppCompatActivity implements BackHandledInterf public final static int TYPE_NONE = 0; public final static int TYPE_SHARE = 1; public final static int TYPE_UPLOAD = 2; - - private int mLastHeightOfContainer; // 记录容器上一次的高度,用于检测高度å˜åŒ– - private int mHeightOfVisibility; - Boolean isKeyboardOpen = false; - Boolean shouldExitSearchView = false; - - private BaseFragment mBackHandedFragment; - private long exitTime = 0; - - private Receiver receiver; - @BindView(R.id.fl_contain) public View rootView; - @BindView(R.id.nav_view) public NavigationView navigationView; - @BindView(R.id.fab) public FloatingActionMenu fam; - @BindView(R.id.fab_share) public FloatingActionButton shareFab; - @BindView(R.id.fab_upload) public FloatingActionButton uploadFab; - @BindView(R.id.fab_preview) public FloatingActionButton previewFab; - @BindView(R.id.fab_clear) public FloatingActionButton clearFab; - -// int lastX, lastY; -// Boolean isMove = false; - public SearchView searchView; public MenuItem homeItem; public MenuItem searchItem; public MenuItem filterMenuItem; - public Set disablePages = new HashSet<>(); public StringBuffer consoleLog = new StringBuffer(); - public SharedPreferences shp; + // int lastX, lastY; +// Boolean isMove = false; + Boolean isKeyboardOpen = false; + Boolean shouldExitSearchView = false; + private int mLastHeightOfContainer; // 记录容器上一次的高度,用于检测高度å˜åŒ– + private int mHeightOfVisibility; + private BaseFragment mBackHandedFragment; + private long exitTime = 0; + private Receiver receiver; + private LoadingDialog loadingDialog; + private String[] uaItem = new String[]{"手机æµè§ˆå™¨", "微信环境", "手Q环境"}; + public NavigationView.OnNavigationItemSelectedListener navigationItemListener = new NavigationView.OnNavigationItemSelectedListener() { + @Override + public boolean onNavigationItemSelected(MenuItem item) { + // Handle navigation view item clicks here. + int id = item.getItemId(); + + if (!SysApplication.isInitProxy) { + Toast.makeText(MainActivity.this, "请等待程åºåˆå§‹åŒ–完æˆ", Toast.LENGTH_LONG).show(); + return true; + } + + switch (id) { + case R.id.nav_camera: { + Intent intent = new Intent(MainActivity.this, QrCodeScanActivity.class); + startActivity(intent); + break; + } + case R.id.nav_gallery: + switchContent(WebViewFragment.getInstance()); + break; + case R.id.nav_preview: + switchContent(PreviewFragment.getInstance()); + break; + case R.id.nav_slideshow: + switchContent(NetworkFragment.getInstance()); + break; + case R.id.nav_manage: { + Intent intent = new Intent(MainActivity.this, SettingsActivity.class); + startActivity(intent); + break; + } + case R.id.nav_ua: + showUaDialog(); + break; + case R.id.nav_modify: + if (shp.getBoolean("enable_filter", false)) { + Intent intent = new Intent(MainActivity.this, ChangeFilterActivity.class); + startActivity(intent); + } else { + Toast.makeText(MainActivity.this, "请å‰å¾€è®¾ç½®å¯ç”¨æ³¨å…¥åŠŸèƒ½", Toast.LENGTH_LONG).show(); + } + break; + case R.id.nav_cosole: + showLogDialog(); + break; + case R.id.nav_host: + showHostDialog(); + break; + case R.id.nav_page: + createPage(); + break; + default: + break; + } + + DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); + drawer.closeDrawer(GravityCompat.START); + return true; + } + }; @Override protected void onCreate(Bundle savedInstanceState) { @@ -339,13 +387,13 @@ public void changeStateBar(Fragment fragment) { public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); - if(id == R.id.action_home){ + if (id == R.id.action_home) { WebViewFragment webViewFragment = WebViewFragment.getInstance(); webViewFragment.loadUrl(HOME_URL); switchContent(webViewFragment); return true; } - if(id == R.id.action_guide){ + if (id == R.id.action_guide) { WebViewFragment webViewFragment = WebViewFragment.getInstance(); webViewFragment.loadUrl(GUIDE_URL); switchContent(webViewFragment); @@ -365,65 +413,6 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } - public NavigationView.OnNavigationItemSelectedListener navigationItemListener = new NavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(MenuItem item) { - // Handle navigation view item clicks here. - int id = item.getItemId(); - - if (!SysApplication.isInitProxy) { - Toast.makeText(MainActivity.this, "请等待程åºåˆå§‹åŒ–完æˆ", Toast.LENGTH_LONG).show(); - return true; - } - - switch (id) { - case R.id.nav_camera: { - Intent intent = new Intent(MainActivity.this, QrCodeScanActivity.class); - startActivity(intent); - break; - } - case R.id.nav_gallery: - switchContent(WebViewFragment.getInstance()); - break; - case R.id.nav_preview: - switchContent(PreviewFragment.getInstance()); - break; - case R.id.nav_slideshow: - switchContent(NetworkFragment.getInstance()); - break; - case R.id.nav_manage: { - Intent intent = new Intent(MainActivity.this, SettingsActivity.class); - startActivity(intent); - break; - } - case R.id.nav_ua: - showUaDialog(); - break; - case R.id.nav_modify: - if (shp.getBoolean("enable_filter", false)) { - Intent intent = new Intent(MainActivity.this, ChangeFilterActivity.class); - startActivity(intent); - } else { - Toast.makeText(MainActivity.this, "请å‰å¾€è®¾ç½®å¯ç”¨æ³¨å…¥åŠŸèƒ½", Toast.LENGTH_LONG).show(); - } - break; - case R.id.nav_cosole: - showLogDialog(); - break; - case R.id.nav_host: - showHostDialog(); - break; - case R.id.nav_page: - createPage(); - break; - } - - DrawerLayout drawer = (DrawerLayout) findViewById(R.id.drawer_layout); - drawer.closeDrawer(GravityCompat.START); - return true; - } - }; - /** * 修改显示的内容 ä¸ä¼šé‡æ–°åŠ è½½ **/ @@ -500,7 +489,7 @@ public void run() { if (!isInstallCert) { Toast.makeText(this, "必须安装è¯ä¹¦æ‰å¯å®žçްHTTPS抓包", Toast.LENGTH_LONG).show(); - FileUtil.checkPermission(this,runnable); + FileUtil.checkPermission(this, runnable); } } @@ -508,7 +497,7 @@ public void run() { public void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == 3) { if (resultCode == Activity.RESULT_OK) { - SharedPreferenceUtils.putBoolean(this,"isInstallNewCert", true); + SharedPreferenceUtils.putBoolean(this, "isInstallNewCert", true); Toast.makeText(this, "安装æˆåŠŸ", Toast.LENGTH_LONG).show(); } else { @@ -558,42 +547,16 @@ public boolean isShouldHideInput(View view, MotionEvent event) { return true; } - private class OnGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener { - private View mView; - - public OnGlobalLayoutListener(View view) { - mView = view; - } - - @Override - public void onGlobalLayout() { - int currentHeight = mView.getHeight(); - if (currentHeight < mLastHeightOfContainer) { // 软键盘打开 - if (mHeightOfVisibility == 0) { - mHeightOfVisibility = currentHeight; - } - isKeyboardOpen = true; - } else if (currentHeight > mLastHeightOfContainer && mLastHeightOfContainer != 0) { // 软键盘关闭 - isKeyboardOpen = false; - // éšè—æœç´¢æ¡† - if (shouldExitSearchView) { - searchItem.collapseActionView(); - } - } - mLastHeightOfContainer = currentHeight; - } - } - @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); try { setIntent(intent); handleUriStartupParams(); - if (intent.getAction().equals("android.intent.action.SEARCH")) { + if ("android.intent.action.SEARCH".equals(intent.getAction())) { switchContent(PreviewFragment.getInstance()); } - }catch (Exception e){ + } catch (Exception e) { e.printStackTrace(); } } @@ -612,14 +575,6 @@ protected void onStop() { super.onStop(); } - public class Receiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - installCert(); - Log.i("~~~~", "Receiver installCert"); - } - } - /** * å¯åŠ¨çš„æ—¶å€™æ ¹æ®bundle傿•°å†³å®šåˆ‡æ¢åˆ°å“ªä¸ªtab */ @@ -702,7 +657,7 @@ public void run() { } }; - FileUtil.checkPermission(this,runnable); + FileUtil.checkPermission(this, runnable); } public void shareZip() { @@ -725,46 +680,6 @@ public void uploadZip() { showUploadDialog(this); } - public class MyUploadDelegate implements UploadStatusDelegate { - @Override - public void onProgress(Context context, UploadInfo uploadInfo) { - Log.e("~~~~", uploadInfo.getProgressPercent() + ""); - } - - @Override - public void onError(Context context, UploadInfo uploadInfo, ServerResponse serverResponse, Exception exception) { - dismissLoading(); - Snackbar.make(rootView, "上传失败ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); - exception.printStackTrace(); - CrashReport.postCatchedException(exception); - } - - @Override - public void onCompleted(Context context, UploadInfo uploadInfo, ServerResponse serverResponse) { - try { - JSONObject jsonObject = new JSONObject(serverResponse.getBodyAsString()); - if (jsonObject.getInt("errId") == 0) { - Snackbar.make(rootView, "上传æˆåŠŸï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); - } else if (jsonObject.getInt("errId") == 2 || jsonObject.getInt("errId") == 11004) { - Snackbar.make(rootView, "验è¯ç é”™è¯¯ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); - showUploadDialog(MainActivity.this); - } else { - Snackbar.make(rootView, "上传失败ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); - } - } catch (Exception e) { - Snackbar.make(rootView, "上传失败ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); - } - dismissLoading(); - } - - @Override - public void onCancelled(Context context, UploadInfo uploadInfo) { - dismissLoading(); - } - } - - private LoadingDialog loadingDialog; - public void showLoading(String text) { try { if (loadingDialog == null) { @@ -808,6 +723,7 @@ public void onClick(View view) { builder.setTitle("请输入验è¯ç "); builder.setView(textEntryView); builder.setPositiveButton("确认", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(edtInput.getWindowToken(), 0); @@ -824,6 +740,7 @@ public void run() { }); builder.setNegativeButton("å–æ¶ˆ", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(edtInput.getWindowToken(), 0); @@ -934,20 +851,6 @@ public Har getFiltedHar() { return proxy.getHar(getPageSet()); } -// @Override -// protected void onSaveInstanceState(Bundle outState) { -// int tab; -// if (mBackHandedFragment instanceof PreviewFragment) { -// tab = 3; -// } else if (mBackHandedFragment instanceof NetworkFragment) { -// tab = 2; -// } else { -// tab = 1; -// } -// outState.putInt("tab", tab); -// super.onSaveInstanceState(outState); -// } - public void initFloatingActionMenu() { fam.setClosedOnTouchOutside(true); AnimatorSet set = new AnimatorSet(); @@ -969,10 +872,10 @@ public void initFloatingActionMenu() { public void onAnimationStart(Animator animation) { fam.getMenuIconView().setImageResource(fam.isOpened() ? R.drawable.ic_file_upload_white_24dp : R.drawable.ic_close_white_24dp); - if(mBackHandedFragment instanceof PreviewFragment){ - if(fam.isOpened()){ + if (mBackHandedFragment instanceof PreviewFragment) { + if (fam.isOpened()) { clearFab.show(true); - }else { + } else { clearFab.hide(true); } } @@ -988,7 +891,7 @@ public void onAnimationStart(Animator animation) { shareFab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showFilter(MainActivity.this,TYPE_SHARE); + showFilter(MainActivity.this, TYPE_SHARE); fam.close(true); } }); @@ -996,7 +899,7 @@ public void onClick(View v) { uploadFab.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - showFilter(MainActivity.this,TYPE_UPLOAD); + showFilter(MainActivity.this, TYPE_UPLOAD); fam.close(true); } }); @@ -1015,12 +918,14 @@ public void onClick(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this); builder.setTitle("è¯·ç¡®è®¤æ˜¯å¦æ¸…除所有请求?"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { - ((SysApplication)getApplication()).proxy.getHar().getLog().clearAllEntries(); + ((SysApplication) getApplication()).proxy.getHar().getLog().clearAllEntries(); PreviewFragment.getInstance().notifyHarChange(); } }); builder.setNegativeButton("å–æ¶ˆ", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { } }); @@ -1029,8 +934,6 @@ public void onClick(DialogInterface dialog, int whichButton) { }); } - private String[] uaItem = new String[]{"手机æµè§ˆå™¨", "微信环境", "手Q环境"}; - public void showUaDialog() { DialogInterface.OnClickListener buttonListener = new ButtonOnClick(); AlertDialog.Builder builder = new AlertDialog.Builder(this); @@ -1049,25 +952,21 @@ public void showUaDialog() { builder.create().show(); } - private class ButtonOnClick implements DialogInterface.OnClickListener { - - private int index = -1; // 表示选项的索引 - - @Override - public void onClick(DialogInterface dialog, int which) { - if (which >= 0) { - index = which; - } else { - //用户å•击的是ã€ç¡®å®šã€‘按钮 - if (which == DialogInterface.BUTTON_POSITIVE) { - SharedPreferenceUtils.putString(MainActivity.this, "select_ua", index + ""); - WebViewFragment.getInstance().setUserAgent(); - } - } - } - } +// @Override +// protected void onSaveInstanceState(Bundle outState) { +// int tab; +// if (mBackHandedFragment instanceof PreviewFragment) { +// tab = 3; +// } else if (mBackHandedFragment instanceof NetworkFragment) { +// tab = 2; +// } else { +// tab = 1; +// } +// outState.putInt("tab", tab); +// super.onSaveInstanceState(outState); +// } - public void showLogDialog(){ + public void showLogDialog() { View textEntryView = LayoutInflater.from(this).inflate(R.layout.alert_textview, null); TextView edtInput = (TextView) textEntryView.findViewById(R.id.tv_content); edtInput.setText(consoleLog); @@ -1086,7 +985,7 @@ public void onClick(DialogInterface dialog, int which) { builder.show(); } - public void showHostDialog(){ + public void showHostDialog() { View textEntryView = LayoutInflater.from(this).inflate(R.layout.alert_edittext, null); final EditText editText = (EditText) textEntryView.findViewById(R.id.et_content); @@ -1103,8 +1002,8 @@ public void showHostDialog(){ public void onClick(DialogInterface dialog, int which) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); - SharedPreferenceUtils.putString(MainActivity.this, "system_host", editText.getText()+""); - DeviceUtils.changeHost(((SysApplication)getApplication()).proxy,editText.getText()+""); + SharedPreferenceUtils.putString(MainActivity.this, "system_host", editText.getText() + ""); + DeviceUtils.changeHost(((SysApplication) getApplication()).proxy, editText.getText() + ""); } }); builder.setNegativeButton("清空", new DialogInterface.OnClickListener() { @@ -1113,9 +1012,99 @@ public void onClick(DialogInterface dialog, int which) { InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(editText.getWindowToken(), 0); SharedPreferenceUtils.putString(MainActivity.this, "system_host", ""); - DeviceUtils.changeHost(((SysApplication)getApplication()).proxy,editText.getText()+""); + DeviceUtils.changeHost(((SysApplication) getApplication()).proxy, editText.getText() + ""); } }); builder.show(); } + + private class OnGlobalLayoutListener implements ViewTreeObserver.OnGlobalLayoutListener { + private View mView; + + public OnGlobalLayoutListener(View view) { + mView = view; + } + + @Override + public void onGlobalLayout() { + int currentHeight = mView.getHeight(); + if (currentHeight < mLastHeightOfContainer) { // 软键盘打开 + if (mHeightOfVisibility == 0) { + mHeightOfVisibility = currentHeight; + } + isKeyboardOpen = true; + } else if (currentHeight > mLastHeightOfContainer && mLastHeightOfContainer != 0) { // 软键盘关闭 + isKeyboardOpen = false; + // éšè—æœç´¢æ¡† + if (shouldExitSearchView) { + searchItem.collapseActionView(); + } + } + mLastHeightOfContainer = currentHeight; + } + } + + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + installCert(); + Log.i("~~~~", "Receiver installCert"); + } + } + + public class MyUploadDelegate implements UploadStatusDelegate { + @Override + public void onProgress(Context context, UploadInfo uploadInfo) { + Log.e("~~~~", uploadInfo.getProgressPercent() + ""); + } + + @Override + public void onError(Context context, UploadInfo uploadInfo, ServerResponse serverResponse, Exception exception) { + dismissLoading(); + Snackbar.make(rootView, "上传失败ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); + exception.printStackTrace(); + CrashReport.postCatchedException(exception); + } + + @Override + public void onCompleted(Context context, UploadInfo uploadInfo, ServerResponse serverResponse) { + try { + JSONObject jsonObject = new JSONObject(serverResponse.getBodyAsString()); + if (jsonObject.getInt("errId") == 0) { + Snackbar.make(rootView, "上传æˆåŠŸï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); + } else if (jsonObject.getInt("errId") == 2 || jsonObject.getInt("errId") == 11004) { + Snackbar.make(rootView, "验è¯ç é”™è¯¯ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); + showUploadDialog(MainActivity.this); + } else { + Snackbar.make(rootView, "上传失败ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); + } + } catch (Exception e) { + Snackbar.make(rootView, "上传失败ï¼", Snackbar.LENGTH_LONG).setAction("Action", null).show(); + } + dismissLoading(); + } + + @Override + public void onCancelled(Context context, UploadInfo uploadInfo) { + dismissLoading(); + } + } + + private class ButtonOnClick implements DialogInterface.OnClickListener { + + private int index = -1; // 表示选项的索引 + + @Override + public void onClick(DialogInterface dialog, int which) { + if (which >= 0) { + index = which; + } else { + //用户å•击的是ã€ç¡®å®šã€‘按钮 + if (which == DialogInterface.BUTTON_POSITIVE) { + SharedPreferenceUtils.putString(MainActivity.this, "select_ua", index + ""); + WebViewFragment.getInstance().setUserAgent(); + } + } + } + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/SettingsActivity.java b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/SettingsActivity.java index e038f6a..234ee8a 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Activity/SettingsActivity.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Activity/SettingsActivity.java @@ -43,10 +43,11 @@ * href="http://developer.android.com/guide/topics/ui/settings.html">Settings * API Guide for more information on developing a Settings UI. */ -public class SettingsActivity extends AppCompatPreferenceActivity implements Preference.OnPreferenceChangeListener { +public class SettingsActivity extends AbstractAppCompatPreferenceActivity implements Preference.OnPreferenceChangeListener { ListPreference lp;//创建一个ListPreference对象 Preference hostPreference; + private LoadingDialog loadingDialog; /** * Helper method to determine if the device has an extra-large screen. For @@ -127,7 +128,6 @@ public boolean onIsMultiPane() { return isXLargeTablet(this); } - //让所选择的项显示出æ¥,获å–å˜åŒ–å¹¶æ˜¾ç¤ºå‡ºæ¥ @Override public boolean onPreferenceChange(Preference preference, Object newValue) { @@ -143,13 +143,13 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { } // 设置hosts - if (preference.getKey().equals("system_host")) { - DeviceUtils.changeHost(((SysApplication)getApplication()).proxy,newValue.toString()); + if ("system_host".equals(preference.getKey())) { + DeviceUtils.changeHost(((SysApplication) getApplication()).proxy, newValue.toString()); hostPreference.setSummary(getHost()); } // é‡å¯æŠ“包进程 - if (preference.getKey().equals("enable_filter")) { + if ("enable_filter".equals(preference.getKey())) { Toast.makeText(this, "é‡å¯ç¨‹åºåŽç”Ÿæ•ˆ", Toast.LENGTH_SHORT).show(); // ((SysApplication)getApplication()).stopProxy(); // ((SysApplication)getApplication()).startProxy(); @@ -158,8 +158,6 @@ public boolean onPreferenceChange(Preference preference, Object newValue) { return true; } - - public void installCert() { final String CERTIFICATE_RESOURCE = Environment.getExternalStorageDirectory() + "/har/littleproxy-mitm.pem"; Toast.makeText(this, "必须安装è¯ä¹¦æ‰å¯å®žçްHTTPS抓包", Toast.LENGTH_LONG).show(); @@ -188,11 +186,9 @@ public void run() { } }; - FileUtil.checkPermission(this,runnable); + FileUtil.checkPermission(this, runnable); } - private LoadingDialog loadingDialog; - public void showLoading(final String text) { runOnUiThread(new Runnable() { @Override diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/ContentFilterAdapter.java b/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/ContentFilterAdapter.java index 2ca0355..3cd1c39 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/ContentFilterAdapter.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/ContentFilterAdapter.java @@ -12,29 +12,23 @@ import java.util.List; import cn.darkal.networkdiagnosis.Activity.ChangeFilterActivity; -import cn.darkal.networkdiagnosis.Activity.MainActivity; import cn.darkal.networkdiagnosis.BR; -import cn.darkal.networkdiagnosis.Bean.PageBean; import cn.darkal.networkdiagnosis.Bean.ResponseFilterRule; -import cn.darkal.networkdiagnosis.Fragment.PreviewFragment; import cn.darkal.networkdiagnosis.R; -import cn.darkal.networkdiagnosis.SysApplication; -import cn.darkal.networkdiagnosis.Utils.DeviceUtils; /** * Created by Darkal on 2016/9/5. */ -public class ContentFilterAdapter extends BaseAdapter{ +public class ContentFilterAdapter extends BaseAdapter { ChangeFilterActivity changeFilterActivity; + private List ruleList; - public ContentFilterAdapter(ChangeFilterActivity changeFilterActivity,List ruleList){ + public ContentFilterAdapter(ChangeFilterActivity changeFilterActivity, List ruleList) { this.ruleList = ruleList; this.changeFilterActivity = changeFilterActivity; } - private List ruleList; - @Override public int getCount() { return ruleList.size(); @@ -60,7 +54,7 @@ public View getView(final int position, View convertView, ViewGroup parent) { convertView = listItemBinding.getRoot(); convertView.setTag(listItemBinding); } - listItemBinding.setVariable(BR.pages,ruleList.get(position)); + listItemBinding.setVariable(BR.pages, ruleList.get(position)); listItemBinding.executePendingBindings(); // listItemBinding.cli(new ButtonClick(MainActivity.this,position)); convertView.setOnClickListener(new View.OnClickListener() { @@ -75,12 +69,14 @@ public boolean onLongClick(View v) { AlertDialog.Builder builder = new AlertDialog.Builder(changeFilterActivity); builder.setTitle("è¯·ç¡®è®¤æ˜¯å¦æ¸…除该注入项?"); builder.setPositiveButton("确定", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { ruleList.remove(ruleList.get(position)); ContentFilterAdapter.this.notifyDataSetChanged(); } }); builder.setNegativeButton("å–æ¶ˆ", new DialogInterface.OnClickListener() { + @Override public void onClick(DialogInterface dialog, int whichButton) { } }); diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/PageFilterAdapter.java b/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/PageFilterAdapter.java index 275a3e9..dc00d4d 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/PageFilterAdapter.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Adapter/PageFilterAdapter.java @@ -9,22 +9,22 @@ import java.util.List; -import cn.darkal.networkdiagnosis.Bean.PageBean; import cn.darkal.networkdiagnosis.BR; +import cn.darkal.networkdiagnosis.Bean.PageBean; import cn.darkal.networkdiagnosis.R; /** * Created by Darkal on 2016/9/5. */ -public class PageFilterAdapter extends BaseAdapter{ +public class PageFilterAdapter extends BaseAdapter { + + private List pageBeenList; - public PageFilterAdapter(List pageBeenList){ + public PageFilterAdapter(List pageBeenList) { this.pageBeenList = pageBeenList; } - private List pageBeenList; - @Override public int getCount() { return pageBeenList.size(); @@ -50,7 +50,7 @@ public View getView(int position, View convertView, ViewGroup parent) { convertView = listItemBinding.getRoot(); convertView.setTag(listItemBinding); } - listItemBinding.setVariable(BR.pages,pageBeenList.get(position)); + listItemBinding.setVariable(BR.pages, pageBeenList.get(position)); listItemBinding.executePendingBindings(); // listItemBinding.setButtonclick(new ButtonClick(MainActivity.this,position)); return convertView; diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Bean/PageBean.java b/app/src/main/java/cn/darkal/networkdiagnosis/Bean/PageBean.java index 1a01f51..e081de1 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Bean/PageBean.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Bean/PageBean.java @@ -28,21 +28,21 @@ public void setName(String name) { } public String getCount() { - return count+"请求"; + return count + "请求"; + } + + public void setCount(String count) { + this.count = count; } public Integer getCountInt() { - try{ + try { return Integer.parseInt(count); - }catch (Exception e){ + } catch (Exception e) { return 0; } } - public void setCount(String count) { - this.count = count; - } - public Boolean getSelected() { return isSelected; } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Bean/ResponseFilterRule.java b/app/src/main/java/cn/darkal/networkdiagnosis/Bean/ResponseFilterRule.java index 7605dc0..b987fc4 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Bean/ResponseFilterRule.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Bean/ResponseFilterRule.java @@ -8,14 +8,8 @@ * Created by darkal on 2017/5/31. */ -public class ResponseFilterRule implements Serializable{ - enum RULE_TYPE{ - STRING_REPLACE, - BEGIN_INSERT, - END_INSERT - } - - private RULE_TYPE ruleType = STRING_REPLACE; +public class ResponseFilterRule implements Serializable { + private RULE_TYPE ruleType = STRING_REPLACE; private String url; private String replaceRegex; private String replaceContent; @@ -60,4 +54,10 @@ public Boolean getEnable() { public void setEnable(Boolean enable) { isEnable = enable; } + + enum RULE_TYPE { + STRING_REPLACE, + BEGIN_INSERT, + END_INSERT + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/BaseFragment.java b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/BaseFragment.java index d4f8850..92620ab 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/BaseFragment.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/BaseFragment.java @@ -23,10 +23,10 @@ public abstract class BaseFragment extends Fragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if(!(getActivity() instanceof BackHandledInterface)){ + if (!(getActivity() instanceof BackHandledInterface)) { throw new ClassCastException("Hosting Activity must implement BackHandledInterface"); - }else{ - this.mBackHandledInterface = (BackHandledInterface)getActivity(); + } else { + this.mBackHandledInterface = (BackHandledInterface) getActivity(); } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/NetworkFragment.java b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/NetworkFragment.java index 6686f4f..08d52f4 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/NetworkFragment.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/NetworkFragment.java @@ -18,28 +18,22 @@ public class NetworkFragment extends BaseFragment { + static NetworkFragment networkFragment; @BindView(R.id.bt_ping) Button pingButton; - @BindView(R.id.bt_dns) Button dnsButton; - @BindView(R.id.bt_trace) Button traceButton; - @BindView(R.id.bt_info) Button infoButton; - @BindView(R.id.tv_result) TextView resultTextView; - @BindView(R.id.et_url) EditText urlEditText; - static NetworkFragment networkFragment; - public static NetworkFragment getInstance() { - if(networkFragment == null){ + if (networkFragment == null) { networkFragment = new NetworkFragment(); } return networkFragment; @@ -64,7 +58,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, pingButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - PingTask pingTask = new PingTask(urlEditText.getText()+"",resultTextView); + PingTask pingTask = new PingTask(urlEditText.getText() + "", resultTextView); pingTask.doTask(); } }); @@ -72,7 +66,7 @@ public void onClick(View v) { dnsButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - DnsTask pingTask = new DnsTask(urlEditText.getText()+"",resultTextView); + DnsTask pingTask = new DnsTask(urlEditText.getText() + "", resultTextView); pingTask.doTask(); } }); @@ -80,7 +74,7 @@ public void onClick(View v) { traceButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - TraceTask pingTask = new TraceTask(getActivity(),urlEditText.getText()+"",resultTextView); + TraceTask pingTask = new TraceTask(getActivity(), urlEditText.getText() + "", resultTextView); pingTask.doTask(); } }); @@ -88,7 +82,7 @@ public void onClick(View v) { infoButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { - InfoTask pingTask = new InfoTask(urlEditText.getText()+"",resultTextView); + InfoTask pingTask = new InfoTask(urlEditText.getText() + "", resultTextView); pingTask.doTask(); } }); diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/PreviewFragment.java b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/PreviewFragment.java index f2ca02f..aa050e0 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/PreviewFragment.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/PreviewFragment.java @@ -28,18 +28,14 @@ public class PreviewFragment extends BaseFragment { + static PreviewFragment previewFragment; @BindView(R.id.rv_preview) RecyclerView recyclerView; - HarLog harLog; List harEntryList = new ArrayList<>(); - PreviewAdapter previewAdapter; - Boolean isHiddenHID = false; - static PreviewFragment previewFragment; - public static PreviewFragment getInstance() { if (previewFragment == null) { previewFragment = new PreviewFragment(); @@ -59,7 +55,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, View view = inflater.inflate(R.layout.fragment_preview, container, false); ButterKnife.bind(this, view); - if(SysApplication.isInitProxy) { + if (SysApplication.isInitProxy) { harLog = ((SysApplication) getActivity().getApplication()).proxy.getHar().getLog(); harEntryList.addAll(harLog.getEntries()); } @@ -67,13 +63,54 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); recyclerView.setAdapter(previewAdapter = new PreviewAdapter()); - if(((MainActivity) getActivity()).searchView!=null){ + if (((MainActivity) getActivity()).searchView != null) { ((MainActivity) getActivity()).searchView.setVisibility(View.VISIBLE); } return view; } + @Override + public void setUserVisibleHint(boolean isVisibleToUser) { + super.setUserVisibleHint(isVisibleToUser); + if (isVisibleToUser) { +// ((MainActivity) getActivity()).navigationView.setCheckedItem(R.id.nav_preview); + notifyHarChange(); + } + } + + public void notifyHarChange() { + if (previewAdapter != null) { + harLog = ((MainActivity) getActivity()).getFiltedHar().getLog(); + harEntryList.clear(); + harEntryList.addAll(harLog.getEntries()); + previewAdapter.notifyDataSetChanged(); + } + } + + @Override + public boolean onBackPressed() { + return false; + } + + public void filterItem(CharSequence s) { + if (previewAdapter != null) { + previewAdapter.getFilter().filter(s); + } + } + + @Override + public void onResume() { + super.onResume(); + + // 这里为了解决返回åŽç„¦ç‚¹åœ¨æœç´¢æ çš„bug + if (recyclerView != null) { + recyclerView.requestFocus(); + if (((MainActivity) getActivity()).searchView != null) { + filterItem(((MainActivity) getActivity()).searchView.getQuery()); + } + } + } private class PreviewAdapter extends RecyclerView.Adapter implements Filterable { @@ -87,13 +124,13 @@ public void onBindViewHolder(MyViewHolder holder, int position) { HarEntry harEntry = harEntryList.get(position); holder.rootView.setOnClickListener(new ClickListner(harEntry)); holder.tv.setText(harEntry.getRequest().getUrl()); - if(harEntry.getResponse().getStatus()>400){ + if (harEntry.getResponse().getStatus() > 400) { holder.iconView.setImageDrawable(getResources().getDrawable(R.drawable.ic_error_black_24dp)); - }else if(harEntry.getResponse().getStatus()>300){ + } else if (harEntry.getResponse().getStatus() > 300) { holder.iconView.setImageDrawable(getResources().getDrawable(R.drawable.ic_directions_black_24dp)); - }else if(harEntry.getResponse().getContent().getMimeType().contains("image")) { + } else if (harEntry.getResponse().getContent().getMimeType().contains("image")) { holder.iconView.setImageDrawable(getResources().getDrawable(R.drawable.ic_photo_black_24dp)); - }else{ + } else { holder.iconView.setImageDrawable(getResources().getDrawable(R.drawable.ic_description_black_24dp)); } holder.detailTextView.setText("Status:" + harEntry.getResponse().getStatus() + @@ -106,23 +143,6 @@ public int getItemCount() { return harEntryList.size(); } - - public class MyViewHolder extends RecyclerView.ViewHolder { - - TextView tv; - TextView detailTextView; - View rootView; - ImageView iconView; - - public MyViewHolder(View view) { - super(view); - tv = (TextView) view.findViewById(R.id.tv_url); - detailTextView = (TextView) view.findViewById(R.id.tv_detail); - rootView = view; - iconView = (ImageView) view.findViewById(R.id.iv_icon); - } - } - @Override public Filter getFilter() { return new Filter() { @@ -172,7 +192,7 @@ protected FilterResults performFiltering(CharSequence constraint) { @Override protected void publishResults(CharSequence constraint, FilterResults results) { harEntryList.clear();//æ¸…é™¤åŽŸå§‹æ•°æ® - if(results.values instanceof List){ + if (results.values instanceof List) { harEntryList.addAll((List) results.values);//将过滤结果添加到这个对象 } if (results.count > 0) { @@ -190,36 +210,34 @@ protected void publishResults(CharSequence constraint, FilterResults results) { } }; } - } - @Override - public void setUserVisibleHint(boolean isVisibleToUser) { - super.setUserVisibleHint(isVisibleToUser); - if (isVisibleToUser) { -// ((MainActivity) getActivity()).navigationView.setCheckedItem(R.id.nav_preview); - notifyHarChange(); - } - } + public class MyViewHolder extends RecyclerView.ViewHolder { - public void notifyHarChange(){ - if (previewAdapter != null) { - harLog = ((MainActivity) getActivity()).getFiltedHar().getLog(); - harEntryList.clear(); - harEntryList.addAll(harLog.getEntries()); - previewAdapter.notifyDataSetChanged(); + TextView tv; + TextView detailTextView; + View rootView; + ImageView iconView; + + public MyViewHolder(View view) { + super(view); + tv = (TextView) view.findViewById(R.id.tv_url); + detailTextView = (TextView) view.findViewById(R.id.tv_detail); + rootView = view; + iconView = (ImageView) view.findViewById(R.id.iv_icon); + } } } public class ClickListner implements View.OnClickListener { HarEntry harEntry; - public ClickListner(HarEntry harEntry){ + public ClickListner(HarEntry harEntry) { this.harEntry = harEntry; } @Override public void onClick(View view) { - if(harLog.getEntries().indexOf(harEntry)>=0) { + if (harLog.getEntries().indexOf(harEntry) >= 0) { isHiddenHID = true; Intent intent = new Intent(getContext(), HarDetailActivity.class); intent.putExtra("pos", ((SysApplication) getActivity().getApplication()).proxy. @@ -228,28 +246,4 @@ public void onClick(View view) { } } } - - @Override - public boolean onBackPressed() { - return false; - } - - public void filterItem(CharSequence s){ - if(previewAdapter!=null) { - previewAdapter.getFilter().filter(s); - } - } - - @Override - public void onResume() { - super.onResume(); - - // 这里为了解决返回åŽç„¦ç‚¹åœ¨æœç´¢æ çš„bug - if(recyclerView!=null) { - recyclerView.requestFocus(); - if(((MainActivity)getActivity()).searchView!=null) { - filterItem(((MainActivity) getActivity()).searchView.getQuery()); - } - } - } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/WebViewFragment.java b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/WebViewFragment.java index 11a10a9..a0f889d 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/WebViewFragment.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Fragment/WebViewFragment.java @@ -1,18 +1,14 @@ package cn.darkal.networkdiagnosis.Fragment; -import android.annotation.TargetApi; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.SharedPreferences; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.preference.PreferenceManager; import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import android.view.KeyEvent; @@ -24,7 +20,6 @@ import android.webkit.ConsoleMessage; import android.webkit.DownloadListener; import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; @@ -44,31 +39,22 @@ import cn.darkal.networkdiagnosis.Utils.SharedPreferenceUtils; public class WebViewFragment extends BaseFragment { + private final static WebViewFragment webViewFragment = new WebViewFragment(); + public Boolean isSetProxy = false; + public String baseUserAgentString = "Mozilla/5.0 (Linux; Android 5.0.2) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/37.0.0.0"; + public String userAgentString = baseUserAgentString; @BindView(R.id.fl_webview) WebView webView; - @BindView(R.id.bt_jump) Button jumpButton; - @BindView(R.id.et_url) EditText urlText; - @BindView(R.id.pb_progress) ProgressBar progressBar; - @BindView(R.id.swipe_container) SwipeRefreshLayout swipeRefreshLayout; - Receiver receiver; - public Boolean isSetProxy = false; - - public String baseUserAgentString = "Mozilla/5.0 (Linux; Android 5.0.2) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/37.0.0.0"; - - public String userAgentString = baseUserAgentString; - - private final static WebViewFragment webViewFragment = new WebViewFragment(); - public static WebViewFragment getInstance() { return webViewFragment; } @@ -108,7 +94,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, webSettings.setDomStorageEnabled(true); webSettings.setGeolocationEnabled(true); - baseUserAgentString = webSettings.getUserAgentString()+" jdhttpmonitor/" + DeviceUtils.getVersion(getContext()); + baseUserAgentString = webSettings.getUserAgentString() + " jdhttpmonitor/" + DeviceUtils.getVersion(getContext()); webSettings.setUserAgentString(userAgentString); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { @@ -130,7 +116,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if(url.startsWith("jdhttpmonitor://webview")) { + if (url.startsWith("jdhttpmonitor://webview")) { Intent intent = new Intent("android.intent.action.VIEW"); intent.setData(Uri.parse(url)); startActivity(intent); @@ -160,10 +146,11 @@ public void onPageStarted(WebView view, String url, Bitmap favicon) { @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { - ((MainActivity)getActivity()).consoleLog.append(consoleMessage.message()).append("\n").append("\n"); + ((MainActivity) getActivity()).consoleLog.append(consoleMessage.message()).append("\n").append("\n"); return super.onConsoleMessage(consoleMessage); } + @Override public void onProgressChanged(WebView view, int progress) { progressBar.setProgress(progress); if (progress == 100) { @@ -209,7 +196,7 @@ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { } }); - swipeRefreshLayout.setColorSchemeResources(R.color.colorAccentDark,R.color.colorAccent); + swipeRefreshLayout.setColorSchemeResources(R.color.colorAccentDark, R.color.colorAccent); swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override @@ -251,26 +238,18 @@ public void initProxyWebView() { webView.post(new Runnable() { @Override public void run() { - if(ProxyUtils.setProxy(webView, "127.0.0.1", SysApplication.proxyPort)){ + if (ProxyUtils.setProxy(webView, "127.0.0.1", SysApplication.proxyPort)) { Log.e("~~~~", "initProxyWebView()"); webView.loadUrl(urlText.getText() + ""); isSetProxy = true; - }else{ - Toast.makeText(webView.getContext(),"Set proxy fail!",Toast.LENGTH_LONG).show(); + } else { + Toast.makeText(webView.getContext(), "Set proxy fail!", Toast.LENGTH_LONG).show(); } } }); } } - public class Receiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - initProxyWebView(); - Log.i("~~~~", "Receiver initProxyWebView"); - } - } - public void loadUrl(String url) { if (webView != null) { if (!isSetProxy) { @@ -299,10 +278,10 @@ public void onSaveInstanceState(Bundle outState) { } - public void setUserAgent(){ + public void setUserAgent() { String originUA = userAgentString; - switch (SharedPreferenceUtils.getString(getContext(),"select_ua", "0")) { + switch (SharedPreferenceUtils.getString(getContext(), "select_ua", "0")) { case "0": userAgentString = baseUserAgentString; break; @@ -312,21 +291,31 @@ public void setUserAgent(){ case "2": userAgentString = baseUserAgentString + " MQQBrowser/6.2 TBS/036524 V1_AND_SQ_6.0.0_300_YYB_D QQ/6.0.0.2605 NetType/WIFI WebP/0.3.0 Pixel/1440"; break; + default: + break; } WebSettings webSettings = webView.getSettings(); webSettings.setUserAgentString(userAgentString); - if(!originUA.equals(userAgentString) && webView!=null){ + if (!originUA.equals(userAgentString) && webView != null) { reload(); } } - public void reload(){ - if(webView!=null && webView.getUrl()!=null) { + public void reload() { + if (webView != null && webView.getUrl() != null) { webView.reload(); } } + public class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + initProxyWebView(); + Log.i("~~~~", "Receiver initProxyWebView"); + } + } + private class MyWebViewDownLoadListener implements DownloadListener { @Override public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/MyVpnService.java b/app/src/main/java/cn/darkal/networkdiagnosis/MyVpnService.java index 3ab2c31..f643ceb 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/MyVpnService.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/MyVpnService.java @@ -36,7 +36,7 @@ public MyVpnService() { private void setupVpn() { ParcelFileDescriptor parcelFileDescriptor; - Log.i("~~~","VpnService: try to setup VPN."); + Log.i("~~~", "VpnService: try to setup VPN."); Builder builder = new Builder(); builder.setSession("firewall"); builder.addAddress("10.0.8.1", 32); @@ -134,10 +134,11 @@ private void setupVpn() { } } - }catch(final IllegalStateException e) { - Log.i("~~~","VpnService: builder.establish() failed."); - if(mHandler != null) { + } catch (final IllegalStateException e) { + Log.i("~~~", "VpnService: builder.establish() failed."); + if (mHandler != null) { mHandler.post(new Runnable() { + @Override public void run() { Toast.makeText(MyVpnService.this, "Cannot establish VPN (" + e.toString() + ")", Toast.LENGTH_LONG).show(); @@ -364,6 +365,8 @@ private void configure(String parameters) throws Exception { case 's': builder.addSearchDomain(fields[1]); break; + default: + break; } } catch (Exception e) { throw new IllegalArgumentException("Bad parameter: " + parameter); diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/SysApplication.java b/app/src/main/java/cn/darkal/networkdiagnosis/SysApplication.java index 88eff04..aecef2b 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/SysApplication.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/SysApplication.java @@ -6,7 +6,9 @@ import android.preference.PreferenceManager; import android.support.multidex.MultiDexApplication; import android.util.Log; + import com.tencent.bugly.Bugly; + import net.gotev.uploadservice.UploadService; import net.lightbody.bmp.BrowserMobProxy; import net.lightbody.bmp.BrowserMobProxyServer; @@ -75,13 +77,13 @@ public void onTerminate() { new Thread(new Runnable() { @Override public void run() { - Log.e("~~~","onTerminate"); + Log.e("~~~", "onTerminate"); proxy.stop(); } }).start(); } - public void startProxy(){ + public void startProxy() { try { proxy = new BrowserMobProxyServer(); proxy.setTrustAllServers(true); @@ -106,18 +108,18 @@ public void startProxy(){ SharedPreferences shp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - if(shp.getBoolean("enable_filter", false)) { + if (shp.getBoolean("enable_filter", false)) { Log.e("~~~enable_filter", ""); initResponseFilter(); } // 设置hosts - if(shp.getString("system_host", "").length()>0){ + if (shp.getString("system_host", "").length() > 0) { AdvancedHostResolver advancedHostResolver = proxy.getHostNameResolver(); - for (String temp : shp.getString("system_host", "").split("\\n")){ - if(temp.split(" ").length==2) { - advancedHostResolver.remapHost(temp.split(" ")[1],temp.split(" ")[0]); - Log.e("~~~~remapHost ",temp.split(" ")[1] +" " + temp.split(" ")[0]); + for (String temp : shp.getString("system_host", "").split("\\n")) { + if (temp.split(" ").length == 2) { + advancedHostResolver.remapHost(temp.split(" ")[1], temp.split(" ")[0]); + Log.e("~~~~remapHost ", temp.split(" ")[1] + " " + temp.split(" ")[0]); } } proxy.setHostNameResolver(advancedHostResolver); @@ -135,15 +137,15 @@ public void startProxy(){ isInitProxy = true; } - public void stopProxy(){ - if(proxy!=null){ + public void stopProxy() { + if (proxy != null) { proxy.stop(); } } - private void initResponseFilter(){ + private void initResponseFilter() { try { - if(ruleList == null){ + if (ruleList == null) { ResponseFilterRule rule = new ResponseFilterRule(); rule.setUrl("xw.qq.com/index.htm"); rule.setReplaceRegex(""); @@ -153,8 +155,8 @@ private void initResponseFilter(){ ruleList.add(rule); } - DeviceUtils.changeResponseFilter(this,ruleList); - }catch (Exception e){ + DeviceUtils.changeResponseFilter(this, ruleList); + } catch (Exception e) { e.printStackTrace(); } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/BaseTask.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/BaseTask.java index 6ed5ab1..1517a7c 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/BaseTask.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/BaseTask.java @@ -17,33 +17,33 @@ public BaseTask(String url, TextView resultTextView) { this.resultTextView = resultTextView; } - public void doTask(){ + public void doTask() { resultTextView.setText(""); - tag = System.currentTimeMillis()+""; + tag = System.currentTimeMillis() + ""; resultTextView.setTag(tag); // TraceTaskè¿è¡ŒäºŽä¸»çº¿ç¨‹ - if(this instanceof TraceTask){ + if (this instanceof TraceTask) { getExecRunnable().run(); - }else { + } else { new Thread(getExecRunnable()).start(); } } - public class updateResultRunnable implements Runnable{ + public abstract Runnable getExecRunnable(); + + public class updateResultRunnable implements Runnable { String resultString; - public updateResultRunnable(String resultString){ + public updateResultRunnable(String resultString) { this.resultString = resultString; } @Override public void run() { - if(resultTextView!=null && resultTextView.getTag().equals(tag)) { + if (resultTextView != null && resultTextView.getTag().equals(tag)) { resultTextView.append(resultString); resultTextView.requestFocus(); } } } - - public abstract Runnable getExecRunnable(); } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/DnsTask.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/DnsTask.java index 7973c72..9a64f9f 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/DnsTask.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/DnsTask.java @@ -1,6 +1,7 @@ package cn.darkal.networkdiagnosis.Task; import android.widget.TextView; + import java.net.InetAddress; /** @@ -9,35 +10,32 @@ public class DnsTask extends BaseTask { String url; TextView resultTextView; - - public DnsTask(String url, TextView resultTextView) { - super(url, resultTextView); - this.url = url; - this.resultTextView = resultTextView; - } - - @Override - public Runnable getExecRunnable() { - return execRunnable; - } - public Runnable execRunnable = new Runnable() { @Override public void run() { StringBuilder sb = new StringBuilder(); - try{ + try { InetAddress aaa = InetAddress.getByName(url); InetAddress[] addrs = InetAddress.getAllByName(url); sb.append("Begin: \n" + aaa.toString() + "\nEnd\n"); - for (InetAddress adr : addrs) - { + for (InetAddress adr : addrs) { sb.append(adr.toString() + "\n"); resultTextView.post(new updateResultRunnable(adr.toString() + "\n")); } - } - catch (Exception e){ + } catch (Exception e) { resultTextView.post(new updateResultRunnable(e.toString() + "\n")); } } }; + + public DnsTask(String url, TextView resultTextView) { + super(url, resultTextView); + this.url = url; + this.resultTextView = resultTextView; + } + + @Override + public Runnable getExecRunnable() { + return execRunnable; + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/InfoTask.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/InfoTask.java index a03f03b..03d5d34 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/InfoTask.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/InfoTask.java @@ -2,7 +2,6 @@ import android.widget.TextView; - import cn.darkal.networkdiagnosis.Utils.NetInfo.NetBasicInfo; import cn.darkal.networkdiagnosis.Utils.NetInfo.SystemBasicInfo; @@ -13,18 +12,6 @@ public class InfoTask extends BaseTask { String url; TextView resultTextView; - - public InfoTask(String url, TextView resultTextView) { - super(url, resultTextView); - this.url = url; - this.resultTextView = resultTextView; - } - - @Override - public Runnable getExecRunnable() { - return execRunnable; - } - public Runnable execRunnable = new Runnable() { @Override public void run() { @@ -38,4 +25,15 @@ public void run() { + "\n")); } }; + + public InfoTask(String url, TextView resultTextView) { + super(url, resultTextView); + this.url = url; + this.resultTextView = resultTextView; + } + + @Override + public Runnable getExecRunnable() { + return execRunnable; + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/PingTask.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/PingTask.java index c8aca57..4f7ce66 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/PingTask.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/PingTask.java @@ -13,18 +13,6 @@ public class PingTask extends BaseTask { String url; TextView resultTextView; - - public PingTask(String url, TextView resultTextView) { - super(url, resultTextView); - this.url = url; - this.resultTextView = resultTextView; - } - - @Override - public Runnable getExecRunnable() { - return execRunnable; - } - public Runnable execRunnable = new Runnable() { @Override public void run() { @@ -48,14 +36,25 @@ public void run() { } catch (IOException e) { resultTextView.post(new updateResultRunnable(e.toString() + "\n")); } finally { - try{ + try { if (in != null) { in.close(); } - }catch (Exception e){ + } catch (Exception e) { e.printStackTrace(); } } } }; + + public PingTask(String url, TextView resultTextView) { + super(url, resultTextView); + this.url = url; + this.resultTextView = resultTextView; + } + + @Override + public Runnable getExecRunnable() { + return execRunnable; + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteContainer.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteContainer.java index ffcf93c..416bddc 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteContainer.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteContainer.java @@ -40,49 +40,40 @@ public TraceRouteContainer(String hostname, String ip, float ms, boolean isSucce this.isSuccessful = isSuccessful; } - public String getHostname() - { + public String getHostname() { return hostname; } - public void setHostname(String hostname) - { + public void setHostname(String hostname) { this.hostname = hostname; } - public String getIp() - { + public String getIp() { return ip; } - public void setIp(String ip) - { + public void setIp(String ip) { this.ip = ip; } - public float getMs() - { + public float getMs() { return ms; } - public void setMs(float ms) - { + public void setMs(float ms) { this.ms = ms; } - public boolean isSuccessful() - { + public boolean isSuccessful() { return isSuccessful; } - public void setSuccessful(boolean isSuccessful) - { + public void setSuccessful(boolean isSuccessful) { this.isSuccessful = isSuccessful; } @Override - public String toString() - { + public String toString() { return "Traceroute : \nHostname : " + hostname + "\nip : " + ip + "\nMilliseconds : " + ms; } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteWithPing.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteWithPing.java index 86cc5e1..d05acef 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteWithPing.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceRouteWithPing.java @@ -22,6 +22,7 @@ import android.annotation.SuppressLint; import android.os.AsyncTask; import android.os.Handler; +import android.text.TextUtils; import android.util.Log; import java.io.BufferedReader; @@ -88,7 +89,7 @@ private String parseIpFromPing(String ping) { // Get ip when ttl exceeded int index = ping.indexOf(FROM_PING); - if(index==0){ + if (index == 0) { index = ping.indexOf(SMALL_FROM_PING); } @@ -300,7 +301,7 @@ private String launchPing(String url) throws Exception { p.destroy(); - if (res.equals("")) { + if (TextUtils.isEmpty(res) || TextUtils.isEmpty(res.trim())) { throw new IllegalArgumentException(); } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceTask.java b/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceTask.java index fcc82e1..2fd0c44 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceTask.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Task/TraceTask.java @@ -2,7 +2,6 @@ import android.Manifest; import android.app.Activity; -import android.content.Context; import android.content.pm.PackageManager; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; @@ -20,27 +19,14 @@ /** * Created by xuzhou on 2016/8/1. */ -public class TraceTask extends BaseTask implements LDNetDiagnoListener { +public class TraceTask extends BaseTask implements LDNetDiagnoListener { String url; TextView resultTextView; Activity context; - - public TraceTask(Activity context , String url, TextView resultTextView) { - super(url, resultTextView); - this.context = context; - this.url = url; - this.resultTextView = resultTextView; - } - - @Override - public Runnable getExecRunnable() { - return execRunnable; - } - public Runnable execRunnable = new Runnable() { @Override public void run() { - try{ + try { int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE); if (permissionCheck != PackageManager.PERMISSION_GRANTED) { @@ -48,25 +34,36 @@ public void run() { } else { // TraceRouteWithPing traceRouteWithPing = new TraceRouteWithPing(url, TraceTask.this); // traceRouteWithPing.executeTraceRoute(); - LDNetDiagnoService _netDiagnoService = new LDNetDiagnoService(context.getApplicationContext(), + LDNetDiagnoService netDiagnoService = new LDNetDiagnoService(context.getApplicationContext(), "NetworkDiagnosis", "网络诊断应用", DeviceUtils.getVersion(context), "", "", url, "", "", "", "", TraceTask.this); // 设置是å¦ä½¿ç”¨JNIC 完æˆtraceroute - _netDiagnoService.setIfUseJNICTrace(true); - _netDiagnoService.execute(); + netDiagnoService.setIfUseJNICTrace(true); + netDiagnoService.execute(); } - } - catch (Exception e){ + } catch (Exception e) { resultTextView.post(new updateResultRunnable(e.toString() + "\n")); } } }; + private Pattern pattern = Pattern.compile("(?<=rom )[\\w\\W]+(?=\\n\\n)"); + + public TraceTask(Activity context, String url, TextView resultTextView) { + super(url, resultTextView); + this.context = context; + this.url = url; + this.resultTextView = resultTextView; + } + + @Override + public Runnable getExecRunnable() { + return execRunnable; + } - public void setResult(String result){ - Pattern pattern = Pattern.compile("(?<=rom )[\\w\\W]+(?=\\n\\n)"); + public void setResult(String result) { Matcher matcher = pattern.matcher(result); - if(matcher.find()){ + if (matcher.find()) { resultTextView.post(new updateResultRunnable(matcher.group(0) + "\n")); } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/X509ExtendedTrustManager.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/AbstractX509ExtendedTrustManager.java similarity index 98% rename from app/src/main/java/cn/darkal/networkdiagnosis/Utils/X509ExtendedTrustManager.java rename to app/src/main/java/cn/darkal/networkdiagnosis/Utils/AbstractX509ExtendedTrustManager.java index 4f407b6..8355100 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/X509ExtendedTrustManager.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/AbstractX509ExtendedTrustManager.java @@ -33,7 +33,7 @@ * @see SSLParameters#setEndpointIdentificationAlgorithm(String) * @since 1.7 */ -public abstract class X509ExtendedTrustManager implements X509TrustManager { +public abstract class AbstractX509ExtendedTrustManager implements X509TrustManager { /** * Checks whether the specified certificate chain (partial or complete) can * be validated and is trusted for client authentication for the specified diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DatatypeConverter.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DatatypeConverter.java index a8b5947..c3c154f 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DatatypeConverter.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DatatypeConverter.java @@ -8,162 +8,179 @@ */ public class DatatypeConverter { - public static String doFormat( String format, Calendar cal ) throws IllegalArgumentException { - int fidx = 0; - int flen = format.length(); - StringBuilder buf = new StringBuilder(); + private static final byte PADDING = 127; + private static final byte[] decodeMap = initDecodeMap(); - while(fidx= 0) - buf.append('+'); - else { - buf.append('-'); - offset *= -1; - } + if (tz == null) { + return; + } - offset /= 60 * 1000; // offset is in milli-seconds + // otherwise print out normally. + int offset; + if (tz.inDaylightTime(cal.getTime())) { + offset = tz.getRawOffset() + (tz.useDaylightTime() ? 3600000 : 0); + } else { + offset = tz.getRawOffset(); + } - formatTwoDigits(offset / 60, buf); - buf.append(':'); - formatTwoDigits(offset % 60, buf); + if (offset == 0) { + buf.append('Z'); + return; } - /** formats Integer into two-character-wide string. */ - private static void formatTwoDigits(int n,StringBuilder buf) { - // n is always non-negative. - if (n < 10) buf.append('0'); - buf.append(n); + if (offset >= 0) { + buf.append('+'); + } else { + buf.append('-'); + offset *= -1; } + offset /= 60 * 1000; // offset is in milli-seconds - // base64 decoder -//==================================== + formatTwoDigits(offset / 60, buf); + buf.append(':'); + formatTwoDigits(offset % 60, buf); + } - private static final byte[] decodeMap = initDecodeMap(); - private static final byte PADDING = 127; + /** + * formats Integer into two-character-wide string. + */ + private static void formatTwoDigits(int n, StringBuilder buf) { + // n is always non-negative. + if (n < 10) { + buf.append('0'); + } + buf.append(n); + } private static byte[] initDecodeMap() { byte[] map = new byte[128]; int i; - for( i=0; i<128; i++ ) map[i] = -1; + for (i = 0; i < 128; i++) { + map[i] = -1; + } - for( i='A'; i<='Z'; i++ ) map[i] = (byte)(i-'A'); - for( i='a'; i<='z'; i++ ) map[i] = (byte)(i-'a'+26); - for( i='0'; i<='9'; i++ ) map[i] = (byte)(i-'0'+52); + for (i = 'A'; i <= 'Z'; i++) { + map[i] = (byte) (i - 'A'); + } + for (i = 'a'; i <= 'z'; i++) { + map[i] = (byte) (i - 'a' + 26); + } + for (i = '0'; i <= '9'; i++) { + map[i] = (byte) (i - '0' + 52); + } map['+'] = 62; map['/'] = 63; map['='] = PADDING; @@ -171,68 +188,75 @@ private static byte[] initDecodeMap() { return map; } - private static int guessLength( String text ) { + private static int guessLength(String text) { final int len = text.length(); // compute the tail '=' chars - int j=len-1; - for(; j>=0; j-- ) { + int j = len - 1; + for (; j >= 0; j--) { byte code = decodeMap[text.charAt(j)]; - if(code==PADDING) + if (code == PADDING) { continue; - if(code==-1) + } + if (code == -1) { // most likely this base64 text is indented. go with the upper bound - return text.length()/4*3; + return text.length() / 4 * 3; + } break; } j++; // text.charAt(j) is now at some base64 char, so +1 to make it the size - int padSize = len-j; - if(padSize >2) // something is wrong with base64. be safe and go with the upper bound - return text.length()/4*3; + int padSize = len - j; + if (padSize > 2) { + // something is wrong with base64. be safe and go with the upper bound + return text.length() / 4 * 3; + } // so far this base64 looks like it's unindented tightly packed base64. // take a chance and create an array with the expected size - return text.length()/4*3-padSize; + return text.length() / 4 * 3 - padSize; } public static byte[] parseBase64Binary(String text) { final int buflen = guessLength(text); final byte[] out = new byte[buflen]; - int o=0; + int o = 0; final int len = text.length(); int i; final byte[] quadruplet = new byte[4]; - int q=0; + int q = 0; // convert each quadruplet to three bytes. - for( i=0; i>4)); - if( quadruplet[2]!=PADDING ) - out[o++] = (byte)((quadruplet[1]<<4)|(quadruplet[2]>>2)); - if( quadruplet[3]!=PADDING ) - out[o++] = (byte)((quadruplet[2]<<6)|(quadruplet[3])); - q=0; + out[o++] = (byte) ((quadruplet[0] << 2) | (quadruplet[1] >> 4)); + if (quadruplet[2] != PADDING) { + out[o++] = (byte) ((quadruplet[1] << 4) | (quadruplet[2] >> 2)); + } + if (quadruplet[3] != PADDING) { + out[o++] = (byte) ((quadruplet[2] << 6) | (quadruplet[3])); + } + q = 0; } } - if(buflen==o) // speculation worked out to be OK + if (buflen == o) { + // speculation worked out to be OK return out; - + } // we overestimated, so need to create a new buffer byte[] nb = new byte[o]; - System.arraycopy(out,0,nb,0,o); + System.arraycopy(out, 0, nb, 0, o); return nb; } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DeviceUtils.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DeviceUtils.java index 199c9df..77be075 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DeviceUtils.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/DeviceUtils.java @@ -6,23 +6,15 @@ import android.util.Log; import net.lightbody.bmp.BrowserMobProxy; -import net.lightbody.bmp.filters.RequestFilter; import net.lightbody.bmp.filters.ResponseFilter; import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; -import org.littleshoot.proxy.HttpFilters; -import org.littleshoot.proxy.HttpFiltersSource; - -import java.net.InetSocketAddress; import java.util.List; import cn.darkal.networkdiagnosis.Bean.ResponseFilterRule; import cn.darkal.networkdiagnosis.SysApplication; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; /** @@ -67,11 +59,11 @@ public static int dip2px(Context context, int dp) { //dpå’Œpx的转æ¢å…³ç³» float density = context.getResources().getDisplayMetrics().density; //2*1.5+0.5 2*0.75 = 1.5+0.5 - return (int)(dp*density+0.5); + return (int) (dp * density + 0.5); } - public static void changeHost(BrowserMobProxy browserMobProxy,String newValue){ + public static void changeHost(BrowserMobProxy browserMobProxy, String newValue) { AdvancedHostResolver advancedHostResolver = browserMobProxy.getHostNameResolver(); advancedHostResolver.clearHostRemappings(); for (String temp : newValue.split("\\n")) { @@ -85,17 +77,17 @@ public static void changeHost(BrowserMobProxy browserMobProxy,String newValue){ browserMobProxy.setHostNameResolver(advancedHostResolver); } - public static void changeResponseFilter(SysApplication sysApplication,final List ruleList){ - if(ruleList == null){ - Log.e("~~~~","changeResponseFilter ruleList == null!"); + public static void changeResponseFilter(SysApplication sysApplication, final List ruleList) { + if (ruleList == null) { + Log.e("~~~~", "changeResponseFilter ruleList == null!"); return; } sysApplication.proxy.addResponseFilter(new ResponseFilter() { @Override public void filterResponse(HttpResponse response, HttpMessageContents contents, HttpMessageInfo messageInfo) { - for (ResponseFilterRule rule: ruleList) { - if(rule.getEnable()) { + for (ResponseFilterRule rule : ruleList) { + if (rule.getEnable()) { if (contents.isText() && messageInfo.getUrl().contains(rule.getUrl())) { String originContent = contents.getTextContents(); if (originContent != null) { diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/FileUtil.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/FileUtil.java index b8a4b70..97d0ed5 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/FileUtil.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/FileUtil.java @@ -9,7 +9,6 @@ import android.os.Environment; import android.provider.MediaStore; import android.support.v4.app.ActivityCompat; -import android.util.Log; import android.widget.Toast; import net.gotev.uploadservice.MultipartUploadRequest; @@ -428,7 +427,7 @@ public static void saveImageToGallery(Context context, String filePath) { } //递归删除文件夹下é¢çš„æ–‡ä»¶ - public static void deleteFiles(File file){ + public static void deleteFiles(File file) { try { if (file.exists()) { if (file.isDirectory()) { @@ -440,10 +439,11 @@ public static void deleteFiles(File file){ file.delete(); } } - }catch (Exception e){} + } catch (Exception e) { + } } - public static void checkPermission(Activity activity,Runnable runnable) { + public static void checkPermission(Activity activity, Runnable runnable) { //检查æƒé™ï¼ˆNEED_PERMISSION)是å¦è¢«æŽˆæƒ PackageManager.PERMISSION_GRANTEDè¡¨ç¤ºåŒæ„æŽˆæƒ if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/NetBasicInfo.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/NetBasicInfo.java index 9e094cf..e8ee8a4 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/NetBasicInfo.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/NetBasicInfo.java @@ -5,6 +5,8 @@ import android.net.NetworkInfo; import android.telephony.TelephonyManager; +import com.netease.LDNetDiagnoUtils.LDNetUtil; + import org.apache.http.conn.util.InetAddressUtils; import java.io.BufferedReader; @@ -191,24 +193,16 @@ public String getMacAddress(String netInterface) { e.printStackTrace(); } - return strMacAddr; } + public String getApnInfo() { TelephonyManager tel = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); String opCode = tel.getSimOperator(); - String operatorName; - if (opCode.startsWith("46000") || opCode.startsWith("46002")) { - operatorName = "中国移动"; - } else if (opCode.equals("46001")) { - operatorName = "中国è”通"; - } else if (opCode.equals("46003")) { - operatorName = "中国电信"; - } else { - operatorName = "未知"; - } + String operatorName = LDNetUtil.getMobileOperator(mContext); + ConnectivityManager mag = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo mobInfo = mag.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); @@ -291,12 +285,12 @@ public String GetIp(Boolean isV4) { for (Enumeration ipAddr = intf.getInetAddresses(); ipAddr.hasMoreElements(); ) { InetAddress inetAddress = ipAddr.nextElement(); - if(isV4) { + if (isV4) { // ipv4åœ°å€ if (!inetAddress.isLoopbackAddress() && InetAddressUtils.isIPv4Address(inetAddress.getHostAddress())) { return inetAddress.getHostAddress(); } - }else{ + } else { // ipv6åœ°å€ if (!inetAddress.isLoopbackAddress() && InetAddressUtils.isIPv6Address(inetAddress.getHostAddress())) { return inetAddress.getHostAddress(); @@ -310,7 +304,7 @@ public String GetIp(Boolean isV4) { return ""; } - private String getLocalDNS(){ + private String getLocalDNS() { Process cmdProcess = null; BufferedReader reader = null; String dnsIP = ""; @@ -321,7 +315,7 @@ private String getLocalDNS(){ return dnsIP; } catch (IOException e) { return null; - } finally{ + } finally { try { if (reader != null) { reader.close(); diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/SystemBasicInfo.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/SystemBasicInfo.java index 76fed93..5de9a59 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/SystemBasicInfo.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/NetInfo/SystemBasicInfo.java @@ -37,7 +37,7 @@ public static String getBuildInfo() { public static String getUUID(Context context) { try { String androidId = getAndroidId(context); - String deviceId = getDeviceId(context)==null? "null":getDeviceId(context); + String deviceId = getDeviceId(context) == null ? "null" : getDeviceId(context); UUID uuid = new UUID(androidId.hashCode(), ((long) deviceId.hashCode() << 32) | deviceId.hashCode()); return uuid.toString(); } catch (Exception e) { diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/SharedPreferenceUtils.java b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/SharedPreferenceUtils.java index 1627148..d8a8d26 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/Utils/SharedPreferenceUtils.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/Utils/SharedPreferenceUtils.java @@ -1,6 +1,5 @@ package cn.darkal.networkdiagnosis.Utils; -import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; @@ -166,7 +165,7 @@ public static void save(Context context, String key, Object saveObject) { /** * 获å–SharedPreferenceä¿å­˜çš„对象 * - * @param context context + * @param context context * @param key 储存对象的key * @return object 返回根æ®key得到的对象 */ diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/View/ClearTextView.java b/app/src/main/java/cn/darkal/networkdiagnosis/View/ClearTextView.java index 63634a1..5c1824b 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/View/ClearTextView.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/View/ClearTextView.java @@ -33,26 +33,12 @@ public class ClearTextView extends EditText { private int mButtonPadding = dp2px(3); - - - - /** - * æŒ‰é’®æ˜¾ç¤ºæ–¹å¼ - * NEVER 䏿˜¾ç¤ºæ¸…空按钮 - * ALWAYS 始终显示清空按钮 - * WHILEEDITING 输入框内容ä¸ä¸ºç©ºä¸”有获得焦点 - * UNLESSEDITING 输入框内容ä¸ä¸ºç©ºä¸”没有获得焦点 - * */ - public enum ClearButtonMode { - NEVER, ALWAYS, WHILEEDITING, UNLESSEDITING - } - - public ClearTextView(Context context) { super(context); init(context, null); } + public ClearTextView(Context context, AttributeSet attrs) { super(context, attrs); init(context, attrs); @@ -63,10 +49,6 @@ public ClearTextView(Context context, AttributeSet attrs, int defStyleAttr) { init(context, attrs); } - - - - /** * åˆå§‹åŒ– */ @@ -101,10 +83,9 @@ private void init(Context context, AttributeSet attributeSet) { mInitPaddingRight = getPaddingRight(); } - - /** * 按钮状æ€ç®¡ç† + * * @param canvas onDrawçš„Canvas */ private void buttonManager(Canvas canvas) { @@ -123,11 +104,10 @@ private void buttonManager(Canvas canvas) { } } - - /** * è®¾ç½®è¾“å…¥æ¡†çš„å†…è¾¹è· - * @param isShow æ˜¯å¦æ˜¾ç¤ºæŒ‰é’® + * + * @param isShow æ˜¯å¦æ˜¾ç¤ºæŒ‰é’® */ private void setPadding(boolean isShow) { int paddingRight = mInitPaddingRight + (isShow ? mClearButton.getWidth() + mButtonPadding + mButtonPadding : 0); @@ -135,19 +115,18 @@ private void setPadding(boolean isShow) { setPadding(getPaddingLeft(), getPaddingTop(), paddingRight, getPaddingBottom()); } - - /** * å–å¾—æ˜¾ç¤ºæŒ‰é’®ä¸Žä¸æ˜¾ç¤ºæŒ‰é’®æ—¶çš„Rect - * @param isShow æ˜¯å¦æ˜¾ç¤ºæŒ‰é’® + * + * @param isShow æ˜¯å¦æ˜¾ç¤ºæŒ‰é’® */ private Rect getRect(boolean isShow) { int left, top, right, bottom; - right = isShow ? getMeasuredWidth() + getScrollX() - mButtonPadding - mButtonPadding : 0; - left = isShow ? right - mClearButton.getWidth() : 0; - top = isShow ? (getMeasuredHeight() - mClearButton.getHeight())/2 : 0; - bottom = isShow ? top + mClearButton.getHeight() : 0; + right = isShow ? getMeasuredWidth() + getScrollX() - mButtonPadding - mButtonPadding : 0; + left = isShow ? right - mClearButton.getWidth() : 0; + top = isShow ? (getMeasuredHeight() - mClearButton.getHeight()) / 2 : 0; + bottom = isShow ? top + mClearButton.getHeight() : 0; //æ›´æ–°è¾“å…¥æ¡†å†…è¾¹è· @@ -157,10 +136,9 @@ private Rect getRect(boolean isShow) { return new Rect(left, top, right, bottom); } - - /** * 绘制按钮图片 + * * @param canvas onDrawçš„Canvas * @param rect 图片ä½ç½® */ @@ -170,9 +148,6 @@ private void drawBitmap(Canvas canvas, Rect rect) { } } - - - @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); @@ -194,19 +169,17 @@ public boolean onTouchEvent(MotionEvent event) { this.setText(""); } break; + default: + break; } return super.onTouchEvent(event); } - - - - - /** * 获å–Drawable - * @param resourseId 资æºID + * + * @param resourseId 资æºID */ private Drawable getDrawableCompat(int resourseId) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { @@ -218,6 +191,7 @@ private Drawable getDrawableCompat(int resourseId) { /** * 设置按钮左å³å†…è¾¹è· + * * @param buttonPadding å•ä½ä¸ºdp */ public void setButtonPadding(int buttonPadding) { @@ -226,6 +200,7 @@ public void setButtonPadding(int buttonPadding) { /** * è®¾ç½®æŒ‰é’®æ˜¾ç¤ºæ–¹å¼ + * * @param clearButtonMode æ˜¾ç¤ºæ–¹å¼ */ public void setClearButtonMode(ClearButtonMode clearButtonMode) { @@ -240,4 +215,15 @@ public int dp2px(float dipValue) { final float scale = getResources().getDisplayMetrics().density; return (int) (dipValue * scale + 0.5f); } + + /** + * æŒ‰é’®æ˜¾ç¤ºæ–¹å¼ + * NEVER 䏿˜¾ç¤ºæ¸…空按钮 + * ALWAYS 始终显示清空按钮 + * WHILEEDITING 输入框内容ä¸ä¸ºç©ºä¸”有获得焦点 + * UNLESSEDITING 输入框内容ä¸ä¸ºç©ºä¸”没有获得焦点 + */ + public enum ClearButtonMode { + NEVER, ALWAYS, WHILEEDITING, UNLESSEDITING + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/View/LoadingDialog.java b/app/src/main/java/cn/darkal/networkdiagnosis/View/LoadingDialog.java index 492dfde..cdd22c4 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/View/LoadingDialog.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/View/LoadingDialog.java @@ -23,6 +23,7 @@ public class LoadingDialog extends ProgressDialog { private boolean mSingleLine; private Context mContext; private ProgressWheel mProgressWheel; + public LoadingDialog(Context context) { super(context, R.style.JzAlertDialogWhite); } @@ -52,7 +53,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.layout_loading2_dialog); TextView mMessage = (TextView) findViewById(R.id.loading_dialog_message); - if ((mText == null) || (mText.equals(""))) { + if (TextUtils.isEmpty(mText)) { mMessage.setVisibility(View.GONE); } else { mMessage.setVisibility(View.VISIBLE); @@ -96,7 +97,7 @@ public void show() { @Override public void onAttachedToWindow() { super.onAttachedToWindow(); - if(mProgressWheel != null) { + if (mProgressWheel != null) { mProgressWheel.spin(); } } @@ -104,7 +105,7 @@ public void onAttachedToWindow() { @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); - if(mProgressWheel != null) { + if (mProgressWheel != null) { mProgressWheel.stopSpinning(); } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/View/ProgressWheel.java b/app/src/main/java/cn/darkal/networkdiagnosis/View/ProgressWheel.java index 83b4586..3e9282e 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/View/ProgressWheel.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/View/ProgressWheel.java @@ -17,138 +17,135 @@ /** * An indicator of progress, similar to Android's ProgressBar. * Can be used in 'spin mode' or 'increment mode' - * @author Todd Davies * + * @author Todd Davies + *

* Licensed under the Creative Commons Attribution 3.0 license see: * http://creativecommons.org/licenses/by/3.0/ */ public class ProgressWheel extends View { - - //Sizes (with defaults) - private int fullRadius = 100; - private int circleRadius = 80; - private int barLength = 60; - private int barWidth = 20; //内部圆弧的宽度 - private int rimWidth = 20; - private int textSize = 20; + + int progress = 0; + boolean isSpinning = false; + //Sizes (with defaults) + private int fullRadius = 100; + private int circleRadius = 80; + private int barLength = 60; + private int barWidth = 20; //内部圆弧的宽度 + private int rimWidth = 20; + private int textSize = 20; private int barDegree = 60; - private float arcR = barWidth/2; //内部圆弧的åŠå¾„ - - //Padding (with defaults) - private int paddingTop = 5; - private int paddingBottom = 5; - private int paddingLeft = 5; - private int paddingRight = 5; - - //Colors (with defaults) - private int barColor = 0xAA000000; - private int circleColor = 0x0000ffff; - private int rimColor = 0xAADDDDDD; + private float arcR = barWidth / 2; //内部圆弧的åŠå¾„ + //Padding (with defaults) + private int paddingTop = 5; + private int paddingBottom = 5; + private int paddingLeft = 5; + private int paddingRight = 5; + //Colors (with defaults) + private int barColor = 0xAA000000; + private int circleColor = 0x0000ffff; + private int rimColor = 0xAADDDDDD; private int spinRimColor = 0xAADDDDDD; - private int textColor = 0xFF000000; - private int spinCircleColor = 0x00000000; - - //Paints - private Paint barPaint = new Paint(); //圆画笔 - private Paint circlePaint = new Paint(); //内部填充圆画笔 - private Paint barCirclePaint = new Paint(); //圆弧上两端点圆的画笔 - private Paint rimPaint = new Paint(); //底部圈画笔 - private Paint textPaint = new Paint(); + private int textColor = 0xFF000000; + private int spinCircleColor = 0x00000000; + //Paints + private Paint barPaint = new Paint(); //圆画笔 + private Paint circlePaint = new Paint(); //内部填充圆画笔 + private Paint barCirclePaint = new Paint(); //圆弧上两端点圆的画笔 + private Paint rimPaint = new Paint(); //底部圈画笔 + private Paint textPaint = new Paint(); private Paint spinRimPaint = new Paint(); //旋转时,底部圈画笔,主è¦ç”¨äºŽæ­¤æ—¶æ”¹å˜åº•部圈颜色 - private Paint spinCirclePaint = new Paint();//旋转时,圆的画笔 - - //Rectangles - @SuppressWarnings("unused") - private RectF rectBounds = new RectF(); - private RectF circleBounds = new RectF(); - - private int startDegree = -90; //圆弧的起始ä½ç½®ï¼Œ -90 顶上 - private float startArcX = 0; - private float startArcY = 0; - - //Animation - //The amount of pixels to move the bar by on each draw - private int spinSpeed = 2; - //The number of milliseconds to wait inbetween each draw - private int delayMillis = 0; - private Handler spinHandler = new Handler() { - /** - * This is the code that will increment the progress variable - * and so spin the wheel - */ - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case 0: - invalidate(); - if (isSpinning) { - progress += spinSpeed; - if (progress > 360) { - progress = 0; - } - spinHandler.sendEmptyMessageDelayed(0, delayMillis); - } - break; - case 1: - spinHandler.removeMessages(0); - isSpinning = false; - invalidate(); - break; - } - } - }; - int progress = 0; - boolean isSpinning = false; - - //Other - private String text = ""; - private String[] splitText = {}; - - /** - * The constructor for the ProgressWheel - * @param context - * @param attrs - */ - public ProgressWheel(Context context, AttributeSet attrs) { - super(context, attrs); - - parseAttributes(context.obtainStyledAttributes(attrs, - R.styleable.ProgressWheel)); - } - - //---------------------------------- - //Setting up stuff - //---------------------------------- - - /** - * Now we know the dimensions of the view, setup the bounds and paints - */ - @Override - public void onAttachedToWindow() { - super.onAttachedToWindow(); - - setupBounds(); - setupPaints(); - invalidate(); - - } - - @Override - protected void onDetachedFromWindow() { - invalidate(); - super.onDetachedFromWindow(); - } - - /** - * Set the properties of the paints we're using to - * draw the progress wheel - */ - private void setupPaints() { - barPaint.setColor(barColor); + private Paint spinCirclePaint = new Paint();//旋转时,圆的画笔 + //Rectangles + @SuppressWarnings("unused") + private RectF rectBounds = new RectF(); + private RectF circleBounds = new RectF(); + private int startDegree = -90; //圆弧的起始ä½ç½®ï¼Œ -90 顶上 + private float startArcX = 0; + private float startArcY = 0; + //Animation + //The amount of pixels to move the bar by on each draw + private int spinSpeed = 2; + //The number of milliseconds to wait inbetween each draw + private int delayMillis = 0; + private Handler spinHandler = new Handler() { + /** + * This is the code that will increment the progress variable + * and so spin the wheel + */ + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case 0: + invalidate(); + if (isSpinning) { + progress += spinSpeed; + if (progress > 360) { + progress = 0; + } + spinHandler.sendEmptyMessageDelayed(0, delayMillis); + } + break; + case 1: + spinHandler.removeMessages(0); + isSpinning = false; + invalidate(); + break; + default: + break; + } + } + }; + //Other + private String text = ""; + private String[] splitText = {}; + + /** + * The constructor for the ProgressWheel + * + * @param context + * @param attrs + */ + public ProgressWheel(Context context, AttributeSet attrs) { + super(context, attrs); + + parseAttributes(context.obtainStyledAttributes(attrs, + R.styleable.ProgressWheel)); + } + + //---------------------------------- + //Setting up stuff + //---------------------------------- + + /** + * Now we know the dimensions of the view, setup the bounds and paints + */ + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + setupBounds(); + setupPaints(); + invalidate(); + + } + + @Override + protected void onDetachedFromWindow() { + invalidate(); + super.onDetachedFromWindow(); + } + + /** + * Set the properties of the paints we're using to + * draw the progress wheel + */ + private void setupPaints() { + barPaint.setColor(barColor); barPaint.setAntiAlias(true); barPaint.setStyle(Style.STROKE); barPaint.setStrokeWidth(barWidth); - + rimPaint.setColor(rimColor); rimPaint.setAntiAlias(true); rimPaint.setStyle(Style.STROKE); @@ -159,331 +156,338 @@ private void setupPaints() { spinRimPaint.setStyle(Style.STROKE); spinRimPaint.setStrokeWidth(rimWidth); - spinCirclePaint.setColor(spinCircleColor); - spinCirclePaint.setAntiAlias(true); - spinCirclePaint.setStyle(Style.FILL); - spinCirclePaint.setShadowLayer(1, 2, 2, 0x40000000); - + spinCirclePaint.setColor(spinCircleColor); + spinCirclePaint.setAntiAlias(true); + spinCirclePaint.setStyle(Style.FILL); + spinCirclePaint.setShadowLayer(1, 2, 2, 0x40000000); + circlePaint.setColor(circleColor); circlePaint.setAntiAlias(true); circlePaint.setStyle(Style.FILL); - barCirclePaint.setColor(barColor); - barCirclePaint.setAntiAlias(true); - barCirclePaint.setStyle(Style.FILL); - barCirclePaint.setStrokeWidth(barWidth); - + barCirclePaint.setColor(barColor); + barCirclePaint.setAntiAlias(true); + barCirclePaint.setStyle(Style.FILL); + barCirclePaint.setStrokeWidth(barWidth); + textPaint.setColor(textColor); textPaint.setStyle(Style.FILL); textPaint.setAntiAlias(true); textPaint.setTextSize(textSize); - } - - /** - * Set the bounds of the component - */ - private void setupBounds() { - paddingTop = this.getPaddingTop(); - paddingBottom = this.getPaddingBottom(); - paddingLeft = this.getPaddingLeft(); - paddingRight = this.getPaddingRight(); - - rectBounds = new RectF(paddingLeft, - paddingTop, + } + + /** + * Set the bounds of the component + */ + private void setupBounds() { + paddingTop = this.getPaddingTop(); + paddingBottom = this.getPaddingBottom(); + paddingLeft = this.getPaddingLeft(); + paddingRight = this.getPaddingRight(); + + rectBounds = new RectF(paddingLeft, + paddingTop, this.getLayoutParams().width - paddingRight, this.getLayoutParams().height - paddingBottom); - - circleBounds = new RectF(paddingLeft + barWidth, - paddingTop + barWidth, + + circleBounds = new RectF(paddingLeft + barWidth, + paddingTop + barWidth, this.getLayoutParams().width - paddingRight - barWidth, this.getLayoutParams().height - paddingBottom - barWidth); - - fullRadius = (this.getLayoutParams().width - paddingRight - barWidth)/2; - circleRadius = (fullRadius - barWidth) + 1; //内部圆的åŠå¾„ - - arcR = barWidth/2; //圆弧的åŠå¾„ - startArcX = (float) (-circleBounds.width()/2* Math.sin( 2* Math.PI/360*(startDegree+270))+this.getLayoutParams().width/2); //è®¡ç®—æ—¶è§’åº¦è¦æ¢æˆå¼§åº¦ - startArcY = (float) (circleBounds.height()/2* Math.cos(2 * Math.PI / 360 * (startDegree+270)) + this.getLayoutParams().height/2); - } - - /** - * Parse the attributes passed to the view from the XML - * @param a the attributes to parse - */ - private void parseAttributes(TypedArray a) { - barWidth = (int) a.getDimension(R.styleable.ProgressWheel_barWidth_progress, - barWidth); - - rimWidth = (int) a.getDimension(R.styleable.ProgressWheel_rimWidth_progress, - rimWidth); - - spinSpeed = (int) a.getInteger(R.styleable.ProgressWheel_spinSpeed_progress, - spinSpeed); - - delayMillis = (int) a.getInteger(R.styleable.ProgressWheel_delayMillis_progress, - delayMillis); - if(delayMillis<0) { - delayMillis = 0; - } - - barColor = a.getColor(R.styleable.ProgressWheel_barColor_progress, barColor); - - barLength = (int) a.getDimension(R.styleable.ProgressWheel_barLength_progress, - barLength); - - textSize = (int) a.getDimension(R.styleable.ProgressWheel_textSize_progress, - textSize); - - textColor = (int) a.getColor(R.styleable.ProgressWheel_textColor_progress, - textColor); - - setText(a.getString(R.styleable.ProgressWheel_text_progress)); - - rimColor = (int) a.getColor(R.styleable.ProgressWheel_rimColor_progress, - rimColor); + + fullRadius = (this.getLayoutParams().width - paddingRight - barWidth) / 2; + circleRadius = (fullRadius - barWidth) + 1; //内部圆的åŠå¾„ + + arcR = barWidth / 2; //圆弧的åŠå¾„ + startArcX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * (startDegree + 270)) + this.getLayoutParams().width / 2); //è®¡ç®—æ—¶è§’åº¦è¦æ¢æˆå¼§åº¦ + startArcY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * (startDegree + 270)) + this.getLayoutParams().height / 2); + } + + /** + * Parse the attributes passed to the view from the XML + * + * @param a the attributes to parse + */ + private void parseAttributes(TypedArray a) { + barWidth = (int) a.getDimension(R.styleable.ProgressWheel_barWidth_progress, + barWidth); + + rimWidth = (int) a.getDimension(R.styleable.ProgressWheel_rimWidth_progress, + rimWidth); + + spinSpeed = (int) a.getInteger(R.styleable.ProgressWheel_spinSpeed_progress, + spinSpeed); + + delayMillis = (int) a.getInteger(R.styleable.ProgressWheel_delayMillis_progress, + delayMillis); + if (delayMillis < 0) { + delayMillis = 0; + } + + barColor = a.getColor(R.styleable.ProgressWheel_barColor_progress, barColor); + + barLength = (int) a.getDimension(R.styleable.ProgressWheel_barLength_progress, + barLength); + + textSize = (int) a.getDimension(R.styleable.ProgressWheel_textSize_progress, + textSize); + + textColor = (int) a.getColor(R.styleable.ProgressWheel_textColor_progress, + textColor); + + setText(a.getString(R.styleable.ProgressWheel_text_progress)); + + rimColor = (int) a.getColor(R.styleable.ProgressWheel_rimColor_progress, + rimColor); spinRimColor = (int) a.getColor(R.styleable.ProgressWheel_spinRimColor_progress, spinRimColor); - - circleColor = (int) a.getColor(R.styleable.ProgressWheel_circleColor_progress, circleColor); - spinCircleColor = (int) a.getColor(R.styleable.ProgressWheel_spinCircleColor_progress, spinCircleColor); - - barDegree = (int) a.getInteger(R.styleable.ProgressWheel_barDegree_progress,-1); - } - - //---------------------------------- - //Animation stuff - //---------------------------------- - - protected void onDraw(Canvas canvas) { - super.onDraw(canvas); - - //Draw the rim - if(isSpinning()){ + + circleColor = (int) a.getColor(R.styleable.ProgressWheel_circleColor_progress, circleColor); + spinCircleColor = (int) a.getColor(R.styleable.ProgressWheel_spinCircleColor_progress, spinCircleColor); + + barDegree = (int) a.getInteger(R.styleable.ProgressWheel_barDegree_progress, -1); + } + + //---------------------------------- + //Animation stuff + //---------------------------------- + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + //Draw the rim + if (isSpinning()) { canvas.drawArc(circleBounds, 360, 360, false, spinRimPaint); - }else { + } else { canvas.drawArc(circleBounds, 360, 360, false, rimPaint); } - //Draw the bar - if(isSpinning) { - if(barDegree != -1) { //按度数 + //Draw the bar + if (isSpinning) { + if (barDegree != -1) { //按度数 canvas.drawArc(circleBounds, progress - 90, barDegree, false, barPaint); - //结æŸåº¦æ•° - double t = (progress - 90+barDegree + 270); - //èµ·å§‹ç‚¹åæ ‡ - float startX = (float) (-circleBounds.width()/2* Math.sin( 2* Math.PI/360*(progress - 90+270))+this.getLayoutParams().width/2); //è®¡ç®—æ—¶è§’åº¦è¦æ¢æˆå¼§åº¦ - float startY = (float) (circleBounds.height()/2* Math.cos(2 * Math.PI / 360 * (progress - 90 + 270)) + this.getLayoutParams().height/2); - //结æŸç‚¹åæ ‡ - float endX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * t) + this.getLayoutParams().width / 2); - float endY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * t) + this.getLayoutParams().height / 2); - //计算两点间è·ç¦» - float tmpR = (float) Math.sqrt((endX-startX)*(endX-startX)+(endY-startY)*(endY-startY)); + //结æŸåº¦æ•° + double t = (progress - 90 + barDegree + 270); + //èµ·å§‹ç‚¹åæ ‡ + float startX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * (progress - 90 + 270)) + this.getLayoutParams().width / 2); //è®¡ç®—æ—¶è§’åº¦è¦æ¢æˆå¼§åº¦ + float startY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * (progress - 90 + 270)) + this.getLayoutParams().height / 2); + //结æŸç‚¹åæ ‡ + float endX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * t) + this.getLayoutParams().width / 2); + float endY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * t) + this.getLayoutParams().height / 2); + //计算两点间è·ç¦» + float tmpR = (float) Math.sqrt((endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)); // canvas.drawCircle(startX, startY, tmpR, barPaint); //画圆弧起始圆 // canvas.drawCircle(x, y, tmpR, barPaint); //画圆弧起始圆 - //确定圆心点 - double tmp = (progress - 90 + barDegree)+(360-barDegree)/2+270; - float x2 = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * tmp) + this.getLayoutParams().width / 2); - float y2 = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * tmp) + this.getLayoutParams().height / 2); - canvas.drawCircle(x2, y2, tmpR, spinCirclePaint); //画圆 - }else{ //按长度 + //确定圆心点 + double tmp = (progress - 90 + barDegree) + (360 - barDegree) / 2 + 270; + float x2 = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * tmp) + this.getLayoutParams().width / 2); + float y2 = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * tmp) + this.getLayoutParams().height / 2); + canvas.drawCircle(x2, y2, tmpR, spinCirclePaint); //画圆 + } else { //按长度 canvas.drawArc(circleBounds, progress - 90, barLength, false, barPaint); - //结æŸåº¦æ•° - double t = (progress - 90+barLength + 270); - //èµ·å§‹ç‚¹åæ ‡ - float startX = (float) (-circleBounds.width()/2* Math.sin( 2* Math.PI/360*(progress - 90+270))+this.getLayoutParams().width/2); //è®¡ç®—æ—¶è§’åº¦è¦æ¢æˆå¼§åº¦ - float startY = (float) (circleBounds.height()/2* Math.cos(2 * Math.PI / 360 * (progress - 90 + 270)) + this.getLayoutParams().height/2); - //结æŸç‚¹åæ ‡ - float endX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * t) + this.getLayoutParams().width / 2); - float endY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * t) + this.getLayoutParams().height / 2); - //计算两点间è·ç¦» - float tmpR = (float) Math.sqrt((endX-startX)*(endX-startX)+(endY-startY)*(endY-startY)); + //结æŸåº¦æ•° + double t = (progress - 90 + barLength + 270); + //èµ·å§‹ç‚¹åæ ‡ + float startX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * (progress - 90 + 270)) + this.getLayoutParams().width / 2); //è®¡ç®—æ—¶è§’åº¦è¦æ¢æˆå¼§åº¦ + float startY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * (progress - 90 + 270)) + this.getLayoutParams().height / 2); + //结æŸç‚¹åæ ‡ + float endX = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * t) + this.getLayoutParams().width / 2); + float endY = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * t) + this.getLayoutParams().height / 2); + //计算两点间è·ç¦» + float tmpR = (float) Math.sqrt((endX - startX) * (endX - startX) + (endY - startY) * (endY - startY)); // canvas.drawCircle(startX, startY, tmpR, barPaint); //画圆弧起始圆 // canvas.drawCircle(x, y, tmpR, barPaint); //画圆弧起始圆 - //确定圆心点 - double tmp = (progress - 90 + barLength)+(360-barLength)/2+270; - float x2 = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * tmp) + this.getLayoutParams().width / 2); - float y2 = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * tmp) + this.getLayoutParams().height / 2); - canvas.drawCircle(x2, y2, tmpR, spinCirclePaint); //画圆 - } - } else { - canvas.drawArc(circleBounds, startDegree, progress, false, barPaint); // -90 从顶上开始 - double t = progress+startDegree+270; - if(progress != 0) { - canvas.drawCircle(startArcX, startArcY, arcR, barCirclePaint); //画圆弧起始圆 - - float x = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * t) + this.getLayoutParams().width / 2); - float y = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * t) + this.getLayoutParams().height / 2); - canvas.drawCircle(x, y, arcR, barCirclePaint); //画圆弧结æŸåœ† - } - } - //Draw the inner circle - canvas.drawCircle((circleBounds.width()/2) + rimWidth + paddingLeft, - (circleBounds.height()/2) + rimWidth + paddingTop, - circleRadius, - circlePaint); - //Draw the text (attempts to center it horizontally and vertically) - int offsetNum = 0; - for(String s : splitText) { - float offset = textPaint.measureText(s) / 2; - canvas.drawText(s, this.getWidth() / 2 - offset, - this.getHeight() / 2 + (textSize*(offsetNum)) - - ((splitText.length-1)*(textSize/2)), textPaint); - offsetNum++; - } - } - - /** - * Reset the count (in increment mode) - */ - public void resetCount() { - progress = 0; - setText("0%"); - invalidate(); - } - - /** - * Turn off spin mode - */ - public void stopSpinning() { - spinHandler.sendEmptyMessageDelayed(1,200); - } - - - /** - * Puts the view on spin mode - */ - public void spin() { - isSpinning = true; - spinHandler.sendEmptyMessage(0); - } - - public boolean isSpinning(){ + //确定圆心点 + double tmp = (progress - 90 + barLength) + (360 - barLength) / 2 + 270; + float x2 = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * tmp) + this.getLayoutParams().width / 2); + float y2 = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * tmp) + this.getLayoutParams().height / 2); + canvas.drawCircle(x2, y2, tmpR, spinCirclePaint); //画圆 + } + } else { + canvas.drawArc(circleBounds, startDegree, progress, false, barPaint); // -90 从顶上开始 + double t = progress + startDegree + 270; + if (progress != 0) { + canvas.drawCircle(startArcX, startArcY, arcR, barCirclePaint); //画圆弧起始圆 + + float x = (float) (-circleBounds.width() / 2 * Math.sin(2 * Math.PI / 360 * t) + this.getLayoutParams().width / 2); + float y = (float) (circleBounds.height() / 2 * Math.cos(2 * Math.PI / 360 * t) + this.getLayoutParams().height / 2); + canvas.drawCircle(x, y, arcR, barCirclePaint); //画圆弧结æŸåœ† + } + } + //Draw the inner circle + canvas.drawCircle((circleBounds.width() / 2) + rimWidth + paddingLeft, + (circleBounds.height() / 2) + rimWidth + paddingTop, + circleRadius, + circlePaint); + //Draw the text (attempts to center it horizontally and vertically) + int offsetNum = 0; + for (String s : splitText) { + float offset = textPaint.measureText(s) / 2; + canvas.drawText(s, this.getWidth() / 2 - offset, + this.getHeight() / 2 + (textSize * (offsetNum)) + - ((splitText.length - 1) * (textSize / 2)), textPaint); + offsetNum++; + } + } + + /** + * Reset the count (in increment mode) + */ + public void resetCount() { + progress = 0; + setText("0%"); + invalidate(); + } + + /** + * Turn off spin mode + */ + public void stopSpinning() { + spinHandler.sendEmptyMessageDelayed(1, 200); + } + + + /** + * Puts the view on spin mode + */ + public void spin() { + isSpinning = true; + spinHandler.sendEmptyMessage(0); + } + + public boolean isSpinning() { return isSpinning; } - /** - * Increment the progress by 1 (of 360) - */ - public void incrementProgress() { - isSpinning = false; - progress++; - setText(Math.round(((float)progress/360)*100) + "%"); - spinHandler.sendEmptyMessage(0); - } - - /** - * Set the progress to a specific value - */ - public void setProgress(int i) { - isSpinning = false; - progress=i; - spinHandler.sendEmptyMessage(0); - } - - //---------------------------------- - //Getters + setters - //---------------------------------- - - /** - * Set the text in the progress bar - * Doesn't invalidate the view - * @param text the text to show ('\n' constitutes a new line) - */ - public void setText(String text) { - this.text = text; - splitText = this.text.split("\n"); - } - - public int getCircleRadius() { - return circleRadius; - } - - public void setCircleRadius(int circleRadius) { - this.circleRadius = circleRadius; - } - - public int getBarLength() { - return barLength; - } - - public void setBarLength(int barLength) { - this.barLength = barLength; - } - - public int getBarWidth() { - return barWidth; - } - - public void setBarWidth(int barWidth) { - this.barWidth = barWidth; - } - - public int getTextSize() { - return textSize; - } - - public void setTextSize(int textSize) { - this.textSize = textSize; - } - - public int getPaddingTop() { - return paddingTop; - } - - public void setPaddingTop(int paddingTop) { - this.paddingTop = paddingTop; - } - - public int getPaddingBottom() { - return paddingBottom; - } - - public void setPaddingBottom(int paddingBottom) { - this.paddingBottom = paddingBottom; - } - - public int getPaddingLeft() { - return paddingLeft; - } - - public void setPaddingLeft(int paddingLeft) { - this.paddingLeft = paddingLeft; - } - - public int getPaddingRight() { - return paddingRight; - } - - public void setPaddingRight(int paddingRight) { - this.paddingRight = paddingRight; - } - - public int getBarColor() { - return barColor; - } - - public void setBarColor(int barColor) { - this.barColor = barColor; - } - - public int getCircleColor() { - return circleColor; - } - - public void setCircleColor(int circleColor) { - this.circleColor = circleColor; - } - - public int getRimColor() { - return rimColor; - } - - public void setRimColor(int rimColor) { - this.rimColor = rimColor; - } + /** + * Increment the progress by 1 (of 360) + */ + public void incrementProgress() { + isSpinning = false; + progress++; + setText(Math.round(((float) progress / 360) * 100) + "%"); + spinHandler.sendEmptyMessage(0); + } + + /** + * Set the progress to a specific value + */ + public void setProgress(int i) { + isSpinning = false; + progress = i; + spinHandler.sendEmptyMessage(0); + } + + //---------------------------------- + //Getters + setters + //---------------------------------- + + /** + * Set the text in the progress bar + * Doesn't invalidate the view + * + * @param text the text to show ('\n' constitutes a new line) + */ + public void setText(String text) { + this.text = text; + splitText = this.text.split("\n"); + } + + public int getCircleRadius() { + return circleRadius; + } + + public void setCircleRadius(int circleRadius) { + this.circleRadius = circleRadius; + } + + public int getBarLength() { + return barLength; + } + + public void setBarLength(int barLength) { + this.barLength = barLength; + } + + public int getBarWidth() { + return barWidth; + } + + public void setBarWidth(int barWidth) { + this.barWidth = barWidth; + } + + public int getTextSize() { + return textSize; + } + + public void setTextSize(int textSize) { + this.textSize = textSize; + } + + @Override + public int getPaddingTop() { + return paddingTop; + } + + public void setPaddingTop(int paddingTop) { + this.paddingTop = paddingTop; + } + + @Override + public int getPaddingBottom() { + return paddingBottom; + } + + public void setPaddingBottom(int paddingBottom) { + this.paddingBottom = paddingBottom; + } + + @Override + public int getPaddingLeft() { + return paddingLeft; + } + + public void setPaddingLeft(int paddingLeft) { + this.paddingLeft = paddingLeft; + } + + @Override + public int getPaddingRight() { + return paddingRight; + } + + public void setPaddingRight(int paddingRight) { + this.paddingRight = paddingRight; + } + + public int getBarColor() { + return barColor; + } + + public void setBarColor(int barColor) { + this.barColor = barColor; + } + + public int getCircleColor() { + return circleColor; + } + + public void setCircleColor(int circleColor) { + this.circleColor = circleColor; + } + + public int getRimColor() { + return rimColor; + } + + public void setRimColor(int rimColor) { + this.rimColor = rimColor; + } public int getSpinRimColor() { return spinRimColor; @@ -493,43 +497,43 @@ public void setSpinRimColor(int spinRimColor) { this.spinRimColor = spinRimColor; } - public Shader getRimShader() { - return rimPaint.getShader(); - } - - public void setRimShader(Shader shader) { - this.rimPaint.setShader(shader); - } - - public int getTextColor() { - return textColor; - } - - public void setTextColor(int textColor) { - this.textColor = textColor; - } - - public int getSpinSpeed() { - return spinSpeed; - } - - public void setSpinSpeed(int spinSpeed) { - this.spinSpeed = spinSpeed; - } - - public int getRimWidth() { - return rimWidth; - } - - public void setRimWidth(int rimWidth) { - this.rimWidth = rimWidth; - } - - public int getDelayMillis() { - return delayMillis; - } - - public void setDelayMillis(int delayMillis) { - this.delayMillis = delayMillis; - } + public Shader getRimShader() { + return rimPaint.getShader(); + } + + public void setRimShader(Shader shader) { + this.rimPaint.setShader(shader); + } + + public int getTextColor() { + return textColor; + } + + public void setTextColor(int textColor) { + this.textColor = textColor; + } + + public int getSpinSpeed() { + return spinSpeed; + } + + public void setSpinSpeed(int spinSpeed) { + this.spinSpeed = spinSpeed; + } + + public int getRimWidth() { + return rimWidth; + } + + public void setRimWidth(int rimWidth) { + this.rimWidth = rimWidth; + } + + public int getDelayMillis() { + return delayMillis; + } + + public void setDelayMillis(int delayMillis) { + this.delayMillis = delayMillis; + } } diff --git a/app/src/main/java/cn/darkal/networkdiagnosis/View/RecycleViewDivider.java b/app/src/main/java/cn/darkal/networkdiagnosis/View/RecycleViewDivider.java index 61d4732..9ad08b8 100644 --- a/app/src/main/java/cn/darkal/networkdiagnosis/View/RecycleViewDivider.java +++ b/app/src/main/java/cn/darkal/networkdiagnosis/View/RecycleViewDivider.java @@ -14,14 +14,11 @@ */ public class RecycleViewDivider extends RecyclerView.ItemDecoration { + public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; + public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; private static final int[] ATTRS = new int[]{ android.R.attr.listDivider }; - - public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; - - public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; - private Drawable mDivider; private int mOrientation; diff --git a/app/src/main/java/com/google/zxing/QrCodeScanActivity.java b/app/src/main/java/com/google/zxing/QrCodeScanActivity.java index c02d3da..b6fabdb 100644 --- a/app/src/main/java/com/google/zxing/QrCodeScanActivity.java +++ b/app/src/main/java/com/google/zxing/QrCodeScanActivity.java @@ -1,4 +1,3 @@ - package com.google.zxing; import android.app.Activity; @@ -45,6 +44,18 @@ */ public class QrCodeScanActivity extends Activity implements SurfaceHolder.Callback { + private static final float BEEP_VOLUME = 1.00f; + private static final int REQUEST_CODE_GALLERY = 0x0708; + private static final long VIBRATE_DURATION = 200L; + /** + * When the beep has finished playing, rewind to queue up another one. + */ + private final OnCompletionListener beepListener = new OnCompletionListener() { + @Override + public void onCompletion(MediaPlayer mediaPlayer) { + mediaPlayer.seekTo(0); + } + }; private CameraManager cameraManager; private CaptureActivityHandler handler; private ViewfinderView viewfinderView; @@ -54,9 +65,7 @@ public class QrCodeScanActivity extends Activity implements SurfaceHolder.Callba private InactivityTimer inactivityTimer; private MediaPlayer mediaPlayer; private boolean playBeep; - private static final float BEEP_VOLUME = 1.00f; private boolean vibrate; - private static final int REQUEST_CODE_GALLERY = 0x0708; public CameraManager getCameraManager() { return cameraManager; @@ -274,8 +283,6 @@ private void initBeepSound() { } } - private static final long VIBRATE_DURATION = 200L; - private void playBeepSoundAndVibrate() { if (playBeep && mediaPlayer != null) { mediaPlayer.start(); @@ -286,15 +293,6 @@ private void playBeepSoundAndVibrate() { } } - /** - * When the beep has finished playing, rewind to queue up another one. - */ - private final OnCompletionListener beepListener = new OnCompletionListener() { - public void onCompletion(MediaPlayer mediaPlayer) { - mediaPlayer.seekTo(0); - } - }; - public void onGalleryClick(View view) { // 设定actionå’ŒminiType Intent intent = new Intent(); @@ -349,6 +347,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } } break; + default: + break; } } } diff --git a/app/src/main/java/com/google/zxing/camera/AutoFocusManager.java b/app/src/main/java/com/google/zxing/camera/AutoFocusManager.java index f42adbb..34ec94a 100644 --- a/app/src/main/java/com/google/zxing/camera/AutoFocusManager.java +++ b/app/src/main/java/com/google/zxing/camera/AutoFocusManager.java @@ -40,10 +40,10 @@ final class AutoFocusManager implements Camera.AutoFocusCallback { FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_MACRO); } - private boolean stopped; - private boolean focusing; private final boolean useAutoFocus; private final Camera camera; + private boolean stopped; + private boolean focusing; private AsyncTask outstandingTask; AutoFocusManager(Context context, Camera camera) { diff --git a/app/src/main/java/com/google/zxing/camera/CameraManager.java b/app/src/main/java/com/google/zxing/camera/CameraManager.java index fb227c9..4ba94d1 100644 --- a/app/src/main/java/com/google/zxing/camera/CameraManager.java +++ b/app/src/main/java/com/google/zxing/camera/CameraManager.java @@ -48,6 +48,11 @@ public final class CameraManager { private final Context context; private final CameraConfigurationManager configManager; + /** + * Preview frames are delivered here, which we pass on to the registered handler. Make sure to + * clear the handler so it will only receive one message. + */ + private final PreviewCallback previewCallback; private Camera camera; private AutoFocusManager autoFocusManager; private Rect framingRect; @@ -57,11 +62,6 @@ public final class CameraManager { private int requestedCameraId = -1; private int requestedFramingRectWidth; private int requestedFramingRectHeight; - /** - * Preview frames are delivered here, which we pass on to the registered handler. Make sure to - * clear the handler so it will only receive one message. - */ - private final PreviewCallback previewCallback; public CameraManager(Context context) { this.context = context; @@ -69,6 +69,17 @@ public CameraManager(Context context) { previewCallback = new PreviewCallback(configManager); } + private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) { + int dim = 5 * resolution / 8; // Target 5/8 of each dimension + if (dim < hardMin) { + return hardMin; + } + if (dim > hardMax) { + return hardMax; + } + return dim; + } + /** * Opens the camera driver and initializes the hardware parameters. * @@ -172,8 +183,6 @@ public synchronized void stopPreview() { } /** - * - * * @param newSetting if {@code true}, light should be turned on if currently off. And vice versa. */ public synchronized void setTorch(boolean newSetting) { @@ -240,17 +249,6 @@ public synchronized Rect getFramingRect() { return framingRect; } - private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) { - int dim = 5 * resolution / 8; // Target 5/8 of each dimension - if (dim < hardMin) { - return hardMin; - } - if (dim > hardMax) { - return hardMax; - } - return dim; - } - /** * Like {@link #getFramingRect} but coordinates are in terms of the preview frame, * not UI / screen. diff --git a/app/src/main/java/com/google/zxing/decoding/CaptureActivityHandler.java b/app/src/main/java/com/google/zxing/decoding/CaptureActivityHandler.java index 66fbe7c..0b77e3c 100644 --- a/app/src/main/java/com/google/zxing/decoding/CaptureActivityHandler.java +++ b/app/src/main/java/com/google/zxing/decoding/CaptureActivityHandler.java @@ -53,14 +53,8 @@ public final class CaptureActivityHandler extends Handler { private final QrCodeScanActivity activity; private final DecodeThread decodeThread; - private State state; private final CameraManager cameraManager; - - private enum State { - PREVIEW, - SUCCESS, - DONE - } + private State state; public CaptureActivityHandler(QrCodeScanActivity activity, Collection decodeFormats, @@ -160,4 +154,10 @@ private void restartPreviewAndDecode() { } } + private enum State { + PREVIEW, + SUCCESS, + DONE + } + } diff --git a/app/src/main/java/com/google/zxing/decoding/DecodeFormatManager.java b/app/src/main/java/com/google/zxing/decoding/DecodeFormatManager.java index 20f8989..48d6c80 100644 --- a/app/src/main/java/com/google/zxing/decoding/DecodeFormatManager.java +++ b/app/src/main/java/com/google/zxing/decoding/DecodeFormatManager.java @@ -31,15 +31,15 @@ final class DecodeFormatManager { - private static final Pattern COMMA_PATTERN = Pattern.compile(","); - static final Set PRODUCT_FORMATS; static final Set INDUSTRIAL_FORMATS; - private static final Set ONE_D_FORMATS; static final Set QR_CODE_FORMATS = EnumSet.of(BarcodeFormat.QR_CODE); static final Set DATA_MATRIX_FORMATS = EnumSet.of(BarcodeFormat.DATA_MATRIX); static final Set AZTEC_FORMATS = EnumSet.of(BarcodeFormat.AZTEC); static final Set PDF417_FORMATS = EnumSet.of(BarcodeFormat.PDF_417); + private static final Pattern COMMA_PATTERN = Pattern.compile(","); + private static final Set ONE_D_FORMATS; + private static final Map> FORMATS_FOR_MODE; static { PRODUCT_FORMATS = EnumSet.of(BarcodeFormat.UPC_A, @@ -57,8 +57,6 @@ final class DecodeFormatManager { ONE_D_FORMATS.addAll(INDUSTRIAL_FORMATS); } - private static final Map> FORMATS_FOR_MODE; - static { FORMATS_FOR_MODE = new HashMap>(); FORMATS_FOR_MODE.put(Intents.Scan.ONE_D_MODE, ONE_D_FORMATS); diff --git a/app/src/main/java/com/google/zxing/decoding/DecodeHandler.java b/app/src/main/java/com/google/zxing/decoding/DecodeHandler.java index 5735af3..eb0f1d6 100644 --- a/app/src/main/java/com/google/zxing/decoding/DecodeHandler.java +++ b/app/src/main/java/com/google/zxing/decoding/DecodeHandler.java @@ -51,12 +51,23 @@ final class DecodeHandler extends Handler { this.activity = activity; } + private static void bundleThumbnail(PlanarYUVLuminanceSource source, Bundle bundle) { + int[] pixels = source.renderThumbnail(); + int width = source.getThumbnailWidth(); + int height = source.getThumbnailHeight(); + Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out); + bundle.putByteArray(DecodeThread.BARCODE_BITMAP, out.toByteArray()); + bundle.putFloat(DecodeThread.BARCODE_SCALED_FACTOR, (float) width / source.getWidth()); + } + @Override public void handleMessage(Message message) { if (!running) { return; } - if (message.what == R.id.decode) { + if (message.what == R.id.decode) { decode((byte[]) message.obj, message.arg1, message.arg2); } else if (message.what == R.id.quit) { running = false; @@ -119,15 +130,4 @@ private void decode(byte[] data, int width, int height) { } } - private static void bundleThumbnail(PlanarYUVLuminanceSource source, Bundle bundle) { - int[] pixels = source.renderThumbnail(); - int width = source.getThumbnailWidth(); - int height = source.getThumbnailHeight(); - Bitmap bitmap = Bitmap.createBitmap(pixels, 0, width, width, height, Bitmap.Config.ARGB_8888); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 50, out); - bundle.putByteArray(DecodeThread.BARCODE_BITMAP, out.toByteArray()); - bundle.putFloat(DecodeThread.BARCODE_SCALED_FACTOR, (float) width / source.getWidth()); - } - } diff --git a/app/src/main/java/com/google/zxing/decoding/DecodeThread.java b/app/src/main/java/com/google/zxing/decoding/DecodeThread.java index 420358a..a2386bb 100644 --- a/app/src/main/java/com/google/zxing/decoding/DecodeThread.java +++ b/app/src/main/java/com/google/zxing/decoding/DecodeThread.java @@ -45,8 +45,8 @@ final class DecodeThread extends Thread { private final QrCodeScanActivity activity; private final Map hints; - private Handler handler; private final CountDownLatch handlerInitLatch; + private Handler handler; DecodeThread(QrCodeScanActivity activity, Collection decodeFormats, diff --git a/app/src/main/java/com/google/zxing/view/ViewfinderView.java b/app/src/main/java/com/google/zxing/view/ViewfinderView.java index 03a9c8c..f173b27 100644 --- a/app/src/main/java/com/google/zxing/view/ViewfinderView.java +++ b/app/src/main/java/com/google/zxing/view/ViewfinderView.java @@ -51,12 +51,6 @@ public final class ViewfinderView extends View { private static final int CURRENT_POINT_OPACITY = 0xA0; private static final int MAX_RESULT_POINTS = 20; private static final int POINT_SIZE = 6; - - /** - * 四个绿色边角对应的长度 - */ - private int ScreenRate; - /** * 四个绿色边角对应的宽度 */ @@ -65,21 +59,14 @@ public final class ViewfinderView extends View { * æ‰«ææ¡†ä¸­çš„中间线的宽度 */ private static final float MIDDLE_LINE_WIDTH = 3; - /** * æ‰«ææ¡†ä¸­çš„ä¸­é—´çº¿çš„ä¸Žæ‰«ææ¡†å·¦å³çš„é—´éš™ */ private static final float MIDDLE_LINE_PADDING = 5; - /** * 中间那æ¡çº¿æ¯æ¬¡åˆ·æ–°ç§»åŠ¨çš„è·ç¦» */ private static final float SPEEN_DISTANCE = 4; - - /** - * 手机的å±å¹•密度 - */ - private static float density; /** * å­—ä½“å¤§å° */ @@ -88,30 +75,32 @@ public final class ViewfinderView extends View { * 字体è·ç¦»æ‰«ææ¡†ä¸‹é¢çš„è·ç¦» */ private static final int TEXT_PADDING_TOP = 24; - + /** + * 手机的å±å¹•密度 + */ + private static float density; + private final int maskColor; + private final int resultColor; + private final int resultPointColor; + boolean isFirst; + /** + * 四个绿色边角对应的长度 + */ + private int ScreenRate; /** * 画笔对象的引用 */ private Paint paint; - /** * 中间滑动线的最顶端ä½ç½® */ private float slideTop; - /** * 中间滑动线的最底端ä½ç½® */ private int slideBottom; - - - boolean isFirst; - private CameraManager cameraManager; private Bitmap resultBitmap; - private final int maskColor; - private final int resultColor; - private final int resultPointColor; private int scannerAlpha; private List possibleResultPoints; private List lastPossibleResultPoints; @@ -153,7 +142,7 @@ public void onDraw(Canvas canvas) { } //åˆå§‹åŒ–中间线滑动的最上边和最下边 - if(!isFirst){ + if (!isFirst) { isFirst = true; slideTop = frame.top; slideBottom = frame.bottom; @@ -183,11 +172,11 @@ public void onDraw(Canvas canvas) { canvas.drawRect(frame.left - 1, frame.top - 1, frame.left - 1 + ScreenRate, frame.top - 1 + CORNER_WIDTH, paint); canvas.drawRect(frame.left - 1, frame.top - 1, frame.left - 1 + CORNER_WIDTH, frame.top - 1 + ScreenRate, paint); canvas.drawRect(frame.right + 2 - ScreenRate, frame.top - 1, frame.right + 2, frame.top - 1 + CORNER_WIDTH, paint); - canvas.drawRect(frame.right+2 - CORNER_WIDTH, frame.top-1, frame.right+2, frame.top-1 + ScreenRate, paint); - canvas.drawRect(frame.left-1, frame.bottom+2 - CORNER_WIDTH, frame.left-1 + ScreenRate, frame.bottom+2, paint); - canvas.drawRect(frame.left-1, frame.bottom+2 - ScreenRate, frame.left-1 + CORNER_WIDTH, frame.bottom+2, paint); - canvas.drawRect(frame.right+2 - ScreenRate, frame.bottom+2 - CORNER_WIDTH, frame.right+2, frame.bottom+2, paint); - canvas.drawRect(frame.right+2 - CORNER_WIDTH, frame.bottom+2 - ScreenRate, frame.right+2, frame.bottom+2, paint); + canvas.drawRect(frame.right + 2 - CORNER_WIDTH, frame.top - 1, frame.right + 2, frame.top - 1 + ScreenRate, paint); + canvas.drawRect(frame.left - 1, frame.bottom + 2 - CORNER_WIDTH, frame.left - 1 + ScreenRate, frame.bottom + 2, paint); + canvas.drawRect(frame.left - 1, frame.bottom + 2 - ScreenRate, frame.left - 1 + CORNER_WIDTH, frame.bottom + 2, paint); + canvas.drawRect(frame.right + 2 - ScreenRate, frame.bottom + 2 - CORNER_WIDTH, frame.right + 2, frame.bottom + 2, paint); + canvas.drawRect(frame.right + 2 - CORNER_WIDTH, frame.bottom + 2 - ScreenRate, frame.right + 2, frame.bottom + 2, paint); paint.setColor(Color.WHITE); canvas.drawLine(frame.left, frame.bottom, frame.left, frame.top, paint); @@ -197,15 +186,15 @@ public void onDraw(Canvas canvas) { //绘制中间的线,æ¯æ¬¡åˆ·æ–°ç•Œé¢ï¼Œä¸­é—´çš„线往下移动SPEEN_DISTANCE slideTop += SPEEN_DISTANCE; - if(slideTop >= frame.bottom) { + if (slideTop >= frame.bottom) { slideTop = frame.top; } Shader shaderNew = new LinearGradient(frame.left + MIDDLE_LINE_PADDING, 0, frame.right - MIDDLE_LINE_PADDING, 0, - new int[] { 0x00FFFFFF, 0xFF00FF00, 0xFF00FF00, 0x00FFFFFF }, null, Shader.TileMode.MIRROR); + new int[]{0x00FFFFFF, 0xFF00FF00, 0xFF00FF00, 0x00FFFFFF}, null, Shader.TileMode.MIRROR); Shader shaderOld = paint.getShader(); paint.setShader(shaderNew); - canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH/2, - frame.right - MIDDLE_LINE_PADDING, slideTop + MIDDLE_LINE_WIDTH/2, paint); + canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH / 2, + frame.right - MIDDLE_LINE_PADDING, slideTop + MIDDLE_LINE_WIDTH / 2, paint); paint.setShader(shaderOld); @@ -215,7 +204,7 @@ public void onDraw(Canvas canvas) { paint.setTextAlign(Paint.Align.CENTER); paint.setTypeface(Typeface.create("system", Typeface.NORMAL)); //paint.setTypeface(Typeface.SANS_SERIF); - canvas.drawText(getResources().getString(R.string.scan_text), frame.centerX(), (float) (frame.bottom + (float)TEXT_PADDING_TOP * density), paint); + canvas.drawText(getResources().getString(R.string.scan_text), frame.centerX(), (float) (frame.bottom + (float) TEXT_PADDING_TOP * density), paint); float scaleX = frame.width() / (float) previewFrame.width(); float scaleY = frame.height() / (float) previewFrame.height(); diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/AbstractLDNetAsyncTaskEx.java b/app/src/main/java/com/netease/LDNetDiagnoService/AbstractLDNetAsyncTaskEx.java new file mode 100644 index 0000000..04695df --- /dev/null +++ b/app/src/main/java/com/netease/LDNetDiagnoService/AbstractLDNetAsyncTaskEx.java @@ -0,0 +1,222 @@ +package com.netease.LDNetDiagnoService; + +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Message; + +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; + +/** + * @author liujie + * + *

+ * The most part is copied for {@link AsyncTask}. + *

+ * What's we do here is to control the executor and the core + *

+ * number of thread parallely. + * + *

+ * Since Starting with HONEYCOMB, tasks are executed on a single thread + *

+ * to avoid common application errors caused by parallel execution. + */ + +public abstract class AbstractLDNetAsyncTaskEx { + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + private static final int MESSAGE_POST_CANCEL = 0x3; + private static final LDNetInternalHandler sHandler = new LDNetInternalHandler(); + private final AbstractLDNetWorkerRunnable mWorker; + private final FutureTask mFuture; + private volatile Status mStatus = Status.PENDING; + + public AbstractLDNetAsyncTaskEx() { + mWorker = new AbstractLDNetWorkerRunnable() { + @Override + public Result call() throws Exception { + // Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + return doInBackground(mParams); + } + }; + + mFuture = new FutureTask(mWorker) { + @SuppressWarnings("unchecked") + @Override + protected void done() { + Message message; + Result result = null; + + try { + result = get(); + } catch (InterruptedException e) { + android.util.Log.w(this.getClass().getSimpleName(), e); + } catch (ExecutionException e) { + throw new RuntimeException( + "An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, + new LDNetAsyncTaskResult(AbstractLDNetAsyncTaskEx.this, + (Result[]) null)); + message.sendToTarget(); + return; + } catch (Throwable t) { +// throw new RuntimeException( +// "An error occured while executing " +// + "doInBackground()", t); + } + + message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new LDNetAsyncTaskResult(AbstractLDNetAsyncTaskEx.this, result)); + message.sendToTarget(); + } + }; + } + + // protected Hashtable mTaskCache = new + // Hashtable(); + + public final Status getStatus() { + return mStatus; + } + + protected abstract Result doInBackground(Params... params); + + /** + * åŽå°çº¿ç¨‹å‡†å¤‡è¿è¡Œé˜¶æ®µ + */ + protected void onPreExecute() { + } + + /** + * åŽå°è¿è¡Œé˜¶æ®µï¼Œå½“å‰è¿è¡Œå·²ç»ç»“æŸ + * + * @param result + */ + protected void onPostExecute(Result result) { + } + + /** + * 进度更新阶段 + * + * @param values + */ + protected void onProgressUpdate(Progress... values) { + } + + /** + * å–æ¶ˆè¿è¡Œ + */ + protected void onCancelled() { + } + + public final boolean isCancelled() { + return mFuture.isCancelled(); + } + + public final boolean cancel(boolean mayInterruptIfRunning) { + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * åˆå§‹åŒ–è¿è¡Œé˜¶æ®µ + * + * @param params + * @return + */ + @SuppressWarnings("incomplete-switch") + public final AbstractLDNetAsyncTaskEx execute(Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + default: + break; + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + ThreadPoolExecutor sExecutor = getThreadPoolExecutor(); + // ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, + // MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, + // sThreadFactory); + if (sExecutor != null) { + sExecutor.execute(mFuture); + return this; + } else { + return null; + } + } + + protected abstract ThreadPoolExecutor getThreadPoolExecutor(); + + protected final void publishProgress(Progress... values) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new LDNetAsyncTaskResult(this, values)).sendToTarget(); + } + + protected void finish(Result result) { + if (isCancelled()) { + result = null; + } + onPostExecute(result); + mStatus = Status.FINISHED; + } + + public enum Status { + PENDING, RUNNING, FINISHED, + } + + private static class LDNetInternalHandler extends Handler { + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void handleMessage(Message msg) { + LDNetAsyncTaskResult result = (LDNetAsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + case MESSAGE_POST_CANCEL: + result.mTask.onCancelled(); + break; + default: + break; + } + } + } + + private static abstract class AbstractLDNetWorkerRunnable implements + Callable { + Params[] mParams; + } + + private static class LDNetAsyncTaskResult { + @SuppressWarnings("rawtypes") + final AbstractLDNetAsyncTaskEx mTask; + final Data[] mData; + + LDNetAsyncTaskResult(@SuppressWarnings("rawtypes") AbstractLDNetAsyncTaskEx task, + Data... data) { + mTask = task; + mData = data; + } + } +} diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetAsyncTaskEx.java b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetAsyncTaskEx.java deleted file mode 100644 index 4deeb2a..0000000 --- a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetAsyncTaskEx.java +++ /dev/null @@ -1,216 +0,0 @@ -package com.netease.LDNetDiagnoService; - -import java.util.concurrent.Callable; -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.ThreadPoolExecutor; - -import android.os.AsyncTask; -import android.os.Handler; -import android.os.Message; - -/** - * - * @author liujie - * - *

- * The most part is copied for {@link AsyncTask}. - * - * What's we do here is to control the executor and the core - * - * number of thread parallely. - * - *

- * Since Starting with HONEYCOMB, tasks are executed on a single thread - * - * to avoid common application errors caused by parallel execution. - */ - -public abstract class LDNetAsyncTaskEx { - private static final int MESSAGE_POST_RESULT = 0x1; - private static final int MESSAGE_POST_PROGRESS = 0x2; - private static final int MESSAGE_POST_CANCEL = 0x3; - - private volatile Status mStatus = Status.PENDING; - - public enum Status { - PENDING, RUNNING, FINISHED, - } - - private static final LDNetInternalHandler sHandler = new LDNetInternalHandler(); - private final LDNetWorkerRunnable mWorker; - private final FutureTask mFuture; - - // protected Hashtable mTaskCache = new - // Hashtable(); - - public LDNetAsyncTaskEx() { - mWorker = new LDNetWorkerRunnable() { - public Result call() throws Exception { - // Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - return doInBackground(mParams); - } - }; - - mFuture = new FutureTask(mWorker) { - @SuppressWarnings("unchecked") - @Override - protected void done() { - Message message; - Result result = null; - - try { - result = get(); - } catch (InterruptedException e) { - android.util.Log.w(this.getClass().getSimpleName(), e); - } catch (ExecutionException e) { - throw new RuntimeException( - "An error occured while executing doInBackground()", - e.getCause()); - } catch (CancellationException e) { - message = sHandler.obtainMessage(MESSAGE_POST_CANCEL, - new LDNetAsyncTaskResult(LDNetAsyncTaskEx.this, - (Result[]) null)); - message.sendToTarget(); - return; - } catch (Throwable t) { -// throw new RuntimeException( -// "An error occured while executing " -// + "doInBackground()", t); - } - - message = sHandler.obtainMessage(MESSAGE_POST_RESULT, - new LDNetAsyncTaskResult(LDNetAsyncTaskEx.this, result)); - message.sendToTarget(); - } - }; - } - - private static class LDNetInternalHandler extends Handler { - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public void handleMessage(Message msg) { - LDNetAsyncTaskResult result = (LDNetAsyncTaskResult) msg.obj; - switch (msg.what) { - case MESSAGE_POST_RESULT: - // There is only one result - result.mTask.finish(result.mData[0]); - break; - case MESSAGE_POST_PROGRESS: - result.mTask.onProgressUpdate(result.mData); - break; - case MESSAGE_POST_CANCEL: - result.mTask.onCancelled(); - break; - } - } - } - - public final Status getStatus() { - return mStatus; - } - - protected abstract Result doInBackground(Params... params); - /** - * åŽå°çº¿ç¨‹å‡†å¤‡è¿è¡Œé˜¶æ®µ - */ - protected void onPreExecute() { - } - - /** - * åŽå°è¿è¡Œé˜¶æ®µï¼Œå½“å‰è¿è¡Œå·²ç»ç»“æŸ - * @param result - */ - protected void onPostExecute(Result result) { - } - - /** - * 进度更新阶段 - * @param values - */ - protected void onProgressUpdate(Progress... values) { - } - - /** - * å–æ¶ˆè¿è¡Œ - */ - protected void onCancelled() { - } - - public final boolean isCancelled() { - return mFuture.isCancelled(); - } - - public final boolean cancel(boolean mayInterruptIfRunning) { - return mFuture.cancel(mayInterruptIfRunning); - } - - /** - * åˆå§‹åŒ–è¿è¡Œé˜¶æ®µ - * @param params - * @return - */ - @SuppressWarnings("incomplete-switch") - public final LDNetAsyncTaskEx execute(Params... params) { - if (mStatus != Status.PENDING) { - switch (mStatus) { - case RUNNING: - throw new IllegalStateException("Cannot execute task:" - + " the task is already running."); - case FINISHED: - throw new IllegalStateException("Cannot execute task:" - + " the task has already been executed " - + "(a task can be executed only once)"); - } - } - - mStatus = Status.RUNNING; - - onPreExecute(); - - mWorker.mParams = params; - ThreadPoolExecutor sExecutor = getThreadPoolExecutor(); - // ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, - // MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, - // sThreadFactory); - if (sExecutor != null) { - sExecutor.execute(mFuture); - return this; - } else { - return null; - } - } - - protected abstract ThreadPoolExecutor getThreadPoolExecutor(); - - protected final void publishProgress(Progress... values) { - sHandler.obtainMessage(MESSAGE_POST_PROGRESS, - new LDNetAsyncTaskResult(this, values)).sendToTarget(); - } - - protected void finish(Result result) { - if (isCancelled()) { - result = null; - } - onPostExecute(result); - mStatus = Status.FINISHED; - } - - private static abstract class LDNetWorkerRunnable implements - Callable { - Params[] mParams; - } - - private static class LDNetAsyncTaskResult { - @SuppressWarnings("rawtypes") - final LDNetAsyncTaskEx mTask; - final Data[] mData; - - LDNetAsyncTaskResult(@SuppressWarnings("rawtypes") LDNetAsyncTaskEx task, - Data... data) { - mTask = task; - mData = data; - } - } -} diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoListener.java b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoListener.java index 00e5344..0e77612 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoListener.java +++ b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoListener.java @@ -2,21 +2,23 @@ /** * ç›‘æŽ§ç½‘ç»œè¯Šæ–­çš„è·Ÿè¸ªä¿¡æ¯ - * @author panghui * + * @author panghui */ public interface LDNetDiagnoListener { - - /** - * 当结æŸä¹‹åŽè¿”回日志 - * @param log - */ - public void OnNetDiagnoFinished(String log); - - /** - * 跟踪过程中更新日志 - * @param log - */ - public void OnNetDiagnoUpdated(String log); + /** + * 当结æŸä¹‹åŽè¿”回日志 + * + * @param log + */ + public void OnNetDiagnoFinished(String log); + + + /** + * 跟踪过程中更新日志 + * + * @param log + */ + public void OnNetDiagnoUpdated(String log); } diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoService.java b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoService.java index 8824273..ff23b84 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoService.java +++ b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetDiagnoService.java @@ -1,5 +1,14 @@ package com.netease.LDNetDiagnoService; +import android.content.Context; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + +import com.netease.LDNetDiagnoService.LDNetPing.LDNetPingListener; +import com.netease.LDNetDiagnoService.LDNetSocket.LDNetSocketListener; +import com.netease.LDNetDiagnoService.LDNetTraceRoute.LDNetTraceRouteListener; +import com.netease.LDNetDiagnoUtils.LDNetUtil; + import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; @@ -15,519 +24,508 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import android.content.Context; -import android.telephony.TelephonyManager; -import android.text.TextUtils; - -import com.netease.LDNetDiagnoService.LDNetPing.LDNetPingListener; -import com.netease.LDNetDiagnoService.LDNetSocket.LDNetSocketListener; -import com.netease.LDNetDiagnoService.LDNetTraceRoute.LDNetTraceRouteListener; -import com.netease.LDNetDiagnoUtils.LDNetUtil; - /** * 网络诊断æœåŠ¡ 通过对制定域å进行ping诊断和traceroute诊断收集诊断日志 * * @author panghui - * */ public class LDNetDiagnoService extends - LDNetAsyncTaskEx implements LDNetPingListener, - LDNetTraceRouteListener, LDNetSocketListener { - private String _appCode; // 客户端标记 - private String _appName; - private String _appVersion; - private String _UID; // 用户ID - private String _deviceID; // 客户端机器ID,如果ä¸ä¼ å…¥ä¼šé»˜è®¤å–APIæä¾›çš„æœºå™¨ID - private String _dormain; // 接å£åŸŸå - private String _carrierName; - private String _ISOCountryCode; - private String _MobileCountryCode; - private String _MobileNetCode; - - private boolean _isNetConnected;// 当剿˜¯å¦è”网 - private boolean _isDomainParseOk;// 域åè§£æžæ˜¯å¦æˆåŠŸ - private boolean _isSocketConnected;// conectedæ˜¯å¦æˆåŠŸ - private Context _context; - private String _netType; - private String _localIp; - private String _gateWay; - private String _dns1; - private String _dns2; - private InetAddress[] _remoteInet; - private List _remoteIpList; - private final StringBuilder _logInfo = new StringBuilder(256); - private LDNetSocket _netSocker;// 监控socket的连接时间 - private LDNetPing _netPinger; // 监控ping命令的执行时间 - private LDNetTraceRoute _traceRouter; // 监控ping模拟traceroute的执行过程 - private boolean _isRunning; - - private LDNetDiagnoListener _netDiagnolistener; // å°†ç›‘æŽ§æ—¥å¿—ä¸ŠæŠ¥åˆ°å‰æ®µé¡µé¢ - private boolean _isUseJNICConn = false; - private boolean _isUseJNICTrace = true; - private TelephonyManager _telManager = null; // 用于获å–ç½‘ç»œåŸºæœ¬ä¿¡æ¯ - - public LDNetDiagnoService() { - super(); - } - - /** - * åˆå§‹åŒ–网络诊断æœåŠ¡ - * - * @param theAppCode - * @param theDeviceID - * @param theUID - * @param theDormain - */ - public LDNetDiagnoService(Context context, String theAppCode, - String theAppName, String theAppVersion, String theUID, - String theDeviceID, String theDormain, String theCarrierName, - String theISOCountryCode, String theMobileCountryCode, - String theMobileNetCode, LDNetDiagnoListener theListener) { - super(); - this._context = context; - this._appCode = theAppCode; - this._appName = theAppName; - this._appVersion = theAppVersion; - this._UID = theUID; - this._deviceID = theDeviceID; - this._dormain = theDormain; - this._carrierName = theCarrierName; - this._ISOCountryCode = theISOCountryCode; - this._MobileCountryCode = theMobileCountryCode; - this._MobileNetCode = theMobileNetCode; - this._netDiagnolistener = theListener; - // - this._isRunning = false; - _remoteIpList = new ArrayList(); - _telManager = (TelephonyManager) context - .getSystemService(Context.TELEPHONY_SERVICE); - sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, - KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory); - - } - - @Override - protected String doInBackground(String... params) { - if (this.isCancelled()) - return null; - // TODO Auto-generated method stub - return this.startNetDiagnosis(); - } - - @Override - protected void onPostExecute(String result) { - if (this.isCancelled()) - return; - super.onPostExecute(result); - // çº¿ç¨‹æ‰§è¡Œç»“æŸ - recordStepInfo("\n网络诊断结æŸ\n"); - this.stopNetDialogsis(); - if (_netDiagnolistener != null) { - _netDiagnolistener.OnNetDiagnoFinished(_logInfo.toString()); + AbstractLDNetAsyncTaskEx implements LDNetPingListener, + LDNetTraceRouteListener, LDNetSocketListener { + private static final int CORE_POOL_SIZE = 1;// 4 + private static final int MAXIMUM_POOL_SIZE = 1;// 10 + private static final int KEEP_ALIVE = 10;// 10 + private static final BlockingQueue sWorkQueue = new LinkedBlockingQueue( + 2);// 2 + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "Trace #" + mCount.getAndIncrement()); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }; + private static ThreadPoolExecutor sExecutor = null; + private final StringBuilder logInfo = new StringBuilder(256); + private String appCode; // 客户端标记 + private String appName; + private String appVersion; + private String UID; // 用户ID + private String deviceID; // 客户端机器ID,如果ä¸ä¼ å…¥ä¼šé»˜è®¤å–APIæä¾›çš„æœºå™¨ID + private String dormain; // 接å£åŸŸå + private String carrierName; + private String ISOCountryCode; + private String MobileCountryCode; + private String MobileNetCode; + private boolean isNetConnected;// 当剿˜¯å¦è”网 + private boolean isDomainParseOk;// 域åè§£æžæ˜¯å¦æˆåŠŸ + private boolean isSocketConnected;// conectedæ˜¯å¦æˆåŠŸ + private Context context; + private String netType; + private String localIp; + private String gateWay; + private String dns1; + private String dns2; + private InetAddress[] remoteInet; + private List remoteIpList; + private LDNetSocket netSocker;// 监控socket的连接时间 + private LDNetPing netPinger; // 监控ping命令的执行时间 + private LDNetTraceRoute traceRouter; // 监控ping模拟traceroute的执行过程 + private boolean isRunning; + private LDNetDiagnoListener netDiagnolistener; // å°†ç›‘æŽ§æ—¥å¿—ä¸ŠæŠ¥åˆ°å‰æ®µé¡µé¢ + private boolean isUseJNICConn = false; + private boolean isUseJNICTrace = true; + private TelephonyManager telManager = null; // 用于获å–ç½‘ç»œåŸºæœ¬ä¿¡æ¯ + + public LDNetDiagnoService() { + super(); } - } - - @Override - protected void onProgressUpdate(String... values) { - if (this.isCancelled()) - return; - // TODO Auto-generated method stub - super.onProgressUpdate(values); - if (_netDiagnolistener != null) { - _netDiagnolistener.OnNetDiagnoUpdated(values[0]); + + /** + * åˆå§‹åŒ–网络诊断æœåŠ¡ + * + * @param theAppCode + * @param theDeviceID + * @param theUID + * @param theDormain + */ + public LDNetDiagnoService(Context context, String theAppCode, + String theAppName, String theAppVersion, String theUID, + String theDeviceID, String theDormain, String theCarrierName, + String theISOCountryCode, String theMobileCountryCode, + String theMobileNetCode, LDNetDiagnoListener theListener) { + super(); + this.context = context; + this.appCode = theAppCode; + this.appName = theAppName; + this.appVersion = theAppVersion; + this.UID = theUID; + this.deviceID = theDeviceID; + this.dormain = theDormain; + this.carrierName = theCarrierName; + this.ISOCountryCode = theISOCountryCode; + this.MobileCountryCode = theMobileCountryCode; + this.MobileNetCode = theMobileNetCode; + this.netDiagnolistener = theListener; + // + this.isRunning = false; + remoteIpList = new ArrayList(); + telManager = (TelephonyManager) context + .getSystemService(Context.TELEPHONY_SERVICE); + sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, + KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory); + } - } - - @Override - protected void onCancelled() { - this.stopNetDialogsis(); - } - - /** - * 开始诊断网络 - */ - public String startNetDiagnosis() { - if (TextUtils.isEmpty(this._dormain)) - return ""; - this._isRunning = true; - this._logInfo.setLength(0); - recordStepInfo("开始诊断...\n"); - recordCurrentAppVersion(); - recordLocalNetEnvironmentInfo(); - - if (_isNetConnected) { - // 获å–è¿è¥å•†ä¿¡æ¯ - //recordStepInfo("\n开始获å–è¿è¥å•†ä¿¡æ¯..."); - //String operatorInfo = requestOperatorInfo(); - //if (operatorInfo != null) { - //recordStepInfo(operatorInfo); - //} - - // TCPä¸‰æ¬¡æ¡æ‰‹æ—¶é—´æµ‹è¯• - recordStepInfo("\n开始TCP连接测试..."); - _netSocker = LDNetSocket.getInstance(); - _netSocker._remoteInet = _remoteInet; - _netSocker._remoteIpList = _remoteIpList; - _netSocker.initListener(this); - _netSocker.isCConn = this._isUseJNICConn;// 设置是å¦å¯ç”¨C进行connected - _isSocketConnected = _netSocker.exec(_dormain); - - // 诊断pingä¿¡æ¯, åŒæ­¥è¿‡ç¨‹ - - if (!(_isNetConnected && _isDomainParseOk && _isSocketConnected)) {// è”网&&DNSè§£æžæˆåŠŸ&&connect测试æˆåŠŸ - recordStepInfo("\n开始ping..."); - _netPinger = new LDNetPing(this, 4); - recordStepInfo("ping...127.0.0.1"); - _netPinger.exec("127.0.0.1", false); - recordStepInfo("ping本机IP..." + _localIp); - _netPinger.exec(_localIp, false); - if (LDNetUtil.NETWORKTYPE_WIFI.equals(_netType)) {// 在wifi下ping网关 - recordStepInfo("ping本地网关..." + _gateWay); - _netPinger.exec(_gateWay, false); + + @Override + protected String doInBackground(String... params) { + if (this.isCancelled()) { + return null; } - recordStepInfo("ping本地DNS1..." + _dns1); - _netPinger.exec(_dns1, false); - recordStepInfo("ping本地DNS2..." + _dns2); - _netPinger.exec(_dns2, false); - } - - if (_netPinger == null) { - _netPinger = new LDNetPing(this, 4); - } - if (_netPinger != null) { - //recordStepInfo("ping..." + LDNetUtil.OPEN_IP); - //_netPinger.exec(LDNetUtil.OPEN_IP, true); - } - - // 开始诊断traceRoute - recordStepInfo("\n开始traceroute..."); - _traceRouter = LDNetTraceRoute.getInstance(); - _traceRouter.initListenter(this); - _traceRouter.isCTrace = this._isUseJNICTrace; - _traceRouter.startTraceRoute(_dormain); - return _logInfo.toString(); - } else { - recordStepInfo("\n\n当å‰ä¸»æœºæœªè”网,请检查网络ï¼"); - return _logInfo.toString(); + // TODO Auto-generated method stub + return this.startNetDiagnosis(); } - } - - /** - * åœæ­¢è¯Šæ–­ç½‘络 - */ - public void stopNetDialogsis() { - if (_isRunning) { - if (_netSocker != null) { - _netSocker.resetInstance(); - _netSocker = null; - } - - if (_netPinger != null) { - _netPinger = null; - } - if (_traceRouter != null) { - _traceRouter.resetInstance(); - _traceRouter = null; - } - cancel(true);// å°è¯•åŽ»å–æ¶ˆçº¿ç¨‹çš„æ‰§è¡Œ - if (sExecutor != null && !sExecutor.isShutdown()) { - sExecutor.shutdown(); - sExecutor = null; - } - - _isRunning = false; + + @Override + protected void onPostExecute(String result) { + if (this.isCancelled()) { + return; + } + super.onPostExecute(result); + // çº¿ç¨‹æ‰§è¡Œç»“æŸ + recordStepInfo("\n网络诊断结æŸ\n"); + this.stopNetDialogsis(); + if (netDiagnolistener != null) { + netDiagnolistener.OnNetDiagnoFinished(logInfo.toString()); + } } - } - - /** - * 设置是å¦éœ€è¦JNICTraceRoute - * - * @param use - */ - public void setIfUseJNICConn(boolean use) { - this._isUseJNICConn = use; - } - - /** - * 设置是å¦éœ€è¦JNICTraceRoute - * - * @param use - */ - public void setIfUseJNICTrace(boolean use) { - this._isUseJNICTrace = use; - } - - /** - * æ‰“å°æ•´ä½“loginInfoï¼› - */ - public void printLogInfo() { - System.out.print(_logInfo); - } - - /** - * 如果调用者实现了stepInfo接å£ï¼Œè¾“å‡ºä¿¡æ¯ - * - * @param stepInfo - */ - private void recordStepInfo(String stepInfo) { - _logInfo.append(stepInfo + "\n"); - publishProgress(stepInfo + "\n"); - } - - /** - * traceroute 消æ¯è·Ÿè¸ª - */ - @Override - public void OnNetTraceFinished() { - } - - @Override - public void OnNetTraceUpdated(String log) { - if (log == null) { - return; + + @Override + protected void onProgressUpdate(String... values) { + if (this.isCancelled()) { + return; + } + // TODO Auto-generated method stub + super.onProgressUpdate(values); + if (netDiagnolistener != null) { + netDiagnolistener.OnNetDiagnoUpdated(values[0]); + } + } + + @Override + protected void onCancelled() { + this.stopNetDialogsis(); + } + + /** + * 开始诊断网络 + */ + public String startNetDiagnosis() { + if (TextUtils.isEmpty(this.dormain)) { + return ""; + } + this.isRunning = true; + this.logInfo.setLength(0); + recordStepInfo("开始诊断...\n"); + recordCurrentAppVersion(); + recordLocalNetEnvironmentInfo(); + + if (isNetConnected) { + // 获å–è¿è¥å•†ä¿¡æ¯ + //recordStepInfo("\n开始获å–è¿è¥å•†ä¿¡æ¯..."); + //String operatorInfo = requestOperatorInfo(); + //if (operatorInfo != null) { + //recordStepInfo(operatorInfo); + //} + + // TCPä¸‰æ¬¡æ¡æ‰‹æ—¶é—´æµ‹è¯• + recordStepInfo("\n开始TCP连接测试..."); + netSocker = LDNetSocket.getInstance(); + netSocker.remoteInet = remoteInet; + netSocker.remoteIpList = remoteIpList; + netSocker.initListener(this); + netSocker.isCConn = this.isUseJNICConn;// 设置是å¦å¯ç”¨C进行connected + isSocketConnected = netSocker.exec(dormain); + + // 诊断pingä¿¡æ¯, åŒæ­¥è¿‡ç¨‹ + + if (!(isNetConnected && isDomainParseOk && isSocketConnected)) {// è”网&&DNSè§£æžæˆåŠŸ&&connect测试æˆåŠŸ + recordStepInfo("\n开始ping..."); + netPinger = new LDNetPing(this, 4); + recordStepInfo("ping...127.0.0.1"); + netPinger.exec("127.0.0.1", false); + recordStepInfo("ping本机IP..." + localIp); + netPinger.exec(localIp, false); + if (LDNetUtil.NETWORKTYPE_WIFI.equals(netType)) {// 在wifi下ping网关 + recordStepInfo("ping本地网关..." + gateWay); + netPinger.exec(gateWay, false); + } + recordStepInfo("ping本地DNS1..." + dns1); + netPinger.exec(dns1, false); + recordStepInfo("ping本地DNS2..." + dns2); + netPinger.exec(dns2, false); + } + + if (netPinger == null) { + netPinger = new LDNetPing(this, 4); + } + if (netPinger != null) { + //recordStepInfo("ping..." + LDNetUtil.OPEN_IP); + //netPinger.exec(LDNetUtil.OPEN_IP, true); + } + + // 开始诊断traceRoute + recordStepInfo("\n开始traceroute..."); + traceRouter = LDNetTraceRoute.getInstance(); + traceRouter.initListenter(this); + traceRouter.isCTrace = this.isUseJNICTrace; + traceRouter.startTraceRoute(dormain); + return logInfo.toString(); + } else { + recordStepInfo("\n\n当å‰ä¸»æœºæœªè”网,请检查网络ï¼"); + return logInfo.toString(); + } } - if (this._traceRouter != null && this._traceRouter.isCTrace) { - if (log.contains("ms") || log.contains("***")) { - log += "\n"; - } - _logInfo.append(log); - publishProgress(log); - } else { - this.recordStepInfo(log); + + /** + * åœæ­¢è¯Šæ–­ç½‘络 + */ + public void stopNetDialogsis() { + if (isRunning) { + if (netSocker != null) { + netSocker.resetInstance(); + netSocker = null; + } + + if (netPinger != null) { + netPinger = null; + } + if (traceRouter != null) { + traceRouter.resetInstance(); + traceRouter = null; + } + cancel(true);// å°è¯•åŽ»å–æ¶ˆçº¿ç¨‹çš„æ‰§è¡Œ + if (sExecutor != null && !sExecutor.isShutdown()) { + sExecutor.shutdown(); + sExecutor = null; + } + + isRunning = false; + } } - } - - /** - * socket完æˆè·Ÿè¸ª - */ - @Override - public void OnNetSocketFinished(String log) { - _logInfo.append(log); - publishProgress(log); - } - - /** - * socket更新跟踪 - */ - @Override - public void OnNetSocketUpdated(String log) { - _logInfo.append(log); - publishProgress(log); - } - - /** - * è¾“å‡ºå…³äºŽåº”ç”¨ã€æœºå™¨ã€ç½‘ç»œè¯Šæ–­çš„åŸºæœ¬ä¿¡æ¯ - */ - private void recordCurrentAppVersion() { - // 输出应用版本信æ¯å’Œç”¨æˆ·ID - recordStepInfo("应用code:\t" + _appCode); - recordStepInfo("应用åç§°:\t" + this._appName); - recordStepInfo("应用版本:\t" + this._appVersion); -// recordStepInfo("用户id:\t" + _UID); - - // è¾“å‡ºæœºå™¨ä¿¡æ¯ - recordStepInfo("机器类型:\t" + android.os.Build.MANUFACTURER + ":" - + android.os.Build.BRAND + ":" + android.os.Build.MODEL); - recordStepInfo("系统版本:\t" + android.os.Build.VERSION.RELEASE); - if (_telManager != null && TextUtils.isEmpty(_deviceID)) { - _deviceID = _telManager.getDeviceId(); + + /** + * 设置是å¦éœ€è¦JNICTraceRoute + * + * @param use + */ + public void setIfUseJNICConn(boolean use) { + this.isUseJNICConn = use; } - recordStepInfo("机器ID:\t" + _deviceID); - // è¿è¥å•†ä¿¡æ¯ - if (TextUtils.isEmpty(_carrierName)) { - _carrierName = LDNetUtil.getMobileOperator(_context); + /** + * 设置是å¦éœ€è¦JNICTraceRoute + * + * @param use + */ + public void setIfUseJNICTrace(boolean use) { + this.isUseJNICTrace = use; } - recordStepInfo("è¿è¥å•†:\t" + _carrierName); - if (_telManager != null && TextUtils.isEmpty(_ISOCountryCode)) { - _ISOCountryCode = _telManager.getNetworkCountryIso(); + /** + * æ‰“å°æ•´ä½“loginInfoï¼› + */ + public void printLogInfo() { + System.out.print(logInfo); } - recordStepInfo("ISOCountryCode:\t" + _ISOCountryCode); - - if (_telManager != null && TextUtils.isEmpty(_MobileCountryCode)) { - String tmp = _telManager.getNetworkOperator(); - if (tmp.length() >= 3) { - _MobileCountryCode = tmp.substring(0, 3); - } - if (tmp.length() >= 5) { - _MobileNetCode = tmp.substring(3, 5); - } + + /** + * 如果调用者实现了stepInfo接å£ï¼Œè¾“å‡ºä¿¡æ¯ + * + * @param stepInfo + */ + private void recordStepInfo(String stepInfo) { + logInfo.append(stepInfo + "\n"); + publishProgress(stepInfo + "\n"); } - recordStepInfo("MobileCountryCode:\t" + _MobileCountryCode); - recordStepInfo("MobileNetworkCode:\t" + _MobileNetCode+"\n"); - } - - /** - * è¾“å‡ºæœ¬åœ°ç½‘ç»œçŽ¯å¢ƒä¿¡æ¯ - */ - private void recordLocalNetEnvironmentInfo() { - recordStepInfo("诊断域å " + _dormain + "..."); - - // ç½‘ç»œçŠ¶æ€ - if (LDNetUtil.isNetworkConnected(_context)) { - _isNetConnected = true; - recordStepInfo("当剿˜¯å¦è”网:\t" + "å·²è”网"); - } else { - _isNetConnected = false; - recordStepInfo("当剿˜¯å¦è”网:\t" + "未è”网"); + + /** + * traceroute 消æ¯è·Ÿè¸ª + */ + @Override + public void OnNetTraceFinished() { } - // 获å–当å‰ç½‘络类型 - _netType = LDNetUtil.getNetWorkType(_context); - recordStepInfo("当å‰è”网类型:\t" + _netType); - if (_isNetConnected) { - if (LDNetUtil.NETWORKTYPE_WIFI.equals(_netType)) { // wifiï¼šèŽ·å–æœ¬åœ°ip和网关,其他类型:åªèŽ·å–ip - _localIp = LDNetUtil.getLocalIpByWifi(_context); - _gateWay = LDNetUtil.pingGateWayInWifi(_context); - } else { - _localIp = LDNetUtil.getLocalIpBy3G(); - } - recordStepInfo("本地IP:\t" + _localIp); - } else { - recordStepInfo("本地IP:\t" + "127.0.0.1"); + @Override + public void OnNetTraceUpdated(String log) { + if (log == null) { + return; + } + if (this.traceRouter != null && this.traceRouter.isCTrace) { + if (log.contains("ms") || log.contains("***")) { + log += "\n"; + } + logInfo.append(log); + publishProgress(log); + } else { + this.recordStepInfo(log); + } } - if (_gateWay != null) { - recordStepInfo("本地网关:\t" + this._gateWay); + + /** + * socket完æˆè·Ÿè¸ª + */ + @Override + public void OnNetSocketFinished(String log) { + logInfo.append(log); + publishProgress(log); } - // èŽ·å–æœ¬åœ°DNSåœ°å€ - if (_isNetConnected) { - _dns1 = LDNetUtil.getLocalDns("dns1"); - _dns2 = LDNetUtil.getLocalDns("dns2"); - recordStepInfo("本地DNS:\t" + this._dns1 + "," + this._dns2); - } else { - recordStepInfo("本地DNS:\t" + "0.0.0.0" + "," + "0.0.0.0"); + /** + * socket更新跟踪 + */ + @Override + public void OnNetSocketUpdated(String log) { + logInfo.append(log); + publishProgress(log); } - // 获å–远端域åçš„DNSè§£æžåœ°å€ - if (_isNetConnected) { - recordStepInfo("远端域å:\t" + this._dormain); - _isDomainParseOk = parseDomain(this._dormain);// 域åè§£æž + /** + * è¾“å‡ºå…³äºŽåº”ç”¨ã€æœºå™¨ã€ç½‘ç»œè¯Šæ–­çš„åŸºæœ¬ä¿¡æ¯ + */ + private void recordCurrentAppVersion() { + // 输出应用版本信æ¯å’Œç”¨æˆ·ID + recordStepInfo("应用code:\t" + appCode); + recordStepInfo("应用åç§°:\t" + this.appName); + recordStepInfo("应用版本:\t" + this.appVersion); +// recordStepInfo("用户id:\t" + UID); + + // è¾“å‡ºæœºå™¨ä¿¡æ¯ + recordStepInfo("机器类型:\t" + android.os.Build.MANUFACTURER + ":" + + android.os.Build.BRAND + ":" + android.os.Build.MODEL); + recordStepInfo("系统版本:\t" + android.os.Build.VERSION.RELEASE); + if (telManager != null && TextUtils.isEmpty(deviceID)) { + deviceID = telManager.getDeviceId(); + } + recordStepInfo("机器ID:\t" + deviceID); + + // è¿è¥å•†ä¿¡æ¯ + if (TextUtils.isEmpty(carrierName)) { + carrierName = LDNetUtil.getMobileOperator(context); + } + recordStepInfo("è¿è¥å•†:\t" + carrierName); + + if (telManager != null && TextUtils.isEmpty(ISOCountryCode)) { + ISOCountryCode = telManager.getNetworkCountryIso(); + } + recordStepInfo("ISOCountryCode:\t" + ISOCountryCode); + + if (telManager != null && TextUtils.isEmpty(MobileCountryCode)) { + String tmp = telManager.getNetworkOperator(); + if (tmp.length() >= 3) { + MobileCountryCode = tmp.substring(0, 3); + } + if (tmp.length() >= 5) { + MobileNetCode = tmp.substring(3, 5); + } + } + recordStepInfo("MobileCountryCode:\t" + MobileCountryCode); + recordStepInfo("MobileNetworkCode:\t" + MobileNetCode + "\n"); } - } - - /** - * 域åè§£æž - */ - private boolean parseDomain(String _dormain) { - boolean flag = false; - int len = 0; - String ipString = ""; - Map map = LDNetUtil.getDomainIp(_dormain); - String useTime = (String) map.get("useTime"); - _remoteInet = (InetAddress[]) map.get("remoteInet"); - String timeShow = null; - if (Integer.parseInt(useTime) > 5000) {// 如果大于1000ms,则æ¢ç”¨sæ¥æ˜¾ç¤º - timeShow = " (" + Integer.parseInt(useTime) / 1000 + "s)"; - } else { - timeShow = " (" + useTime + "ms)"; + + /** + * è¾“å‡ºæœ¬åœ°ç½‘ç»œçŽ¯å¢ƒä¿¡æ¯ + */ + private void recordLocalNetEnvironmentInfo() { + recordStepInfo("诊断域å " + dormain + "..."); + + // ç½‘ç»œçŠ¶æ€ + if (LDNetUtil.isNetworkConnected(context)) { + isNetConnected = true; + recordStepInfo("当剿˜¯å¦è”网:\t" + "å·²è”网"); + } else { + isNetConnected = false; + recordStepInfo("当剿˜¯å¦è”网:\t" + "未è”网"); + } + + // 获å–当å‰ç½‘络类型 + netType = LDNetUtil.getNetWorkType(context); + recordStepInfo("当å‰è”网类型:\t" + netType); + if (isNetConnected) { + if (LDNetUtil.NETWORKTYPE_WIFI.equals(netType)) { // wifiï¼šèŽ·å–æœ¬åœ°ip和网关,其他类型:åªèŽ·å–ip + localIp = LDNetUtil.getLocalIpByWifi(context); + gateWay = LDNetUtil.pingGateWayInWifi(context); + } else { + localIp = LDNetUtil.getLocalIpBy3G(); + } + recordStepInfo("本地IP:\t" + localIp); + } else { + recordStepInfo("本地IP:\t" + "127.0.0.1"); + } + if (gateWay != null) { + recordStepInfo("本地网关:\t" + this.gateWay); + } + + // èŽ·å–æœ¬åœ°DNSåœ°å€ + if (isNetConnected) { + dns1 = LDNetUtil.getLocalDns("dns1"); + dns2 = LDNetUtil.getLocalDns("dns2"); + recordStepInfo("本地DNS:\t" + this.dns1 + "," + this.dns2); + } else { + recordStepInfo("本地DNS:\t" + "0.0.0.0" + "," + "0.0.0.0"); + } + + // 获å–远端域åçš„DNSè§£æžåœ°å€ + if (isNetConnected) { + recordStepInfo("远端域å:\t" + this.dormain); + isDomainParseOk = parseDomain(this.dormain);// 域åè§£æž + } } - if (_remoteInet != null) {// è§£æžæ­£ç¡® - len = _remoteInet.length; - for (int i = 0; i < len; i++) { - _remoteIpList.add(_remoteInet[i].getHostAddress()); - ipString += _remoteInet[i].getHostAddress() + ","; - } - ipString = ipString.substring(0, ipString.length() - 1); - recordStepInfo("DNSè§£æžç»“æžœ:\t" + ipString + timeShow); - flag = true; - } else {// è§£æžä¸åˆ°ï¼Œåˆ¤æ–­ç¬¬ä¸€æ¬¡è§£æžè€—时,如果大于10sè¿›è¡Œç¬¬äºŒæ¬¡è§£æž - if (Integer.parseInt(useTime) > 10000) { - map = LDNetUtil.getDomainIp(_dormain); - useTime = (String) map.get("useTime"); - _remoteInet = (InetAddress[]) map.get("remoteInet"); + + /** + * 域åè§£æž + */ + private boolean parseDomain(String _dormain) { + boolean flag = false; + int len = 0; + String ipString = ""; + Map map = LDNetUtil.getDomainIp(_dormain); + String useTime = (String) map.get("useTime"); + remoteInet = (InetAddress[]) map.get("remoteInet"); + String timeShow = null; if (Integer.parseInt(useTime) > 5000) {// 如果大于1000ms,则æ¢ç”¨sæ¥æ˜¾ç¤º - timeShow = " (" + Integer.parseInt(useTime) / 1000 + "s)"; + timeShow = " (" + Integer.parseInt(useTime) / 1000 + "s)"; } else { - timeShow = " (" + useTime + "ms)"; + timeShow = " (" + useTime + "ms)"; } - if (_remoteInet != null) { - len = _remoteInet.length; - for (int i = 0; i < len; i++) { - _remoteIpList.add(_remoteInet[i].getHostAddress()); - ipString += _remoteInet[i].getHostAddress() + ","; - } - ipString = ipString.substring(0, ipString.length() - 1); - recordStepInfo("DNSè§£æžç»“æžœ:\t" + ipString + timeShow); - flag = true; - } else { - recordStepInfo("DNSè§£æžç»“æžœ:\t" + "è§£æžå¤±è´¥" + timeShow); + if (remoteInet != null) {// è§£æžæ­£ç¡® + len = remoteInet.length; + for (int i = 0; i < len; i++) { + remoteIpList.add(remoteInet[i].getHostAddress()); + ipString += remoteInet[i].getHostAddress() + ","; + } + ipString = ipString.substring(0, ipString.length() - 1); + recordStepInfo("DNSè§£æžç»“æžœ:\t" + ipString + timeShow); + flag = true; + } else {// è§£æžä¸åˆ°ï¼Œåˆ¤æ–­ç¬¬ä¸€æ¬¡è§£æžè€—时,如果大于10sè¿›è¡Œç¬¬äºŒæ¬¡è§£æž + if (Integer.parseInt(useTime) > 10000) { + map = LDNetUtil.getDomainIp(_dormain); + useTime = (String) map.get("useTime"); + remoteInet = (InetAddress[]) map.get("remoteInet"); + if (Integer.parseInt(useTime) > 5000) {// 如果大于1000ms,则æ¢ç”¨sæ¥æ˜¾ç¤º + timeShow = " (" + Integer.parseInt(useTime) / 1000 + "s)"; + } else { + timeShow = " (" + useTime + "ms)"; + } + if (remoteInet != null) { + len = remoteInet.length; + for (int i = 0; i < len; i++) { + remoteIpList.add(remoteInet[i].getHostAddress()); + ipString += remoteInet[i].getHostAddress() + ","; + } + ipString = ipString.substring(0, ipString.length() - 1); + recordStepInfo("DNSè§£æžç»“æžœ:\t" + ipString + timeShow); + flag = true; + } else { + recordStepInfo("DNSè§£æžç»“æžœ:\t" + "è§£æžå¤±è´¥" + timeShow); + } + } else { + recordStepInfo("DNSè§£æžç»“æžœ:\t" + "è§£æžå¤±è´¥" + timeShow); + } } - } else { - recordStepInfo("DNSè§£æžç»“æžœ:\t" + "è§£æžå¤±è´¥" + timeShow); - } + return flag; } - return flag; - } - - /** - * 获å–è¿è¥å•†ä¿¡æ¯ - */ - private String requestOperatorInfo() { - String res = null; - String url = LDNetUtil.OPERATOR_URL; - HttpURLConnection conn = null; - URL Operator_url; - try { - Operator_url = new URL(url); - conn = (HttpURLConnection) Operator_url.openConnection(); - conn.setRequestMethod("GET"); - conn.setConnectTimeout(1000 * 10); - conn.connect(); - int responseCode = conn.getResponseCode(); - if (responseCode == 200) { - res = LDNetUtil.getStringFromStream(conn.getInputStream()); - if (conn != null) { - conn.disconnect(); + + /** + * 获å–è¿è¥å•†ä¿¡æ¯ + */ + private String requestOperatorInfo() { + String res = null; + String url = LDNetUtil.OPERATOR_URL; + HttpURLConnection conn = null; + URL Operator_url; + try { + Operator_url = new URL(url); + conn = (HttpURLConnection) Operator_url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(1000 * 10); + conn.connect(); + int responseCode = conn.getResponseCode(); + if (responseCode == 200) { + res = LDNetUtil.getStringFromStream(conn.getInputStream()); + if (conn != null) { + conn.disconnect(); + } + } + return res; + } catch (MalformedURLException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } finally { + if (conn != null) { + conn.disconnect(); + } } - } - return res; - } catch (MalformedURLException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } finally { - if (conn != null) { - conn.disconnect(); - } + return res; } - return res; - } - - /** - * ping 消æ¯è·Ÿè¸ª - */ - @Override - public void OnNetPingFinished(String log) { - this.recordStepInfo(log); - } - - private static final int CORE_POOL_SIZE = 1;// 4 - private static final int MAXIMUM_POOL_SIZE = 1;// 10 - private static final int KEEP_ALIVE = 10;// 10 - - private static final BlockingQueue sWorkQueue = new LinkedBlockingQueue( - 2);// 2 - private static final ThreadFactory sThreadFactory = new ThreadFactory() { - private final AtomicInteger mCount = new AtomicInteger(1); + /** + * ping 消æ¯è·Ÿè¸ª + */ @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r, "Trace #" + mCount.getAndIncrement()); - t.setPriority(Thread.MIN_PRIORITY); - return t; + public void OnNetPingFinished(String log) { + this.recordStepInfo(log); } - }; - - private static ThreadPoolExecutor sExecutor = null; - @Override - protected ThreadPoolExecutor getThreadPoolExecutor() { - // TODO Auto-generated method stub - return sExecutor; - } + @Override + protected ThreadPoolExecutor getThreadPoolExecutor() { + // TODO Auto-generated method stub + return sExecutor; + } } diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetPing.java b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetPing.java index d2b1fc5..8494fdc 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetPing.java +++ b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetPing.java @@ -1,131 +1,128 @@ package com.netease.LDNetDiagnoService; +import android.util.Log; + +import com.netease.LDNetDiagnoUtils.LDPingParse; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.regex.Matcher; import java.util.regex.Pattern; -import android.util.Log; - -import com.netease.LDNetDiagnoUtils.LDPingParse; - /** * 直接通过ping命令监测网络 */ public class LDNetPing { - LDNetPingListener listener; // 回传ping的结果 - private final int _sendCount; // æ¯æ¬¡pingå‘逿•°æ®åŒ…的个数 - - public LDNetPing(LDNetPingListener listener, int theSendCount) { - super(); - this.listener = listener; - this._sendCount = theSendCount; - } - - /** - * 监控NetPing的日志输出到Service - * - * @author panghui - * - */ - public interface LDNetPingListener { - public void OnNetPingFinished(String log); - } - - private static final String MATCH_PING_IP = "(?<=from ).*(?=: icmp_seq=1 ttl=)"; - - /** - * 执行ping命令,返回ping命令的全部控制å°è¾“出 - * - * @param ping - * @return - */ - private String execPing(PingTask ping, boolean isNeedL) { - String cmd = "ping -c "; - if (isNeedL) { - cmd = "ping -s 8185 -c "; + private static final String MATCH_PING_IP = "(?<=from ).*(?=: icmp_seq=1 ttl=)"; + private final int sendCount; // æ¯æ¬¡pingå‘逿•°æ®åŒ…的个数 + LDNetPingListener listener; // 回传ping的结果 + + public LDNetPing(LDNetPingListener listener, int theSendCount) { + super(); + this.listener = listener; + this.sendCount = theSendCount; } - Process process = null; - String str = ""; - BufferedReader reader = null; - try { - process = Runtime.getRuntime().exec( - cmd + this._sendCount + " " + ping.getHost()); - reader = new BufferedReader(new InputStreamReader( - process.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - str += line; - } - reader.close(); - process.waitFor(); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - try { - if (reader != null) { - reader.close(); + /** + * 执行ping命令,返回ping命令的全部控制å°è¾“出 + * + * @param ping + * @return + */ + private String execPing(PingTask ping, boolean isNeedL) { + String cmd = "ping -c "; + if (isNeedL) { + cmd = "ping -s 8185 -c "; + } + Process process = null; + String str = ""; + BufferedReader reader = null; + try { + process = Runtime.getRuntime().exec( + cmd + this.sendCount + " " + ping.getHost()); + reader = new BufferedReader(new InputStreamReader( + process.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + str += line; + } + reader.close(); + process.waitFor(); + + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); + } + process.destroy(); + } catch (Exception e) { + } } - process.destroy(); - } catch (Exception e) { - } + return str; } - return str; - } - /** - * 执行指定hostçš„traceroute - * - * @param host - * @return - */ - public void exec(String host, boolean isNeedL) { - PingTask pingTask = new PingTask(host); - StringBuilder log = new StringBuilder(256); - String status = execPing(pingTask, isNeedL); - if (Pattern.compile(MATCH_PING_IP).matcher(status).find()) { - Log.i("LDNetPing", "status" + status); - log.append("\t" + status); - } else { - if (status.length() == 0) { - log.append("unknown host or network error"); - } else { - - log.append("timeout"); - } + /** + * 执行指定hostçš„traceroute + * + * @param host + * @return + */ + public void exec(String host, boolean isNeedL) { + PingTask pingTask = new PingTask(host); + StringBuilder log = new StringBuilder(256); + String status = execPing(pingTask, isNeedL); + if (Pattern.compile(MATCH_PING_IP).matcher(status).find()) { + Log.i("LDNetPing", "status" + status); + log.append("\t" + status); + } else { + if (status.length() == 0) { + log.append("unknown host or network error"); + } else { + + log.append("timeout"); + } + } + String logStr = LDPingParse.getFormattingStr(host, log.toString()); + this.listener.OnNetPingFinished(logStr); } - String logStr = LDPingParse.getFormattingStr(host, log.toString()); - this.listener.OnNetPingFinished(logStr); - } - /** - * Ping任务 - * - * @author panghui - * - */ - private class PingTask { - - private String host; - private static final String MATCH_PING_HOST_IP = "(?<=\\().*?(?=\\))"; - - public String getHost() { - return host; + /** + * 监控NetPing的日志输出到Service + * + * @author panghui + */ + public interface LDNetPingListener { + public void OnNetPingFinished(String log); } - public PingTask(String host) { - super(); - this.host = host; - Pattern p = Pattern.compile(MATCH_PING_HOST_IP); - Matcher m = p.matcher(host); - if (m.find()) { - this.host = m.group(); - } + /** + * Ping任务 + * + * @author panghui + */ + private class PingTask { + + private static final String MATCH_PING_HOST_IP = "(?<=\\().*?(?=\\))"; + private String host; + + public PingTask(String host) { + super(); + this.host = host; + Pattern p = Pattern.compile(MATCH_PING_HOST_IP); + Matcher m = p.matcher(host); + if (m.find()) { + this.host = m.group(); + } + } + + public String getHost() { + return host; + } } - } } diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetSocket.java b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetSocket.java index 32f42c7..52f8d0c 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetSocket.java +++ b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetSocket.java @@ -1,5 +1,7 @@ package com.netease.LDNetDiagnoService; +import android.util.Log; + import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -7,205 +9,204 @@ import java.net.SocketTimeoutException; import java.util.List; -import android.util.Log; - public class LDNetSocket { - private static final int PORT = 80; - private static final int CONN_TIMES = 4; - private static final String TIMEOUT = "DNSè§£æžæ­£å¸¸,连接超时,TCP建立失败"; - private static final String IOERR = "DNSè§£æžæ­£å¸¸,IO异常,TCP建立失败"; - private static final String HOSTERR = "DNSè§£æžå¤±è´¥,主机地å€ä¸å¯è¾¾"; - private static LDNetSocket instance=null; - private LDNetSocketListener listener; - private int timeOut = 6000;// è®¾ç½®æ¯æ¬¡è¿žæŽ¥çš„timeoutæ—¶é—´ - public InetAddress[] _remoteInet; - public List _remoteIpList; - private boolean[] isConnnected; - private final long[] RttTimes = new long[CONN_TIMES];// ç”¨äºŽå­˜å‚¨ä¸‰æ¬¡æµ‹è¯•ä¸­æ¯æ¬¡çš„RTT值 - - public boolean isCConn = true; - - private LDNetSocket() { - - } - public static LDNetSocket getInstance(){ - if(instance==null){ - instance=new LDNetSocket(); - } - return instance; - } - - public void initListener(LDNetSocketListener listener){ - this.listener=listener; - } - - /** - * 通过connect函数测试TCPçš„RTTæ—¶å»¶ - */ - public boolean exec(String host) { - if (isCConn && loaded) { - try{ - startJNITelnet(host, "80"); //默认80ç«¯å£ - return true; - }catch(UnsatisfiedLinkError e){ - e.printStackTrace(); - Log.i("LDNetSocket", "call jni failed, call execUseJava"); - return execUseJava(host); - } - } else { - return execUseJava(host); - } - } - - /** - * 使用java执行connected - */ - private boolean execUseJava(String host) { - if (_remoteInet != null && _remoteIpList != null) { - int len = _remoteInet.length; - isConnnected = new boolean[len]; - for (int i = 0; i < len; i++) { - if (i != 0) { - this.listener.OnNetSocketUpdated("\n"); + private static final int PORT = 80; + private static final int CONN_TIMES = 4; + private static final String TIMEOUT = "DNSè§£æžæ­£å¸¸,连接超时,TCP建立失败"; + private static final String IOERR = "DNSè§£æžæ­£å¸¸,IO异常,TCP建立失败"; + private static final String HOSTERR = "DNSè§£æžå¤±è´¥,主机地å€ä¸å¯è¾¾"; + static boolean loaded; + private static LDNetSocket instance = null; + + static { + try { + System.loadLibrary("tracepath"); + loaded = true; + } catch (UnsatisfiedLinkError e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); } - isConnnected[i] = execIP(_remoteInet[i], _remoteIpList.get(i)); - } - for (Boolean i : isConnnected) { - if (i == true) {// 一个连接æˆåŠŸå³è®¤ä¸ºæˆåŠŸ - this.listener.OnNetSocketFinished("\n"); - return true; + } + + private final long[] RttTimes = new long[CONN_TIMES];// ç”¨äºŽå­˜å‚¨ä¸‰æ¬¡æµ‹è¯•ä¸­æ¯æ¬¡çš„RTT值 + public InetAddress[] remoteInet; + public List remoteIpList; + public boolean isCConn = true; + private LDNetSocketListener listener; + private int timeOut = 6000;// è®¾ç½®æ¯æ¬¡è¿žæŽ¥çš„timeoutæ—¶é—´ + private boolean[] isConnnected; + + private LDNetSocket() { + + } + + public static LDNetSocket getInstance() { + if (instance == null) { + instance = new LDNetSocket(); } - } + return instance; + } - } else { - this.listener.OnNetSocketFinished(HOSTERR); + public void initListener(LDNetSocketListener listener) { + this.listener = listener; } - this.listener.OnNetSocketFinished("\n"); - return false; - } - - /** - * 返回æŸä¸ªIP进行5次connect的最终结果 - */ - private boolean execIP(InetAddress inetAddress, String ip) { - boolean isConnected = true; - StringBuilder log = new StringBuilder(); - InetSocketAddress socketAddress = null; - if (inetAddress != null && ip != null) { - socketAddress = new InetSocketAddress(inetAddress, PORT); - int flag = 0; - this.listener.OnNetSocketUpdated("Connect to host: " + ip + "..." + "\n"); - for (int i = 0; i < CONN_TIMES; i++) { - execSocket(socketAddress, timeOut, i); - if (RttTimes[i] == -1) {// 一旦å‘生timeOut,则å°è¯•加长连接时间 - this.listener.OnNetSocketUpdated((i + 1) + "'s time=" + "TimeOut" - + ", "); - timeOut += 4000; - if (i > 0 && RttTimes[i - 1] == -1) {// 连续两次连接超时,åœæ­¢åŽç»­æµ‹è¯• - flag = -1; - break; - } - } else if (RttTimes[i] == -2) { - this.listener - .OnNetSocketUpdated((i + 1) + "'s time=" + "IOException"); - if (i > 0 && RttTimes[i - 1] == -2) {// 连续两次出现IO异常,åœæ­¢åŽç»­æµ‹è¯• - flag = -2; - break; - } + + /** + * 通过connect函数测试TCPçš„RTTæ—¶å»¶ + */ + public boolean exec(String host) { + if (isCConn && loaded) { + try { + startJNITelnet(host, "80"); //默认80ç«¯å£ + return true; + } catch (UnsatisfiedLinkError e) { + e.printStackTrace(); + Log.i("LDNetSocket", "call jni failed, call execUseJava"); + return execUseJava(host); + } } else { - this.listener.OnNetSocketUpdated((i + 1) + "'s time=" + RttTimes[i] - + "ms, "); + return execUseJava(host); } - } - long time = 0; - int count = 0; - if (flag == -1) { - // log.append(TIMEOUT); - isConnected = false; - } else if (flag == -2) { - // log.append(IOERR); - isConnected = false; - } else { - for (int i = 0; i < CONN_TIMES; i++) { - if (RttTimes[i] > 0) { - time += RttTimes[i]; - count++; - } + } + + /** + * 使用java执行connected + */ + private boolean execUseJava(String host) { + if (remoteInet != null && remoteIpList != null) { + int len = remoteInet.length; + isConnnected = new boolean[len]; + for (int i = 0; i < len; i++) { + if (i != 0) { + this.listener.OnNetSocketUpdated("\n"); + } + isConnnected[i] = execIP(remoteInet[i], remoteIpList.get(i)); + } + for (Boolean i : isConnnected) { + if (i == true) {// 一个连接æˆåŠŸå³è®¤ä¸ºæˆåŠŸ + this.listener.OnNetSocketFinished("\n"); + return true; + } + } + + } else { + this.listener.OnNetSocketFinished(HOSTERR); } - if (count > 0) { - time = time / count; - log.append("average=" + time + "ms"); + this.listener.OnNetSocketFinished("\n"); + return false; + } + + /** + * 返回æŸä¸ªIP进行5次connect的最终结果 + */ + private boolean execIP(InetAddress inetAddress, String ip) { + boolean isConnected = true; + StringBuilder log = new StringBuilder(); + InetSocketAddress socketAddress = null; + if (inetAddress != null && ip != null) { + socketAddress = new InetSocketAddress(inetAddress, PORT); + int flag = 0; + this.listener.OnNetSocketUpdated("Connect to host: " + ip + "..." + "\n"); + for (int i = 0; i < CONN_TIMES; i++) { + execSocket(socketAddress, timeOut, i); + if (RttTimes[i] == -1) {// 一旦å‘生timeOut,则å°è¯•加长连接时间 + this.listener.OnNetSocketUpdated((i + 1) + "'s time=" + "TimeOut" + + ", "); + timeOut += 4000; + if (i > 0 && RttTimes[i - 1] == -1) {// 连续两次连接超时,åœæ­¢åŽç»­æµ‹è¯• + flag = -1; + break; + } + } else if (RttTimes[i] == -2) { + this.listener + .OnNetSocketUpdated((i + 1) + "'s time=" + "IOException"); + if (i > 0 && RttTimes[i - 1] == -2) {// 连续两次出现IO异常,åœæ­¢åŽç»­æµ‹è¯• + flag = -2; + break; + } + } else { + this.listener.OnNetSocketUpdated((i + 1) + "'s time=" + RttTimes[i] + + "ms, "); + } + } + long time = 0; + int count = 0; + if (flag == -1) { + // log.append(TIMEOUT); + isConnected = false; + } else if (flag == -2) { + // log.append(IOERR); + isConnected = false; + } else { + for (int i = 0; i < CONN_TIMES; i++) { + if (RttTimes[i] > 0) { + time += RttTimes[i]; + count++; + } + } + if (count > 0) { + time = time / count; + log.append("average=" + time + "ms"); + } + } + } else { + isConnected = false; } - } - } else { - isConnected = false; + this.listener.OnNetSocketUpdated(log.toString()); + return isConnected; } - this.listener.OnNetSocketUpdated(log.toString()); - return isConnected; - } - - /** - * 针对æŸä¸ªIP第index次connect - */ - private void execSocket(InetSocketAddress socketAddress, int timeOut, - int index) { - Socket socket = null; - long start = 0; - long end = 0; - try { - socket = new Socket(); - start = System.currentTimeMillis(); - socket.connect(socketAddress, timeOut); - end = System.currentTimeMillis(); - RttTimes[index] = end - start; - } catch (SocketTimeoutException e) { - RttTimes[index] = -1;// 作为TIMEOUT标识 - e.printStackTrace(); - } catch (IOException e) { - RttTimes[index] = -2;// 作为IO异常标识 - e.printStackTrace(); - } finally { - if (socket != null) { + + /** + * 针对æŸä¸ªIP第index次connect + */ + private void execSocket(InetSocketAddress socketAddress, int timeOut, + int index) { + Socket socket = null; + long start = 0; + long end = 0; try { - socket.close(); + socket = new Socket(); + start = System.currentTimeMillis(); + socket.connect(socketAddress, timeOut); + end = System.currentTimeMillis(); + RttTimes[index] = end - start; + } catch (SocketTimeoutException e) { + RttTimes[index] = -1;// 作为TIMEOUT标识 + e.printStackTrace(); } catch (IOException e) { - e.printStackTrace(); + RttTimes[index] = -2;// 作为IO异常标识 + e.printStackTrace(); + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } - } } - } - public void resetInstance() { - if (instance != null) { - instance = null; + public void resetInstance() { + if (instance != null) { + instance = null; + } } - } - - /* - * 调用jni中native方法 - */ - public native void startJNITelnet(String host, String port); - - static boolean loaded; - static { - try { - System.loadLibrary("tracepath"); - loaded = true; - } catch (UnsatisfiedLinkError e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); + + /* + * 调用jni中native方法 + */ + public native void startJNITelnet(String host, String port); + + public void printSocketInfo(String log) { + listener.OnNetSocketUpdated(log); + } + + public interface LDNetSocketListener { + public void OnNetSocketFinished(String log); + + public void OnNetSocketUpdated(String log); } - } - - public void printSocketInfo(String log){ - listener.OnNetSocketUpdated(log); - } - - public interface LDNetSocketListener { - public void OnNetSocketFinished(String log); - - public void OnNetSocketUpdated(String log); - } } diff --git a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetTraceRoute.java b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetTraceRoute.java index 9df2502..c425705 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoService/LDNetTraceRoute.java +++ b/app/src/main/java/com/netease/LDNetDiagnoService/LDNetTraceRoute.java @@ -1,316 +1,311 @@ package com.netease.LDNetDiagnoService; +import android.util.Log; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.regex.Matcher; import java.util.regex.Pattern; -import android.util.Log; - /** * 通过ping模拟traceroute过程 - * + * * @author panghui - * */ public class LDNetTraceRoute { - private final String LOG_TAG = "LDNetTraceRoute"; - private static LDNetTraceRoute instance; + private static final String MATCH_TRACE_IP = "(?<=From )(?:[0-9]{1,3}\\.){3}[0-9]{1,3}"; + private static final String MATCH_PING_IP = "(?<=from ).*(?=: icmp_seq=1 ttl=)"; + private static final String MATCH_PING_TIME = "(?<=time=).*?ms"; + static boolean loaded; + private static LDNetTraceRoute instance; + + static { + try { + System.loadLibrary("tracepath"); + loaded = true; + } catch (UnsatisfiedLinkError e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } + } - private LDNetTraceRoute() { - } + private final String LOG_TAG = "LDNetTraceRoute"; + public boolean isCTrace = true; + LDNetTraceRouteListener listener; - public static LDNetTraceRoute getInstance() { - if (instance == null) { - instance = new LDNetTraceRoute(); - } - return instance; - } - - LDNetTraceRouteListener listener; - public boolean isCTrace = true; - - public void initListenter(LDNetTraceRouteListener listener) { - this.listener = listener; - } - - /** - * 监控NetPing的日志输出到Service - * - * @author panghui - * - */ - public interface LDNetTraceRouteListener { - public void OnNetTraceUpdated(String log); - - public void OnNetTraceFinished(); - } - - /** - * 执行指定hostçš„traceroute - * - * @param host - * @return - */ - public void startTraceRoute(String host) { - if (isCTrace && loaded) { - try { - startJNICTraceRoute(host); - } catch (UnsatisfiedLinkError e) { - e.printStackTrace(); - // 如果c调用失败改调JAVAä»£ç  - Log.i("LDNetTraceRoute", "调用java模拟traceRoute"); - TraceTask trace = new TraceTask(host, 1); - execTrace(trace); - } - } else { - TraceTask trace = new TraceTask(host, 1); - execTrace(trace); + private LDNetTraceRoute() { } - } - public void resetInstance() { - if (instance != null) { - instance = null; + public static LDNetTraceRoute getInstance() { + if (instance == null) { + instance = new LDNetTraceRoute(); + } + return instance; } - } - - /** - * 调用jni c函数执行traceroute过程 - */ - public native void startJNICTraceRoute(String traceCommand); - - static boolean loaded; - static { - try { - System.loadLibrary("tracepath"); - loaded = true; - } catch (UnsatisfiedLinkError e) { - e.printStackTrace(); - } catch (Exception e) { - e.printStackTrace(); + + public void initListenter(LDNetTraceRouteListener listener) { + this.listener = listener; } - } - - /** - * ä¾›jni c函数回调 - * - * @param log - */ - public void printTraceInfo(String log) { - // Log.i(LOG_TAG, log); - listener.OnNetTraceUpdated(log); - } - - private static final String MATCH_TRACE_IP = "(?<=From )(?:[0-9]{1,3}\\.){3}[0-9]{1,3}"; - private static final String MATCH_PING_IP = "(?<=from ).*(?=: icmp_seq=1 ttl=)"; - private static final String MATCH_PING_TIME = "(?<=time=).*?ms"; - - /** - * 执行ping命令,返回ping命令的全部控制å°è¾“出 - * - * @param ping - * @return - */ - private String execPing(PingTask ping) { - Process process = null; - String str = ""; - BufferedReader reader = null; - try { - process = Runtime.getRuntime().exec("ping -c 1 " + ping.getHost()); - reader = new BufferedReader(new InputStreamReader( - process.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - str += line; - } - reader.close(); - process.waitFor(); - - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - try { - if (reader != null) { - reader.close(); + + /** + * 执行指定hostçš„traceroute + * + * @param host + * @return + */ + public void startTraceRoute(String host) { + if (isCTrace && loaded) { + try { + startJNICTraceRoute(host); + } catch (UnsatisfiedLinkError e) { + e.printStackTrace(); + // 如果c调用失败改调JAVAä»£ç  + Log.i("LDNetTraceRoute", "调用java模拟traceRoute"); + TraceTask trace = new TraceTask(host, 1); + execTrace(trace); + } + } else { + TraceTask trace = new TraceTask(host, 1); + execTrace(trace); } - process.destroy(); - } catch (Exception e) { - } } - return str; - } - - /** - * 通过ping命令模拟执行traceroute的过程 - * - * @param trace - * @return - */ - private void execTrace(TraceTask trace) { - Pattern patternTrace = Pattern.compile(MATCH_TRACE_IP); - Pattern patternIp = Pattern.compile(MATCH_PING_IP); - Pattern patternTime = Pattern.compile(MATCH_PING_TIME); - - Process process = null; - BufferedReader reader = null; - boolean finish = false; - try { - // 通过ping的跳数控制,å–得相应跳输的ip地å€ï¼Œç„¶åŽå†æ¬¡æ‰§è¡Œpingå‘½ä»¤è¯»å–æ—¶é—´ - while (!finish && trace.getHop() < 30) { - // å…ˆå‘出ping命令获得æŸä¸ªè·³æ•°çš„ipåœ°å€ - String str = ""; - // -c 1 åŒæ—¶å‘逿¶ˆæ¯æ¬¡æ•° ï¼t是指跳数 - String command = "ping -c 1 -t " + trace.getHop() + " " - + trace.getHost(); - - process = Runtime.getRuntime().exec(command); - reader = new BufferedReader(new InputStreamReader( - process.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - str += line; - } - reader.close(); - process.waitFor(); - - Matcher m = patternTrace.matcher(str); - - // 如果æˆåŠŸèŽ·å¾—trace:IPï¼Œåˆ™å†æ¬¡å‘é€ping命令获å–ping的时间 - StringBuilder log = new StringBuilder(256); - if (m.find()) { - String pingIp = m.group(); - PingTask pingTask = new PingTask(pingIp); - - String status = execPing(pingTask); - if (status.length() == 0) { - log.append("unknown host or network error\n"); - finish = true; - } else { - Matcher matcherTime = patternTime.matcher(status); - if (matcherTime.find()) { - String time = matcherTime.group(); - log.append(trace.getHop()); - log.append("\t\t"); - log.append(pingIp); - log.append("\t\t"); - log.append(time); - log.append("\t"); - } else { - log.append(trace.getHop()); - log.append("\t\t"); - log.append(pingIp); - log.append("\t\t timeout \t"); - } - listener.OnNetTraceUpdated(log.toString()); - trace.setHop(trace.getHop() + 1); - } + public void resetInstance() { + if (instance != null) { + instance = null; } + } + + /** + * 调用jni c函数执行traceroute过程 + */ + public native void startJNICTraceRoute(String traceCommand); + + /** + * ä¾›jni c函数回调 + * + * @param log + */ + public void printTraceInfo(String log) { + // Log.i(LOG_TAG, log); + listener.OnNetTraceUpdated(log); + } - // å¦åˆ™ï¼šwhat - else { - Matcher matchPingIp = patternIp.matcher(str); - if (matchPingIp.find()) { - String pingIp = matchPingIp.group(); - Matcher matcherTime = patternTime.matcher(str); - if (matcherTime.find()) { - String time = matcherTime.group(); - log.append(trace.getHop()); - log.append("\t\t"); - log.append(pingIp); - log.append("\t\t"); - log.append(time); - log.append("\t"); - listener.OnNetTraceUpdated(log.toString()); + /** + * 执行ping命令,返回ping命令的全部控制å°è¾“出 + * + * @param ping + * @return + */ + private String execPing(PingTask ping) { + Process process = null; + String str = ""; + BufferedReader reader = null; + try { + process = Runtime.getRuntime().exec("ping -c 1 " + ping.getHost()); + reader = new BufferedReader(new InputStreamReader( + process.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + str += line; } - finish = true; - } else { - if (str.length() == 0) { - log.append("unknown host or network error\t"); - finish = true; - } else { - log.append(trace.getHop()); - log.append("\t\t timeout \t"); - trace.setHop(trace.getHop() + 1); + reader.close(); + process.waitFor(); + + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); + } + process.destroy(); + } catch (Exception e) { } - listener.OnNetTraceUpdated(log.toString()); - } - }// else no match traceIPPattern - }// while - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - try { - if (reader != null) { - reader.close(); } - process.destroy(); - } catch (Exception e) { - } + return str; } - listener.OnNetTraceFinished(); - } + /** + * 通过ping命令模拟执行traceroute的过程 + * + * @param trace + * @return + */ + private void execTrace(TraceTask trace) { + Pattern patternTrace = Pattern.compile(MATCH_TRACE_IP); + Pattern patternIp = Pattern.compile(MATCH_PING_IP); + Pattern patternTime = Pattern.compile(MATCH_PING_TIME); + + Process process = null; + BufferedReader reader = null; + boolean finish = false; + try { + // 通过ping的跳数控制,å–得相应跳输的ip地å€ï¼Œç„¶åŽå†æ¬¡æ‰§è¡Œpingå‘½ä»¤è¯»å–æ—¶é—´ + while (!finish && trace.getHop() < 30) { + // å…ˆå‘出ping命令获得æŸä¸ªè·³æ•°çš„ipåœ°å€ + String str = ""; + // -c 1 åŒæ—¶å‘逿¶ˆæ¯æ¬¡æ•° ï¼t是指跳数 + String command = "ping -c 1 -t " + trace.getHop() + " " + + trace.getHost(); + + process = Runtime.getRuntime().exec(command); + reader = new BufferedReader(new InputStreamReader( + process.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + str += line; + } + reader.close(); + process.waitFor(); + + Matcher m = patternTrace.matcher(str); + + // 如果æˆåŠŸèŽ·å¾—trace:IPï¼Œåˆ™å†æ¬¡å‘é€ping命令获å–ping的时间 + StringBuilder log = new StringBuilder(256); + if (m.find()) { + String pingIp = m.group(); + PingTask pingTask = new PingTask(pingIp); + + String status = execPing(pingTask); + if (status.length() == 0) { + log.append("unknown host or network error\n"); + finish = true; + } else { + Matcher matcherTime = patternTime.matcher(status); + if (matcherTime.find()) { + String time = matcherTime.group(); + log.append(trace.getHop()); + log.append("\t\t"); + log.append(pingIp); + log.append("\t\t"); + log.append(time); + log.append("\t"); + } else { + log.append(trace.getHop()); + log.append("\t\t"); + log.append(pingIp); + log.append("\t\t timeout \t"); + } + listener.OnNetTraceUpdated(log.toString()); + trace.setHop(trace.getHop() + 1); + } + + } + + // å¦åˆ™ï¼šwhat + else { + Matcher matchPingIp = patternIp.matcher(str); + if (matchPingIp.find()) { + String pingIp = matchPingIp.group(); + Matcher matcherTime = patternTime.matcher(str); + if (matcherTime.find()) { + String time = matcherTime.group(); + log.append(trace.getHop()); + log.append("\t\t"); + log.append(pingIp); + log.append("\t\t"); + log.append(time); + log.append("\t"); + listener.OnNetTraceUpdated(log.toString()); + } + finish = true; + } else { + if (str.length() == 0) { + log.append("unknown host or network error\t"); + finish = true; + } else { + log.append(trace.getHop()); + log.append("\t\t timeout \t"); + trace.setHop(trace.getHop() + 1); + } + listener.OnNetTraceUpdated(log.toString()); + } + }// else no match traceIPPattern + }// while + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); + } + process.destroy(); + } catch (Exception e) { + } + } - /** - * Ping任务 - * - * @author panghui - * - */ - private class PingTask { + listener.OnNetTraceFinished(); + } - private String host; - private static final String MATCH_PING_HOST_IP = "(?<=\\().*?(?=\\))"; + /** + * 监控NetPing的日志输出到Service + * + * @author panghui + */ + public interface LDNetTraceRouteListener { + public void OnNetTraceUpdated(String log); - public String getHost() { - return host; + public void OnNetTraceFinished(); } - public PingTask(String host) { - super(); - this.host = host; - Pattern p = Pattern.compile(MATCH_PING_HOST_IP); - Matcher m = p.matcher(host); - if (m.find()) { - this.host = m.group(); - } + /** + * Ping任务 + * + * @author panghui + */ + private class PingTask { + + private static final String MATCH_PING_HOST_IP = "(?<=\\().*?(?=\\))"; + private String host; + + public PingTask(String host) { + super(); + this.host = host; + Pattern p = Pattern.compile(MATCH_PING_HOST_IP); + Matcher m = p.matcher(host); + if (m.find()) { + this.host = m.group(); + } - } - } - - /** - * 生æˆtrace任务 - * - * @author panghui - * - */ - private class TraceTask { - private final String host; - private int hop; - - public TraceTask(String host, int hop) { - super(); - this.host = host; - this.hop = hop; - } + } - public String getHost() { - return host; + public String getHost() { + return host; + } } - public int getHop() { - return hop; - } + /** + * 生æˆtrace任务 + * + * @author panghui + */ + private class TraceTask { + private final String host; + private int hop; + + public TraceTask(String host, int hop) { + super(); + this.host = host; + this.hop = hop; + } + + public String getHost() { + return host; + } + + public int getHop() { + return hop; + } - public void setHop(int hop) { - this.hop = hop; + public void setHop(int hop) { + this.hop = hop; + } } - } } diff --git a/app/src/main/java/com/netease/LDNetDiagnoUtils/LDNetUtil.java b/app/src/main/java/com/netease/LDNetDiagnoUtils/LDNetUtil.java index 5da1a79..48ccc17 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoUtils/LDNetUtil.java +++ b/app/src/main/java/com/netease/LDNetDiagnoUtils/LDNetUtil.java @@ -1,5 +1,15 @@ package com.netease.LDNetDiagnoUtils; +import android.annotation.SuppressLint; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.DhcpInfo; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.telephony.TelephonyManager; +import android.text.TextUtils; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -12,278 +22,285 @@ import java.util.HashMap; import java.util.Map; -import android.annotation.SuppressLint; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.DhcpInfo; -import android.net.NetworkInfo; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; -import android.telephony.TelephonyManager; -import android.text.TextUtils; - @SuppressLint("DefaultLocale") public class LDNetUtil { - public static final String OPEN_IP = "";// å¯pingçš„IPåœ°å€ - public static final String OPERATOR_URL = ""; + public static final String OPEN_IP = "";// å¯pingçš„IPåœ°å€ + public static final String OPERATOR_URL = ""; - public static final String NETWORKTYPE_INVALID = "UNKNOWN";// 没有网络 - public static final String NETWORKTYPE_WAP = "WAP"; // wap网络 - public static final String NETWORKTYPE_WIFI = "WIFI"; // wifi网络 + public static final String NETWORKTYPE_INVALID = "UNKNOWN";// 没有网络 + public static final String NETWORKTYPE_WAP = "WAP"; // wap网络 + public static final String NETWORKTYPE_WIFI = "WIFI"; // wifi网络 - @SuppressWarnings({ "deprecation" }) - public static String getNetWorkType(Context context) { - String mNetWorkType = null; - ConnectivityManager manager = (ConnectivityManager) context - .getSystemService(Context.CONNECTIVITY_SERVICE); - if (manager == null) { - return "ConnectivityManager not found"; - } - NetworkInfo networkInfo = manager.getActiveNetworkInfo(); - if (networkInfo != null && networkInfo.isConnected()) { - String type = networkInfo.getTypeName(); - if (type.equalsIgnoreCase("WIFI")) { - mNetWorkType = NETWORKTYPE_WIFI; - } else if (type.equalsIgnoreCase("MOBILE")) { - String proxyHost = android.net.Proxy.getDefaultHost(); - if (TextUtils.isEmpty(proxyHost)) { - mNetWorkType = mobileNetworkType(context); + @SuppressWarnings({"deprecation"}) + public static String getNetWorkType(Context context) { + String mNetWorkType = null; + ConnectivityManager manager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + if (manager == null) { + return "ConnectivityManager not found"; + } + NetworkInfo networkInfo = manager.getActiveNetworkInfo(); + if (networkInfo != null && networkInfo.isConnected()) { + String type = networkInfo.getTypeName(); + if ("WIFI".equalsIgnoreCase(type)) { + mNetWorkType = NETWORKTYPE_WIFI; + } else if ("MOBILE".equalsIgnoreCase(type)) { + String proxyHost = android.net.Proxy.getDefaultHost(); + if (TextUtils.isEmpty(proxyHost)) { + mNetWorkType = mobileNetworkType(context); + } else { + mNetWorkType = NETWORKTYPE_WAP; + } + } } else { - mNetWorkType = NETWORKTYPE_WAP; + mNetWorkType = NETWORKTYPE_INVALID; } - } - } else { - mNetWorkType = NETWORKTYPE_INVALID; + return mNetWorkType; } - return mNetWorkType; - } - /** - * 判断网络是å¦è¿žæŽ¥ - */ - public static Boolean isNetworkConnected(Context context) { - ConnectivityManager manager = (ConnectivityManager) context - .getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - if (manager == null) { - return false; - } - NetworkInfo networkinfo = manager.getActiveNetworkInfo(); - if (networkinfo == null || !networkinfo.isAvailable()) { - return false; + /** + * 判断网络是å¦è¿žæŽ¥ + */ + public static Boolean isNetworkConnected(Context context) { + ConnectivityManager manager = (ConnectivityManager) context + .getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); + if (manager == null) { + return false; + } + NetworkInfo networkinfo = manager.getActiveNetworkInfo(); + if (networkinfo == null || !networkinfo.isAvailable()) { + return false; + } + return true; } - return true; - } - public static String getMobileOperator(Context context) { - TelephonyManager telManager = (TelephonyManager) context - .getSystemService(Context.TELEPHONY_SERVICE); - if(telManager==null) - return "未知è¿è¥å•†"; - String operator = telManager.getSimOperator(); - if (operator != null) { - if (operator.equals("46000") || operator.equals("46002") - || operator.equals("46007")) { - return "中国移动"; - } else if (operator.equals("46001")) { - return "中国è”通"; - } else if (operator.equals("46003")) { - return "中国电信"; - } + /** + * 获å–è¿è¥å•† + * + * @param context + * @return + */ + public static String getMobileOperator(Context context) { + TelephonyManager telManager = (TelephonyManager) context.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); + if (telManager == null) { + return "未知è¿è¥å•†"; + } + // // 这个是以460开头 + // String imsi = telManager.getSubscriberId(); + // 这个是460xx + String operator = telManager.getSimOperator(); + // 中国移动46000ã€46002ã€46007 中国è”通46001ã€46006 中国电信46003ã€46005 + if (operator != null) { + // 46000 GSM 46002 TD-S 46007 TD-S + if ("46000".equals(operator) || "46002".equals(operator) || "46007".equals(operator)) { + return "中国移动"; + //46001 GSM 46006 WCDMA + } else if ("46001".equals(operator) || "46006".equals(operator)) { + return "中国è”通"; + //46011 FDD-LTE 46003 CDMA 46003 CDMA + } else if ("46003".equals(operator) || "46005".equals(operator) || "46011".equals(operator)) { + return "中国电信"; +// } else if ("46004".equals(operator)) { +// return "空è¿è¥å•†-测试"; + //中国动车和高é“的调度网,是移动的GSM-Ré“路专网,是网络管ç†å™¨ä¸“门为宽带无线接入的网络。 + } else if ("46020".equals(operator)) { + return "中国é“通"; + } + } + return "未知è¿è¥å•†"; } - return "未知è¿è¥å•†"; - } - /** - * èŽ·å–æœ¬æœºIP(wifi) - */ - public static String getLocalIpByWifi(Context context) { - WifiManager wifiManager = (WifiManager) context - .getSystemService(Context.WIFI_SERVICE); - if (wifiManager == null) { - return "wifiManager not found"; - } - WifiInfo wifiInfo = wifiManager.getConnectionInfo(); - if (wifiInfo == null) { - return "wifiInfo not found"; + /** + * èŽ·å–æœ¬æœºIP(wifi) + */ + public static String getLocalIpByWifi(Context context) { + WifiManager wifiManager = (WifiManager) context + .getSystemService(Context.WIFI_SERVICE); + if (wifiManager == null) { + return "wifiManager not found"; + } + WifiInfo wifiInfo = wifiManager.getConnectionInfo(); + if (wifiInfo == null) { + return "wifiInfo not found"; + } + int ipAddress = wifiInfo.getIpAddress(); + return String.format("%d.%d.%d.%d", (ipAddress & 0xff), + (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), + (ipAddress >> 24 & 0xff)); } - int ipAddress = wifiInfo.getIpAddress(); - return String.format("%d.%d.%d.%d", (ipAddress & 0xff), - (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), - (ipAddress >> 24 & 0xff)); - } - /** - * èŽ·å–æœ¬æœºIP(2G/3G/4G) - */ - public static String getLocalIpBy3G() { - try { - for (Enumeration en = NetworkInterface - .getNetworkInterfaces(); en.hasMoreElements();) { - NetworkInterface intf = en.nextElement(); - for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr - .hasMoreElements();) { - InetAddress inetAddress = enumIpAddr.nextElement(); - if (!inetAddress.isLoopbackAddress() - && inetAddress instanceof Inet4Address) { - // if (!inetAddress.isLoopbackAddress() && inetAddress - // instanceof Inet6Address) { - return inetAddress.getHostAddress().toString(); - } + /** + * èŽ·å–æœ¬æœºIP(2G/3G/4G) + */ + public static String getLocalIpBy3G() { + try { + for (Enumeration en = NetworkInterface + .getNetworkInterfaces(); en.hasMoreElements(); ) { + NetworkInterface intf = en.nextElement(); + for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr + .hasMoreElements(); ) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (!inetAddress.isLoopbackAddress() + && inetAddress instanceof Inet4Address) { + // if (!inetAddress.isLoopbackAddress() && inetAddress + // instanceof Inet6Address) { + return inetAddress.getHostAddress().toString(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); } - } - } catch (Exception e) { - e.printStackTrace(); + return null; } - return null; - } - /** - * wifi状æ€ä¸‹èŽ·å–网关 - */ - public static String pingGateWayInWifi(Context context) { - String gateWay = null; - WifiManager wifiManager = (WifiManager) context - .getSystemService(Context.WIFI_SERVICE); - if (wifiManager == null) { - return "wifiManager not found"; - } - DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); - if (dhcpInfo != null) { - int tmp = dhcpInfo.gateway; - gateWay = String.format("%d.%d.%d.%d", (tmp & 0xff), (tmp >> 8 & 0xff), - (tmp >> 16 & 0xff), (tmp >> 24 & 0xff)); + /** + * wifi状æ€ä¸‹èŽ·å–网关 + */ + public static String pingGateWayInWifi(Context context) { + String gateWay = null; + WifiManager wifiManager = (WifiManager) context + .getSystemService(Context.WIFI_SERVICE); + if (wifiManager == null) { + return "wifiManager not found"; + } + DhcpInfo dhcpInfo = wifiManager.getDhcpInfo(); + if (dhcpInfo != null) { + int tmp = dhcpInfo.gateway; + gateWay = String.format("%d.%d.%d.%d", (tmp & 0xff), (tmp >> 8 & 0xff), + (tmp >> 16 & 0xff), (tmp >> 24 & 0xff)); + } + return gateWay; } - return gateWay; - } - /** - * èŽ·å–æœ¬åœ°DNS - */ - public static String getLocalDns(String dns) { - Process process = null; - String str = ""; - BufferedReader reader = null; - try { - process = Runtime.getRuntime().exec("getprop net." + dns); - reader = new BufferedReader(new InputStreamReader( - process.getInputStream())); - String line = null; - while ((line = reader.readLine()) != null) { - str += line; - } - reader.close(); - process.waitFor(); - } catch (IOException e) { - e.printStackTrace(); - } catch (InterruptedException e) { - e.printStackTrace(); - } finally { - try { - if (reader != null) { - reader.close(); + /** + * èŽ·å–æœ¬åœ°DNS + */ + public static String getLocalDns(String dns) { + Process process = null; + String str = ""; + BufferedReader reader = null; + try { + process = Runtime.getRuntime().exec("getprop net." + dns); + reader = new BufferedReader(new InputStreamReader( + process.getInputStream())); + String line = null; + while ((line = reader.readLine()) != null) { + str += line; + } + reader.close(); + process.waitFor(); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + if (reader != null) { + reader.close(); + } + process.destroy(); + } catch (Exception e) { + } } - process.destroy(); - } catch (Exception e) { - } + return str.trim(); } - return str.trim(); - } - /** - * 域åè§£æž - */ - public static Map getDomainIp(String _dormain) { - Map map = new HashMap(); - long start = 0; - long end = 0; - String time = null; - InetAddress[] remoteInet = null; - try { - start = System.currentTimeMillis(); - remoteInet = InetAddress.getAllByName(_dormain); - if (remoteInet != null) { - end = System.currentTimeMillis(); - time = (end - start) + ""; - } - } catch (UnknownHostException e) { - end = System.currentTimeMillis(); - time = (end - start) + ""; - remoteInet = null; - e.printStackTrace(); - } finally { - map.put("remoteInet", remoteInet); - map.put("useTime", time); + /** + * 域åè§£æž + */ + public static Map getDomainIp(String dormain) { + Map map = new HashMap(); + long start = 0; + long end = 0; + String time = null; + InetAddress[] remoteInet = null; + try { + start = System.currentTimeMillis(); + remoteInet = InetAddress.getAllByName(dormain); + if (remoteInet != null) { + end = System.currentTimeMillis(); + time = (end - start) + ""; + } + } catch (UnknownHostException e) { + end = System.currentTimeMillis(); + time = (end - start) + ""; + remoteInet = null; + e.printStackTrace(); + } finally { + map.put("remoteInet", remoteInet); + map.put("useTime", time); + } + return map; } - return map; - } - private static String mobileNetworkType(Context context) { - TelephonyManager telephonyManager = (TelephonyManager) context - .getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager == null) { - return "TM==null"; - } - switch (telephonyManager.getNetworkType()) { - case TelephonyManager.NETWORK_TYPE_1xRTT:// ~ 50-100 kbps - return "2G"; - case TelephonyManager.NETWORK_TYPE_CDMA:// ~ 14-64 kbps - return "2G"; - case TelephonyManager.NETWORK_TYPE_EDGE:// ~ 50-100 kbps - return "2G"; - case TelephonyManager.NETWORK_TYPE_EVDO_0:// ~ 400-1000 kbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_EVDO_A:// ~ 600-1400 kbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_GPRS:// ~ 100 kbps - return "2G"; - case TelephonyManager.NETWORK_TYPE_HSDPA:// ~ 2-14 Mbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_HSPA:// ~ 700-1700 kbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_HSUPA: // ~ 1-23 Mbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_UMTS:// ~ 400-7000 kbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_EHRPD:// ~ 1-2 Mbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_EVDO_B: // ~ 5 Mbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_HSPAP:// ~ 10-20 Mbps - return "3G"; - case TelephonyManager.NETWORK_TYPE_IDEN:// ~25 kbps - return "2G"; - case TelephonyManager.NETWORK_TYPE_LTE:// ~ 10+ Mbps - return "4G"; - case TelephonyManager.NETWORK_TYPE_UNKNOWN: - return "UNKNOWN"; - default: - return "4G"; + private static String mobileNetworkType(Context context) { + TelephonyManager telephonyManager = (TelephonyManager) context + .getSystemService(Context.TELEPHONY_SERVICE); + if (telephonyManager == null) { + return "TM==null"; + } + switch (telephonyManager.getNetworkType()) { + case TelephonyManager.NETWORK_TYPE_1xRTT:// ~ 50-100 kbps + return "2G"; + case TelephonyManager.NETWORK_TYPE_CDMA:// ~ 14-64 kbps + return "2G"; + case TelephonyManager.NETWORK_TYPE_EDGE:// ~ 50-100 kbps + return "2G"; + case TelephonyManager.NETWORK_TYPE_EVDO_0:// ~ 400-1000 kbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_EVDO_A:// ~ 600-1400 kbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_GPRS:// ~ 100 kbps + return "2G"; + case TelephonyManager.NETWORK_TYPE_HSDPA:// ~ 2-14 Mbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_HSPA:// ~ 700-1700 kbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_HSUPA: // ~ 1-23 Mbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_UMTS:// ~ 400-7000 kbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_EHRPD:// ~ 1-2 Mbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_EVDO_B: // ~ 5 Mbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_HSPAP:// ~ 10-20 Mbps + return "3G"; + case TelephonyManager.NETWORK_TYPE_IDEN:// ~25 kbps + return "2G"; + case TelephonyManager.NETWORK_TYPE_LTE:// ~ 10+ Mbps + return "4G"; + case TelephonyManager.NETWORK_TYPE_UNKNOWN: + return "UNKNOWN"; + default: + return "4G"; + } } - } - /** - * 输入æµè½¬å˜æˆå­—符串 - */ - public static String getStringFromStream(InputStream is) { - byte[] bytes = new byte[1024]; - int len = 0; - String res = ""; - try { - while ((len = is.read(bytes)) != -1) { - res = res + new String(bytes, 0, len, "gbk"); - } - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (is != null) { + /** + * 输入æµè½¬å˜æˆå­—符串 + */ + public static String getStringFromStream(InputStream is) { + byte[] bytes = new byte[1024]; + int len = 0; + String res = ""; try { - is.close(); + while ((len = is.read(bytes)) != -1) { + res = res + new String(bytes, 0, len, "gbk"); + } } catch (IOException e) { - e.printStackTrace(); + e.printStackTrace(); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } - } + return res; } - return res; - } } diff --git a/app/src/main/java/com/netease/LDNetDiagnoUtils/LDPingParse.java b/app/src/main/java/com/netease/LDNetDiagnoUtils/LDPingParse.java index f4de482..244f4a4 100644 --- a/app/src/main/java/com/netease/LDNetDiagnoUtils/LDPingParse.java +++ b/app/src/main/java/com/netease/LDNetDiagnoUtils/LDPingParse.java @@ -6,92 +6,92 @@ import java.util.regex.Pattern; public class LDPingParse { - public static String getFormattingStr(String host, String log) { - StringBuilder logRes = new StringBuilder(); - if (log.contains("timeout")) { - logRes.append("ping: cannot resolve " + host + ": Timeout"); - } else if (log.contains("unknown")) { - logRes.append("ping: cannot resolve " + host + ": Unknown host"); - } else { - makePingResponse(log, logRes); + public static String getFormattingStr(String host, String log) { + StringBuilder logRes = new StringBuilder(); + if (log.contains("timeout")) { + logRes.append("ping: cannot resolve " + host + ": Timeout"); + } else if (log.contains("unknown")) { + logRes.append("ping: cannot resolve " + host + ": Unknown host"); + } else { + makePingResponse(log, logRes); + } + return logRes.toString(); } - return logRes.toString(); - } - public static void makePingResponse(String log, StringBuilder logRes) { - String hostIp = getIP(log); - List bytesList = getSumBytes(log); - List ttlList = getTTL(log); - List timeList = getTime(log); - List icmpList = getIcmp_seq(log); - int len = timeList.size(); - for (int i = 0; i < len - 1; i++) { - logRes.append(bytesList.get(i) + "bytes from " + hostIp + ": icmp_seq=#" - + icmpList.get(i) + " ttl=" + ttlList.get(i) + " time=" - + timeList.get(i) + "ms" + "\n"); + public static void makePingResponse(String log, StringBuilder logRes) { + String hostIp = getIP(log); + List bytesList = getSumBytes(log); + List ttlList = getTTL(log); + List timeList = getTime(log); + List icmpList = getIcmp_seq(log); + int len = timeList.size(); + for (int i = 0; i < len - 1; i++) { + logRes.append(bytesList.get(i) + "bytes from " + hostIp + ": icmp_seq=#" + + icmpList.get(i) + " ttl=" + ttlList.get(i) + " time=" + + timeList.get(i) + "ms" + "\n"); + } + logRes.append(bytesList.get(len - 1) + "bytes from " + hostIp + + ": icmp_seq=#" + icmpList.get(len - 1) + " ttl=" + + ttlList.get(len - 1) + " time=" + timeList.get(len - 1) + "ms"); } - logRes.append(bytesList.get(len - 1) + "bytes from " + hostIp - + ": icmp_seq=#" + icmpList.get(len - 1) + " ttl=" - + ttlList.get(len - 1) + " time=" + timeList.get(len - 1) + "ms"); - } - private static List getTime(String log) { - List timeList = new ArrayList(); - String regex = "(?<==)([\\.0-9\\s]+)(?=ms)"; - Pattern p = Pattern.compile(regex); - Matcher m = p.matcher(log); - while (m.find()) { - timeList.add(m.group().toString().trim()); + private static List getTime(String log) { + List timeList = new ArrayList(); + String regex = "(?<==)([\\.0-9\\s]+)(?=ms)"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(log); + while (m.find()) { + timeList.add(m.group().toString().trim()); + } + return timeList; } - return timeList; - } - private static List getSumBytes(String log) { - List bytesList = new ArrayList(); - String regex = "(?<=\\D)([\\s0-9]+)(?=bytes)"; - Pattern p = Pattern.compile(regex); - Matcher m = p.matcher(log); - while (m.find()) { - String string = m.group().toString().trim(); - if (m.group().toString().trim().matches("\\d+")) { - bytesList.add(string); - } + private static List getSumBytes(String log) { + List bytesList = new ArrayList(); + String regex = "(?<=\\D)([\\s0-9]+)(?=bytes)"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(log); + while (m.find()) { + String string = m.group().toString().trim(); + if (m.group().toString().trim().matches("\\d+")) { + bytesList.add(string); + } + } + return bytesList; } - return bytesList; - } - private static List getTTL(String log) { - List ttlList = new ArrayList(); - String regex = "(?<=ttl=)([0-9]+)(?=\\s)"; - Pattern p = Pattern.compile(regex); - Matcher m = p.matcher(log); - while (m.find()) { - String tmp = m.group().toString().trim(); - ttlList.add(tmp); + private static List getTTL(String log) { + List ttlList = new ArrayList(); + String regex = "(?<=ttl=)([0-9]+)(?=\\s)"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(log); + while (m.find()) { + String tmp = m.group().toString().trim(); + ttlList.add(tmp); + } + return ttlList; } - return ttlList; - } - private static String getIP(String log) { - String hostIp = null; - String regex = "(?<=\\()([\\d]+\\.)+[\\d]+(?=\\))"; - Pattern p = Pattern.compile(regex); - Matcher m = p.matcher(log); - while (m.find()) { - hostIp = m.group().toString().trim(); + private static String getIP(String log) { + String hostIp = null; + String regex = "(?<=\\()([\\d]+\\.)+[\\d]+(?=\\))"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(log); + while (m.find()) { + hostIp = m.group().toString().trim(); + } + return hostIp; } - return hostIp; - } - private static List getIcmp_seq(String log) { - List icmpList = new ArrayList(); - String regex = "(?<=icmp_seq=)([0-9]+)(?=\\s)"; - Pattern p = Pattern.compile(regex); - Matcher m = p.matcher(log); - while (m.find()) { - String tmp = m.group().toString().trim(); - icmpList.add(tmp); + private static List getIcmp_seq(String log) { + List icmpList = new ArrayList(); + String regex = "(?<=icmp_seq=)([0-9]+)(?=\\s)"; + Pattern p = Pattern.compile(regex); + Matcher m = p.matcher(log); + while (m.find()) { + String tmp = m.group().toString().trim(); + icmpList.add(tmp); + } + return icmpList; } - return icmpList; - } } diff --git a/app/src/main/java/net/lightbody/bmp/BrowserMobProxy.java b/app/src/main/java/net/lightbody/bmp/BrowserMobProxy.java index 28a9d80..3286b4e 100644 --- a/app/src/main/java/net/lightbody/bmp/BrowserMobProxy.java +++ b/app/src/main/java/net/lightbody/bmp/BrowserMobProxy.java @@ -8,6 +8,7 @@ import net.lightbody.bmp.proxy.CaptureType; import net.lightbody.bmp.proxy.auth.AuthType; import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; + import org.littleshoot.proxy.HttpFiltersSource; import org.littleshoot.proxy.MitmManager; @@ -39,7 +40,7 @@ public interface BrowserMobProxy { * Starts the proxy on the specified port. The proxy will listen for connections on the network interface specified by the bindAddress, and will * also initiate connections to upstream servers on the same network interface. * - * @param port port to listen on + * @param port port to listen on * @param bindAddress address of the network interface on which the proxy will listen for connections and also attempt to connect to upstream servers. * @throws java.lang.IllegalStateException if the proxy has already been started */ @@ -49,7 +50,7 @@ public interface BrowserMobProxy { * Starts the proxy on the specified port. The proxy will listen for connections on the network interface specified by the clientBindAddress, and will * initiate connections to upstream servers from the network interface specified by the serverBindAddress. * - * @param port port to listen on + * @param port port to listen on * @param clientBindAddress address of the network interface on which the proxy will listen for connections * @param serverBindAddress address of the network interface on which the proxy will connect to upstream servers * @throws java.lang.IllegalStateException if the proxy has already been started @@ -68,7 +69,7 @@ public interface BrowserMobProxy { * @throws java.lang.IllegalStateException if the proxy has not been started. */ void stop(); - + /** * Like {@link #stop()}, shuts down the proxy server and no longer accepts incoming connections, but does not wait for any existing * network traffic to cease. Any existing connections to clients or to servers may be force-killed immediately. @@ -129,12 +130,17 @@ public interface BrowserMobProxy { /** * Starts a new HAR file with the specified page name and page title. Enables HAR capture if it was not previously enabled. * - * @param initialPageRef initial page name of the new HAR file + * @param initialPageRef initial page name of the new HAR file * @param initialPageTitle initial page title of the new HAR file * @return existing HAR file, or null if none exists or HAR capture was disabled */ Har newHar(String initialPageRef, String initialPageTitle); + /** + * @return A copy of HAR capture types currently in effect. The EnumSet cannot be used to modify the HAR capture types currently in effect. + */ + EnumSet getHarCaptureTypes(); + /** * Sets the data types that will be captured in the HAR file for future requests. Replaces any existing capture types with the specified * capture types. A null or empty set will not disable HAR capture, but will disable collection of @@ -161,11 +167,6 @@ public interface BrowserMobProxy { */ void setHarCaptureTypes(CaptureType... captureTypes); - /** - * @return A copy of HAR capture types currently in effect. The EnumSet cannot be used to modify the HAR capture types currently in effect. - */ - EnumSet getHarCaptureTypes(); - /** * Enables the specified HAR capture types. Does not replace or disable any other capture types that may already be enabled. * @@ -219,7 +220,7 @@ public interface BrowserMobProxy { * Starts a new HAR page using the specified pageRef as the page name and the pageTitle as the page title. Populates the * {@link net.lightbody.bmp.core.har.HarPageTimings#onLoad} value based on the amount of time the current page has been captured. * - * @param pageRef name of the new page + * @param pageRef name of the new page * @param pageTitle title of the new page * @return the HAR as it existed immediately after ending the current page * @throws java.lang.IllegalStateException if HAR capture has not been enabled via {@link #newHar()} or {@link #newHar(String)} @@ -234,6 +235,11 @@ public interface BrowserMobProxy { */ Har endHar(); + /** + * Returns the current bandwidth limit for reading, in bytes per second. + */ + long getReadBandwidthLimit(); + /** * Sets the maximum bandwidth to consume when reading server responses. * @@ -242,9 +248,9 @@ public interface BrowserMobProxy { void setReadBandwidthLimit(long bytesPerSecond); /** - * Returns the current bandwidth limit for reading, in bytes per second. + * Returns the current bandwidth limit for writing, in bytes per second. */ - long getReadBandwidthLimit(); + long getWriteBandwidthLimit(); /** * Sets the maximum bandwidth to consume when sending requests to servers. @@ -253,16 +259,11 @@ public interface BrowserMobProxy { */ void setWriteBandwidthLimit(long bytesPerSecond); - /** - * Returns the current bandwidth limit for writing, in bytes per second. - */ - long getWriteBandwidthLimit(); - /** * The minimum amount of time that will elapse between the time the proxy begins receiving a response from the server and the time the * proxy begins sending the response to the client. * - * @param latency minimum latency, or 0 for no minimum + * @param latency minimum latency, or 0 for no minimum * @param timeUnit TimeUnit for the latency */ void setLatency(long latency, TimeUnit timeUnit); @@ -272,7 +273,7 @@ public interface BrowserMobProxy { * specified time, the proxy will respond with an HTTP 502 Bad Gateway. The default value is 60 seconds. * * @param connectionTimeout maximum time to wait to establish a connection to a server, or 0 to wait indefinitely - * @param timeUnit TimeUnit for the connectionTimeout + * @param timeUnit TimeUnit for the connectionTimeout */ void setConnectTimeout(int connectionTimeout, TimeUnit timeUnit); @@ -283,7 +284,7 @@ public interface BrowserMobProxy { * connection to the client may be closed abruptly. The default value is 60 seconds. * * @param idleConnectionTimeout maximum time to allow a connection to remain idle, or 0 to wait indefinitely. - * @param timeUnit TimeUnit for the idleConnectionTimeout + * @param timeUnit TimeUnit for the idleConnectionTimeout */ void setIdleConnectionTimeout(int idleConnectionTimeout, TimeUnit timeUnit); @@ -294,7 +295,7 @@ public interface BrowserMobProxy { * connection to the client may be closed abruptly. The default value is 0 (wait indefinitely). * * @param requestTimeout maximum time to wait for an HTTP response, or 0 to wait indefinitely - * @param timeUnit TimeUnit for the requestTimeout + * @param timeUnit TimeUnit for the requestTimeout */ void setRequestTimeout(int requestTimeout, TimeUnit timeUnit); @@ -302,7 +303,7 @@ public interface BrowserMobProxy { * Enables automatic authorization for the specified domain and auth type. Every request sent to the specified domain will contain the * specified authorization information. * - * @param domain domain automatically send authorization information to + * @param domain domain automatically send authorization information to * @param username authorization username * @param password authorization password * @param authType authorization type @@ -344,7 +345,7 @@ public interface BrowserMobProxy { * For example, the following rewrite rule: * *

   {@code proxy.rewriteUrl("http://www\\.(yahoo|bing)\\.com/\\?(\\w+)=(\\w+)", "http://www.google.com/?originalDomain=$1&$2=$3");}
- * + *

* will match an HTTP request (but not HTTPS!) to www.yahoo.com or www.bing.com with exactly 1 query parameter, * and replace it with a call to www.google.com with an 'originalDomain' query parameter, as well as the original query parameter. *

@@ -357,7 +358,7 @@ public interface BrowserMobProxy { * will result in the proxy making a request to: *

   {@code http://www.google.com?originalDomain=bing&anotherParam=anotherValue}
* - * @param urlPattern URL-matching regular expression + * @param urlPattern URL-matching regular expression * @param replacementExpression an expression, which may optionally contain capture groups, which will replace any URL which matches urlPattern */ void rewriteUrl(String urlPattern, String replacementExpression); @@ -414,20 +415,12 @@ public interface BrowserMobProxy { *

* See {@link #blacklistRequests(String, int)} for details on the URL the urlPattern will match. * - * @param urlPattern URL-matching regular expression to blacklist - * @param statusCode HTTP status code to return + * @param urlPattern URL-matching regular expression to blacklist + * @param statusCode HTTP status code to return * @param httpMethodPattern regular expression matching a request's HTTP method */ void blacklistRequests(String urlPattern, int statusCode, String httpMethodPattern); - /** - * Replaces any existing blacklist with the specified blacklist. URLs will be evaluated against the blacklist in the order - * specified by the Collection's iterator. - * - * @param blacklist new blacklist entries - */ - void setBlacklist(Collection blacklist); - /** * Returns all blacklist entries currently in effect. Iterating over the returned Collection is guaranteed to return * blacklist entries in the order in which URLs are actually evaluated against the blacklist. @@ -436,6 +429,14 @@ public interface BrowserMobProxy { */ Collection getBlacklist(); + /** + * Replaces any existing blacklist with the specified blacklist. URLs will be evaluated against the blacklist in the order + * specified by the Collection's iterator. + * + * @param blacklist new blacklist entries + */ + void setBlacklist(Collection blacklist); + /** * Clears any existing blacklist. */ @@ -451,7 +452,7 @@ public interface BrowserMobProxy { * whitelist response code. * * @param urlPatterns URL-matching regular expressions to whitelist; null or an empty collection will enable an empty whitelist - * @param statusCode HTTP status code to return to clients when a URL matches a pattern + * @param statusCode HTTP status code to return to clients when a URL matches a pattern */ void whitelistRequests(Collection urlPatterns, int statusCode); @@ -505,7 +506,7 @@ public interface BrowserMobProxy { /** * Adds a new HTTP header to every request. If the header already exists on the request, it will be replaced with the specified header. * - * @param name name of the header to add + * @param name name of the header to add * @param value new header's value */ void addHeader(String name, String value); @@ -530,31 +531,38 @@ public interface BrowserMobProxy { Map getAllHeaders(); /** - * Sets the resolver that will be used to look up host names. To chain multiple resolvers, wrap a list - * of resolvers in a {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver}. + * Returns the current host name resolver. * - * @param resolver host name resolver + * @return current host name resolver */ - void setHostNameResolver(AdvancedHostResolver resolver); + AdvancedHostResolver getHostNameResolver(); /** - * Returns the current host name resolver. + * Sets the resolver that will be used to look up host names. To chain multiple resolvers, wrap a list + * of resolvers in a {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver}. * - * @return current host name resolver + * @param resolver host name resolver */ - AdvancedHostResolver getHostNameResolver(); + void setHostNameResolver(AdvancedHostResolver resolver); /** * Waits for existing network traffic to stop, and for the specified quietPeriod to elapse. Returns true if there is no network traffic * for the quiet period within the specified timeout, otherwise returns false. * * @param quietPeriod amount of time after which network traffic will be considered "stopped" - * @param timeout maximum amount of time to wait for network traffic to stop - * @param timeUnit TimeUnit for the quietPeriod and timeout + * @param timeout maximum amount of time to wait for network traffic to stop + * @param timeUnit TimeUnit for the quietPeriod and timeout * @return true if network traffic is stopped, otherwise false */ boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUnit); + /** + * Returns the address and port of the upstream proxy. + * + * @return address and port of the upstream proxy, or null of there is none. + */ + InetSocketAddress getChainedProxy(); + /** * Instructs this proxy to route traffic through an upstream proxy. * @@ -564,13 +572,6 @@ public interface BrowserMobProxy { */ void setChainedProxy(InetSocketAddress chainedProxyAddress); - /** - * Returns the address and port of the upstream proxy. - * - * @return address and port of the upstream proxy, or null of there is none. - */ - InetSocketAddress getChainedProxy(); - /** * Adds a new filter factory (request/response interceptor) to the beginning of the HttpFilters chain. *

@@ -591,7 +592,7 @@ public interface BrowserMobProxy { * {@link org.littleshoot.proxy.HttpFilters} instance (typically, a subclass of {@link org.littleshoot.proxy.HttpFiltersAdapter}). * To disable or bypass a filter on a per-request basis, the filterRequest() method may return null. * - * @param filterFactory factory to generate HttpFilters + * @param filterFactory factory to generate HttpFilters */ void addLastHttpFilterFactory(HttpFiltersSource filterFactory); diff --git a/app/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java b/app/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java index f539a17..6759d1e 100644 --- a/app/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java +++ b/app/src/main/java/net/lightbody/bmp/BrowserMobProxyServer.java @@ -3,10 +3,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.MapMaker; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; + import net.lightbody.bmp.client.ClientUtil; import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarLog; @@ -31,11 +28,7 @@ import net.lightbody.bmp.filters.RewriteUrlFilter; import net.lightbody.bmp.filters.UnregisterRequestFilter; import net.lightbody.bmp.filters.WhitelistFilter; -import net.lightbody.bmp.mitm.KeyStoreFileCertificateSource; import net.lightbody.bmp.mitm.TrustSource; -import net.lightbody.bmp.mitm.keys.ECKeyGenerator; -import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; -import net.lightbody.bmp.mitm.manager.ImpersonatingMitmManager; import net.lightbody.bmp.proxy.ActivityMonitor; import net.lightbody.bmp.proxy.BlacklistEntry; import net.lightbody.bmp.proxy.CaptureType; @@ -46,6 +39,7 @@ import net.lightbody.bmp.proxy.dns.DelegatingHostResolver; import net.lightbody.bmp.util.BrowserMobHttpUtil; import net.lightbody.bmp.util.BrowserMobProxyUtil; + import org.littleshoot.proxy.ChainedProxy; import org.littleshoot.proxy.ChainedProxyAdapter; import org.littleshoot.proxy.ChainedProxyManager; @@ -69,7 +63,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.EnumSet; import java.util.List; import java.util.Map; @@ -83,26 +76,27 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Pattern; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; + /** * A LittleProxy-based implementation of {@link net.lightbody.bmp.BrowserMobProxy}. */ public class BrowserMobProxyServer implements BrowserMobProxy { + /** + * The default pseudonym to use when adding the Via header to proxied requests. + */ + public static final String VIA_HEADER_ALIAS = "browsermobproxy"; private static final Logger log = LoggerFactory.getLogger(BrowserMobProxyServer.class); - private static final HarNameVersion HAR_CREATOR_VERSION = new HarNameVersion("BrowserMob Proxy", BrowserMobProxyUtil.getVersionString()); - /* Default MITM resources */ private static final String RSA_KEYSTORE_RESOURCE = "/sslSupport/ca-keystore-rsa.p12"; private static final String EC_KEYSTORE_RESOURCE = "/sslSupport/ca-keystore-ec.p12"; private static final String KEYSTORE_TYPE = "PKCS12"; private static final String KEYSTORE_PRIVATE_KEY_ALIAS = "key"; private static final String KEYSTORE_PASSWORD = "password"; - - /** - * The default pseudonym to use when adding the Via header to proxied requests. - */ - public static final String VIA_HEADER_ALIAS = "browsermobproxy"; - /** * True only after the proxy has been successfully started. */ @@ -117,42 +111,64 @@ public class BrowserMobProxyServer implements BrowserMobProxy { * Tracks the current page count, for use when auto-generating HAR page names. */ private final AtomicInteger harPageCount = new AtomicInteger(0); - + /** + * The list of filterFactories that will generate the filters that implement browsermob-proxy behavior. + */ + private final List filterFactories = new CopyOnWriteArrayList<>(); + /** + * List of accepted URL patterns. Unlisted URL patterns will be rejected with the response code contained in the Whitelist. + */ + private final AtomicReference whitelist = new AtomicReference<>(Whitelist.WHITELIST_DISABLED); + /** + * Set to true once the HAR capture filter has been added to the filter chain. + */ + private final AtomicBoolean harCaptureFilterEnabled = new AtomicBoolean(false); + /** + * Set to true when LittleProxy has been bootstrapped with the default chained proxy. This allows modifying the chained proxy + * after the proxy has been started. + */ + private final AtomicBoolean bootstrappedWithDefaultChainedProxy = new AtomicBoolean(false); + /** + * Resolver to use when resolving hostnames to IP addresses. This is a bridge between {@link org.littleshoot.proxy.HostResolver} and + * {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver}. It allows the resolvers to be changed on-the-fly without re-bootstrapping the + * littleproxy server. The default resolver (native JDK resolver) can be changed using {@link #setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)} and + * supplying one of the pre-defined resolvers in {@link ClientUtil}, such as {@link ClientUtil#createDnsJavaWithNativeFallbackResolver()} + * or {@link ClientUtil#createDnsJavaResolver()}. You can also build your own resolver, or use {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver} + * to chain together multiple DNS resolvers. + */ + private final DelegatingHostResolver delegatingResolver = new DelegatingHostResolver(ClientUtil.createNativeCacheManipulatingResolver()); + private final ActivityMonitor activityMonitor = new ActivityMonitor(); + /** + * A mapping of hostnames to base64-encoded Basic auth credentials that will be added to the Authorization header for + * matching requests. + */ + private final ConcurrentMap basicAuthCredentials = new MapMaker() + .concurrencyLevel(1) + .makeMap(); /** * When true, MITM will be disabled. The proxy will no longer intercept HTTPS requests, but they will still be proxied. */ private volatile boolean mitmDisabled = false; - /** * The MITM manager that will be used for HTTPS requests. */ private volatile MitmManager mitmManager; - - /** - * The list of filterFactories that will generate the filters that implement browsermob-proxy behavior. - */ - private final List filterFactories = new CopyOnWriteArrayList<>(); - /** * List of rejected URL patterns */ private volatile Collection blacklistEntries = new CopyOnWriteArrayList<>(); - /** * List of URLs to rewrite */ private volatile CopyOnWriteArrayList rewriteRules = new CopyOnWriteArrayList<>(); - /** * The LittleProxy instance that performs all proxy operations. */ private volatile HttpProxyServer proxyServer; - /** * No capture types are enabled by default. */ private volatile EnumSet harCaptureTypes = EnumSet.noneOf(CaptureType.class); - /** * The current HAR being captured. */ @@ -169,94 +185,48 @@ public class BrowserMobProxyServer implements BrowserMobProxy { * Maximum bandwidth to consume when writing requests to servers. */ private volatile long writeBandwidthLimitBps; - /** - * List of accepted URL patterns. Unlisted URL patterns will be rejected with the response code contained in the Whitelist. - */ - private final AtomicReference whitelist = new AtomicReference<>(Whitelist.WHITELIST_DISABLED); - /** * Additional headers that will be sent with every request. The map is declared as a ConcurrentMap to indicate that writes may be performed * by other threads concurrently (e.g. due to an incoming REST call), but the concurrencyLevel is set to 1 because modifications to the * additionalHeaders are rare, and in most cases happen only once, at start-up. */ private volatile ConcurrentMap additionalHeaders = new MapMaker().concurrencyLevel(1).makeMap(); - /** * The amount of time to wait while connecting to a server. */ private volatile int connectTimeoutMs; - /** * The amount of time a connection to a server can remain idle while receiving data from the server. */ private volatile int idleConnectionTimeoutSec; - /** * The amount of time to wait before forwarding the response to the client. */ private volatile int latencyMs; - - /** - * Set to true once the HAR capture filter has been added to the filter chain. - */ - private final AtomicBoolean harCaptureFilterEnabled = new AtomicBoolean(false); - - /** - * Set to true when LittleProxy has been bootstrapped with the default chained proxy. This allows modifying the chained proxy - * after the proxy has been started. - */ - private final AtomicBoolean bootstrappedWithDefaultChainedProxy = new AtomicBoolean(false); - /** * The address of an upstream chained proxy to route traffic through. */ private volatile InetSocketAddress upstreamProxyAddress; - /** * The chained proxy manager that manages upstream proxies. */ private volatile ChainedProxyManager chainedProxyManager; - /** * The address of the network interface from which the proxy will initiate connections. */ private volatile InetAddress serverBindAddress; - /** * The TrustSource that will be used to validate servers' certificates. If null, will not validate server certificates. */ private volatile TrustSource trustSource = TrustSource.defaultTrustSource(); - /** * When true, use Elliptic Curve keys and certificates when impersonating upstream servers. */ private volatile boolean useEcc = false; - - /** - * Resolver to use when resolving hostnames to IP addresses. This is a bridge between {@link org.littleshoot.proxy.HostResolver} and - * {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver}. It allows the resolvers to be changed on-the-fly without re-bootstrapping the - * littleproxy server. The default resolver (native JDK resolver) can be changed using {@link #setHostNameResolver(net.lightbody.bmp.proxy.dns.AdvancedHostResolver)} and - * supplying one of the pre-defined resolvers in {@link ClientUtil}, such as {@link ClientUtil#createDnsJavaWithNativeFallbackResolver()} - * or {@link ClientUtil#createDnsJavaResolver()}. You can also build your own resolver, or use {@link net.lightbody.bmp.proxy.dns.ChainedHostResolver} - * to chain together multiple DNS resolvers. - */ - private final DelegatingHostResolver delegatingResolver = new DelegatingHostResolver(ClientUtil.createNativeCacheManipulatingResolver()); - - private final ActivityMonitor activityMonitor = new ActivityMonitor(); - /** * The acceptor and worker thread configuration for the Netty thread pools. */ private volatile ThreadPoolConfiguration threadPoolConfiguration; - - /** - * A mapping of hostnames to base64-encoded Basic auth credentials that will be added to the Authorization header for - * matching requests. - */ - private final ConcurrentMap basicAuthCredentials = new MapMaker() - .concurrencyLevel(1) - .makeMap(); - /** * Base64-encoded credentials to use to authenticate with the upstream proxy. */ @@ -331,7 +301,7 @@ public int getMaximumResponseBufferSizeInBytes() { try { bootstrap.withManInTheMiddle(new CertificateSniffingMitmManager( new Authority())); - }catch (Exception e){ + } catch (Exception e) { e.printStackTrace(); } } @@ -363,7 +333,7 @@ public void filterRequest(HttpObject httpObject) { String chainedProxyAuth = chainedProxyCredentials; if (chainedProxyAuth != null) { if (httpObject instanceof HttpRequest) { - HttpHeaders.addHeader((HttpRequest)httpObject, HttpHeaders.Names.PROXY_AUTHORIZATION, "Basic " + chainedProxyAuth); + HttpHeaders.addHeader((HttpRequest) httpObject, HttpHeaders.Names.PROXY_AUTHORIZATION, "Basic " + chainedProxyAuth); } } } @@ -487,13 +457,18 @@ public Har newHar(String initialPageRef, String initialPageTitle) { harPageCount.set(0); - this.har = new Har(new HarLog(HAR_CREATOR_VERSION,this)); + this.har = new Har(new HarLog(HAR_CREATOR_VERSION, this)); newPage(initialPageRef, initialPageTitle); return oldHar; } + @Override + public EnumSet getHarCaptureTypes() { + return EnumSet.copyOf(harCaptureTypes); + } + @Override public void setHarCaptureTypes(Set harCaptureSettings) { if (harCaptureSettings == null || harCaptureSettings.isEmpty()) { @@ -512,11 +487,6 @@ public void setHarCaptureTypes(CaptureType... captureTypes) { } } - @Override - public EnumSet getHarCaptureTypes() { - return EnumSet.copyOf(harCaptureTypes); - } - @Override public void enableHarCaptureTypes(Set captureTypes) { harCaptureTypes.addAll(captureTypes); @@ -602,6 +572,11 @@ public Har endHar() { return oldHar; } + @Override + public long getReadBandwidthLimit() { + return readBandwidthLimitBps; + } + @Override public void setReadBandwidthLimit(long bytesPerSecond) { this.readBandwidthLimitBps = bytesPerSecond; @@ -612,8 +587,8 @@ public void setReadBandwidthLimit(long bytesPerSecond) { } @Override - public long getReadBandwidthLimit() { - return readBandwidthLimitBps; + public long getWriteBandwidthLimit() { + return writeBandwidthLimitBps; } @Override @@ -625,11 +600,6 @@ public void setWriteBandwidthLimit(long bytesPerSecond) { } } - @Override - public long getWriteBandwidthLimit() { - return writeBandwidthLimitBps; - } - public void endPage() { if (har == null) { throw new IllegalStateException("No HAR exists for this proxy. Use newHar() to create a new HAR."); @@ -642,7 +612,7 @@ public void endPage() { return; } - previousPage.getPageTimings().setOnLoad(new Date().getTime() - previousPage.getStartedDateTime().getTime()); + previousPage.getPageTimings().setOnLoad(System.currentTimeMillis() - previousPage.getStartedDateTime().getTime()); } @Override @@ -758,13 +728,13 @@ public void blacklistRequests(String pattern, int responseCode, String method) { } @Override - public void setBlacklist(Collection blacklist) { - this.blacklistEntries = new CopyOnWriteArrayList<>(blacklist); + public Collection getBlacklist() { + return Collections.unmodifiableCollection(blacklistEntries); } @Override - public Collection getBlacklist() { - return Collections.unmodifiableCollection(blacklistEntries); + public void setBlacklist(Collection blacklist) { + this.blacklistEntries = new CopyOnWriteArrayList<>(blacklist); } @Override @@ -872,13 +842,13 @@ public Map getAllHeaders() { } @Override - public void setHostNameResolver(AdvancedHostResolver resolver) { - delegatingResolver.setResolver(resolver); + public AdvancedHostResolver getHostNameResolver() { + return delegatingResolver.getResolver(); } @Override - public AdvancedHostResolver getHostNameResolver() { - return delegatingResolver.getResolver(); + public void setHostNameResolver(AdvancedHostResolver resolver) { + delegatingResolver.setResolver(resolver); } @Override @@ -886,6 +856,11 @@ public boolean waitForQuiescence(long quietPeriod, long timeout, TimeUnit timeUn return activityMonitor.waitForQuiescence(quietPeriod, timeout, timeUnit); } + @Override + public InetSocketAddress getChainedProxy() { + return upstreamProxyAddress; + } + /** * Instructs this proxy to route traffic through an upstream proxy. * @@ -903,11 +878,6 @@ public void setChainedProxy(InetSocketAddress chainedProxyAddress) { upstreamProxyAddress = chainedProxyAddress; } - @Override - public InetSocketAddress getChainedProxy() { - return upstreamProxyAddress; - } - /** * Allows access to the LittleProxy {@link ChainedProxyManager} for fine-grained control of the chained proxies. To enable a single * chained proxy, {@link BrowserMobProxy#setChainedProxy(InetSocketAddress)} is generally more convenient. @@ -1004,15 +974,6 @@ public List getFilterFactories() { return filterFactories; } - @Override - public void setMitmDisabled(boolean mitmDisabled) throws IllegalStateException { - if (isStarted()) { - throw new IllegalStateException("Cannot disable MITM after the proxy has been started"); - } - - this.mitmDisabled = mitmDisabled; - } - @Override public void setMitmManager(MitmManager mitmManager) { this.mitmManager = mitmManager; @@ -1046,6 +1007,15 @@ public boolean isMitmDisabled() { return this.mitmDisabled; } + @Override + public void setMitmDisabled(boolean mitmDisabled) throws IllegalStateException { + if (isStarted()) { + throw new IllegalStateException("Cannot disable MITM after the proxy has been started"); + } + + this.mitmDisabled = mitmDisabled; + } + public void setUseEcc(boolean useEcc) { this.useEcc = useEcc; } diff --git a/app/src/main/java/net/lightbody/bmp/client/ClientUtil.java b/app/src/main/java/net/lightbody/bmp/client/ClientUtil.java index 0937d48..f62f79f 100644 --- a/app/src/main/java/net/lightbody/bmp/client/ClientUtil.java +++ b/app/src/main/java/net/lightbody/bmp/client/ClientUtil.java @@ -1,7 +1,7 @@ package net.lightbody.bmp.client; import com.google.common.collect.ImmutableList; -import net.lightbody.bmp.BrowserMobProxy; + import net.lightbody.bmp.proxy.dns.AdvancedHostResolver; import net.lightbody.bmp.proxy.dns.ChainedHostResolver; import net.lightbody.bmp.proxy.dns.DnsJavaResolver; @@ -9,7 +9,6 @@ import net.lightbody.bmp.proxy.dns.NativeResolver; import java.net.InetAddress; -import java.net.InetSocketAddress; import java.net.UnknownHostException; /** diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarCookie.java b/app/src/main/java/net/lightbody/bmp/core/har/HarCookie.java index 2057823..edaa04f 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarCookie.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarCookie.java @@ -29,18 +29,18 @@ public String getValue() { return value; } - public String getDecodeValue(){ + public void setValue(String value) { + this.value = value; + } + + public String getDecodeValue() { try { return URLDecoder.decode(value); - }catch (Exception e){ + } catch (Exception e) { return value; } } - public void setValue(String value) { - this.value = value; - } - public String getPath() { return path; } diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarEntry.java b/app/src/main/java/net/lightbody/bmp/core/har/HarEntry.java index 1c3f474..2e7e40e 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarEntry.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarEntry.java @@ -49,13 +49,14 @@ public void setStartedDateTime(Date startedDateTime) { * Rather than storing the time directly, calculate the time from the HarTimings as required in the HAR spec. * From https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html, * section 4.2.16 timings: -

-     Following must be true in case there are no -1 values (entry is an object in log.entries) :
-
-     entry.time == entry.timings.blocked + entry.timings.dns +
-     entry.timings.connect + entry.timings.send + entry.timings.wait +
-     entry.timings.receive;
-     
+ *
+     * Following must be true in case there are no -1 values (entry is an object in log.entries) :
+     *
+     * entry.time == entry.timings.blocked + entry.timings.dns +
+     * entry.timings.connect + entry.timings.send + entry.timings.wait +
+     * entry.timings.receive;
+     * 
+ * * @return time for this HAR entry, in milliseconds */ public long getTime() { diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarLog.java b/app/src/main/java/net/lightbody/bmp/core/har/HarLog.java index dc765ca..e739537 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarLog.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarLog.java @@ -14,7 +14,7 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class HarLog { private final String version = "1.2"; - private volatile HarNameVersion creator = new HarNameVersion("BrowserMob Proxy", BrowserMobProxyUtil.getVersionString()); + private volatile HarNameVersion creator = new HarNameVersion("BrowserMob Proxy", BrowserMobProxyUtil.getVersionString()); private volatile HarNameVersion browser; private List pages = new CopyOnWriteArrayList(); private List entries = new CopyOnWriteArrayList(); @@ -24,7 +24,7 @@ public class HarLog { public HarLog() { } - public HarLog(HarNameVersion creator,BrowserMobProxyServer server) { + public HarLog(HarNameVersion creator, BrowserMobProxyServer server) { this.creator = creator; this.server = server; } @@ -33,30 +33,30 @@ public void addPage(HarPage page) { pages.add(page); } - public Boolean deletePage(HarPage page){ + public Boolean deletePage(HarPage page) { return pages.remove(page); } public synchronized void addEntry(HarEntry entry) { int count = 0; - for (HarEntry har:entries) { + for (HarEntry har : entries) { if (entry.getPageref().equals(har.getPageref())) { count++; } } - if(count >= 999) { - if(server!=null){ + if (count >= 999) { + if (server != null) { String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA) .format(new Date(System.currentTimeMillis())); // 检查是å¦å­˜åœ¨é‡å¤æ·»åŠ  Boolean repeatAdd = false; - for (HarPage page:pages) { - if(page.getId().equals(time)){ + for (HarPage page : pages) { + if (page.getId().equals(time)) { repeatAdd = true; } } - if(!repeatAdd) { + if (!repeatAdd) { server.newPage(time); } } @@ -64,7 +64,7 @@ public synchronized void addEntry(HarEntry entry) { entries.add(entry); } - public void clearAllEntries(){ + public void clearAllEntries() { entries.clear(); } @@ -92,14 +92,14 @@ public List getPages() { return pages; } - public List getEntries() { - return entries; - } - public void setPages(List pages) { this.pages = pages; } + public List getEntries() { + return entries; + } + public void setEntries(List entries) { this.entries = entries; } diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarNameValuePair.java b/app/src/main/java/net/lightbody/bmp/core/har/HarNameValuePair.java index 67dcd47..3fb6503 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarNameValuePair.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarNameValuePair.java @@ -19,27 +19,36 @@ public String getValue() { return value; } - public String getDecodeValue(){ + public String getDecodeValue() { try { return URLDecoder.decode(value); - }catch (Exception e){ + } catch (Exception e) { return value; } } + @Override public String toString() { return name + "=" + value; } @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } HarNameValuePair that = (HarNameValuePair) o; - if (name != null ? !name.equals(that.name) : that.name != null) return false; - if (value != null ? !value.equals(that.value) : that.value != null) return false; + if (name != null ? !name.equals(that.name) : that.name != null) { + return false; + } + if (value != null ? !value.equals(that.value) : that.value != null) { + return false; + } return true; } diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarPage.java b/app/src/main/java/net/lightbody/bmp/core/har/HarPage.java index 61d3ca5..09f101d 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarPage.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarPage.java @@ -7,10 +7,10 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class HarPage { + private final HarPageTimings pageTimings = new HarPageTimings(); private volatile String id; private volatile Date startedDateTime; private volatile String title = ""; - private final HarPageTimings pageTimings = new HarPageTimings(); private volatile String comment = ""; public HarPage() { diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarRequest.java b/app/src/main/java/net/lightbody/bmp/core/har/HarRequest.java index 2cfe689..51f9f62 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarRequest.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarRequest.java @@ -7,12 +7,12 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class HarRequest { - private volatile String method; - private volatile String url; - private volatile String httpVersion; private final List cookies = new CopyOnWriteArrayList(); private final List headers = new CopyOnWriteArrayList(); private final List queryString = new CopyOnWriteArrayList(); + private volatile String method; + private volatile String url; + private volatile String httpVersion; private volatile HarPostData postData; private volatile long headersSize; // Odd grammar in spec private volatile long bodySize; diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarResponse.java b/app/src/main/java/net/lightbody/bmp/core/har/HarResponse.java index f82f248..0e47a00 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarResponse.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarResponse.java @@ -8,12 +8,12 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class HarResponse { - private volatile int status; - private volatile String statusText; - private volatile String httpVersion; private final List cookies = new CopyOnWriteArrayList(); private final List headers = new CopyOnWriteArrayList(); private final HarContent content = new HarContent(); + private volatile int status; + private volatile String statusText; + private volatile String httpVersion; private volatile String redirectURL = ""; /* the values of headersSize and bodySize are set to -1 by default, in accordance with the HAR spec: diff --git a/app/src/main/java/net/lightbody/bmp/core/har/HarTimings.java b/app/src/main/java/net/lightbody/bmp/core/har/HarTimings.java index efb8d86..bdb05d9 100644 --- a/app/src/main/java/net/lightbody/bmp/core/har/HarTimings.java +++ b/app/src/main/java/net/lightbody/bmp/core/har/HarTimings.java @@ -49,7 +49,7 @@ public long getDns(TimeUnit timeUnit) { public void setDns(long dns, TimeUnit timeUnit) { if (dns == -1) { this.dnsNanos = -1; - } else{ + } else { this.dnsNanos = TimeUnit.NANOSECONDS.convert(dns, timeUnit); } } diff --git a/app/src/main/java/net/lightbody/bmp/filters/AddHeadersFilter.java b/app/src/main/java/net/lightbody/bmp/filters/AddHeadersFilter.java index 5b584ec..312dc5b 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/AddHeadersFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/AddHeadersFilter.java @@ -1,13 +1,14 @@ package net.lightbody.bmp.filters; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; import org.littleshoot.proxy.HttpFiltersAdapter; import java.util.Collections; import java.util.Map; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + /** * Adds the headers specified in the constructor to this request. The filter does not make a defensive copy of the map, so there is no guarantee * that the map at the time of construction will contain the same values when the filter is actually invoked, if the map is modified concurrently. diff --git a/app/src/main/java/net/lightbody/bmp/filters/AutoBasicAuthFilter.java b/app/src/main/java/net/lightbody/bmp/filters/AutoBasicAuthFilter.java index 758f9bf..0ffa804 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/AutoBasicAuthFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/AutoBasicAuthFilter.java @@ -1,13 +1,14 @@ package net.lightbody.bmp.filters; +import org.littleshoot.proxy.impl.ProxyUtils; + +import java.util.Map; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; -import org.littleshoot.proxy.impl.ProxyUtils; - -import java.util.Map; /** * A filter that adds Basic authentication information to non-CONNECT requests. Takes a map of domain names to base64-encoded diff --git a/app/src/main/java/net/lightbody/bmp/filters/BlacklistFilter.java b/app/src/main/java/net/lightbody/bmp/filters/BlacklistFilter.java index b267673..14d2bf1 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/BlacklistFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/BlacklistFilter.java @@ -1,5 +1,10 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.proxy.BlacklistEntry; + +import java.util.Collection; +import java.util.Collections; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; @@ -8,10 +13,6 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; -import net.lightbody.bmp.proxy.BlacklistEntry; - -import java.util.Collection; -import java.util.Collections; /** * Applies blacklist entries to this request. The filter does not make a defensive copy of the blacklist entries, so there is no guarantee diff --git a/app/src/main/java/net/lightbody/bmp/filters/BrowserMobHttpFilterChain.java b/app/src/main/java/net/lightbody/bmp/filters/BrowserMobHttpFilterChain.java index 6ddf133..26b7013 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/BrowserMobHttpFilterChain.java +++ b/app/src/main/java/net/lightbody/bmp/filters/BrowserMobHttpFilterChain.java @@ -1,13 +1,7 @@ package net.lightbody.bmp.filters; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.DefaultFullHttpResponse; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpResponseStatus; import net.lightbody.bmp.BrowserMobProxyServer; + import org.littleshoot.proxy.HttpFilters; import org.littleshoot.proxy.HttpFiltersAdapter; import org.littleshoot.proxy.HttpFiltersSource; @@ -19,6 +13,14 @@ import java.util.Collections; import java.util.List; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; + /** * The filter "driver" that delegates to all chained filters specified by the proxy server. */ diff --git a/app/src/main/java/net/lightbody/bmp/filters/ClientRequestCaptureFilter.java b/app/src/main/java/net/lightbody/bmp/filters/ClientRequestCaptureFilter.java index d6bd1b5..c25bf96 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/ClientRequestCaptureFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/ClientRequestCaptureFilter.java @@ -1,5 +1,12 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.util.BrowserMobHttpUtil; + +import org.littleshoot.proxy.HttpFiltersAdapter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; @@ -8,11 +15,6 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; -import net.lightbody.bmp.util.BrowserMobHttpUtil; -import org.littleshoot.proxy.HttpFiltersAdapter; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; /** * This filter captures requests from the client (headers and content). @@ -22,18 +24,16 @@ * {@link net.lightbody.bmp.filters.HarCaptureFilter} for an example of the latter). */ public class ClientRequestCaptureFilter extends HttpFiltersAdapter { - /** - * Populated by clientToProxyRequest() when processing the HttpRequest object. Unlike originalRequest, - * this represents the "real" request that is being sent to the server, including headers. - */ - private volatile HttpRequest httpRequest; - /** * Populated by clientToProxyRequest() when processing the HttpContent objects. If the request is chunked, * it will be populated across multiple calls to clientToProxyRequest(). */ private final ByteArrayOutputStream requestContents = new ByteArrayOutputStream(); - + /** + * Populated by clientToProxyRequest() when processing the HttpRequest object. Unlike originalRequest, + * this represents the "real" request that is being sent to the server, including headers. + */ + private volatile HttpRequest httpRequest; /** * Populated by clientToProxyRequest() when processing the LastHttpContent. */ @@ -60,7 +60,7 @@ public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpContent instanceof LastHttpContent) { LastHttpContent lastHttpContent = (LastHttpContent) httpContent; - trailingHeaders = lastHttpContent .trailingHeaders(); + trailingHeaders = lastHttpContent.trailingHeaders(); } } diff --git a/app/src/main/java/net/lightbody/bmp/filters/HarCaptureFilter.java b/app/src/main/java/net/lightbody/bmp/filters/HarCaptureFilter.java index ca8c044..7bdc36c 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/HarCaptureFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/HarCaptureFilter.java @@ -2,18 +2,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.io.BaseEncoding; -import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.LastHttpContent; -import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http.cookie.ClientCookieDecoder; -import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http.cookie.ServerCookieDecoder; + import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarCookie; import net.lightbody.bmp.core.har.HarEntry; @@ -27,6 +16,7 @@ import net.lightbody.bmp.filters.util.HarCaptureUtil; import net.lightbody.bmp.proxy.CaptureType; import net.lightbody.bmp.util.BrowserMobHttpUtil; + import org.littleshoot.proxy.impl.ProxyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +34,19 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; + public class HarCaptureFilter extends HttpsAwareFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(HarCaptureFilter.class); @@ -75,37 +78,29 @@ public class HarCaptureFilter extends HttpsAwareFiltersAdapter { * The CaptureType data types to capture in this request. */ private final EnumSet dataToCapture; - - /** - * Populated by proxyToServerResolutionStarted when DNS resolution starts. If any previous filters already resolved the address, their resolution time - * will not be included in this time. - */ - private volatile long dnsResolutionStartedNanos; - - private volatile long connectionQueuedNanos; - private volatile long connectionStartedNanos; - - private volatile long sendStartedNanos; - private volatile long sendFinishedNanos; - - private volatile long responseReceiveStartedNanos; - /** * The address of the client making the request. Captured in the constructor and used when calculating and capturing ssl handshake and connect * timing information for SSL connections. */ private final InetSocketAddress clientAddress; - /** * Request body size is determined by the actual size of the data the client sends. The filter does not use the Content-Length header to determine request size. */ private final AtomicInteger requestBodySize = new AtomicInteger(0); - /** * Response body size is determined by the actual size of the data the server sends. */ private final AtomicInteger responseBodySize = new AtomicInteger(0); - + /** + * Populated by proxyToServerResolutionStarted when DNS resolution starts. If any previous filters already resolved the address, their resolution time + * will not be included in this time. + */ + private volatile long dnsResolutionStartedNanos; + private volatile long connectionQueuedNanos; + private volatile long connectionStartedNanos; + private volatile long sendStartedNanos; + private volatile long sendFinishedNanos; + private volatile long responseReceiveStartedNanos; /** * The "real" original request, as captured by the {@link #clientToProxyRequest(io.netty.handler.codec.http.HttpObject)} method. */ @@ -123,16 +118,16 @@ public class HarCaptureFilter extends HttpsAwareFiltersAdapter { *

* Regardless of the CaptureTypes specified in dataToCapture, the HarCaptureFilter will always capture: *

    - *
  • Request and response sizes
  • - *
  • HTTP request and status lines
  • - *
  • Page timing information
  • + *
  • Request and response sizes
  • + *
  • HTTP request and status lines
  • + *
  • Page timing information
  • *
* * @param originalRequest the original HttpRequest from the HttpFiltersSource factory - * @param har a reference to the ProxyServer's current HAR file at the time this request is received (can be null if HAR capture is not required) - * @param currentPageRef the ProxyServer's currentPageRef at the time this request is received from the client - * @param dataToCapture the data types to capture for this request. null or empty set indicates only basic information will be - * captured (see {@link net.lightbody.bmp.proxy.CaptureType} for information on data collected for each CaptureType) + * @param har a reference to the ProxyServer's current HAR file at the time this request is received (can be null if HAR capture is not required) + * @param currentPageRef the ProxyServer's currentPageRef at the time this request is received from the client + * @param dataToCapture the data types to capture for this request. null or empty set indicates only basic information will be + * captured (see {@link net.lightbody.bmp.proxy.CaptureType} for information on data collected for each CaptureType) */ public HarCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Har har, String currentPageRef, Set dataToCapture) { super(originalRequest, ctx); @@ -408,7 +403,7 @@ protected void captureRequestContent(HttpRequest httpRequest, byte[] fullMessage Charset charset; try { - charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentType); + charset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentType); } catch (UnsupportedCharsetException e) { log.warn("Found unsupported character set in Content-Type header '{}' in HTTP request to {}. Content will not be captured in HAR.", contentType, httpRequest.getUri(), e); return; diff --git a/app/src/main/java/net/lightbody/bmp/filters/HttpConnectHarCaptureFilter.java b/app/src/main/java/net/lightbody/bmp/filters/HttpConnectHarCaptureFilter.java index 1717370..3716e00 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/HttpConnectHarCaptureFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/HttpConnectHarCaptureFilter.java @@ -1,10 +1,7 @@ package net.lightbody.bmp.filters; import com.google.common.cache.CacheBuilder; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; + import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarRequest; @@ -13,6 +10,7 @@ import net.lightbody.bmp.filters.support.HttpConnectTiming; import net.lightbody.bmp.filters.util.HarCaptureUtil; import net.lightbody.bmp.util.HttpUtil; + import org.littleshoot.proxy.impl.ProxyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,6 +21,11 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + /** * This filter captures HAR data for HTTP CONNECT requests. CONNECTs are "meta" requests that must be made before HTTPS * requests, but are not populated as separate requests in the HAR. Most information from HTTP CONNECTs (such as SSL @@ -31,89 +34,74 @@ * static methods. This filter also handles HTTP CONNECT errors and creates HAR entries for those errors, since there * would otherwise not be any record in the HAR of the error (if the CONNECT fails, there will be no subsequent "real" * request in which to record the error). - * */ public class HttpConnectHarCaptureFilter extends HttpsAwareFiltersAdapter implements ModifiedRequestAwareFilter { private static final Logger log = LoggerFactory.getLogger(HttpConnectHarCaptureFilter.class); - + /** + * The maximum amount of time to save timing information between an HTTP CONNECT and the subsequent HTTP request. Typically this is done + * immediately, but if for some reason it is not (e.g. due to a client crash or dropped connection), the timing information will be + * kept for this long before being evicted to prevent a memory leak. If a subsequent request does come through after eviction, it will still + * be recorded, but the timing information will not be populated in the HAR. + */ + private static final int HTTP_CONNECT_TIMING_EVICTION_SECONDS = 60; + /** + * Concurrency of the httpConnectTiming map. Should be approximately equal to the maximum number of simultaneous connection + * attempts (but not necessarily simultaneous connections). A lower value will inhibit performance. + * TODO: tune this value for a large number of concurrent requests. develop a non-cache-based mechanism of passing ssl timings to subsequent requests. + */ + private static final int HTTP_CONNECT_TIMING_CONCURRENCY_LEVEL = 50; + /** + * Stores SSL connection timing information from HTTP CONNNECT requests. This timing information is stored in the first HTTP request + * after the CONNECT, not in the CONNECT itself, so it needs to be stored across requests. + *

+ * This is the only state stored across multiple requests. + */ + private static final ConcurrentMap httpConnectTimes = + CacheBuilder.newBuilder() + .expireAfterWrite(HTTP_CONNECT_TIMING_EVICTION_SECONDS, TimeUnit.SECONDS) + .concurrencyLevel(HTTP_CONNECT_TIMING_CONCURRENCY_LEVEL) + .build() + .asMap(); /** * The currently active HAR at the time the current request is received. */ private final Har har; - /** * The currently active page ref at the time the current request is received. */ private final String currentPageRef; - + /** + * The address of the client making the request. Captured in the constructor and used when calculating and capturing ssl handshake and connect + * timing information for SSL connections. + */ + private final InetSocketAddress clientAddress; + /** + * Stores HTTP CONNECT timing information for this request, if it is an HTTP CONNECT. + */ + private final HttpConnectTiming httpConnectTiming; /** * The time this CONNECT began. Used to populate the HAR entry in case of failure. */ private volatile Date requestStartTime; - /** * True if this filter instance processed a {@link #proxyToServerResolutionSucceeded(String, java.net.InetSocketAddress)} call, indicating * that the hostname was resolved and populated in the HAR (if this is not a CONNECT). */ // private volatile boolean addressResolved = false; private volatile InetAddress resolvedAddress; - /** * Populated by proxyToServerResolutionStarted when DNS resolution starts. If any previous filters already resolved the address, their resolution time * will not be included in this time. See {@link HarCaptureFilter#dnsResolutionStartedNanos}. */ private volatile long dnsResolutionStartedNanos; - private volatile long dnsResolutionFinishedNanos; - private volatile long connectionQueuedNanos; private volatile long connectionStartedNanos; private volatile long connectionSucceededTimeNanos; private volatile long sendStartedNanos; private volatile long sendFinishedNanos; - private volatile long responseReceiveStartedNanos; private volatile long sslHandshakeStartedNanos; - - /** - * The address of the client making the request. Captured in the constructor and used when calculating and capturing ssl handshake and connect - * timing information for SSL connections. - */ - private final InetSocketAddress clientAddress; - - /** - * Stores HTTP CONNECT timing information for this request, if it is an HTTP CONNECT. - */ - private final HttpConnectTiming httpConnectTiming; - - /** - * The maximum amount of time to save timing information between an HTTP CONNECT and the subsequent HTTP request. Typically this is done - * immediately, but if for some reason it is not (e.g. due to a client crash or dropped connection), the timing information will be - * kept for this long before being evicted to prevent a memory leak. If a subsequent request does come through after eviction, it will still - * be recorded, but the timing information will not be populated in the HAR. - */ - private static final int HTTP_CONNECT_TIMING_EVICTION_SECONDS = 60; - - /** - * Concurrency of the httpConnectTiming map. Should be approximately equal to the maximum number of simultaneous connection - * attempts (but not necessarily simultaneous connections). A lower value will inhibit performance. - * TODO: tune this value for a large number of concurrent requests. develop a non-cache-based mechanism of passing ssl timings to subsequent requests. - */ - private static final int HTTP_CONNECT_TIMING_CONCURRENCY_LEVEL = 50; - - /** - * Stores SSL connection timing information from HTTP CONNNECT requests. This timing information is stored in the first HTTP request - * after the CONNECT, not in the CONNECT itself, so it needs to be stored across requests. - * - * This is the only state stored across multiple requests. - */ - private static final ConcurrentMap httpConnectTimes = - CacheBuilder.newBuilder() - .expireAfterWrite(HTTP_CONNECT_TIMING_EVICTION_SECONDS, TimeUnit.SECONDS) - .concurrencyLevel(HTTP_CONNECT_TIMING_CONCURRENCY_LEVEL) - .build() - .asMap(); - private volatile HttpRequest modifiedHttpRequest; public HttpConnectHarCaptureFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, Har har, String currentPageRef) { @@ -137,6 +125,16 @@ public HttpConnectHarCaptureFilter(HttpRequest originalRequest, ChannelHandlerCo httpConnectTimes.put(clientAddress, httpConnectTiming); } + /** + * Retrieves and removes (thus "consumes") the SSL timing information from the connection cache for the specified address. + * + * @param clientAddress the address of the client connection that established the HTTP tunnel + * @return the timing information for the tunnel previously established from the clientAddress + */ + public static HttpConnectTiming consumeConnectTimingForConnection(InetSocketAddress clientAddress) { + return httpConnectTimes.remove(clientAddress); + } + @Override public HttpResponse clientToProxyRequest(HttpObject httpObject) { if (httpObject instanceof HttpRequest) { @@ -226,7 +224,6 @@ public void proxyToServerConnectionQueued() { this.connectionQueuedNanos = System.nanoTime(); } - @Override public InetSocketAddress proxyToServerResolutionStarted(String resolvingServerHostAndPort) { dnsResolutionStartedNanos = System.nanoTime(); @@ -376,16 +373,6 @@ private HarRequest createRequestForFailedConnect(HttpRequest httpConnectRequest) return new HarRequest(httpConnectRequest.getMethod().toString(), url, httpConnectRequest.getProtocolVersion().text()); } - /** - * Retrieves and removes (thus "consumes") the SSL timing information from the connection cache for the specified address. - * - * @param clientAddress the address of the client connection that established the HTTP tunnel - * @return the timing information for the tunnel previously established from the clientAddress - */ - public static HttpConnectTiming consumeConnectTimingForConnection(InetSocketAddress clientAddress) { - return httpConnectTimes.remove(clientAddress); - } - @Override public void setModifiedHttpRequest(HttpRequest modifiedHttpRequest) { this.modifiedHttpRequest = modifiedHttpRequest; diff --git a/app/src/main/java/net/lightbody/bmp/filters/HttpsAwareFiltersAdapter.java b/app/src/main/java/net/lightbody/bmp/filters/HttpsAwareFiltersAdapter.java index e727f2b..16cfc06 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/HttpsAwareFiltersAdapter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/HttpsAwareFiltersAdapter.java @@ -1,14 +1,17 @@ package net.lightbody.bmp.filters; import com.google.common.net.HostAndPort; + +import net.lightbody.bmp.util.BrowserMobHttpUtil; +import net.lightbody.bmp.util.HttpUtil; + +import org.littleshoot.proxy.HttpFiltersAdapter; +import org.littleshoot.proxy.impl.ProxyUtils; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.Attribute; import io.netty.util.AttributeKey; -import net.lightbody.bmp.util.HttpUtil; -import net.lightbody.bmp.util.BrowserMobHttpUtil; -import org.littleshoot.proxy.HttpFiltersAdapter; -import org.littleshoot.proxy.impl.ProxyUtils; /** * The HttpsAwareFiltersAdapter exposes the original host and the "real" host (after filter modifications) to filters for HTTPS diff --git a/app/src/main/java/net/lightbody/bmp/filters/HttpsHostCaptureFilter.java b/app/src/main/java/net/lightbody/bmp/filters/HttpsHostCaptureFilter.java index f2a52a0..6486178 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/HttpsHostCaptureFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/HttpsHostCaptureFilter.java @@ -1,14 +1,16 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.util.BrowserMobHttpUtil; + +import org.littleshoot.proxy.HttpFiltersAdapter; +import org.littleshoot.proxy.impl.ProxyUtils; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.util.Attribute; import io.netty.util.AttributeKey; -import net.lightbody.bmp.util.BrowserMobHttpUtil; -import org.littleshoot.proxy.HttpFiltersAdapter; -import org.littleshoot.proxy.impl.ProxyUtils; /** * Captures the host for HTTPS requests and stores the value in the ChannelHandlerContext for use by {@link HttpsAwareFiltersAdapter} diff --git a/app/src/main/java/net/lightbody/bmp/filters/HttpsOriginalHostCaptureFilter.java b/app/src/main/java/net/lightbody/bmp/filters/HttpsOriginalHostCaptureFilter.java index 4a6894c..029e665 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/HttpsOriginalHostCaptureFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/HttpsOriginalHostCaptureFilter.java @@ -1,11 +1,11 @@ package net.lightbody.bmp.filters; +import org.littleshoot.proxy.impl.ProxyUtils; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpRequest; import io.netty.util.Attribute; import io.netty.util.AttributeKey; -import org.littleshoot.proxy.HttpFiltersAdapter; -import org.littleshoot.proxy.impl.ProxyUtils; /** * Captures the original host for HTTPS requests and stores the value in the ChannelHandlerContext for use by {@link HttpsAwareFiltersAdapter} diff --git a/app/src/main/java/net/lightbody/bmp/filters/LatencyFilter.java b/app/src/main/java/net/lightbody/bmp/filters/LatencyFilter.java index 10f90df..841f6e0 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/LatencyFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/LatencyFilter.java @@ -1,14 +1,15 @@ package net.lightbody.bmp.filters; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; import org.littleshoot.proxy.HttpFiltersAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.TimeUnit; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + /** * Adds latency to a response before sending it to the client. This filter always adds the specified latency, even if the latency * between the proxy and the remote server already exceeds this value. diff --git a/app/src/main/java/net/lightbody/bmp/filters/RegisterRequestFilter.java b/app/src/main/java/net/lightbody/bmp/filters/RegisterRequestFilter.java index 32dc128..e63f957 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/RegisterRequestFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/RegisterRequestFilter.java @@ -1,11 +1,13 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.proxy.ActivityMonitor; + +import org.littleshoot.proxy.HttpFiltersAdapter; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; -import net.lightbody.bmp.proxy.ActivityMonitor; -import org.littleshoot.proxy.HttpFiltersAdapter; /** * Registers this request with the {@link net.lightbody.bmp.proxy.ActivityMonitor} when the HttpRequest is received from the client. diff --git a/app/src/main/java/net/lightbody/bmp/filters/RequestFilter.java b/app/src/main/java/net/lightbody/bmp/filters/RequestFilter.java index 4718d2b..4f7ce73 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/RequestFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/RequestFilter.java @@ -1,10 +1,11 @@ package net.lightbody.bmp.filters; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + /** * A functional interface to simplify modification and manipulation of requests. */ @@ -15,8 +16,8 @@ public interface RequestFilter { * contents may be modified using the {@link HttpMessageContents#setTextContents(String)} or {@link HttpMessageContents#setBinaryContents(byte[])} * methods. The request can be "short-circuited" by returning a non-null value. * - * @param request The request object, including method, URI, headers, etc. Modifications to the request object will be reflected in the request sent to the server. - * @param contents The request contents. + * @param request The request object, including method, URI, headers, etc. Modifications to the request object will be reflected in the request sent to the server. + * @param contents The request contents. * @param messageInfo Additional information relating to the HTTP message. * @return if the return value is non-null, the proxy will suppress the request and send the specified response to the client immediately */ diff --git a/app/src/main/java/net/lightbody/bmp/filters/RequestFilterAdapter.java b/app/src/main/java/net/lightbody/bmp/filters/RequestFilterAdapter.java index 2139ceb..e682ed9 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/RequestFilterAdapter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/RequestFilterAdapter.java @@ -1,14 +1,16 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.util.HttpMessageContents; +import net.lightbody.bmp.util.HttpMessageInfo; + +import org.littleshoot.proxy.HttpFilters; +import org.littleshoot.proxy.HttpFiltersSourceAdapter; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; -import net.lightbody.bmp.util.HttpMessageContents; -import net.lightbody.bmp.util.HttpMessageInfo; -import org.littleshoot.proxy.HttpFilters; -import org.littleshoot.proxy.HttpFiltersSourceAdapter; /** * A filter adapter for {@link RequestFilter} implementations. Executes the filter when the {@link HttpFilters#clientToProxyRequest(HttpObject)} @@ -78,7 +80,7 @@ public FilterSource(RequestFilter filter) { * be enabled if any filter has a maximum request or response buffer size greater than 0. See * {@link org.littleshoot.proxy.HttpFiltersSource#getMaximumRequestBufferSizeInBytes()} for details.) * - * @param filter RequestFilter to invoke + * @param filter RequestFilter to invoke * @param maximumRequestBufferSizeInBytes maximum buffer size when aggregating Requests for filtering */ public FilterSource(RequestFilter filter, int maximumRequestBufferSizeInBytes) { diff --git a/app/src/main/java/net/lightbody/bmp/filters/ResolvedHostnameCacheFilter.java b/app/src/main/java/net/lightbody/bmp/filters/ResolvedHostnameCacheFilter.java index 0cc4dca..0c2cb83 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/ResolvedHostnameCacheFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/ResolvedHostnameCacheFilter.java @@ -3,14 +3,16 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.net.HostAndPort; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpRequest; + import org.littleshoot.proxy.HttpFiltersAdapter; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.concurrent.TimeUnit; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpRequest; + /** * Caches hostname resolutions reported by the {@link org.littleshoot.proxy.HttpFilters#proxyToServerResolutionSucceeded(String, InetSocketAddress)} * filter method. Allows access to the resolved IP address on subsequent requests, when the address is not re-resolved because @@ -45,6 +47,16 @@ public ResolvedHostnameCacheFilter(HttpRequest originalRequest, ChannelHandlerCo super(originalRequest, ctx); } + /** + * Returns the (cached) address that was previously resolved for the specified host. + * + * @param host hostname that was previously resolved (without a port) + * @return the resolved IP address for the host, or null if the resolved address is not in the cache + */ + public static String getPreviouslyResolvedAddressForHost(String host) { + return resolvedAddresses.getIfPresent(host); + } + @Override public void proxyToServerResolutionSucceeded(String serverHostAndPort, InetSocketAddress resolvedRemoteAddress) { // the address *should* always be resolved at this point @@ -60,14 +72,4 @@ public void proxyToServerResolutionSucceeded(String serverHostAndPort, InetSocke } } } - - /** - * Returns the (cached) address that was previously resolved for the specified host. - * - * @param host hostname that was previously resolved (without a port) - * @return the resolved IP address for the host, or null if the resolved address is not in the cache - */ - public static String getPreviouslyResolvedAddressForHost(String host) { - return resolvedAddresses.getIfPresent(host); - } } diff --git a/app/src/main/java/net/lightbody/bmp/filters/ResponseFilter.java b/app/src/main/java/net/lightbody/bmp/filters/ResponseFilter.java index 14ed7d4..4d72a38 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/ResponseFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/ResponseFilter.java @@ -1,9 +1,10 @@ package net.lightbody.bmp.filters; -import io.netty.handler.codec.http.HttpResponse; import net.lightbody.bmp.util.HttpMessageContents; import net.lightbody.bmp.util.HttpMessageInfo; +import io.netty.handler.codec.http.HttpResponse; + /** * A functional interface to simplify modification and manipulation of responses. */ @@ -14,8 +15,8 @@ public interface ResponseFilter { * contents may be modified using the {@link HttpMessageContents#setTextContents(String)} or {@link HttpMessageContents#setBinaryContents(byte[])} * methods. * - * @param response The response object, including URI, headers, status line, etc. Modifications to the response object will be reflected in the client response. - * @param contents The response contents. + * @param response The response object, including URI, headers, status line, etc. Modifications to the response object will be reflected in the client response. + * @param contents The response contents. * @param messageInfo Additional information relating to the HTTP message. */ void filterResponse(HttpResponse response, HttpMessageContents contents, HttpMessageInfo messageInfo); diff --git a/app/src/main/java/net/lightbody/bmp/filters/ResponseFilterAdapter.java b/app/src/main/java/net/lightbody/bmp/filters/ResponseFilterAdapter.java index 7edc34d..1bf691d 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/ResponseFilterAdapter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/ResponseFilterAdapter.java @@ -1,14 +1,16 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.util.HttpMessageContents; +import net.lightbody.bmp.util.HttpMessageInfo; + +import org.littleshoot.proxy.HttpFilters; +import org.littleshoot.proxy.HttpFiltersSourceAdapter; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpMessage; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; -import net.lightbody.bmp.util.HttpMessageContents; -import net.lightbody.bmp.util.HttpMessageInfo; -import org.littleshoot.proxy.HttpFilters; -import org.littleshoot.proxy.HttpFiltersSourceAdapter; /** * A filter adapter for {@link ResponseFilter} implementations. Executes the filter when the {@link HttpFilters#serverToProxyResponse(HttpObject)} @@ -85,7 +87,7 @@ public FilterSource(ResponseFilter filter) { * be enabled if any filter has a maximum request or response buffer size greater than 0. See * {@link org.littleshoot.proxy.HttpFiltersSource#getMaximumResponseBufferSizeInBytes()} for details.) * - * @param filter ResponseFilter to invoke + * @param filter ResponseFilter to invoke * @param maximumResponseBufferSizeInBytes maximum buffer size when aggregating responses for filtering */ public FilterSource(ResponseFilter filter, int maximumResponseBufferSizeInBytes) { diff --git a/app/src/main/java/net/lightbody/bmp/filters/RewriteUrlFilter.java b/app/src/main/java/net/lightbody/bmp/filters/RewriteUrlFilter.java index ef0e223..1a69a6c 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/RewriteUrlFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/RewriteUrlFilter.java @@ -1,13 +1,9 @@ package net.lightbody.bmp.filters; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpObject; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import net.lightbody.bmp.util.HttpUtil; import net.lightbody.bmp.proxy.RewriteRule; import net.lightbody.bmp.util.BrowserMobHttpUtil; +import net.lightbody.bmp.util.HttpUtil; + import org.littleshoot.proxy.impl.ProxyUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -17,6 +13,12 @@ import java.util.Collections; import java.util.regex.Matcher; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + /** * Applies rewrite rules to the specified request. If a rewrite rule matches, the request's URI will be overwritten with the rewritten URI. * The filter does not make a defensive copy of the rewrite rule collection, so there is no guarantee diff --git a/app/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java b/app/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java index e44cbde..b84ccdd 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/ServerResponseCaptureFilter.java @@ -1,5 +1,14 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.util.BrowserMobHttpUtil; + +import org.littleshoot.proxy.HttpFiltersAdapter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpContent; @@ -8,13 +17,6 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; -import net.lightbody.bmp.util.BrowserMobHttpUtil; -import org.littleshoot.proxy.HttpFiltersAdapter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; /** * This filter captures responses from the server (headers and content). The filter can also decompress contents if desired. @@ -25,49 +27,41 @@ */ public class ServerResponseCaptureFilter extends HttpFiltersAdapter { private static final Logger log = LoggerFactory.getLogger(ServerResponseCaptureFilter.class); - - /** - * Populated by serverToProxyResponse() when processing the HttpResponse object - */ - private volatile HttpResponse httpResponse; - /** * Populated by serverToProxyResponse() as it receives HttpContent responses. If the response is chunked, it will * be populated across multiple calls to proxyToServerResponse(). */ private final ByteArrayOutputStream rawResponseContents = new ByteArrayOutputStream(); - + /** + * User option indicating compressed content should be uncompressed. + */ + private final boolean decompressEncodedContent; + /** + * Populated by serverToProxyResponse() when processing the HttpResponse object + */ + private volatile HttpResponse httpResponse; /** * Populated when processing the LastHttpContent. If the response is compressed and decompression is requested, * this contains the entire decompressed response. Otherwise it contains the raw response. */ private volatile byte[] fullResponseContents; - /** * Populated by serverToProxyResponse() when it processes the LastHttpContent object. */ private volatile HttpHeaders trailingHeaders; - /** * Set to true when processing the LastHttpContent if the server indicates there is a content encoding. */ private volatile boolean responseCompressed; - /** * Set to true when processing the LastHttpContent if decompression was requested and successful. */ private volatile boolean decompressionSuccessful; - /** * Populated when processing the LastHttpContent. */ private volatile String contentEncoding; - /** - * User option indicating compressed content should be uncompressed. - */ - private final boolean decompressEncodedContent; - public ServerResponseCaptureFilter(HttpRequest originalRequest, boolean decompressEncodedContent) { super(originalRequest); @@ -116,7 +110,7 @@ protected void captureFullResponseContents() { if (decompressEncodedContent) { decompressContents(); - } else { + } else { // will not decompress response } } else { @@ -128,12 +122,12 @@ protected void captureFullResponseContents() { protected void decompressContents() { if (contentEncoding.equalsIgnoreCase(HttpHeaders.Values.GZIP) || contentEncoding.equalsIgnoreCase(HttpHeaders.Values.DEFLATE)) { try { - fullResponseContents = BrowserMobHttpUtil.decompressContents(getRawResponseContents(),contentEncoding); + fullResponseContents = BrowserMobHttpUtil.decompressContents(getRawResponseContents(), contentEncoding); decompressionSuccessful = true; } catch (RuntimeException e) { log.warn("Failed to decompress response with encoding type " + contentEncoding + " when decoding request from " + originalRequest.getUri(), e); } - } else{ + } else { log.warn("Cannot decode unsupported content encoding type {}", contentEncoding); } } diff --git a/app/src/main/java/net/lightbody/bmp/filters/UnregisterRequestFilter.java b/app/src/main/java/net/lightbody/bmp/filters/UnregisterRequestFilter.java index 1ffff0f..94bc03e 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/UnregisterRequestFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/UnregisterRequestFilter.java @@ -1,11 +1,13 @@ package net.lightbody.bmp.filters; +import net.lightbody.bmp.proxy.ActivityMonitor; + +import org.littleshoot.proxy.HttpFiltersAdapter; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.LastHttpContent; -import net.lightbody.bmp.proxy.ActivityMonitor; -import org.littleshoot.proxy.HttpFiltersAdapter; /** * Unregisters this request with the {@link net.lightbody.bmp.proxy.ActivityMonitor} when the LastHttpContent is sent to the client. diff --git a/app/src/main/java/net/lightbody/bmp/filters/WhitelistFilter.java b/app/src/main/java/net/lightbody/bmp/filters/WhitelistFilter.java index ad8fbf6..5196716 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/WhitelistFilter.java +++ b/app/src/main/java/net/lightbody/bmp/filters/WhitelistFilter.java @@ -1,5 +1,11 @@ package net.lightbody.bmp.filters; +import org.littleshoot.proxy.impl.ProxyUtils; + +import java.util.Collection; +import java.util.Collections; +import java.util.regex.Pattern; + import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpHeaders; @@ -7,11 +13,6 @@ import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; -import org.littleshoot.proxy.impl.ProxyUtils; - -import java.util.Collection; -import java.util.Collections; -import java.util.regex.Pattern; /** * Checks this request against the whitelist, and returns the modified response if the request is not in the whitelist. The filter does not @@ -23,7 +24,7 @@ public class WhitelistFilter extends HttpsAwareFiltersAdapter { private final int whitelistResponseCode; private final Collection whitelistUrls; - public WhitelistFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, boolean whitelistEnabled,int whitelistResponseCode, + public WhitelistFilter(HttpRequest originalRequest, ChannelHandlerContext ctx, boolean whitelistEnabled, int whitelistResponseCode, Collection whitelistUrls) { super(originalRequest, ctx); diff --git a/app/src/main/java/net/lightbody/bmp/filters/support/HttpConnectTiming.java b/app/src/main/java/net/lightbody/bmp/filters/support/HttpConnectTiming.java index c18070f..408890f 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/support/HttpConnectTiming.java +++ b/app/src/main/java/net/lightbody/bmp/filters/support/HttpConnectTiming.java @@ -5,11 +5,11 @@ * "real" request to the same host. The HTTP CONNECT and the "real" HTTP requests are processed in different HarCaptureFilter instances. *

* Note: The connect time must include the ssl time. According to the HAR spec at https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.htm: -

- ssl [number, optional] (new in 1.2) - Time required for SSL/TLS negotiation. If this field is defined then the time is also
- included in the connect field (to ensure backward compatibility with HAR 1.1). Use -1 if the timing does not apply to the
- current request.
- 
+ *
+ * ssl [number, optional] (new in 1.2) - Time required for SSL/TLS negotiation. If this field is defined then the time is also
+ * included in the connect field (to ensure backward compatibility with HAR 1.1). Use -1 if the timing does not apply to the
+ * current request.
+ * 
*/ public class HttpConnectTiming { private volatile long blockedTimeNanos = -1; @@ -17,35 +17,35 @@ public class HttpConnectTiming { private volatile long connectTimeNanos = -1; private volatile long sslHandshakeTimeNanos = -1; - public void setConnectTimeNanos(long connectTimeNanos) { - this.connectTimeNanos = connectTimeNanos; - } - - public void setSslHandshakeTimeNanos(long sslHandshakeTimeNanos) { - this.sslHandshakeTimeNanos = sslHandshakeTimeNanos; - } - - public void setBlockedTimeNanos(long blockedTimeNanos) { - this.blockedTimeNanos = blockedTimeNanos; - } - - public void setDnsTimeNanos(long dnsTimeNanos) { - this.dnsTimeNanos = dnsTimeNanos; - } - public long getConnectTimeNanos() { return connectTimeNanos; } + public void setConnectTimeNanos(long connectTimeNanos) { + this.connectTimeNanos = connectTimeNanos; + } + public long getSslHandshakeTimeNanos() { return sslHandshakeTimeNanos; } + public void setSslHandshakeTimeNanos(long sslHandshakeTimeNanos) { + this.sslHandshakeTimeNanos = sslHandshakeTimeNanos; + } + public long getBlockedTimeNanos() { return blockedTimeNanos; } + public void setBlockedTimeNanos(long blockedTimeNanos) { + this.blockedTimeNanos = blockedTimeNanos; + } + public long getDnsTimeNanos() { return dnsTimeNanos; } + + public void setDnsTimeNanos(long dnsTimeNanos) { + this.dnsTimeNanos = dnsTimeNanos; + } } diff --git a/app/src/main/java/net/lightbody/bmp/filters/util/HarCaptureUtil.java b/app/src/main/java/net/lightbody/bmp/filters/util/HarCaptureUtil.java index 1ac47f9..92f8859 100644 --- a/app/src/main/java/net/lightbody/bmp/filters/util/HarCaptureUtil.java +++ b/app/src/main/java/net/lightbody/bmp/filters/util/HarCaptureUtil.java @@ -87,7 +87,7 @@ public static String getResponseTimedOutErrorMessage() { /** * Returns the error message for the HAR response when no response was received from the server (e.g. when the * browser is closed). - * + * * @return the no response received error message */ public static String getNoResponseReceivedErrorMessage() { diff --git a/app/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java b/app/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java index 21c064f..ac86b50 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/CertificateInfoGenerator.java @@ -11,7 +11,7 @@ public interface CertificateInfoGenerator { /** * Generate a certificate for the specified hostnames, optionally using parameters from the originalCertificate. * - * @param hostnames the hostnames to generate the certificate for, which may include wildcards + * @param hostnames the hostnames to generate the certificate for, which may include wildcards * @param originalCertificate original X.509 certificate sent by the upstream server, which may be null * @return CertificateInfo to be used to create an X509Certificate for the specified hostnames */ diff --git a/app/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java b/app/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java index 5771723..719f79c 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/KeyStoreFileCertificateSource.java @@ -2,9 +2,11 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; + import net.lightbody.bmp.mitm.exception.CertificateSourceException; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +44,8 @@ public CertificateAndKey get() { /** * Creates a {@link CertificateAndKeySource} that loads an existing {@link KeyStore} from a classpath resource. - * @param keyStoreType the KeyStore type, such as PKCS12 or JKS + * + * @param keyStoreType the KeyStore type, such as PKCS12 or JKS * @param keyStoreClasspathResource classpath resource to load (for example, "/keystore.jks") * @param privateKeyAlias the alias of the private key in the KeyStore * @param keyStorePassword te KeyStore password @@ -70,7 +73,8 @@ public KeyStoreFileCertificateSource(String keyStoreType, String keyStoreClasspa /** * Creates a {@link CertificateAndKeySource} that loads an existing {@link KeyStore} from a classpath resource. - * @param keyStoreType the KeyStore type, such as PKCS12 or JKS + * + * @param keyStoreType the KeyStore type, such as PKCS12 or JKS * @param keyStoreFile KeyStore file to load * @param privateKeyAlias the alias of the private key in the KeyStore * @param keyStorePassword te KeyStore password diff --git a/app/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java b/app/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java index 6bf5253..73d44a8 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/PemFileCertificateSource.java @@ -2,9 +2,11 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; + import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.mitm.util.EncryptionUtil; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/app/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java b/app/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java index ac4c7a0..c7d1452 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/RootCertificateGenerator.java @@ -2,12 +2,14 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; + import net.lightbody.bmp.mitm.keys.KeyGenerator; import net.lightbody.bmp.mitm.keys.RSAKeyGenerator; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.mitm.util.EncryptionUtil; import net.lightbody.bmp.mitm.util.MitmConstants; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,20 +36,14 @@ */ public class RootCertificateGenerator implements CertificateAndKeySource { private static final Logger log = LoggerFactory.getLogger(RootCertificateGenerator.class); - - private final CertificateInfo rootCertificateInfo; - - private final String messageDigest; - - private final KeyGenerator keyGenerator; - - private final SecurityProviderTool securityProviderTool; - /** * The default algorithm to use when encrypting objects in PEM files (such as private keys). */ private static final String DEFAULT_PEM_ENCRYPTION_ALGORITHM = "AES-128-CBC"; - + private final CertificateInfo rootCertificateInfo; + private final String messageDigest; + private final KeyGenerator keyGenerator; + private final SecurityProviderTool securityProviderTool; /** * The new root certificate and private key are generated only once, even across multiple calls to {@link #load()}}, * to allow users to save the new generated root certificate for use in browsers/other HTTP clients. @@ -85,6 +81,34 @@ public RootCertificateGenerator(CertificateInfo rootCertificateInfo, this.securityProviderTool = securityProviderTool; } + /** + * Convenience method to return a new {@link Builder} instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a default CN field for a certificate, using the hostname of this machine and the current time. + */ + private static String getDefaultCommonName() { + String hostname; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "localhost"; + } + + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz"); + + String currentDateTime = dateFormat.format(new Date()); + + String defaultCN = "Generated CA (" + hostname + ") " + currentDateTime; + + // CN fields can only be 64 characters + return defaultCN.length() <= 64 ? defaultCN : defaultCN.substring(0, 63); + } + @Override public CertificateAndKey load() { // only generate the materials once, so they can can be saved if desired @@ -175,13 +199,6 @@ public void saveRootCertificateAndKey(String keyStoreType, securityProviderTool.saveKeyStore(file, keyStore, password); } - /** - * Convenience method to return a new {@link Builder} instance. - */ - public static Builder builder() { - return new Builder(); - } - /** * A Builder for {@link RootCertificateGenerator}s. Initialized with suitable default values suitable for most purposes. */ @@ -235,25 +252,4 @@ public RootCertificateGenerator build() { return new RootCertificateGenerator(certificateInfo, messageDigest, keyGenerator, securityProviderTool); } } - - /** - * Creates a default CN field for a certificate, using the hostname of this machine and the current time. - */ - private static String getDefaultCommonName() { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostname = "localhost"; - } - - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz"); - - String currentDateTime = dateFormat.format(new Date()); - - String defaultCN = "Generated CA (" + hostname + ") " + currentDateTime; - - // CN fields can only be 64 characters - return defaultCN.length() <= 64 ? defaultCN : defaultCN.substring(0, 63); - } } diff --git a/app/src/main/java/net/lightbody/bmp/mitm/TrustSource.java b/app/src/main/java/net/lightbody/bmp/mitm/TrustSource.java index 0aa3ebf..6d91161 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/TrustSource.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/TrustSource.java @@ -2,6 +2,7 @@ import com.google.common.collect.ObjectArrays; import com.google.common.io.Files; + import net.lightbody.bmp.mitm.exception.UncheckedIOException; import net.lightbody.bmp.mitm.util.TrustUtil; @@ -15,19 +16,19 @@ /** * A source of trusted root certificate authorities. Provides static methods to obtain default trust sources: *
    - *
  • {@link #defaultTrustSource()}- both the built-in and JVM-trusted CAs
  • - *
  • {@link #javaTrustSource()} - only default CAs trusted by the JVM
  • - *
  • {@link #builtinTrustSource()} - only built-in trusted CAs (ultimately derived from Firefox's trust list)
  • + *
  • {@link #defaultTrustSource()}- both the built-in and JVM-trusted CAs
  • + *
  • {@link #javaTrustSource()} - only default CAs trusted by the JVM
  • + *
  • {@link #builtinTrustSource()} - only built-in trusted CAs (ultimately derived from Firefox's trust list)
  • *
- * + *

* Custom TrustSources can be built by starting with {@link #empty()}, then calling the various add() methods to add * PEM-encoded files and Strings, KeyStores, and X509Certificates to the TrustSource. For example: *

* - * TrustSource customTrustSource = TrustSource.empty() - * .add(myX509Certificate) - * .add(pemFileContainingMyCA) - * .add(javaKeyStore); + * TrustSource customTrustSource = TrustSource.empty() + * .add(myX509Certificate) + * .add(pemFileContainingMyCA) + * .add(javaKeyStore); * *

* Note: This class is immutable, so calls to add() will return a new instance, rather than modifying the existing instance. @@ -64,14 +65,6 @@ protected TrustSource(X509Certificate... trustedCAs) { } } - /** - * Returns the X509 certificates considered "trusted" by this TrustSource. This method will not return null, but - * may return an empty array. - */ - public X509Certificate[] getTrustedCAs() { - return trustedCAs; - } - /** * Returns a TrustSource that contains no trusted CAs. Can be used in conjunction with the add() methods to build * a TrustSource containing custom CAs from a variety of sources (PEM files, KeyStores, etc.). @@ -103,6 +96,14 @@ public static TrustSource javaTrustSource() { return new TrustSource(TrustUtil.getJavaTrustedCAs()); } + /** + * Returns the X509 certificates considered "trusted" by this TrustSource. This method will not return null, but + * may return an empty array. + */ + public X509Certificate[] getTrustedCAs() { + return trustedCAs; + } + /** * Returns a new TrustSource containing the same trusted CAs as this TrustSource, plus zero or more CAs contained in * the PEM-encoded String. The String may contain multiple certificates and may contain comments or other non-PEM-encoded diff --git a/app/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java b/app/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java index 6e14426..fcc5de7 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/manager/ImpersonatingMitmManager.java @@ -7,11 +7,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableList; -import io.netty.buffer.ByteBufAllocator; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SupportedCipherSuiteFilter; + import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateAndKeySource; import net.lightbody.bmp.mitm.CertificateInfo; @@ -31,14 +27,11 @@ import net.lightbody.bmp.mitm.util.MitmConstants; import net.lightbody.bmp.mitm.util.SslUtil; import net.lightbody.bmp.util.HttpUtil; + import org.littleshoot.proxy.MitmManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSession; import java.security.KeyPair; import java.security.PrivateKey; import java.security.cert.X509Certificate; @@ -49,6 +42,17 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + /** * An {@link MitmManager} that will create SSLEngines for clients that present impersonated certificates for upstream servers. The impersonated * certificates will be signed using the certificate and private key specified in an {@link #rootCertificateSource}. The impersonated server @@ -66,46 +70,39 @@ public class ImpersonatingMitmManager implements MitmManager { * Cipher suites allowed on client connections to the proxy. */ private final List clientCipherSuites; - - /** - * The SSLContext that will be used for communications with all upstream servers. This can be reused, so store it as a lazily-loaded singleton. - */ - private final Supplier upstreamServerSslContext = Suppliers.memoize(new Supplier() { - @Override - public SslContext get() { - return SslUtil.getUpstreamServerSslContext(serverCipherSuites, trustSource); - } - }); - /** * Cache for impersonating netty SslContexts. SslContexts can be safely reused, so caching the impersonating contexts avoids * repeatedly re-impersonating upstream servers. */ private final Cache sslContextCache; - /** * Generator used to create public and private keys for the server certificates. */ private final KeyGenerator serverKeyGenerator; - /** * The source of the CA's {@link CertificateAndKey} that will be used to sign generated server certificates. */ private final CertificateAndKeySource rootCertificateSource; - /** * The message digest used to sign the server certificate, such as SHA512. * See https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest for information * on supported message digests. */ private final String serverCertificateMessageDigest; - /** * The source of trusted root CAs. May be null, which disables all upstream certificate validation. Disabling upstream * certificate validation allows attackers to intercept communciations and should only be used during testing. */ private final TrustSource trustSource; - + /** + * The SSLContext that will be used for communications with all upstream servers. This can be reused, so store it as a lazily-loaded singleton. + */ + private final Supplier upstreamServerSslContext = Suppliers.memoize(new Supplier() { + @Override + public SslContext get() { + return SslUtil.getUpstreamServerSslContext(serverCipherSuites, trustSource); + } + }); /** * Utility used to generate {@link CertificateInfo} objects when impersonating an upstream server. */ @@ -115,7 +112,10 @@ public SslContext get() { * Tool implementation that is used to generate, sign, and otherwise manipulate server certificates. */ private final SecurityProviderTool securityProviderTool; - + /** + * Simple server certificate generation statistics. + */ + private final CertificateGenerationStatistics statistics = new CertificateGenerationStatistics(); /** * The CA root root certificate used to sign generated server certificates. {@link CertificateAndKeySource#load()} * is only called once to retrieve the CA root certificate, which will be used to impersonate all server certificates. @@ -127,11 +127,6 @@ public CertificateAndKey get() { } }); - /** - * Simple server certificate generation statistics. - */ - private final CertificateGenerationStatistics statistics = new CertificateGenerationStatistics(); - /** * Creates a new ImpersonatingMitmManager. In general, use {@link ImpersonatingMitmManager.Builder} * to construct new instances. @@ -190,6 +185,26 @@ public ImpersonatingMitmManager(CertificateAndKeySource rootCertificateSource, log.debug("Allowed ciphers for client connections to proxy (some ciphers may not be available): {}", clientCipherSuites); } + /** + * Convenience method to return a new {@link Builder} instance default default values: a {@link RootCertificateGenerator} + * that dynamically generates an RSA root certificate and RSA server certificates. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Convenience method to return a new {@link Builder} instance that will dynamically create EC root certificates and + * EC server certificates, but otherwise uses default values. + */ + public static Builder builderWithECC() { + return new Builder() + .serverKeyGenerator(new ECKeyGenerator()) + .rootCertificateSource(RootCertificateGenerator.builder() + .keyGenerator(new ECKeyGenerator()) + .build()); + } + @Override public SSLEngine serverSslEngine() { try { @@ -238,7 +253,7 @@ public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession sslSessi * which impersonates the specified hostname. * * @param hostnameToImpersonate the hostname for which the impersonated SSLContext is being requested - * @param sslSession the upstream server SSLSession + * @param sslSession the upstream server SSLSession * @return SSLContext which will present an impersonated certificate */ private SslContext getHostnameImpersonatingSslContext(final String hostnameToImpersonate, final SSLSession sslSession) { @@ -261,7 +276,7 @@ public SslContext call() throws Exception { * This is a convenience method for {@link #createImpersonatingSslContext(CertificateInfo)} that generates the * {@link CertificateInfo} from the specified hostname using the {@link #certificateInfoGenerator}. * - * @param sslSession sslSession between the proxy and the upstream server + * @param sslSession sslSession between the proxy and the upstream server * @param hostnameToImpersonate hostname (supplied by the client's HTTP CONNECT) that will be impersonated * @return an SSLContext presenting a certificate matching the hostnameToImpersonate */ @@ -343,26 +358,6 @@ public CertificateGenerationStatistics getStatistics() { return this.statistics; } - /** - * Convenience method to return a new {@link Builder} instance default default values: a {@link RootCertificateGenerator} - * that dynamically generates an RSA root certificate and RSA server certificates. - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Convenience method to return a new {@link Builder} instance that will dynamically create EC root certificates and - * EC server certificates, but otherwise uses default values. - */ - public static Builder builderWithECC() { - return new Builder() - .serverKeyGenerator(new ECKeyGenerator()) - .rootCertificateSource(RootCertificateGenerator.builder() - .keyGenerator(new ECKeyGenerator()) - .build()); - } - /** * A Builder for {@link ImpersonatingMitmManager}s. Initialized with suitable default values suitable for most purposes. */ diff --git a/app/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java b/app/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java index ae27cf5..005c76c 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/tools/BouncyCastleSecurityProviderTool.java @@ -1,12 +1,14 @@ package net.lightbody.bmp.mitm.tools; import com.google.common.net.InetAddresses; + import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateInfo; import net.lightbody.bmp.mitm.exception.CertificateCreationException; import net.lightbody.bmp.mitm.exception.ExportException; import net.lightbody.bmp.mitm.exception.ImportException; import net.lightbody.bmp.mitm.util.EncryptionUtil; + import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; @@ -40,7 +42,6 @@ import org.bouncycastle.operator.OperatorCreationException; import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import javax.net.ssl.KeyManager; import java.io.File; import java.io.IOException; import java.io.Reader; @@ -58,15 +59,142 @@ import java.util.ArrayList; import java.util.List; +import javax.net.ssl.KeyManager; + public class BouncyCastleSecurityProviderTool implements SecurityProviderTool { + /** + * The size of certificate serial numbers, in bits. + */ + private static final int CERTIFICATE_SERIAL_NUMBER_SIZE = 160; + static { Security.addProvider(new BouncyCastleProvider()); } /** - * The size of certificate serial numbers, in bits. + * Creates an X500Name based on the specified certificateInfo. + * + * @param certificateInfo information to populate the X500Name with + * @return a new X500Name object for use as a subject or issuer */ - private static final int CERTIFICATE_SERIAL_NUMBER_SIZE = 160; + private static X500Name createX500NameForCertificate(CertificateInfo certificateInfo) { + X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE); + + if (certificateInfo.getCommonName() != null) { + x500NameBuilder.addRDN(BCStyle.CN, certificateInfo.getCommonName()); + } + + if (certificateInfo.getOrganization() != null) { + x500NameBuilder.addRDN(BCStyle.O, certificateInfo.getOrganization()); + } + + if (certificateInfo.getOrganizationalUnit() != null) { + x500NameBuilder.addRDN(BCStyle.OU, certificateInfo.getOrganizationalUnit()); + } + + if (certificateInfo.getEmail() != null) { + x500NameBuilder.addRDN(BCStyle.E, certificateInfo.getEmail()); + } + + if (certificateInfo.getLocality() != null) { + x500NameBuilder.addRDN(BCStyle.L, certificateInfo.getLocality()); + } + + if (certificateInfo.getState() != null) { + x500NameBuilder.addRDN(BCStyle.ST, certificateInfo.getState()); + } + + if (certificateInfo.getCountryCode() != null) { + x500NameBuilder.addRDN(BCStyle.C, certificateInfo.getCountryCode()); + } + + // TODO: Add more X.509 certificate fields as needed + + return x500NameBuilder.build(); + } + + /** + * Converts a list of domain name Subject Alternative Names into ASN1Encodable GeneralNames objects, for use with + * the Bouncy Castle certificate builder. + * + * @param subjectAlternativeNames domain name SANs to convert + * @return a GeneralNames instance that includes the specifie dsubjectAlternativeNames as DNS name fields + */ + private static GeneralNames getDomainNameSANsAsASN1Encodable(List subjectAlternativeNames) { + List encodedSANs = new ArrayList<>(subjectAlternativeNames.size()); + for (String subjectAlternativeName : subjectAlternativeNames) { + // IP addresses use the IP Address tag instead of the DNS Name tag in the SAN list + boolean isIpAddress = InetAddresses.isInetAddress(subjectAlternativeName); + GeneralName generalName = new GeneralName(isIpAddress ? GeneralName.iPAddress : GeneralName.dNSName, subjectAlternativeName); + encodedSANs.add(generalName); + } + + return new GeneralNames(encodedSANs.toArray(new GeneralName[encodedSANs.size()])); + } + + /** + * Creates a ContentSigner that can be used to sign certificates with the given private key and signature algorithm. + * + * @param certAuthorityPrivateKey the private key to use to sign certificates + * @param signatureAlgorithm the algorithm to use to sign certificates + * @return a ContentSigner + */ + private static ContentSigner getCertificateSigner(PrivateKey certAuthorityPrivateKey, String signatureAlgorithm) { + try { + return new JcaContentSignerBuilder(signatureAlgorithm) + .build(certAuthorityPrivateKey); + } catch (OperatorCreationException e) { + throw new CertificateCreationException("Unable to create ContentSigner using signature algorithm: " + signatureAlgorithm, e); + } + } + + /** + * Converts a Bouncy Castle X509CertificateHolder into a JCA X590Certificate. + * + * @param bouncyCastleCertificate BC X509CertificateHolder + * @return JCA X509Certificate + */ + private static X509Certificate convertToJcaCertificate(X509CertificateHolder bouncyCastleCertificate) { + try { + return new JcaX509CertificateConverter() + .getCertificate(bouncyCastleCertificate); + } catch (CertificateException e) { + throw new CertificateCreationException("Unable to convert X590CertificateHolder to JCA X590Certificate", e); + } + } + + /** + * Creates the SubjectKeyIdentifier for a Bouncy Castle X590CertificateHolder. + * + * @param key public key to identify + * @return SubjectKeyIdentifier for the specified key + */ + private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) { + SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(key.getEncoded()); + + return new BcX509ExtensionUtils().createSubjectKeyIdentifier(publicKeyInfo); + } + + /** + * Encodes the specified security object in PEM format, using the specified encryptor. If the encryptor is null, + * the object will not be encrypted in the generated String. + * + * @param object object to encrypt (certificate, private key, etc.) + * @param encryptor engine to encrypt the resulting PEM String, or null if no encryption should be used + * @return a PEM-encoded String + */ + private static String encodeObjectAsPemString(Object object, PEMEncryptor encryptor) { + StringWriter stringWriter = new StringWriter(); + + try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { + pemWriter.writeObject(object, encryptor); + pemWriter.flush(); + } catch (IOException e) { + throw new ExportException("Unable to generate PEM string representing object", e); + } + + return stringWriter.toString(); + } @Override public CertificateAndKey createServerCertificate(CertificateInfo certificateInfo, @@ -258,130 +386,4 @@ public void saveKeyStore(File file, KeyStore keyStore, String keystorePassword) public KeyManager[] getKeyManagers(KeyStore keyStore, String keyStorePassword) { return new KeyManager[0]; } - - - /** - * Creates an X500Name based on the specified certificateInfo. - * - * @param certificateInfo information to populate the X500Name with - * @return a new X500Name object for use as a subject or issuer - */ - private static X500Name createX500NameForCertificate(CertificateInfo certificateInfo) { - X500NameBuilder x500NameBuilder = new X500NameBuilder(BCStyle.INSTANCE); - - if (certificateInfo.getCommonName() != null) { - x500NameBuilder.addRDN(BCStyle.CN, certificateInfo.getCommonName()); - } - - if (certificateInfo.getOrganization() != null) { - x500NameBuilder.addRDN(BCStyle.O, certificateInfo.getOrganization()); - } - - if (certificateInfo.getOrganizationalUnit() != null) { - x500NameBuilder.addRDN(BCStyle.OU, certificateInfo.getOrganizationalUnit()); - } - - if (certificateInfo.getEmail() != null) { - x500NameBuilder.addRDN(BCStyle.E, certificateInfo.getEmail()); - } - - if (certificateInfo.getLocality() != null) { - x500NameBuilder.addRDN(BCStyle.L, certificateInfo.getLocality()); - } - - if (certificateInfo.getState() != null) { - x500NameBuilder.addRDN(BCStyle.ST, certificateInfo.getState()); - } - - if (certificateInfo.getCountryCode() != null) { - x500NameBuilder.addRDN(BCStyle.C, certificateInfo.getCountryCode()); - } - - // TODO: Add more X.509 certificate fields as needed - - return x500NameBuilder.build(); - } - - /** - * Converts a list of domain name Subject Alternative Names into ASN1Encodable GeneralNames objects, for use with - * the Bouncy Castle certificate builder. - * - * @param subjectAlternativeNames domain name SANs to convert - * @return a GeneralNames instance that includes the specifie dsubjectAlternativeNames as DNS name fields - */ - private static GeneralNames getDomainNameSANsAsASN1Encodable(List subjectAlternativeNames) { - List encodedSANs = new ArrayList<>(subjectAlternativeNames.size()); - for (String subjectAlternativeName : subjectAlternativeNames) { - // IP addresses use the IP Address tag instead of the DNS Name tag in the SAN list - boolean isIpAddress = InetAddresses.isInetAddress(subjectAlternativeName); - GeneralName generalName = new GeneralName(isIpAddress ? GeneralName.iPAddress : GeneralName.dNSName, subjectAlternativeName); - encodedSANs.add(generalName); - } - - return new GeneralNames(encodedSANs.toArray(new GeneralName[encodedSANs.size()])); - } - - /** - * Creates a ContentSigner that can be used to sign certificates with the given private key and signature algorithm. - * - * @param certAuthorityPrivateKey the private key to use to sign certificates - * @param signatureAlgorithm the algorithm to use to sign certificates - * @return a ContentSigner - */ - private static ContentSigner getCertificateSigner(PrivateKey certAuthorityPrivateKey, String signatureAlgorithm) { - try { - return new JcaContentSignerBuilder(signatureAlgorithm) - .build(certAuthorityPrivateKey); - } catch (OperatorCreationException e) { - throw new CertificateCreationException("Unable to create ContentSigner using signature algorithm: " + signatureAlgorithm, e); - } - } - - /** - * Converts a Bouncy Castle X509CertificateHolder into a JCA X590Certificate. - * - * @param bouncyCastleCertificate BC X509CertificateHolder - * @return JCA X509Certificate - */ - private static X509Certificate convertToJcaCertificate(X509CertificateHolder bouncyCastleCertificate) { - try { - return new JcaX509CertificateConverter() - .getCertificate(bouncyCastleCertificate); - } catch (CertificateException e) { - throw new CertificateCreationException("Unable to convert X590CertificateHolder to JCA X590Certificate", e); - } - } - - /** - * Creates the SubjectKeyIdentifier for a Bouncy Castle X590CertificateHolder. - * - * @param key public key to identify - * @return SubjectKeyIdentifier for the specified key - */ - private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) { - SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfo.getInstance(key.getEncoded()); - - return new BcX509ExtensionUtils().createSubjectKeyIdentifier(publicKeyInfo); - } - - /** - * Encodes the specified security object in PEM format, using the specified encryptor. If the encryptor is null, - * the object will not be encrypted in the generated String. - * - * @param object object to encrypt (certificate, private key, etc.) - * @param encryptor engine to encrypt the resulting PEM String, or null if no encryption should be used - * @return a PEM-encoded String - */ - private static String encodeObjectAsPemString(Object object, PEMEncryptor encryptor) { - StringWriter stringWriter = new StringWriter(); - - try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) { - pemWriter.writeObject(object, encryptor); - pemWriter.flush(); - } catch (IOException e) { - throw new ExportException("Unable to generate PEM string representing object", e); - } - - return stringWriter.toString(); - } } diff --git a/app/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java b/app/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java index 501f5a6..a62ce4f 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/tools/DefaultSecurityProviderTool.java @@ -1,13 +1,13 @@ package net.lightbody.bmp.mitm.tools; import com.google.common.io.CharStreams; + import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateInfo; import net.lightbody.bmp.mitm.exception.ImportException; import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; import net.lightbody.bmp.mitm.util.KeyStoreUtil; -import javax.net.ssl.KeyManager; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -26,6 +26,8 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import javax.net.ssl.KeyManager; + /** * A {@link SecurityProviderTool} implementation that uses the default system Security provider where possible, but uses the * Bouncy Castle provider for operations that the JCA does not provide or implement (e.g. certificate generation and signing). diff --git a/app/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java b/app/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java index 8b6df2a..55c3247 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/tools/SecurityProviderTool.java @@ -3,7 +3,6 @@ import net.lightbody.bmp.mitm.CertificateAndKey; import net.lightbody.bmp.mitm.CertificateInfo; -import javax.net.ssl.KeyManager; import java.io.File; import java.io.Reader; import java.security.KeyPair; @@ -12,6 +11,8 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; +import javax.net.ssl.KeyManager; + /** * Generic interface for functionality provided by a Security Provider. */ diff --git a/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureExtendedTrustManager.java b/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureExtendedTrustManager.java index 14d23f8..465efdb 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureExtendedTrustManager.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureExtendedTrustManager.java @@ -1,12 +1,8 @@ package net.lightbody.bmp.mitm.trustmanager; -import io.netty.util.internal.EmptyArrays; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; import java.net.Socket; import java.security.KeyStore; import java.security.KeyStoreException; @@ -14,20 +10,25 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; -import cn.darkal.networkdiagnosis.Utils.X509ExtendedTrustManager; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import cn.darkal.networkdiagnosis.Utils.AbstractX509ExtendedTrustManager; +import io.netty.util.internal.EmptyArrays; /** - * An {@link X509ExtendedTrustManager} and {@link javax.net.ssl.X509TrustManager} that will accept all server and client - * certificates. Before accepting a certificate, the InsecureExtendedTrustManager uses the default X509ExtendedTrustManager + * An {@link AbstractX509ExtendedTrustManager} and {@link javax.net.ssl.X509TrustManager} that will accept all server and client + * certificates. Before accepting a certificate, the InsecureExtendedTrustManager uses the default AbstractX509ExtendedTrustManager * to determine if the certificate would otherwise be trusted, and logs a debug-level message if it is not trusted. */ -public class InsecureExtendedTrustManager extends X509ExtendedTrustManager { +public class InsecureExtendedTrustManager extends AbstractX509ExtendedTrustManager { private static final Logger log = LoggerFactory.getLogger(InsecureExtendedTrustManager.class); /** - * An {@link X509ExtendedTrustManager} that does no certificate validation whatsoever. + * An {@link AbstractX509ExtendedTrustManager} that does no certificate validation whatsoever. */ - private static final X509ExtendedTrustManager NOOP_EXTENDED_TRUST_MANAGER = new X509ExtendedTrustManager() { + private static final AbstractX509ExtendedTrustManager NOOP_EXTENDED_TRUST_MANAGER = new AbstractX509ExtendedTrustManager() { @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { } @@ -61,7 +62,33 @@ public X509Certificate[] getAcceptedIssuers() { /** * The default extended trust manager, which will be used to determine if certificates would otherwise be trusted. */ - protected static final X509ExtendedTrustManager DEFAULT_EXTENDED_TRUST_MANAGER = getDefaultExtendedTrustManager(); + protected static final AbstractX509ExtendedTrustManager DEFAULT_EXTENDED_TRUST_MANAGER = getDefaultExtendedTrustManager(); + + /** + * Returns the JDK's default AbstractX509ExtendedTrustManager, or a no-op trust manager if the default cannot be found. + */ + private static AbstractX509ExtendedTrustManager getDefaultExtendedTrustManager() { + TrustManagerFactory trustManagerFactory; + try { + trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + // initialize the TrustManagerFactory with the default KeyStore + trustManagerFactory.init((KeyStore) null); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + log.debug("Unable to initialize default TrustManagerFactory. Using no-op AbstractX509ExtendedTrustManager.", e); + return NOOP_EXTENDED_TRUST_MANAGER; + } + + // find the AbstractX509ExtendedTrustManager in the list of registered trust managers + for (TrustManager tm : trustManagerFactory.getTrustManagers()) { + if (tm instanceof AbstractX509ExtendedTrustManager) { + return (AbstractX509ExtendedTrustManager) tm; + } + } + + // no default AbstractX509ExtendedTrustManager found, so return a no-op + log.debug("No default AbstractX509ExtendedTrustManager found. Using no-op."); + return NOOP_EXTENDED_TRUST_MANAGER; + } @Override public void checkClientTrusted(X509Certificate[] x509Certificates, String s, Socket socket) throws CertificateException { @@ -121,30 +148,4 @@ public void checkServerTrusted(X509Certificate[] x509Certificates, String s) thr public X509Certificate[] getAcceptedIssuers() { return EmptyArrays.EMPTY_X509_CERTIFICATES; } - - /** - * Returns the JDK's default X509ExtendedTrustManager, or a no-op trust manager if the default cannot be found. - */ - private static X509ExtendedTrustManager getDefaultExtendedTrustManager() { - TrustManagerFactory trustManagerFactory; - try { - trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - // initialize the TrustManagerFactory with the default KeyStore - trustManagerFactory.init((KeyStore) null); - } catch (NoSuchAlgorithmException | KeyStoreException e) { - log.debug("Unable to initialize default TrustManagerFactory. Using no-op X509ExtendedTrustManager.", e); - return NOOP_EXTENDED_TRUST_MANAGER; - } - - // find the X509ExtendedTrustManager in the list of registered trust managers - for (TrustManager tm : trustManagerFactory.getTrustManagers()) { - if (tm instanceof X509ExtendedTrustManager) { - return (X509ExtendedTrustManager) tm; - } - } - - // no default X509ExtendedTrustManager found, so return a no-op - log.debug("No default X509ExtendedTrustManager found. Using no-op."); - return NOOP_EXTENDED_TRUST_MANAGER; - } } diff --git a/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureTrustManagerFactory.java b/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureTrustManagerFactory.java index 176e02b..544d809 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureTrustManagerFactory.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/trustmanager/InsecureTrustManagerFactory.java @@ -16,17 +16,18 @@ package net.lightbody.bmp.mitm.trustmanager; -import cn.darkal.networkdiagnosis.Utils.X509ExtendedTrustManager; -import io.netty.handler.ssl.util.SimpleTrustManagerFactory; +import java.security.KeyStore; import javax.net.ssl.ManagerFactoryParameters; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import java.security.KeyStore; + +import cn.darkal.networkdiagnosis.Utils.AbstractX509ExtendedTrustManager; +import io.netty.handler.ssl.util.SimpleTrustManagerFactory; /** * Note: This is a modified version of {@link io.netty.handler.ssl.util.InsecureTrustManagerFactory} from Netty - * 4.0.36. Unlike the netty version, this class returns an {@link X509ExtendedTrustManager} instead of an + * 4.0.36. Unlike the netty version, this class returns an {@link AbstractX509ExtendedTrustManager} instead of an * {@link javax.net.ssl.X509TrustManager} instance, which allows us to bypass additional certificate validations. *

* An insecure {@link TrustManagerFactory} that trusts all X.509 certificates without any verification. @@ -40,7 +41,7 @@ public class InsecureTrustManagerFactory extends SimpleTrustManagerFactory { public static final TrustManagerFactory INSTANCE = new InsecureTrustManagerFactory(); - public static final X509ExtendedTrustManager tm = new InsecureExtendedTrustManager(); + public static final AbstractX509ExtendedTrustManager tm = new InsecureExtendedTrustManager(); @Override protected void engineInit(KeyStore keyStore) throws Exception { diff --git a/app/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java b/app/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java index 037f37c..687776f 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/util/EncryptionUtil.java @@ -3,7 +3,6 @@ import net.lightbody.bmp.mitm.exception.ExportException; import net.lightbody.bmp.mitm.exception.ImportException; -import javax.crypto.Cipher; import java.io.File; import java.io.IOException; import java.math.BigInteger; @@ -16,6 +15,8 @@ import java.security.interfaces.RSAKey; import java.util.Random; +import javax.crypto.Cipher; + /** * A collection of simple JCA-related utilities. */ @@ -84,7 +85,7 @@ public static boolean isEcKey(Key key) { /** * Convenience method to write PEM data to a file. The file will be encoded in the US_ASCII character set. * - * @param file file to write to + * @param file file to write to * @param pemDataToWrite PEM data to write to the file */ public static void writePemStringToFile(File file, String pemDataToWrite) { diff --git a/app/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java b/app/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java index 7edfc47..d8a7043 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/util/KeyStoreUtil.java @@ -2,8 +2,6 @@ import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; -import javax.net.ssl.KeyManager; -import javax.net.ssl.KeyManagerFactory; import java.io.IOException; import java.security.KeyStore; import java.security.KeyStoreException; @@ -15,6 +13,9 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; + /** * Utility for loading, saving, and manipulating {@link KeyStore}s. */ diff --git a/app/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java b/app/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java index bb24ad2..351417a 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/util/MitmConstants.java @@ -10,7 +10,7 @@ public class MitmConstants { * this question for details: http://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256. SHA384 is * SHA512 with a smaller output size. */ - public static final String DEFAULT_MESSAGE_DIGEST = is32BitJvm() ? "SHA256": "SHA384"; + public static final String DEFAULT_MESSAGE_DIGEST = is32BitJvm() ? "SHA256" : "SHA384"; /** * The default {@link java.security.KeyStore} type to use when creating KeyStores (e.g. for impersonated server diff --git a/app/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java b/app/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java index 198f194..ddd8a91 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/util/SslUtil.java @@ -3,20 +3,14 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.io.CharStreams; -import io.netty.handler.ssl.OpenSsl; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SupportedCipherSuiteFilter; -import net.lightbody.bmp.mitm.trustmanager.InsecureTrustManagerFactory; + import net.lightbody.bmp.mitm.TrustSource; import net.lightbody.bmp.mitm.exception.SslContextInitializationException; +import net.lightbody.bmp.mitm.trustmanager.InsecureTrustManagerFactory; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLPeerUnverifiedException; -import javax.net.ssl.SSLSession; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -29,6 +23,16 @@ import java.util.Collections; import java.util.List; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + /** * Utility for creating SSLContexts. */ @@ -74,8 +78,8 @@ public List get() { * supply an appropriate trustSource except in extraordinary circumstances (e.g. testing with dynamically-generated * certificates). * - * @param cipherSuites cipher suites to allow when connecting to the upstream server - * @param trustSource the trust store that will be used to validate upstream servers' certificates, or null to accept all upstream server certificates + * @param cipherSuites cipher suites to allow when connecting to the upstream server + * @param trustSource the trust store that will be used to validate upstream servers' certificates, or null to accept all upstream server certificates * @return an SSLContext to connect to upstream servers with */ public static SslContext getUpstreamServerSslContext(Collection cipherSuites, TrustSource trustSource) { diff --git a/app/src/main/java/net/lightbody/bmp/mitm/util/TrustUtil.java b/app/src/main/java/net/lightbody/bmp/mitm/util/TrustUtil.java index 156f910..536e7d5 100644 --- a/app/src/main/java/net/lightbody/bmp/mitm/util/TrustUtil.java +++ b/app/src/main/java/net/lightbody/bmp/mitm/util/TrustUtil.java @@ -2,18 +2,17 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; + import net.lightbody.bmp.mitm.exception.KeyStoreAccessException; import net.lightbody.bmp.mitm.exception.TrustSourceException; import net.lightbody.bmp.mitm.exception.UncheckedIOException; import net.lightbody.bmp.mitm.tools.DefaultSecurityProviderTool; import net.lightbody.bmp.mitm.tools.SecurityProviderTool; import net.lightbody.bmp.util.ClasspathResourceUtil; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.security.KeyStore; @@ -28,27 +27,27 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + /** * Utility class for interacting with the default trust stores on this JVM. */ public class TrustUtil { + /** + * Empty X509 certificate array, useful for indicating an empty root CA trust store. + */ + public static final X509Certificate[] EMPTY_CERTIFICATE_ARRAY = new X509Certificate[0]; private static final Logger log = LoggerFactory.getLogger(TrustUtil.class); - /** * Regex that matches a single certificate within a PEM file containing (potentially multiple) certificates. */ private static final Pattern CA_PEM_PATTERN = Pattern.compile("-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----", Pattern.DOTALL); - /** * The file containing the built-in list of trusted CAs. */ private static final String DEFAULT_TRUSTED_CA_RESOURCE = "/cacerts.pem"; - - /** - * Empty X509 certificate array, useful for indicating an empty root CA trust store. - */ - public static final X509Certificate[] EMPTY_CERTIFICATE_ARRAY = new X509Certificate[0]; - /** * Security provider used to transform PEM files into Certificates. * TODO: Modify the architecture of TrustUtil and TrustSource so that they do not need a hard-coded SecurityProviderTool. @@ -103,7 +102,7 @@ public static X509Certificate[] getBuiltinTrustedCAs() { * Returns the list of root CAs trusted by default in this JVM, according to the TrustManager returned by * {@link #getDefaultJavaTrustManager()}. */ - public static X509Certificate[] getJavaTrustedCAs() { + public static X509Certificate[] getJavaTrustedCAs() { return javaTrustedCAs.get(); } diff --git a/app/src/main/java/net/lightbody/bmp/proxy/BlacklistEntry.java b/app/src/main/java/net/lightbody/bmp/proxy/BlacklistEntry.java index 671fff9..71d078d 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/BlacklistEntry.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/BlacklistEntry.java @@ -24,8 +24,8 @@ public BlacklistEntry(String urlPattern, int statusCode) { /** * Creates a new BlacklistEntry which will match both a URL and an HTTP method * - * @param urlPattern URL pattern to blacklist - * @param statusCode status code to return for blacklisted URL + * @param urlPattern URL pattern to blacklist + * @param statusCode status code to return for blacklisted URL * @param httpMethodPattern HTTP method to match (e.g. GET, PUT, PATCH, etc.) */ public BlacklistEntry(String urlPattern, int statusCode, String httpMethodPattern) { @@ -42,7 +42,7 @@ public BlacklistEntry(String urlPattern, int statusCode, String httpMethodPatter * Determines if this BlacklistEntry matches the given URL. Attempts to match both the URL and the * HTTP method. * - * @param url possibly-blacklisted URL + * @param url possibly-blacklisted URL * @param httpMethod HTTP method this URL is being accessed with * @return true if the URL matches this BlacklistEntry */ diff --git a/app/src/main/java/net/lightbody/bmp/proxy/CaptureType.java b/app/src/main/java/net/lightbody/bmp/proxy/CaptureType.java index cf3b98c..9a6e536 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/CaptureType.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/CaptureType.java @@ -21,7 +21,6 @@ public enum CaptureType { * Non-binary HTTP request content, such as post data or other text-based request payload. * See {@link net.lightbody.bmp.util.BrowserMobHttpUtil#hasTextualContent(String)} for a list of Content-Types that * are considered non-binary. - * */ REQUEST_CONTENT, diff --git a/app/src/main/java/net/lightbody/bmp/proxy/RewriteRule.java b/app/src/main/java/net/lightbody/bmp/proxy/RewriteRule.java index c5ec97e..5faf166 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/RewriteRule.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/RewriteRule.java @@ -24,13 +24,21 @@ public String getReplace() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } RewriteRule that = (RewriteRule) o; - if (!pattern.equals(that.pattern)) return false; - if (!replace.equals(that.replace)) return false; + if (!pattern.equals(that.pattern)) { + return false; + } + if (!replace.equals(that.replace)) { + return false; + } return true; } diff --git a/app/src/main/java/net/lightbody/bmp/proxy/Whitelist.java b/app/src/main/java/net/lightbody/bmp/proxy/Whitelist.java index ea015ee..868e46a 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/Whitelist.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/Whitelist.java @@ -15,14 +15,13 @@ * whitelist reference to a new whitelist. */ public class Whitelist { - private final List patterns; - private final int statusCode; - private final boolean enabled; - /** * A disabled Whitelist. */ public static final Whitelist WHITELIST_DISABLED = new Whitelist(); + private final List patterns; + private final int statusCode; + private final boolean enabled; /** * Creates an empty, disabled Whitelist. @@ -56,7 +55,7 @@ public Whitelist(String[] patterns, int statusCode) { * Creates a whitelist for the specified patterns, returning the given statusCode when a URL does not match one of the patterns. * A null or empty collection will result in an empty whitelist. * - * @param patterns URL-matching regular expression patterns to whitelist + * @param patterns URL-matching regular expression patterns to whitelist * @param statusCode the HTTP status code to return when a request URL matches a whitelist pattern */ public Whitelist(Collection patterns, int statusCode) { diff --git a/app/src/main/java/net/lightbody/bmp/proxy/dns/BasicHostResolver.java b/app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractBasicHostResolver.java similarity index 97% rename from app/src/main/java/net/lightbody/bmp/proxy/dns/BasicHostResolver.java rename to app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractBasicHostResolver.java index b07bd6d..8378e93 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/dns/BasicHostResolver.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractBasicHostResolver.java @@ -9,7 +9,7 @@ * Use this class to supply a {@link HostResolver} to {@link net.lightbody.bmp.BrowserMobProxy#setHostNameResolver(AdvancedHostResolver)} * if you do not need {@link AdvancedHostResolver} functionality. */ -public abstract class BasicHostResolver implements AdvancedHostResolver { +public abstract class AbstractBasicHostResolver implements AdvancedHostResolver { @Override public void remapHosts(Map hostRemappings) { throw new UnsupportedOperationException(new Throwable().getStackTrace()[0].getMethodName() + " is not supported by this host resolver (" + this.getClass().getName() + ")"); diff --git a/app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractHostNameRemapper.java b/app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractHostNameRemapper.java index 253b5f4..20040e3 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractHostNameRemapper.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/dns/AbstractHostNameRemapper.java @@ -19,7 +19,7 @@ public abstract class AbstractHostNameRemapper implements AdvancedHostResolver { * Host name remappings, maintained as a reference to an ImmutableMap. The ImmutableMap type is specified explicitly because ImmutableMap * guarantees the iteration order of the map's entries. Specifying ImmutableMap also makes clear that the underlying map will never change, * and that any modifications to the host name remappings will result in an entirely new map. - * + *

* The current implementation does not actually use any of the special features of AtomicReference, but it does rely on synchronizing on * the AtomicReference when performing write operations. It could be replaced by a volatile reference to a Map and separate lock object. */ diff --git a/app/src/main/java/net/lightbody/bmp/proxy/dns/AdvancedHostResolver.java b/app/src/main/java/net/lightbody/bmp/proxy/dns/AdvancedHostResolver.java index 5773e36..78b4f6a 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/dns/AdvancedHostResolver.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/dns/AdvancedHostResolver.java @@ -71,7 +71,7 @@ public interface AdvancedHostResolver extends HostResolver { * DNS implementation. For example, the Oracle JVM's DNS cache only supports timeouts in whole seconds, so specifying a timeout of 1200ms will result * in a timeout of 1 second. * - * @param timeout maximum lookup time + * @param timeout maximum lookup time * @param timeUnit units of the timeout value */ void setPositiveDNSCacheTimeout(int timeout, TimeUnit timeUnit); @@ -83,7 +83,7 @@ public interface AdvancedHostResolver extends HostResolver { * DNS implementation. For example, the Oracle JVM's DNS cache only supports timeouts in whole seconds, so specifying a timeout of 1200ms will result * in a timeout of 1 second. * - * @param timeout maximum lookup time + * @param timeout maximum lookup time * @param timeUnit units of the timeout value */ void setNegativeDNSCacheTimeout(int timeout, TimeUnit timeUnit); diff --git a/app/src/main/java/net/lightbody/bmp/proxy/dns/ChainedHostResolver.java b/app/src/main/java/net/lightbody/bmp/proxy/dns/ChainedHostResolver.java index dd5ae00..a88fe1c 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/dns/ChainedHostResolver.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/dns/ChainedHostResolver.java @@ -25,13 +25,13 @@ *

* The atomic write methods specified by AdvancedHostResolver are: *

    - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#remapHost(String, String)}
  • - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#remapHosts(java.util.Map)}
  • - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#removeHostRemapping(String)}
  • - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#clearHostRemappings()}
  • - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#setNegativeDNSCacheTimeout(int, java.util.concurrent.TimeUnit)}
  • - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#setPositiveDNSCacheTimeout(int, java.util.concurrent.TimeUnit)}
  • - *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#clearDNSCache()}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#remapHost(String, String)}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#remapHosts(java.util.Map)}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#removeHostRemapping(String)}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#clearHostRemappings()}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#setNegativeDNSCacheTimeout(int, java.util.concurrent.TimeUnit)}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#setPositiveDNSCacheTimeout(int, java.util.concurrent.TimeUnit)}
  • + *
  • {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver#clearDNSCache()}
  • *
*/ public class ChainedHostResolver implements AdvancedHostResolver { diff --git a/app/src/main/java/net/lightbody/bmp/proxy/dns/DelegatingHostResolver.java b/app/src/main/java/net/lightbody/bmp/proxy/dns/DelegatingHostResolver.java index 7f6bdf2..7074b02 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/dns/DelegatingHostResolver.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/dns/DelegatingHostResolver.java @@ -10,7 +10,7 @@ /** * A LittleProxy HostResolver that delegates to the specified {@link net.lightbody.bmp.proxy.dns.AdvancedHostResolver} instance. This class * serves as a bridge between {@link AdvancedHostResolver} and {@link org.littleshoot.proxy.HostResolver}. -*/ + */ public class DelegatingHostResolver implements org.littleshoot.proxy.HostResolver { private volatile AdvancedHostResolver resolver; diff --git a/app/src/main/java/net/lightbody/bmp/proxy/dns/DnsJavaResolver.java b/app/src/main/java/net/lightbody/bmp/proxy/dns/DnsJavaResolver.java index 432032e..d123adf 100644 --- a/app/src/main/java/net/lightbody/bmp/proxy/dns/DnsJavaResolver.java +++ b/app/src/main/java/net/lightbody/bmp/proxy/dns/DnsJavaResolver.java @@ -1,6 +1,7 @@ package net.lightbody.bmp.proxy.dns; import com.google.common.net.InetAddresses; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xbill.DNS.AAAARecord; @@ -28,16 +29,14 @@ */ public class DnsJavaResolver extends AbstractHostNameRemapper implements AdvancedHostResolver { private static final Logger log = LoggerFactory.getLogger(DnsJavaResolver.class); - - /** - * DNS cache used for dnsjava lookups. - */ - private final Cache cache = new Cache(); - /** * Maximum number of times to retry a DNS lookup due to a failure to connect to the DNS server. */ private static final int DNS_NETWORK_FAILURE_RETRY_COUNT = 5; + /** + * DNS cache used for dnsjava lookups. + */ + private final Cache cache = new Cache(); @Override public void clearDNSCache() { diff --git a/app/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java b/app/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java index c260ca7..9947fa3 100644 --- a/app/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java +++ b/app/src/main/java/net/lightbody/bmp/util/BrowserMobHttpUtil.java @@ -3,12 +3,10 @@ import com.google.common.io.BaseEncoding; import com.google.common.net.HostAndPort; import com.google.common.net.MediaType; -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; + import net.lightbody.bmp.exception.DecompressionException; import net.lightbody.bmp.exception.UnsupportedCharsetException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,12 +22,15 @@ import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; + /** * Utility class with static methods for processing HTTP requests and responses. */ public class BrowserMobHttpUtil { - private static final Logger log = LoggerFactory.getLogger(BrowserMobHttpUtil.class); - /** * Default MIME content type if no Content-Type header is present. According to the HTTP 1.1 spec, section 7.2.1: *
@@ -41,7 +42,6 @@ public class BrowserMobHttpUtil {
      * 
*/ public static final String UNKNOWN_CONTENT_TYPE = "application/octet-stream"; - /** * The default charset when the Content-Type header does not specify a charset. According to RFC 7231 Appendix B: *
@@ -50,16 +50,16 @@ public class BrowserMobHttpUtil {
      *     Likewise, special treatment of ISO-8859-1 has been removed from the
      *     Accept-Charset header field.
      * 
- * + *

* Technically, we would have to determine the charset on a per-content-type basis, but generally speaking, UTF-8 is a * pretty safe default. (NOTE: In the previous HTTP/1.1 spec, section 3.7.1, the default charset was defined as ISO-8859-1.) */ public static final Charset DEFAULT_HTTP_CHARSET = StandardCharsets.UTF_8; - /** * Buffer size when decompressing content. */ public static final int DECOMPRESS_BUFFER_SIZE = 16192; + private static final Logger log = LoggerFactory.getLogger(BrowserMobHttpUtil.class); /** * Returns the size of the headers, including the 2 CRLFs at the end of the header block. @@ -83,13 +83,13 @@ public static long getHeaderSize(HttpHeaders headers) { * @return decompressed bytes * @throws DecompressionException thrown if the fullMessage cannot be read or decompressed for any reason */ - public static byte[] decompressContents(byte[] fullMessage,String type) throws DecompressionException { + public static byte[] decompressContents(byte[] fullMessage, String type) throws DecompressionException { InflaterInputStream reader = null; ByteArrayOutputStream uncompressed; try { - if(type.equalsIgnoreCase(HttpHeaders.Values.GZIP)) { + if (type.equalsIgnoreCase(HttpHeaders.Values.GZIP)) { reader = new GZIPInputStream(new ByteArrayInputStream(fullMessage)); - }else if(type.equalsIgnoreCase(HttpHeaders.Values.DEFLATE)) { + } else if (type.equalsIgnoreCase(HttpHeaders.Values.DEFLATE)) { reader = new InflaterInputStream(new ByteArrayInputStream(fullMessage), new Inflater(true)); } @@ -135,11 +135,11 @@ public static byte[] decompressContents(byte[] fullMessage,String type) throws D public static boolean hasTextualContent(String contentType) { return contentType != null && (contentType.startsWith("text/") || - contentType.startsWith("application/x-javascript") || - contentType.startsWith("application/javascript") || - contentType.startsWith("application/json") || - contentType.startsWith("application/xml") || - contentType.startsWith("application/xhtml+xml") + contentType.startsWith("application/x-javascript") || + contentType.startsWith("application/javascript") || + contentType.startsWith("application/json") || + contentType.startsWith("application/xml") || + contentType.startsWith("application/xhtml+xml") ); } @@ -190,7 +190,7 @@ public static Charset readCharsetInContentTypeHeader(String contentTypeHeader) t MediaType mediaType; try { - mediaType = MediaType.parse(contentTypeHeader); + mediaType = MediaType.parse(contentTypeHeader); } catch (IllegalArgumentException e) { log.info("Unable to parse Content-Type header: {}. Content-Type header will be ignored.", contentTypeHeader, e); return null; @@ -275,7 +275,7 @@ public static boolean isRedirect(HttpResponse httpResponse) { * parsing the hostname, but makes no guarantees. In general, it should be validated externally, if necessary. * * @param hostWithPort string containing a hostname and optional port - * @param portNumber port to remove from the string + * @param portNumber port to remove from the string * @return string with the specified port removed, or the original string if it did not contain the portNumber */ public static String removeMatchingPort(String hostWithPort, int portNumber) { diff --git a/app/src/main/java/net/lightbody/bmp/util/BrowserMobProxyUtil.java b/app/src/main/java/net/lightbody/bmp/util/BrowserMobProxyUtil.java index 62f33cf..f11a71c 100644 --- a/app/src/main/java/net/lightbody/bmp/util/BrowserMobProxyUtil.java +++ b/app/src/main/java/net/lightbody/bmp/util/BrowserMobProxyUtil.java @@ -2,11 +2,13 @@ import com.google.common.base.Supplier; import com.google.common.base.Suppliers; + import net.lightbody.bmp.core.har.Har; import net.lightbody.bmp.core.har.HarEntry; import net.lightbody.bmp.core.har.HarLog; import net.lightbody.bmp.core.har.HarPage; import net.lightbody.bmp.mitm.exception.UncheckedIOException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +47,7 @@ public String get() { * the specified pageRef. Does not perform a "deep copy", so any subsequent modification to the entries or pages will * be reflected in the copied har. * - * @param har existing har to copy + * @param har existing har to copy * @param pageRef last page ID to copy * @return copy of a {@link Har} with entries and pages from the original har, or null if the input har is null */ diff --git a/app/src/main/java/net/lightbody/bmp/util/ClasspathResourceUtil.java b/app/src/main/java/net/lightbody/bmp/util/ClasspathResourceUtil.java index 66bd3b1..2cb339c 100644 --- a/app/src/main/java/net/lightbody/bmp/util/ClasspathResourceUtil.java +++ b/app/src/main/java/net/lightbody/bmp/util/ClasspathResourceUtil.java @@ -1,6 +1,7 @@ package net.lightbody.bmp.util; import com.google.common.io.CharStreams; + import net.lightbody.bmp.mitm.exception.UncheckedIOException; import java.io.FileNotFoundException; @@ -21,7 +22,7 @@ public class ClasspathResourceUtil { * method throws a FileNotFoundException wrapped in an UncheckedIOException. * * @param resource classpath resource to load - * @param charset charset to use to decode the classpath resource + * @param charset charset to use to decode the classpath resource * @return a String * @throws UncheckedIOException if the classpath resource cannot be found or cannot be read for any reason */ diff --git a/app/src/main/java/net/lightbody/bmp/util/HttpMessageContents.java b/app/src/main/java/net/lightbody/bmp/util/HttpMessageContents.java index 969a4d8..d904f1e 100644 --- a/app/src/main/java/net/lightbody/bmp/util/HttpMessageContents.java +++ b/app/src/main/java/net/lightbody/bmp/util/HttpMessageContents.java @@ -1,17 +1,19 @@ package net.lightbody.bmp.util; -import io.netty.handler.codec.http.FullHttpMessage; -import io.netty.handler.codec.http.HttpHeaders; import net.lightbody.bmp.exception.UnsupportedCharsetException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.Charset; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.HttpHeaders; + /** * Helper class to wrap the contents of an {@link io.netty.handler.codec.http.HttpMessage}. Contains convenience methods to extract and * manipulate the contents of the wrapped {@link io.netty.handler.codec.http.HttpMessage}. - * + *

* TODO: Currently this class only wraps FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well */ public class HttpMessageContents { @@ -27,36 +29,6 @@ public HttpMessageContents(FullHttpMessage httpMessage) { this.httpMessage = httpMessage; } - /** - * Replaces the contents of the wrapped HttpMessage with the specified text contents, encoding them in the character set specified by the - * message's Content-Type header. Note that this method does not update the Content-Type header, so if the content type will change as a - * result of this call, the Content-Type header should be updated before calling this method. - * - * @param newContents new message contents - */ - public void setTextContents(String newContents) { - HttpObjectUtil.replaceTextHttpEntityBody(httpMessage, newContents); - - // replaced the contents, so clear the local cache - textContents = null; - binaryContents = null; - } - - /** - * Replaces the contents of the wrapped HttpMessage with the specified binary contents. Note that this method does not update the - * Content-Type header, so if the content type will change as a result of this call, the Content-Type header should be updated before - * calling this method. - * - * @param newBinaryContents new message contents - */ - public void setBinaryContents(byte[] newBinaryContents) { - HttpObjectUtil.replaceBinaryHttpEntityBody(httpMessage, newBinaryContents); - - // replaced the contents, so clear the local cache - binaryContents = null; - textContents = null; - } - /** * Retrieves the contents of this message as a String, decoded according to the message's Content-Type header. This method caches * the contents, so repeated calls to this method should not incur a penalty; however, modifications to the message contents @@ -74,6 +46,21 @@ public String getTextContents() throws java.nio.charset.UnsupportedCharsetExcept return textContents; } + /** + * Replaces the contents of the wrapped HttpMessage with the specified text contents, encoding them in the character set specified by the + * message's Content-Type header. Note that this method does not update the Content-Type header, so if the content type will change as a + * result of this call, the Content-Type header should be updated before calling this method. + * + * @param newContents new message contents + */ + public void setTextContents(String newContents) { + HttpObjectUtil.replaceTextHttpEntityBody(httpMessage, newContents); + + // replaced the contents, so clear the local cache + textContents = null; + binaryContents = null; + } + /** * Retrieves the binary contents of this message. This method caches the contents, so repeated calls to this method should not incur a * penalty; however, modifications to the message contents outside of this class will result in stale data returned from this method. @@ -89,6 +76,21 @@ public byte[] getBinaryContents() { return binaryContents; } + /** + * Replaces the contents of the wrapped HttpMessage with the specified binary contents. Note that this method does not update the + * Content-Type header, so if the content type will change as a result of this call, the Content-Type header should be updated before + * calling this method. + * + * @param newBinaryContents new message contents + */ + public void setBinaryContents(byte[] newBinaryContents) { + HttpObjectUtil.replaceBinaryHttpEntityBody(httpMessage, newBinaryContents); + + // replaced the contents, so clear the local cache + binaryContents = null; + textContents = null; + } + /** * Retrieves the Content-Type header of this message. If no Content-Type is present, returns the assumed default Content-Type (see * {@link BrowserMobHttpUtil#UNKNOWN_CONTENT_TYPE}). diff --git a/app/src/main/java/net/lightbody/bmp/util/HttpObjectUtil.java b/app/src/main/java/net/lightbody/bmp/util/HttpObjectUtil.java index 169a878..e93eccd 100644 --- a/app/src/main/java/net/lightbody/bmp/util/HttpObjectUtil.java +++ b/app/src/main/java/net/lightbody/bmp/util/HttpObjectUtil.java @@ -1,15 +1,17 @@ package net.lightbody.bmp.util; -import io.netty.handler.codec.http.FullHttpMessage; -import io.netty.handler.codec.http.HttpContent; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpMessage; import net.lightbody.bmp.exception.UnsupportedCharsetException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.charset.Charset; +import io.netty.handler.codec.http.FullHttpMessage; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMessage; + /** * Utility class to assist with manipulation of {@link io.netty.handler.codec.http.HttpObject} instances, including * {@link io.netty.handler.codec.http.HttpMessage} and {@link io.netty.handler.codec.http.HttpContent}. @@ -21,10 +23,10 @@ public class HttpObjectUtil { * Replaces the entity body of the message with the specified contents. Encodes the message contents according to charset in the message's * Content-Type header, or uses {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET} if none is specified. * Note: If the charset of the message is not supported on this platform, this will throw an {@link java.nio.charset.UnsupportedCharsetException}. - * + *

* TODO: Currently this method only works for FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well * - * @param message the HTTP message to manipulate + * @param message the HTTP message to manipulate * @param newContents the new entity body contents * @throws java.nio.charset.UnsupportedCharsetException if the charset in the message is not supported on this platform */ @@ -36,7 +38,7 @@ public static void replaceTextHttpEntityBody(FullHttpMessage message, String new try { messageCharset = BrowserMobHttpUtil.readCharsetInContentTypeHeader(contentTypeHeader); } catch (UnsupportedCharsetException e) { - java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause() ; + java.nio.charset.UnsupportedCharsetException cause = e.getUnsupportedCharsetExceptionCause(); log.error("Found unsupported character set in Content-Type header '{}' while attempting to replace contents of HTTP message.", contentTypeHeader, cause); throw cause; @@ -56,7 +58,7 @@ public static void replaceTextHttpEntityBody(FullHttpMessage message, String new * Replaces an HTTP entity body with the specified binary contents. * TODO: Currently this method only works for FullHttpMessages, since it must modify the Content-Length header; determine if this may be applied to chunked messages as well * - * @param message the HTTP message to manipulate + * @param message the HTTP message to manipulate * @param newBinaryContents the new entity body contents */ public static void replaceBinaryHttpEntityBody(FullHttpMessage message, byte[] newBinaryContents) { @@ -74,7 +76,7 @@ public static void replaceBinaryHttpEntityBody(FullHttpMessage message, byte[] n * the character set is not specified or is unknown, you still must specify a suitable default charset (see {@link BrowserMobHttpUtil#DEFAULT_HTTP_CHARSET}). * * @param httpContent HTTP content object to extract the entity body from - * @param charset character set of the entity body + * @param charset character set of the entity body * @return String representation of the entity body * @throws IllegalArgumentException if the charset is null */ diff --git a/app/src/main/java/net/lightbody/bmp/util/HttpUtil.java b/app/src/main/java/net/lightbody/bmp/util/HttpUtil.java index 2cc2a21..360db12 100644 --- a/app/src/main/java/net/lightbody/bmp/util/HttpUtil.java +++ b/app/src/main/java/net/lightbody/bmp/util/HttpUtil.java @@ -1,14 +1,15 @@ package net.lightbody.bmp.util; import com.google.common.net.HostAndPort; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.HttpRequest; import java.net.URI; import java.net.URISyntaxException; import java.util.List; import java.util.Locale; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpRequest; + /** * Contains utility methods for netty {@link HttpRequest} and related objects. */ diff --git a/app/src/main/java/org/littleshoot/proxy/mitm/Authority.java b/app/src/main/java/org/littleshoot/proxy/mitm/Authority.java index a7451f5..95a616f 100755 --- a/app/src/main/java/org/littleshoot/proxy/mitm/Authority.java +++ b/app/src/main/java/org/littleshoot/proxy/mitm/Authority.java @@ -6,7 +6,7 @@ /** * Parameter object holding personal informations given to a SSLEngineSource. - * + *

* XXX consider to inline within the interface SslEngineSource, if MITM is core */ public class Authority { @@ -37,7 +37,7 @@ public Authority() { password = "Be Your Own Lantern".toCharArray(); organization = "LittleProxy-mitm"; // proxy name commonName = organization + ", describe proxy here"; // MITM is bad - // normally + // normally organizationalUnitName = "Certificate Authority"; certOrganization = organization; // proxy name certOrganizationalUnitName = organization @@ -49,9 +49,9 @@ public Authority() { * authority informations */ public Authority(File keyStoreDir, String alias, char[] password, - String commonName, String organization, - String organizationalUnitName, String certOrganization, - String certOrganizationalUnitName) { + String commonName, String organization, + String organizationalUnitName, String certOrganization, + String certOrganizationalUnitName) { super(); this.keyStoreDir = keyStoreDir; this.alias = alias; diff --git a/app/src/main/java/org/littleshoot/proxy/mitm/BouncyCastleSslEngineSource.java b/app/src/main/java/org/littleshoot/proxy/mitm/BouncyCastleSslEngineSource.java index 24973dc..c91044c 100755 --- a/app/src/main/java/org/littleshoot/proxy/mitm/BouncyCastleSslEngineSource.java +++ b/app/src/main/java/org/littleshoot/proxy/mitm/BouncyCastleSslEngineSource.java @@ -1,6 +1,14 @@ package org.littleshoot.proxy.mitm; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + +import org.apache.commons.io.IOUtils; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.OperatorCreationException; +import org.littleshoot.proxy.SslEngineSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; @@ -28,28 +36,20 @@ import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManager; -import org.apache.commons.io.IOUtils; -import org.bouncycastle.openssl.jcajce.JcaPEMWriter; -import org.bouncycastle.operator.OperatorCreationException; -import org.littleshoot.proxy.SslEngineSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; /** * A {@link SslEngineSource} which creates a key store with a Root Certificate * Authority. The certificates are generated lazily if the given key store file * doesn't yet exist. - * + *

* The root certificate is exported in PEM format to be used in a browser. The * proxy application presents for every host a dynamically created certificate * to the browser, signed by this certificate authority. - * + *

* This facilitates the proxy to handle as a "Man In The Middle" to filter the * decrypted content in clear text. - * + *

* The hard part was done by mawoki. It's derived from Zed Attack Proxy (ZAP). * ZAP is an HTTP/HTTPS proxy for assessing web application security. Copyright * 2011 mawoki@ymail.com Licensed under the Apache License, Version 2.0 @@ -84,24 +84,19 @@ public class BouncyCastleSslEngineSource implements SslEngineSource { * initializes a SSL context. Exceptions will be thrown to let the manager * decide how to react. Don't install a MITM manager in the proxy in case of * a failure. - * - * @param authority - * a parameter object to provide personal informations of the - * Certificate Authority and the dynamic certificates. - * + * + * @param authority a parameter object to provide personal informations of the + * Certificate Authority and the dynamic certificates. * @param trustAllServers - * * @param sendCerts - * - * @param sslContexts - * a cache to store dynamically created server certificates. - * Generation takes between 50 to 500ms, but only once per - * thread, since there is a connection cache too. It's save to - * give a null cache to prevent memory or locking issues. + * @param sslContexts a cache to store dynamically created server certificates. + * Generation takes between 50 to 500ms, but only once per + * thread, since there is a connection cache too. It's save to + * give a null cache to prevent memory or locking issues. */ public BouncyCastleSslEngineSource(Authority authority, - boolean trustAllServers, boolean sendCerts, - Cache sslContexts) + boolean trustAllServers, boolean sendCerts, + Cache sslContexts) throws GeneralSecurityException, OperatorCreationException, RootCertificateException, IOException { this.authority = authority; @@ -118,17 +113,14 @@ public BouncyCastleSslEngineSource(Authority authority, * dynamically created server certificates. Exceptions will be thrown to let * the manager decide how to react. Don't install a MITM manager in the * proxy in case of a failure. - * - * @param authority - * a parameter object to provide personal informations of the - * Certificate Authority and the dynamic certificates. - * + * + * @param authority a parameter object to provide personal informations of the + * Certificate Authority and the dynamic certificates. * @param trustAllServers - * * @param sendCerts */ public BouncyCastleSslEngineSource(Authority authority, - boolean trustAllServers, boolean sendCerts) + boolean trustAllServers, boolean sendCerts) throws RootCertificateException, GeneralSecurityException, IOException, OperatorCreationException { this(authority, trustAllServers, sendCerts, @@ -145,7 +137,9 @@ private static Cache initDefaultCertificateCache() { private void filterWeakCipherSuites(SSLEngine sslEngine) { List ciphers = new LinkedList(); for (String each : sslEngine.getEnabledCipherSuites()) { - if (each.equals("TLS_DHE_RSA_WITH_AES_128_CBC_SHA") || each.equals("TLS_DHE_RSA_WITH_AES_256_CBC_SHA")) { + if ("TLS_DHE_RSA_WITH_AES_128_CBC_SHA".equals(each) + || "TLS_DHE_RSA_WITH_AES_256_CBC_SHA".equals(each) + ) { LOG.debug("Removed cipher {}", each); } else { ciphers.add(each); @@ -166,6 +160,7 @@ private void filterWeakCipherSuites(SSLEngine sslEngine) { } } + @Override public SSLEngine newSslEngine() { SSLEngine sslEngine = sslContext.createSSLEngine(); filterWeakCipherSuites(sslEngine); @@ -246,7 +241,7 @@ private void initializeSSLContext() throws GeneralSecurityException, trustManagers = InsecureTrustManagerFactory.INSTANCE .getTrustManagers(); } else { - trustManagers = new TrustManager[] { new MergeTrustManager(ks) }; + trustManagers = new TrustManager[]{new MergeTrustManager(ks)}; } KeyManager[] keyManagers; @@ -282,25 +277,17 @@ private KeyStore loadKeyStore() throws GeneralSecurityException, * Generates an 1024 bit RSA key pair using SHA1PRNG. Thoughts: 2048 takes * much longer time on older CPUs. And for almost every client, 1024 is * sufficient. - * + *

* Derived from Zed Attack Proxy (ZAP). ZAP is an HTTP/HTTPS proxy for * assessing web application security. Copyright 2011 mawoki@ymail.com * Licensed under the Apache License, Version 2.0 - * - * @param commonName - * the common name to use in the server certificate - * - * @param subjectAlternativeNames - * a List of the subject alternative names to use in the server - * certificate, could be empty, but must not be null - * - * @see org.parosproxy.paros.security.SslCertificateServiceImpl. - * createCertForHost(String) - * @see org.parosproxy.paros.network.SSLConnector.getTunnelSSLSocketFactory( - * String) + * + * @param commonName the common name to use in the server certificate + * @param subjectAlternativeNames a List of the subject alternative names to use in the server + * certificate, could be empty, but must not be null */ public SSLEngine createCertForHost(final String commonName, - final SubjectAlternativeNameHolder subjectAlternativeNames) + final SubjectAlternativeNameHolder subjectAlternativeNames) throws GeneralSecurityException, OperatorCreationException, IOException, ExecutionException { if (commonName == null) { @@ -328,7 +315,7 @@ public SSLContext call() throws Exception { } private SSLContext createServerContext(String commonName, - SubjectAlternativeNameHolder subjectAlternativeNames) + SubjectAlternativeNameHolder subjectAlternativeNames) throws GeneralSecurityException, IOException, OperatorCreationException { @@ -346,7 +333,7 @@ private SSLContext createServerContext(String commonName, } public void initializeServerCertificates(String commonName, - SubjectAlternativeNameHolder subjectAlternativeNames) + SubjectAlternativeNameHolder subjectAlternativeNames) throws GeneralSecurityException, OperatorCreationException, IOException { diff --git a/app/src/main/java/org/littleshoot/proxy/mitm/CertificateHelper.java b/app/src/main/java/org/littleshoot/proxy/mitm/CertificateHelper.java index a4358e2..fd823e2 100755 --- a/app/src/main/java/org/littleshoot/proxy/mitm/CertificateHelper.java +++ b/app/src/main/java/org/littleshoot/proxy/mitm/CertificateHelper.java @@ -1,5 +1,31 @@ package org.littleshoot.proxy.mitm; +import org.apache.commons.io.IOUtils; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.bc.BcX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.math.BigInteger; @@ -30,46 +56,12 @@ import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; -import org.apache.commons.io.IOUtils; -import org.bouncycastle.asn1.ASN1EncodableVector; -import org.bouncycastle.asn1.ASN1InputStream; -import org.bouncycastle.asn1.ASN1Sequence; -import org.bouncycastle.asn1.DERSequence; -import org.bouncycastle.asn1.x500.X500Name; -import org.bouncycastle.asn1.x500.X500NameBuilder; -import org.bouncycastle.asn1.x500.style.BCStyle; -import org.bouncycastle.asn1.x509.BasicConstraints; -import org.bouncycastle.asn1.x509.Extension; -import org.bouncycastle.asn1.x509.KeyPurposeId; -import org.bouncycastle.asn1.x509.KeyUsage; -import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; -import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; -import org.bouncycastle.cert.X509CertificateHolder; -import org.bouncycastle.cert.X509v3CertificateBuilder; -import org.bouncycastle.cert.bc.BcX509ExtensionUtils; -import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; -import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.operator.ContentSigner; -import org.bouncycastle.operator.OperatorCreationException; -import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - public final class CertificateHelper { - private static final Logger log = LoggerFactory.getLogger(CertificateHelper.class); - public static final String PROVIDER_NAME = BouncyCastleProvider.PROVIDER_NAME; - - static { - Security.addProvider(new BouncyCastleProvider()); - } - + private static final Logger log = LoggerFactory.getLogger(CertificateHelper.class); private static final String KEYGEN_ALGORITHM = "RSA"; - private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; - /** * The signature algorithm starting with the message digest to use when * signing certificates. On 64-bit systems this should be set to SHA512, on @@ -78,29 +70,25 @@ public final class CertificateHelper { * http://crypto.stackexchange.com/questions/26336/sha512-faster-than-sha256 */ private static final String SIGNATURE_ALGORITHM = (is32BitJvm() ? "SHA256" : "SHA512") + "WithRSAEncryption"; - private static final int ROOT_KEYSIZE = 2048; - private static final int FAKE_KEYSIZE = 1024; - - /** The milliseconds of a day */ + /** + * The milliseconds of a day + */ private static final long ONE_DAY = 86400000L; - /** * Current time minus 1 year, just in case software clock goes back due to * time synchronization */ private static final Date NOT_BEFORE = new Date(System.currentTimeMillis() - ONE_DAY * 365); - /** * The maximum possible value in X.509 specification: 9999-12-31 23:59:59, * new Date(253402300799000L), but Apple iOS 8 fails with a certificate * expiration date grater than Mon, 24 Jan 6084 02:07:59 GMT (issue #6). - * + *

* Hundred years in the future from starting the proxy should be enough. */ private static final Date NOT_AFTER = new Date(System.currentTimeMillis() + ONE_DAY * 365 * 100); - /** * Enforce TLS 1.2 if available, since it's not default up to Java 8. *

@@ -120,7 +108,12 @@ public final class CertificateHelper { */ private static final String SSL_CONTEXT_FALLBACK_PROTOCOL = "TLSv1"; - private CertificateHelper() {} + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private CertificateHelper() { + } public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException { @@ -139,7 +132,7 @@ public static KeyPair generateKeyPair(int keySize) * if sun.arch.data.model explicitly indicates a 32-bit JVM. * * @return true if we can determine definitively that this is a 32-bit JVM, - * otherwise false + * otherwise false */ private static boolean is32BitJvm() { Integer bits = Integer.getInteger("sun.arch.data.model"); @@ -147,7 +140,7 @@ private static boolean is32BitJvm() { } public static KeyStore createRootCertificate(Authority authority, - String keyStoreType) throws NoSuchAlgorithmException, + String keyStoreType) throws NoSuchAlgorithmException, NoSuchProviderException, IOException, OperatorCreationException, CertificateException, KeyStoreException { @@ -189,7 +182,7 @@ public static KeyStore createRootCertificate(Authority authority, .getInstance(keyStoreType/* , PROVIDER_NAME */); result.load(null, null); result.setKeyEntry(authority.alias(), keyPair.getPrivate(), - authority.password(), new Certificate[] { cert }); + authority.password(), new Certificate[]{cert}); return result; } @@ -208,8 +201,8 @@ private static SubjectKeyIdentifier createSubjectKeyIdentifier(Key key) } public static KeyStore createServerCertificate(String commonName, - SubjectAlternativeNameHolder subjectAlternativeNames, - Authority authority, Certificate caCert, PrivateKey caPrivKey) + SubjectAlternativeNameHolder subjectAlternativeNames, + Authority authority, Certificate caCert, PrivateKey caPrivKey) throws NoSuchAlgorithmException, NoSuchProviderException, IOException, OperatorCreationException, CertificateException, InvalidKeyException, SignatureException, KeyStoreException { @@ -242,9 +235,9 @@ public static KeyStore createServerCertificate(String commonName, cert.verify(caCert.getPublicKey()); KeyStore result = KeyStore.getInstance(KeyStore.getDefaultType() - /* , PROVIDER_NAME */); + /* , PROVIDER_NAME */); result.load(null, null); - Certificate[] chain = { cert, caCert }; + Certificate[] chain = {cert, caCert}; result.setKeyEntry(authority.alias(), keyPair.getPrivate(), authority.password(), chain); @@ -266,24 +259,24 @@ public static TrustManager[] getTrustManagers(KeyStore keyStore) NoSuchProviderException { String trustManAlg = TrustManagerFactory.getDefaultAlgorithm(); TrustManagerFactory tmf = TrustManagerFactory.getInstance(trustManAlg - /* , PROVIDER_NAME */); + /* , PROVIDER_NAME */); tmf.init(keyStore); return tmf.getTrustManagers(); } public static KeyManager[] getKeyManagers(KeyStore keyStore, - Authority authority) throws NoSuchAlgorithmException, + Authority authority) throws NoSuchAlgorithmException, NoSuchProviderException, UnrecoverableKeyException, KeyStoreException { String keyManAlg = KeyManagerFactory.getDefaultAlgorithm(); KeyManagerFactory kmf = KeyManagerFactory.getInstance(keyManAlg - /* , PROVIDER_NAME */); + /* , PROVIDER_NAME */); kmf.init(keyStore, authority.password()); return kmf.getKeyManagers(); } public static SSLContext newClientContext(KeyManager[] keyManagers, - TrustManager[] trustManagers) throws NoSuchAlgorithmException, + TrustManager[] trustManagers) throws NoSuchAlgorithmException, KeyManagementException, NoSuchProviderException { SSLContext result = newSSLContext(); result.init(keyManagers, trustManagers, null); @@ -304,12 +297,12 @@ private static SSLContext newSSLContext() throws NoSuchAlgorithmException { try { log.debug("Using protocol {}", SSL_CONTEXT_PROTOCOL); return SSLContext.getInstance(SSL_CONTEXT_PROTOCOL - /* , PROVIDER_NAME */); + /* , PROVIDER_NAME */); } catch (NoSuchAlgorithmException e) { log.warn("Protocol {} not available, falling back to {}", SSL_CONTEXT_PROTOCOL, SSL_CONTEXT_FALLBACK_PROTOCOL); return SSLContext.getInstance(SSL_CONTEXT_FALLBACK_PROTOCOL - /* , PROVIDER_NAME */); + /* , PROVIDER_NAME */); } } diff --git a/app/src/main/java/org/littleshoot/proxy/mitm/CertificateSniffingMitmManager.java b/app/src/main/java/org/littleshoot/proxy/mitm/CertificateSniffingMitmManager.java index 0bdd0c5..4189e77 100755 --- a/app/src/main/java/org/littleshoot/proxy/mitm/CertificateSniffingMitmManager.java +++ b/app/src/main/java/org/littleshoot/proxy/mitm/CertificateSniffingMitmManager.java @@ -1,5 +1,9 @@ package org.littleshoot.proxy.mitm; +import org.littleshoot.proxy.MitmManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.security.cert.Certificate; import java.security.cert.X509Certificate; @@ -7,10 +11,6 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; -import org.littleshoot.proxy.MitmManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import io.netty.handler.codec.http.HttpRequest; /** @@ -39,14 +39,17 @@ public CertificateSniffingMitmManager(Authority authority) } } + @Override public SSLEngine serverSslEngine(String peerHost, int peerPort) { return sslEngineSource.newSslEngine(peerHost, peerPort); } + @Override public SSLEngine serverSslEngine() { return sslEngineSource.newSslEngine(); } + @Override public SSLEngine clientSslEngineFor(HttpRequest httpRequest, SSLSession serverSslSession) { try { X509Certificate upstreamCert = getCertificateFromSession(serverSslSession); diff --git a/app/src/main/java/org/littleshoot/proxy/mitm/SubjectAlternativeNameHolder.java b/app/src/main/java/org/littleshoot/proxy/mitm/SubjectAlternativeNameHolder.java index b24c750..7973e14 100755 --- a/app/src/main/java/org/littleshoot/proxy/mitm/SubjectAlternativeNameHolder.java +++ b/app/src/main/java/org/littleshoot/proxy/mitm/SubjectAlternativeNameHolder.java @@ -1,11 +1,5 @@ package org.littleshoot.proxy.mitm; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - import org.bouncycastle.asn1.ASN1Encodable; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.x509.Extension; @@ -13,6 +7,12 @@ import org.bouncycastle.cert.CertIOException; import org.bouncycastle.cert.X509v3CertificateBuilder; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public class SubjectAlternativeNameHolder { private static final Pattern TAGS_PATTERN = Pattern.compile("[" diff --git a/app/src/main/res/drawable-v21/ic_info_black_24dp.xml b/app/src/main/res/drawable-v21/ic_info_black_24dp.xml index 34b8202..158e2a7 100644 --- a/app/src/main/res/drawable-v21/ic_info_black_24dp.xml +++ b/app/src/main/res/drawable-v21/ic_info_black_24dp.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_camera.xml b/app/src/main/res/drawable-v21/ic_menu_camera.xml index 0d9ea10..634fe92 100644 --- a/app/src/main/res/drawable-v21/ic_menu_camera.xml +++ b/app/src/main/res/drawable-v21/ic_menu_camera.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_gallery.xml b/app/src/main/res/drawable-v21/ic_menu_gallery.xml index 9e11b4a..10046e2 100644 --- a/app/src/main/res/drawable-v21/ic_menu_gallery.xml +++ b/app/src/main/res/drawable-v21/ic_menu_gallery.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_manage.xml b/app/src/main/res/drawable-v21/ic_menu_manage.xml index c1be60b..aeb047d 100644 --- a/app/src/main/res/drawable-v21/ic_menu_manage.xml +++ b/app/src/main/res/drawable-v21/ic_menu_manage.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_send.xml b/app/src/main/res/drawable-v21/ic_menu_send.xml index 00c668c..fdf1c90 100644 --- a/app/src/main/res/drawable-v21/ic_menu_send.xml +++ b/app/src/main/res/drawable-v21/ic_menu_send.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_share.xml b/app/src/main/res/drawable-v21/ic_menu_share.xml index a28fb9e..338d95a 100644 --- a/app/src/main/res/drawable-v21/ic_menu_share.xml +++ b/app/src/main/res/drawable-v21/ic_menu_share.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_slideshow.xml b/app/src/main/res/drawable-v21/ic_menu_slideshow.xml index 7c370ac..112a29d 100644 --- a/app/src/main/res/drawable-v21/ic_menu_slideshow.xml +++ b/app/src/main/res/drawable-v21/ic_menu_slideshow.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_menu_webview.xml b/app/src/main/res/drawable-v21/ic_menu_webview.xml index a8bdeeb..0520282 100644 --- a/app/src/main/res/drawable-v21/ic_menu_webview.xml +++ b/app/src/main/res/drawable-v21/ic_menu_webview.xml @@ -5,5 +5,5 @@ android:viewportHeight="24.0"> + android:pathData="M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml b/app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml index e3400cf..81db489 100644 --- a/app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml +++ b/app/src/main/res/drawable-v21/ic_notifications_black_24dp.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable-v21/ic_sync_black_24dp.xml b/app/src/main/res/drawable-v21/ic_sync_black_24dp.xml index 5a283aa..5ede6fb 100644 --- a/app/src/main/res/drawable-v21/ic_sync_black_24dp.xml +++ b/app/src/main/res/drawable-v21/ic_sync_black_24dp.xml @@ -1,8 +1,8 @@ + android:viewportWidth="24.0" + android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/jz_toast_bg.xml b/app/src/main/res/drawable/jz_toast_bg.xml index 0da1ca3..cb78b93 100644 --- a/app/src/main/res/drawable/jz_toast_bg.xml +++ b/app/src/main/res/drawable/jz_toast_bg.xml @@ -2,8 +2,8 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/layout-v21/fragment_webview.xml b/app/src/main/res/layout-v21/fragment_webview.xml index 056fa53..6ed0c52 100644 --- a/app/src/main/res/layout-v21/fragment_webview.xml +++ b/app/src/main/res/layout-v21/fragment_webview.xml @@ -6,42 +6,42 @@ + android:layout_marginTop="-6.5dp" + android:layout_marginBottom="15dp" />