From ff206e56435b47630dbf58c4d0e9f10d86b123d6 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Sat, 5 Jan 2013 20:07:32 +0800 Subject: [PATCH] update to 1.3 with polipo support --- .gitignore | 3 + AndroidManifest.xml | 4 +- assets/polipo | Bin 0 -> 136812 bytes jni/Android.mk | 13 + jni/polipo/atom.c | 376 +++ jni/polipo/atom.h | 58 + jni/polipo/auth.c | 91 + jni/polipo/auth.h | 24 + jni/polipo/chunk.c | 469 +++ jni/polipo/chunk.h | 51 + jni/polipo/client.c | 2148 ++++++++++++ jni/polipo/client.h | 64 + jni/polipo/config.c | 869 +++++ jni/polipo/config.h | 68 + jni/polipo/dirent_compat.c | 149 + jni/polipo/dirent_compat.h | 50 + jni/polipo/diskcache.c | 2573 +++++++++++++++ jni/polipo/diskcache.h | 71 + jni/polipo/dns.c | 1747 ++++++++++ jni/polipo/dns.h | 48 + jni/polipo/event.c | 834 +++++ jni/polipo/event.h | 90 + jni/polipo/forbidden.c | 835 +++++ jni/polipo/forbidden.h | 50 + jni/polipo/fts_compat.c | 365 +++ jni/polipo/fts_compat.h | 69 + jni/polipo/ftsimport.c | 12 + jni/polipo/ftsimport.h | 6 + jni/polipo/http.c | 1087 ++++++ jni/polipo/http.h | 175 + jni/polipo/http_parse.c | 1548 +++++++++ jni/polipo/http_parse.h | 58 + jni/polipo/io.c | 1140 +++++++ jni/polipo/io.h | 161 + jni/polipo/local.c | 743 +++++ jni/polipo/local.h | 48 + jni/polipo/log.c | 492 +++ jni/polipo/log.h | 140 + jni/polipo/main.c | 169 + jni/polipo/md5.c | 322 ++ jni/polipo/md5.h | 60 + jni/polipo/md5import.c | 15 + jni/polipo/md5import.h | 9 + jni/polipo/mingw.c | 505 +++ jni/polipo/mingw.h | 197 ++ jni/polipo/object.c | 1040 ++++++ jni/polipo/object.h | 197 ++ jni/polipo/parse_time.c | 215 ++ jni/polipo/parse_time.h | 27 + jni/polipo/polipo.h | 241 ++ jni/polipo/server.c | 2899 +++++++++++++++++ jni/polipo/server.h | 113 + jni/polipo/socks.c | 519 +++ jni/polipo/socks.h | 36 + jni/polipo/tunnel.c | 572 ++++ jni/polipo/tunnel.h | 50 + jni/polipo/util.c | 839 +++++ jni/polipo/util.h | 108 + libs/armeabi/pdnsd | Bin 153256 -> 0 bytes libs/armeabi/shadowsocks | Bin 128488 -> 0 bytes pom.xml | 2 +- res/values/strings.xml | 3 + res/xml/shadowsocks_preference.xml | 7 + src/com/github/shadowsocks/Shadowsocks.java | 3 + .../shadowsocks/ShadowsocksService.java | 58 +- 65 files changed, 24909 insertions(+), 26 deletions(-) create mode 100755 assets/polipo create mode 100644 jni/polipo/atom.c create mode 100644 jni/polipo/atom.h create mode 100644 jni/polipo/auth.c create mode 100644 jni/polipo/auth.h create mode 100644 jni/polipo/chunk.c create mode 100644 jni/polipo/chunk.h create mode 100644 jni/polipo/client.c create mode 100644 jni/polipo/client.h create mode 100644 jni/polipo/config.c create mode 100644 jni/polipo/config.h create mode 100644 jni/polipo/dirent_compat.c create mode 100644 jni/polipo/dirent_compat.h create mode 100644 jni/polipo/diskcache.c create mode 100644 jni/polipo/diskcache.h create mode 100644 jni/polipo/dns.c create mode 100644 jni/polipo/dns.h create mode 100644 jni/polipo/event.c create mode 100644 jni/polipo/event.h create mode 100644 jni/polipo/forbidden.c create mode 100644 jni/polipo/forbidden.h create mode 100644 jni/polipo/fts_compat.c create mode 100644 jni/polipo/fts_compat.h create mode 100644 jni/polipo/ftsimport.c create mode 100644 jni/polipo/ftsimport.h create mode 100644 jni/polipo/http.c create mode 100644 jni/polipo/http.h create mode 100644 jni/polipo/http_parse.c create mode 100644 jni/polipo/http_parse.h create mode 100644 jni/polipo/io.c create mode 100644 jni/polipo/io.h create mode 100644 jni/polipo/local.c create mode 100644 jni/polipo/local.h create mode 100644 jni/polipo/log.c create mode 100644 jni/polipo/log.h create mode 100644 jni/polipo/main.c create mode 100644 jni/polipo/md5.c create mode 100644 jni/polipo/md5.h create mode 100644 jni/polipo/md5import.c create mode 100644 jni/polipo/md5import.h create mode 100644 jni/polipo/mingw.c create mode 100644 jni/polipo/mingw.h create mode 100644 jni/polipo/object.c create mode 100644 jni/polipo/object.h create mode 100644 jni/polipo/parse_time.c create mode 100644 jni/polipo/parse_time.h create mode 100644 jni/polipo/polipo.h create mode 100644 jni/polipo/server.c create mode 100644 jni/polipo/server.h create mode 100644 jni/polipo/socks.c create mode 100644 jni/polipo/socks.h create mode 100644 jni/polipo/tunnel.c create mode 100644 jni/polipo/tunnel.h create mode 100644 jni/polipo/util.c create mode 100644 jni/polipo/util.h delete mode 100755 libs/armeabi/pdnsd delete mode 100755 libs/armeabi/shadowsocks diff --git a/.gitignore b/.gitignore index 4663e7f6..ee44a9ad 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ tests/bin tests/gen tests/local.properties NUL +libs/armeabi/polipo +libs/armeabi/pdnsd +libs/armeabi/shadowsocks #Intellij IDEA *.iml diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 1f3fd5b9..75679785 100755 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,8 +2,8 @@ + android:versionCode="10" + android:versionName="1.3"> diff --git a/assets/polipo b/assets/polipo new file mode 100755 index 0000000000000000000000000000000000000000..9c473340dd5b831c5cb6ece4b21dc8556155d3b5 GIT binary patch literal 136812 zcmbTf34B!56+eF8EZIYb1jrdB5DMy zEG|HzWiC;19)|`f<~z`h>nCHE}q1ZAOz63j0zvf z58xpc!YC+d9Qcf9d=LYk0*ynk3_yfb_#i7(K`5wcn8p`B0Dhpx2W}L&DU`_N(s){< z(_sp7y4D9h;3?30(K41^FJBMzfBU2H^bd6?8>nQ+Kn(H+q=ge1=(7}-;d%e^Rd|rX zNdD8dJ-)5HsB~IE$>wRBN;YpS+Ddc%-#^;Uk-umVks}N+<>Lc*LjXTHfKLnH=YY?x zBZNK|jh^8G;ui++RzH3;=nZ~6tgCk6161NdnH{LBFUo&cT;;1>q)%LDjz0eooy|3m=)s{sC`0REK#9!vf0 zKPsQM0{Gts@C^a{rvdz*0{FiM@ZSdT7r=*JVdQm!uD4F``Bxbt0#m*f_&Vb0x=DFA z_>n(a-`)V8p_}CJAB_(Q;70}UV*~i{0en&bKP7-q58&qp@MiE`M}3vlkpFru0sO<@ zh2wYPSNlD0K>f@8c-o$we!LF+O9A|w0X$i^Zc|A?;kuH|CFP(CcjT|jFE82wrnIQE zbj$W4QkuVM)0RTY7M49uwr|^9wzXt)c`*&xS}1HK#aoMt@Z`4g{KAdv3Wbg9it|f0 z{ZGInkL7P&PqvkBE!w(u%T{tbhewLai#Bg3+kB{eCkRryvAm?Th&;A=I22E9D%wnn zi#I*CO(4Zvwr-^9x0S3Xg`2i)D-A5(g}-iHS^gtM z>$a8bL#xir6wl+vHV+nq&)S-f5r56JBWj}?>cBTpCmWLNBy3@rvCp{?4s4JABQ z?)xqDRR`@Y-%6^GjsOZY+Ud%1X+LsQk*dZYeCy-?ov~SJ<+xT(0jh zUGVt-a{24mZzUL2`J3e4Mh%Kb2=pZ_a1(i?R1R9V4tcB-hWnM4UkJ$;(qRL6ZY_F5 z9)B2pv_l>#+=7-Amv39=t1SrHOLB;>%}C=Lg_PJ)RES|yRJf^(tS{MGjC6$p2zgLZ zObh$v@JOS)FP7#=If~^mwXSG=etAB|T*0<&bRJ;*tfQ4f)i*!3?iZVXiJo7_(;tk{ z^*hMc{1S{@-()K&dZc9Yy24FG`J2&jUs(C$Wku^s^UDi`;Q-pJ>mJ)&w4)4zrihNB zf^FsN3sX|2Zrehc(qUXUjMs0X1j(oC17PHDK8SUIr2rdX<|+ZU#IEpnpqzh-3rbGfY1Oyatd5xC=0%FOM7?CLyc< zK>w(&%*zxZ4**XC%mQo#%mu^(iU2sGkYxaRhM<3`0D5Lv3|Igd`AZrma)H+l(^S8w z>*l?~e9SN&3ryG7vSFUelKzbX6v`AK$-oZ})1|}MG>jpBQVf^}7&9F9{};*Uv;U6| z2P1t{z%wDkJk`tSUziX5zy3!2A7XU@VH3dPC@23-8OGy>F-87ot}MTBO49SJZ+9a^ z4|*yM#NT8ey8XLom`VqxXFBe7%pD>Dx9nfCZogXaTDLs{y|N&_5QS zktyu8z&XS8OyJpo6@Zn)VGM90;OjaC84Ce^OEC)Wp%?{cz5$HRUqUe^gbjEc9$HT^ z8Y)nXifp171GbD}bZ|Mv7_>VmMn~G@FK;SI4u+#32{-3iFb`+43svCCqn0;7?a&iv5AnMC`PCE zP>hK+KyeBL{U$Ibw2ER3t`LebNFpenhJK?MlXwip=x{B?7z8ZE7%V!9G02iAMu(ax z#v+nRF*-MmVk~MI6k`$0q!^3KJc==Svna;mWu_R5L=MGRFc(prg?^@ZKGr;nF(|DR zXJdR(j6rClcmevKVjlfZF&4c|6l0*3QH%wtoZ>w6KgAfVyC}w>ub>zM_*se z0R2z#67)aCOVR%nFGK%Ryd3>c@q_4piZRgNruZTBKgAED|0%{IUQaOw_(v3Dkbgok zCdg@uF-e*z#)5r@;(YW!#RcepiVM;I6t74BQ(T1prx*)g3&oG1|0x#G{}h*?|0&*p z{-<~&`k&%5^gqRzH$40w z)7MAnF@AJ}AFcAE2m0=$|H+T;@}sZ$(Jg-T1wZ;*Kl+RxecF%y$d7*CkAB;aKH^7L z`Oz=<(a-wPyZq>KKYEiNz21*r?MFZ8M=$cD&3^PeKRUyYPW7Xc{AkvX9^*$x_|Ymq zdZ5=&{(f|qAAQY_Zt8713iB7_oKV~=xcs- ziywW#kN(z=KI2E9_M<=Yqu=+V-}a-A_|a8<^b3CUvwrk0Kf2tH-sDHG_oG+)(GU93 zi~MM_A3e{H&hVpC{pch=n)Rc{_|Xx5w91bj@c7B!kM8oLuTdI*tDD}uPI9KSThrdW zuBw0WiblNuN<`DkE@j#gX-r!9bu$eo2d+?F13nWx(Hy->`3Uf-;3*vfI*HOc`kdv; zZ-=~{D$#O<>}_n*1}TVb(Cb|f5CEG1Wq=T!FE54eW!Jky-j1vg1u1J#2=}up_9PeX zd`?&v-$}kEKWdpd=SZ41j`e!DZiB$gVRQytSz}p45SznBB6ME2DltgZvD4XPnIp!+ zU+CDf#wI?qA%Y#tlF1s!Ax`07bXiJsASXRkWd5B`9hj_B|BdZi%VfH z>Wtd#vV~22PlK9`V#g&0J4sUJ8n36@YeGFf;(F{Rwg>WXH!S2UKC0l6h9x?Z9F24; z*RQzI7ilVun9XbP)H$!3rBDCE`zy|Uj!I2+`XKLaWORKJ(e*$^Jq-`=Jl}+z_VKYS z*+G)^PGY)C#NjO|68Y(nAI)nbOH5-$#`tS01!F=`l<4&w_gZ*0TaEnRK>lrV{t?K( zhVOxFPII^H*@@Njf0h{CkJ6oTBG_2AhCjrOZ{}C0p_jLNi78ej6M|gXeBv4!Qi_mZ z@f2ATS4z_EWioyfcbaqCE&O6sIwD>;5H+WU`AhP}@;`;sl`KGTer3ZsLm z;nglPYeL*#Iu|V%-=YKcL8m$~PMnKSJb+v z%V6%3$Fd12KRp?R1P@5G4X=$^{8Z#Sg78P5~5&Ju?an7|C zO1(Ge?OFFgRYlTZ>5CS;8mmxt2RmyP+as`(dFC1Xx5G6vb`<3(rUZ+VS;pY?tVWCtF=dTj zkI>O0okRX#19}?h&SBg~unrXL(_3me2Kz`)<6V%G*E6RhIB}|&iTobyP_w#yCau>K z)De_8QA}g+miL;Vj%{(w+=Ly|QWWAeS{Jfqq;LMK9MxvuMgWu>51%5|$7;}i8wno!| zwmKmdJ*BDiNXPgbMvhY;uEoKmYEUx<@uWkMdV$swbYFX6B9rp>OZ17h_HQJGp5}g| z{ftD*a<_XuKeb2XF#@C8i56_=p|Pz9?Pw3V9lA~qZEjcKqRfRpJOkK3`x4-8NLl1C zJYP>+`GtNNF$%$~y3*_Uq+Oku;P86BZBNQmvKswPGhKN$vzaJ?J-e6l?j<Xuq+fGyOZm0cv68x*}qi)wT zTu${ILbuBCwe2D3o5#&c_vc>PuAS{oytbbqHq<)|paa~%m~}U-;~(Ph=gqwPqdoO1 zmZWgv->Uvodi>Vm1E^ubN1VReaPA%Yxa>IXe`g%)uy~CTllOblD3?^WGhw zhMZh|A7dy>GsI>s<72aLu6d-)vLLzn`Or^!=FLyZyh(Gi9_Hs{pIm*cEPFwG^L>*t zvy!nJUMW+iE2pbwsWOf%_-Bi!K3k>NIp6rla85Hg#gO zcsDzPB}OXC3ab6t`(SB_1vVE;>24d z(#6apse190FoO-5Q@x^2yFy^5@8z^D)1lYhC49mWXC|vfo-4Gq+FW5dWI>w4+2uB> z1$aG+`iRNd%s~G*B^4$P>OL4Vv;es{s8Geb7v*0yHv`~pW zW7u%y$?-MXi*a>vD}`(t3Rzi(^VK2W7CGOirB#GUxAB*dtJkAwdlnM@i+I9j!qXdU zkej1Wx{A0$lwD}f=M@R_&02QIv+U+6!6Y2vAHuq5L4VP;{%}2Wg02llVRYq?=gLiD zigJv4Jv>MK#$qv(Z*%PYZ}uUboc+No+L5G5}p_$b)@o1GgGCg8uC=#q#;o{@`{gtLFTc}5A)B; zyy7;$OXkCF^W`!hahu-sEeGF)nv zuheBDr4I9XBc%@W+(@az{G5?ehxr*dRrPe<5rd+PE8AW6Wtmc7@?x@;ke(8D#9+%4 z=2ZeoQHk+_!nR7tMmS?!#VKvswqD5cjXt`^`pyb_`LWR8iD{K%9-c}N5`}fbbI>_s z(}p~&Z>SOraQc{Nd&FkUS4~+Wk|}EJRb$d}`D8jSZlvLv6#|)jpJ+gj)N*MKWs6O) zU?!$Gk`Y%eC=oguq4^k>HCzPr+pD%D%Te1I+Y{y^erEtJy9YH67UP8dTo2M02`hvb zxgQ-a+aBRKfTU=}7TdG7>k=_uwjH;k${SKFKtnM5YOD_yp|g+BNRD^)J9GK@j( z6*qn>MOdCiIje1FrLf^0yq{emA0FZ@iIK018_Gy z%rE8_@UD-N7FF=O%o9qkVkL}gE-v|XiK}EIYs~K?x3t-Ox-HZAVZ}*8j6YUto&UfV-^Zz0-6H2*rsACZ7cd_TrScsLi8e_@Y#OJs~XF2aQEEpAEYblr6 zt(NPbm>*v-a>{Gs*P|>gAhRLfvY$k<7y%`3YmLGlPA_H)&j>ZPvFKArNv$>pVeybN zC#-~Qle>-R|FPnzyl%47ysYFGaTxi!xV3dcST;Q!W*XzOPgZ0$?h%X_57|PD?V3b) zz)J5=QjZ5ZFW?Nox0eR7S~eyviap4W#~AMDQYE5veqzNV6<2MlDUqUSO7!qJ*Q5p2 z&~g6nu91CH#|G7?3>u8C3te=q1=W~j?%S?zSzn|&NqN6=c!XVrjL$v|of&0kV&5dj zrnTeI!nhJLIl{T7Bn7QZ!A`4mHItLp-JDuv(htBY;PqS^!hV`^mxCCN*`o8R(Q13i z;gV2oq@d9j*%SgvjugX$DqBRJgGAVJ2}#yCSCfDpIYl_FB_v5n z_ka6r*H!YB^#B(lG8t9Khtb^` znDgfV?*n=oEb z^<)ha6C0+urS)>H)GApW> zI`l3^qgG516e*--=4Is(Mz_ms6c})GFGCk}j~X=6IOn668FzJmw7YpoafE7@Dx4cm z4ybCEu*5#(%HiowL7F2k2eqhcvt>H?GHFpGj~tYHrW5OWE!|^CbH7Vn4QH^+xx!v> zKfyNfgDWfke0){zYUWz}+aycdr@yM*2jkWl_s4AqEe0();*-crmh09O$TZCUg5|0; zLGY!&s&x?L>x_p?U+pA%6-!Ip-w9`e<`!41oj$S8PV2jDAP2SZ)wDvoQ`4OiY1x63 zm%=@Jpj_I%m>5%RPnlrQvBDhA%@9dyi>T1s)*4wZc=-jD{#jn>Qt0B>j^-BG>(GWCyNzcIUjzqTcpGE) zFQP(cTpQ2z;JGI8sz7x5;n|dH(P3>)z#PiOZ0Z?Y#>asE04r5Yfm%`-!@1FTCJN6R z*2Zw-?M7Cmdx4gvU%6I;bDyuQe4atdN~AnAoboIzPPF8GsAZvC@)wekW!WZas%4~y zCJgrw5i=#D+(%T$J4|;zG7VO(2}5UE@5Zcl4tyvn-O;^&Y0VX$fy9Pd6BnJwZbhI0 z_O&Y9+xow09rA4LPztIPVm}Fb6X=MPnc@&^4P$Q40%uS3u;y9adhEaOC4>z;19L$K zm;;cMxNrTDAr%OJ77!2m#t#f>1*`^i;F&BgitO0Qkj6Hy^qW-JPCo@5J5{(x4RLh22~P)mTy(k?yA1GPPT%#lHGf zdr0GMAzs#3=(+AISPS%Q7Drb(ugB%}o$X>RDj^8tVdVy9#>xVc%Gl#wsjP3jyd%){ zH&RR&j&ozMYce970BwaRBT9T7wHop)f@MXrQ;l@x259zkfD6hRXWH7NnYJ6K<07n; z5dalHar+F=#9zajl-9s5iY#IC=!*Azy-`qC65~eO9xc@ch$%)qAz*dG2ug6B+m%9iN+-rb0Vq|8BAaRpEms7)L6+6i2G}!Z|w$s+Nw0Y15cSG!==j%tw z7NJO1GfS?i2{$bzGvpZ+Vb>ThycmsjK~e*X6VKN3h3A=w9_oJ##okFznapL*rs9_w%i&^S@N&JL3jo z+F)m@ah5BohzVaZq8!hE8GMbKf~sAre))eB>4>CQ{U zoXcbXXU>gQlkc8$?ao!`_VACJt6{^r=IST^UGk%L1;4`PH8QhlNmz?elIV)YdN~Jo zR70MRQPSBKDwPASA{VKqe_(`;513Si!44P#v^#S@C5yW)j?_?6J~99hu+tKDnZ7L~?+ET>lT=(XxWvM8pZV@~odH&8BW!mWUnjUqUaw-;-%fH0= zj4KEy{UE*KAo~2x&$zc7WRmIwL!RbD80>}Fyz<~6Yzl9c6qXqFGl$-m4x+7}wocr@ zxR>;#*^U+v)5GFhh$}2%4t>xXZ8*ACRkKwX^i<#MX>H-p$Z~9L?IkbpqjIw_ww^&h z?=$D{`53b+_>atgi_eBsvu(`m^Q})|?p)cFB*nqc_)19)T5*s{^F@i7bWn=W z`D8;^&)>BUdcMCg=vfat$LA=AuJgXNe&j6pLVXY$!j7qle^c@PUY@O~;kB>t=clVe zBgWO~D{K{H+TG%${J1wC$*-97G;?>hzG7_6aocsggL=VtAF9>g5#40gz=~M0vt`?C zL$8{RfNpt5`%ZY1Oq++2D<=Kv|4{0g?-ueqxixVGHZn!)R2rkEjB~1t-YGFoZLZq* zA{XV1%?$z$cMf`5Z|L{pj)jTi-nTF0pWq5@@8LvpF22C_j?@WV!>74_D4N(o&zu`% z!l#l-|CThLe}LELKa&4E=x@AO?-!vWY+NY!{DxH77ad1ch=A(fgs z#Vg9HQ0xw>Ds@#5&NX@7aIE{9hW+&sIOjwjVX8(QQB>jlQRPb!TorXBq)Kxn6lpWO zVX&sqv`l409nD)&8Ce%xrK}uPr$QNOD12R%tQnqo5>D-6+$BAea})0uyD&!Fd6w^CS>GEN-yya6g7=Pbq)ap)!(66YfV82E{3iJ9rPwhLtIcSG}jKR8^`ggHWEUn=zT}3gZU8 z@|kp1d|1M$nCuPw`DaY5@;wc5`W{bBaL`;}N5_qIilahrV!t7S9uYBV4qfgH=(vxY z74Bd4n{dmNhm+WY-g?}%zH5)qIUx;t?(L3)PN~3ci^Bb3|K|wF=pH+)Wh&e!@zkJa zO!vzOH+5@nhrc0*hji~jINKe5JN&R5KG5|`gx7a{e}y6BQHJz;hrAVK4A~FZJ&a*j zfb}%Uq_Yot2D+FeD%bTKwE>Js=^^-G+;aGCkj9~H?q*1-OO3m$LC@JP$OccT@ys=p z71r&4{@b$n`Ff3k-g7V0MzHCylB+8ZlvHCc*=Y~mKn#cZNTyU9I&*>f6l{nEC8u^& za&e-r1ijgHVu#oBJ8y6zX@1P5FZm-Eh8sH?uFI$Rr7mL$)kpTYPVFenKea=Jo4%lw zJoNtAu4TO9u=23#@J3!?sI4g6kjpdE6Ylg`h2n$?JvOFG2aSsMoeDif98bCiJ-fP; z!UpWYzXZJ<^tNHT3^d)dA4T{E(4$h$qNMepLR0=M9xoZKpRhr@FUCEuotepC4K}ec z?vReFB{jH%))%}0xh(FYa{aq>U<35s3B)wrt{%RJ&xPi8=T^bpIOxelzWK1dVg!I% zlQPPFr-U%S625PzCH%uqEwje_cQ#-IbWyEgCA5YU3z-Rjr#r0s>fk&1%!I#6G(N&F z{%8qte>VsX6|tX4$2ZXD=)U}eL?+au#;s_aTxLh5k6!IdV&>aL4uydTKl2hOJker1~#O3dg$Sme(a;$SrB$-wY8;F&|)8 zu_V^aR~eysRtD*nrx=Wd)=qVOQ!J6!&tKIudNsycAN&m_IU;m}o}W5YtnwgwPn?)b zjM=t5HnmN8>Tz?JPKDFK_JN}|<&opi_7lasuvXGGsBn|U=y6^cQr6jENhc{LSZx?x z8v5A`8Es4%gMQBH963)a+zq|KE;_Ts4vl>7Q@A(v(z!pb^9p(;8PAN8$HqiZp(+2x ztXG@SC`UQvqq$?JXcN3S;tZCniK+4R4<|Jq!Y@Mir1zTdnG_g2Ad3})r{%0kvk zzS3SKgPz?T3|dI+M_o82rY>?awb&KkqkcU5`Ok!0em^;HW!drj>UM@@K))5YxtI*& z4(6WFls-|H(jyj5A{;6z;VHKyH6+DUdQY!2tiH<9p z;MDOgb6I0ngm_KEycZUi8aMiJ!_K^{`TXDFbNQOA(Ky>Ox~05YwuzsZ#!q5eqK`ol6QIZeIc{6It9vw6VE8`1T&OL=OEs zLd$T!-s*sz1MQ6lkcM$Nd+K+XiQC*iCU2~TK&yWO^PND4QMgS*qv5AP+_U;Md7}g3 z;)f#TI8%Q_9_*CzbEFba;<<<+CG4RFS_aBW0=^f}*US(zZ~@>7==H$vhTRYBwp1iN zZSJ=c!+Iv(wg>CPpl8A$W89N<9B0A~*AtdqIfK?+mSiDX8yz04OtuYJjY5JpAv{58 zwEfaDWKAFD(rxANFd;3ky0*!9yDJ^NLt374mE|7btj@BW(b@ytgUwqO>xF}H&v2^d ziisaHx3s_GG%e@i$1%T;|2}qB)bF*?qwBPLxEU>La}u=iDA8aelha!YEQO0Jbp6(+ z%oPe&$gt_zX!q(HRAbxkN)?jA@5)#oT5fY;=DyulkDGU5-|ix*p&~RKV=f0Tv~7EPFd`z7)fL zF2%FH_?yk~Y_cs!YBDTSCELuBK^}#S`{EFTHM*+pu3Y$VF%>;0Qz&J*@4R{B_b->#JiGW2Vbd1rPniko{UjPd?BktxVU5Z248d!8qB5a)c zl>*C>Rz;`9FPOJ^)Ffv>iZV5*AnU2@mv` z)vmd)%dHZajN@F4a|XN9{EBdubZg-;kp5r00PENdk(Pj67Pbv&@35X-mqsD4X0%8l zF2iq_=(8LScv^09JoOi1+{=f~NzJw&q=6QO6hjslfv*AT0c8!^MOq8>^tx_^9mzfp zGmKi&6T&e9&XK93?J<5Ks`Hv*dv1tW))>Mv?%-ZBwHr3ON^gf0X2H$7;j)!LOZWrE zQa5~3Q*0g53zzQnDecP>fcF8KhD6J5oc>_TN86&EN8o)xpkEoOUx*>d#RA8%A$N!3 zepYE`X6)vEwv(hnq)~_ptPq+;#;}a5(38VVCD9p-`xHv-VexI*EF;=%-O_+(<;{`% z5ymaT(nrq&&v{AGe=ANwA zvnSZFiYS@%bXX9z+OozstfTXeSZAQdR8nE*yq;<=ZUl8C)h&iwCLp&KdY6q8nAdZ^ z*C@=ud5@;Ne6C&EjI#G%ydx79yrv=4LUg`csBmX3R?wwrwL zniO?q>s#X6%FL*#p+1lWH=8hsiF;~s}L4|vYqXyVV|x$WOEgnwjY z3?`x1aUM*n;q({7j6_HMx(7Y~?7PcSL7p(*CETZ-huNaEk8`aDj)DK)DXdx4+fT=a zrP0&_o-c8VylKbnP`c4JVzV}}5orUSiSRY}88-~cuz(wF6P}?@4B&RVC@z~nwRNsg zXbZB;!(Be=3m=_$TZ`1hQ(MkcsOMiUP zWxw0_`Vm`uGAlqQ()0?r&w-DlH<<0j7`oWK1oe5~D?;eaDm*jNsihzFOPnC0<#Ua= zPt@7E@)nm46SBg-%WBSj%paZ3AW*AY%B#Xq?v*fwN&ad|zQ(wZb;l=ER95cH6&M{=vN4{R%Qy=<%#kWk=pyy<71#F(_ zI8W-)Uo(5jM4WI;Mv;~q#PS$R1Sj5UxQ(E1A52DB5jRZu4eCA`N2Vo+W8lZJK%Whn zXu8d&aTlj+sXSgBqPpdP=cg`ZeVXq!1$9!QPP`8O^ApZX!FcWpZqx@nSGvf=PIwq# z%>>mdRjg;o5&+dm!_weu10AOy<1-my^nHj3T_nx{8z{njq?P-jO;p! z*%%=f;UwViM_<#+-10WOjEuW7bNgd_fhgCidw)0R?H65N5 zF6NtRP9-ukj?wvw+YRcKSB7)rULgXK9V1$ViNYIrPvT=TbBS;6mNjlC%&gy`{w~}w zM~KC?qqd2{4QY%HJu9~;L{u9KZC8f9A{cjj-!;j1*Uh-E_WfJRDun$Nkk(*@E@@pv zePPEBJ#5)!R_d=uXQ~|YHr|a5eBJfHu&d1N&zB z+(mOS3a(pCLX>HZ^KQ#!YdBWHOffh`;Z)pJ$%Q(;jEaU=X_zg2Sm*t$*Cb5O`zk6y z`>u7BW+wcrl3NHf;2H0oVv($jF`nI>m1(QyXJFPP+b7>Y8K?gz%_+8xwtTAw-jfq^ z^R3}Fx)tu)sQ&OM6PIeQD8Q#Xgk!NX~H)>Z2ao7Q$g z8aG^*$9m@o**w+v&Pvg_8lys&0)1O!VB7*~`I-dJcv(-+@1;8}+d0+llO>g*2{NEE z?gEJDFE{W{fD4fM62LV;NCTN1Wasv)EHPQxyBYVSzK}-7ZRn%xjTdQ^d$HeC(05g+ z-bjqq{3Pt#QShp*<>t0bz{r5ECJcCf95Tv#PMvVZN>Zna$9H_lrPvMoPjV)^OSl^? z?81rFU_ZW@-W*@q++2KBrjBi{Egp-NFa|fivGQ8FRCo&%Fhc+mRx%;TG>Z`Nw)mX|j#1=YkqfPJ#*?+iJMwf-<*FJLQRPw6*gg*JRI zGdCQqp+3UdrO=lq9^ew26&pzMBu7FH?AY4qm_zUZ!P{gL$rN}cz=C|Vv>oeG72Xnv z7Hy?OpB&#r&XG**fM-VMuZxO`((opMi48$-Cv_ezBBr11%*5?@+pCtG(|#BGo2cJt zSF*p+oP_t!>+Fl%x#mb#q_V}?B2%tIBzu5Uv@m#+puu~*v>R_Wr9kE>@IUkSV^*>I z@eWosY;q*UCK@(4<4Cg7F{nkp&1YPN`zs8U*@e3cOB0l@qog-obJff3*>dXGmK>L{ z^y#?IkaAMdV-?3smqIsL1)X;QxBt|?5pPm>v-wpCC(W?MrldLhV-q4BaS1Usd|_$L zf?x-=m{Q3XVMiy;Pr7{19K{=6@0L199lzGR4KwN?vBJ23U&T}}V4M{bN||kYU+2DT z@ukg@^M5+dSHeGV+i=Im+0!@_o9tCNL)N6vqZnc6p2h=$VFxRjPzk)Oqt z>`hQ7MAju7;EXL56Sp(B;`s&hTK*LGsocgNv>Pgpp_RWkuQV@#_jD8bDrvZ-{X^M& zT|zZqhMR@?ePyyRo#a%Pn0aq;Jx+!3ZxWM{q~!K9#zX2qUtru} zJy)W>iap9LpZ~dpnFe2zKQ#YQqT}DUreyC|v3vQ8!NC{kc|&7}!8rEK?t;xEnZ8AX zyBqg;Z{hy@Qbr!-keu^)DS;;vNshNuZHi0^avd+dLF(dsp;7jg&}c%%br|h+aaP=1 z-q&-y^o_V*+K|W07r8b^lEtw9=nlGiU6$9c$^kvrFY4)5Cw#f&&f3Ic?TLpDJwZH% z+A!kP&F71?2wRCT1GKNJSf!|!>4+8`GeYA`K@7*&l9X_GgJK=CS4jo?HAdRHm!)Hy z<8A+f?nm6Cd-h6KHm|f@!nja;gZ20yIHzO%x&rw8YleIbz*@DLzHOoqsU#Ld63xL` z7hEM?7j~of=tf`I?{B(>!>;ufmXeehy!S^Y`Q$}U zzDoG5{1$6ZUEDg`amWX9swgZy#dWtFE%oJa@Mf^jhl`$;a8~H|?7i6oO_H82S#K}u z-tt$vmteP;o1jXFP*y1H*{CJ?E(GuByy@-7ILzKZhBcu7ve3h7HhcFoyPHaUHLdA| ze_O$+4JS5szLETA_SDTTh0AG~>D6TER`4tHt28%n{gPbosrLtllinQ@F8>pWH3 zioU09xdj=mTT7<>U?1`<@SZAVGH9z#(Y~+PPNUU06j9f1n!}j1%mQr7Lrl4@Y!k}a6N>AKNuExLn)qF)hkxIohsMAMspU=&{bQ&fE7g169`w_lcbq0!Ou4(^3A>NKOPFZsHRthm z=uQVPny1>yw87u$Dp37kFDF)*gp= zlJp%cGu3>u!00ns*dpE1V2Y3gYtTZqui$ALi5PiVSzB+(;m=zY zSTU$AWzPA+{VVu5E?qonQP$>{o`>D4F1&_IxkM&UzP&;v*_jDsVyNpy?yhD!=a`uz zt5mFg)!xszA1*P5$;xH8VL2Vc z->&7umz6ayTFAtD{;c%@yqi{uH;~luZ6@Mr7nw3eOl8T`So>UIcU~4B7oWrLwnhul z;fJ`$i!1mrP?cQB#iuM^TA#8+3tyr3)NVcgGJzlGb!cbJ_Cl<<`cfUcjH||u7KPDE z=Z1sygZ)Yvps{ksBt* z&bm~%pV7zR-P=b>=djG2gM2dlPn_gFCC{r|YjQ*Gd0NZdt2=+ZbX6SDTi4=zZu$|j zTE4&yS>27=P+5IFK;y0O(pw5yar^_htKwEdT2E_%D?;vJ?)~>UDzDG&x#(rZuw@V@ zpAoQj^yf&*BU+0V{8d=q69nR3 z(f_tI3-e@(GLt^nvcp&5G&arM(Qpe6_JgY#KX_yD>LAt~VY-lo9$%q7#GP+mm3I=oQOl`Y9=3Ghu7~d7&$-HU{hppq zRpM7-WL+MAtgO&Bs%{5+96MrHX9e%VX^+@8Y3qodeX5;MnhI+nMwj8)Gz9z-I|9T`nz`wU!5DEoEH4 zMZ#??>VCA?Rs4;Vj@0QkNs5KVJ&S9{tr;;6cm@aHi3*#$(Y6t1vXAj(zo)#DBuyDU zr4sk3zO~qo)_%p@K8w<^MaLW+hxD9U^=I4>0IY!b!SfFxt>2!sc@ei?iQ9rb^{~F` z4h1j1uxoaJq&?OibDLJ+*7}$_{6f_2kWknhsjjSr{&QtW(`X2mzU#-i&7l6_JC78U z`?wkRIVqw&La<{jni?6R1Iz%#1Iij=EHSzCPDm?6Tf)+)_TTSWk1qpYZaSjybiZdF z=0ClKAbJMlzTY#aliF2jsvuaP`#m`w{hqZQLAD=}zwS$h%va$XW=#w+JxKG{<>(ge z#%(uzR{HN+yvux-B?<2cJz<_Po>VII7+>z+4khFy+)q=S?vJ-TX->wi9H|V&4K8uN zH53b75BhrlILi$9lF%3${>o5{9Dck%IxiYlJ9>joZ^CJQ&ksde6wrebgcIo-ZDozu zmg7F~H0)@olM8eQ;8pZZEATwj(G0KxGLhyX;A5ZRI}liDtOEU|xirORb5?w@rimYg z=T;*=q(LR8@M-NI;Y|SenInJF5Q2A)@YLg4E?5j2PNNW+!~LGOJLnC46aVU;V5dUb zQ6G_pVBE+lPd;H*oK1L~IjrUA>Bi|*)cb7bO?)xZ;x3idAlGs9trFzR*p>JeKnNs% zW-vHWBZlE!A{*AyA4Pgg#Jtk)+1wG7YZK<+T;A`ww?mRt4?Jz|laPt!n8lGJ`iQbKU^NynUg z$Adh>an}}&QgjHpWv3nxL!O2{9o}*9dVT||gHnv7vb1%g98-I^h5Gu@yl6>b>0VEb zT-KQOs!y?wqMfyXYs)bIfHwiof(}97_#_CAKTxYs50x0gj>gwC`aLeW4+k(OrXoG< zU0+QZk)Cj9-?#n|euKz|(iDiUY+)eKaGX74Da-sD z9|pV)pzUk}`0pW2dGKFlEBU>6H>Rfd^PGP3YM#nh-FF%@=c-j*`I$vsJ2JD{20oFc zf2;QomQcj1DnGWUZpU6481%e3D~dzs@O!kvnuDt(8tibo z&BqDHzhO<^F(pNBPlJ7`-^1c<4`P4YmBuawH%8|AU9aS($zwYL{C~v5!(0%J70X?? zL(WiEzR&&3@uFoV|1WDM#uTyFy81m^2Qy`jrOL&)ZT)ngJm-3!i)w!Mfp-aSe%j3Z z<8FBD!r#el!F@Vu{tD;q_qiY8WmbeXzlk>AKcK8r95H4Wzze9MXOug9K;eE9?f(+X5% zI7OCoI57@-yuGx(Pq}ax+JEPXKF{YjgX@W(B@u5l54U`88!`RTUd%Rd<<2Fy+sweu zQP`W!e+=bn$TO-}k&$V$z*@BpbL!>6ew_0^#yw$34}9p*l6U(R?m1}5O0?u4TJmn6 z#=WTjEs5UT40&$$^n1<^Q0axD_C(+Bc@Sml9kOi2ho!q06a7=>G`brgTZUF%~(i)=tb*;X~Z2lK+}fY)L_lNhyjg$1*dR^!K^Q=ILuzPyQKS z%=)o;dR*=fgp4eV#ElDh%Y{VWK=i z^z@$c*F%a#jYD~Y(WkStM-{H=z!Puok$u?1?avzP$m`~^#vrVLVTN?9fqkB!n>QuQ zjBa|i3;PIXKm4a4clrizqQYx|e)(mTiMiB=HzhRgfA?)CyUBiXnEa7^4V6_QKf=X{ zFfmLNnU8-5W5?gE$tPQSPzIja=46}E~1Ct zo;=n0>%0@Whp;a0$NQkvMs#pN5=xp6zf(K>g^2z4m&54}EWUXeRuB8}%k&M!rHS(w zv^neiO4@nj1WJe(oYJegr{MkYIJ7$2`a7fPlyE3l$HohfLxxjt{2-kYUb@XsywM>Y z5XRFtZ|!ef-evC(}3EOzb?p z#vRrfmiUeHDdb0UAL{-JxzRnpM8sRo_BEGxp#PMzRM5LOK9XZ;jP)|!bGFyGX!}-P zE<-ByaH{AVZy}XdP@1wYZS;FO=*BB@xXLu+Qn6onU-v$Q7cABa8ux=;Pni{LI4qiU zG=0*&3o&u<^??>6%d*5y?MIJFRy-d(TwZf`i9~BrEG zJ<+(|=`C2fY{Md?7^&yy-S)VfLL-Ks`>30fL+_Ka{X$Q4&z3@Dd8sqn?5*}pznGQX3*a>|5xvmSp6(aExH}DBLSyu*;qrEMhe=`b4OZ5W zT1r98NV_(754M`XhYzPL?*6%T0{D^oukG$`HTdQ9Q1^A%Eiy5u>H%*6rUKs2!+kjL z?)#xz0c?P$u{Kr!qDB#t3| zkSwhc={0pNZoLcTjg0M2y7*SsuRir%_saRsZPg-_=1Nzvx4Y8i(6rVVgoX}>)^?fX z&LfcD2~fE8c{;Qbh~7_bO13vg#Wyzc=O?w+1D-v{67 z8vY*4aHT!URa!!(M2IN@^^u*OYhc4>z(W)^qTlvzgoWE`*J1~~g)hV{#u*TAWejqB znr*9XR5D#r4|itq8n><|WcY73Fn}!QKeUCwV)TP8J%6rHi(TaZ;_F@DqN=v|@pI~45JQ=3W5&=E7Be~j7MO6Yxg2#4Gijm0k2uzYiWj9rdWw!iC|@VD_^&3n%%Oz zS@!Q;_e|-{c+?=tQSd?eIP+rW|6OMWqWk;(|N7bNGiUE}&VKK;*ZQutlAz_xBrfJ6 zG6<)wa@tDw;O;0vuOACseZ+0aXyY+J!~E8q49XAtJFZ8{vZR6d`$*9PMZd3J3%R4# z|G+9m4Y=%A#D(1IqI;l)4J#4A1_Y!_nbQNG2At9_jh6 z9dhpAdg*Z1<}n7y0>MXjn;U_&N)qxp6F10ox9}`_av)S8!ye;VZLo`X3M7V$$Tx^BKV*p)?P)I3DObGcm%=wk~;xuvX@ z&b6>J`TM&0UFXB%q<+7@?6k}4YVXeQai>kBb+vb$J<(Yx(z@EaQXK7EB+|Otn|`p< zBGO9Ho4&U*8{<>Jn7r{1;DHeT7jX{et^#v?2m-}E+~0wFe?|QDEZFnH^?wm)9r*4r z%VgpH9$epp@NEuoo%sDG!ixxxBNQX#A*3U$L`dO3%*~KzJB^-{&JX6E%sXjWZ)R|& z9(sEf>5<0y?SBHNu;ySz@T) zZtpzYuile758zjLC@r~D?lZ?&*qQN2>7D2mN0PV~IU++jhIjtnYfCyPeT>MorSJ{? z%Kh!7opbB+>OXf{mT8zB`WtpVT)Yu;HunoX|K2mzXyiq z%y*YqBaM5qyZIlFUDjuBc0P&ulgbmN6OjQ7#ETxB$v259{MvryTYqWI+{f}B3zct7 zsC*H9D;AxjB$;5y}5x?)F^9%UtA&dw$r%2uP9Qx9gF&XWq#-@!^C<0 zzGA)V z7nIU?Jb&@aj=TSGzX2oABmMWH4t8bf%`}d1BDr)M-q^cu`Y*iHqCCi74s6E=)v@L@^(8pkaS-Zv4c-J}Z9z384(38uQ1Dcn{+9Z?MeA zG@p>?b;P?71|XE+_btpr2G=I!!3<}<#P0#P{sr+lghvqS5stnNng{vU<9aoMbtcjf zR)=D`)|jC`fqWW-6y!-pxMjjze+{?@q@60UjdWwQgzn z1TWs?#+xMJ?g?rEGVPR^0&%jMMA+I{8lH;uRHVbM^bBsXE=@)hZ+5*G+C8y5rwkBc zVDnAo2y@QhBIycyc|j?q+`>6`sGHuY!qZ>LyT#h`Cj4d`=i1iC{om>{gUjXF7ntn> z$4CC?mKI;zgV96UYK7oz@19Hw&SGGlyIKerNNnE;CbzMV0UrlY9sr*(3=%v&a*~XU#i2IL#S)RSq@yw7;C0T zoQc++FzC-Qj$>`>;tcxm1F+8xEJpZC8vS7t^ZiWmN<4GcB(4p%Muu*UULT&~77Qtp z5S4Nh{N9O>@Bd1@6=z&ScE1fk8%CwF4tDA-G5i2{_ITeoypPqt1}XJ7&airqbh0N- zWEwNEnng@608UAB&@b(jv2IbKrs3XL>VbW2)A2r_k-+nfLLFXnvUl8syaT&0YOioi z0)OjPM;gxhZmIa%pzh*}d7QciPYU;e-&eN^&$RVMCv-!=y72X(4yM{^E#-nC;fl+b(%h`!6SwD7{PfV$t-@t`-LdF-!x^hj(I=MP?UWMY4su zMp#ueh{2Q?wo#{Jzt_b>Yj1cckA*3WrF#^baX&(|fSPB7YZn&h76OO)n0X=QHk3(|Z@F7xNv|ha?41LH{5sKqE)fpu^ZXaKXfV59ze(09H@K z-V5~hhaK|BLT)8EQn4<2R@=ZDbkdP05^BQ#!^ zfVIPUfmWU@PA#YXl=Ci7 z&xCgn7en2lN*w`<35t$jyVx$o;4S;*9Igw*?YMbCnR*rLgUX>aUb~<`n@qkyGHg{{ zUg)h_`5R*{?4N=C10e@t0D=kO-7MM*66Pe#*6PWU(QBaEQuz&a%w&Zy9ULO9V}oqp zXQE@H%r)yi8Jin%MzSg&+Z4u8+7#dK6Yv)?C@`4(y`)I!zMsC z);qT}{|amPQFKcyFNZ_o60|!F3w|Ez!*6h3arqSY_8!o!;J;Cs9!J;++za)}Dp}s~ z6dwjI%qN!&=+!~6lfRKyfgebHu0Ws1`1e6$l5%Il?(8#1hUpiVscRINf4iJ^>#`0d=nJ8s-F_K5^p% zY-$#4vAp1`OkBto@%tc8Hsw_svKW%g!g^mV1G&-LJz80#UETPkAvSA}BTf}QkcCZ= zNJl32K_=xaovBNin&21Io^M3btSQhxnGgXxN-Qh@-6t@~Ve*~9SwP#2pV~?q2yJ4l ziW$fxHjB{-29)&c%SJvaN0m@ey~a$n9g7-{>FLGd^+=z3*|C+Oyzd~dDnVUeP@QF} z&er8p?#IkN*rE#}=18OdP2}g{vw>_l{v-5U1D+fFKi~Mx7X^70J@EpbmEBA^qp*n-YtFG>6fD2s{J16s#oQ7OWm>+JTvGg)dBTEH*9BsN@@mO0GZ)_QrQMwg zdIn=c)3YelUne~plxj^UmF=&c=P_HWJLiP-(_x9y?Ug!QKcMGnp1<5hd5b$qg9qcp zYYM%i(IZ=4@gBg_<(*mmzUg#P{@^#i_T3)JztQuiNN>8Q(<3Fwtv6q8kC}b#0fY?N zLtD>c{H{_1an8(sl=txNc&O^{-hyOshSfw6KaSaXA1^Nve2^LJc|K>gQad_VpY zXmtdAXdHVmj^0ElK^Tglq2G6tOnXvJ)sacizw${-y%V8%L)f(max32FO$90>)|w*! zn(G}e{~9m%V zd3EqfqrIc#6w-7KKBEGvf>MS6Q$ZSBy1o>t2l9Q$r9Vgc-$otBK)yxS$%sEiJR$Tu z4e_`5Jr(g8#F>bHLhM1%<64C_7=v&ZuE!xZA!Ok?2XWZ(NWCIgC+|0YNY-6=rvKSx z;}KB7HM?_m&)Etd>tO6*wLrUFgawMJLcJx3RbzyAU{{ZAj7W>y!glL>Fxwo1+Q3B| z13R@XKfulaY3wmA-#L##i;scC538p!A`QBv*ek9zv1yFsH|Hj~6}EX4`p*;QbGfh- zFa2_hI{Pb=}gElzJ`>xkJ%lfXjJNe#gp%(JpBQAQwb`NR-Sx@%N zQLV>9vYr9ZKi$GbY%%EX#vHi^lI_nNIncR$H1xI49Z}t9z4Y|zzNeinRIg{f^DPT< z7IIzrYk*)_R9sg~Wn9G7m&~@zL4CJY(WuaRV27kyC{h5SzJRk-F~-Q^FlTV~yWKM} z%gA2IZdd^EL2rbvhqw#iM*!UIBf!KPu&eY#Eq9+J-!IVTbZ3U1eA3khiQswv^CD0~ z(9u2Q-Xc=z?#3)gLn(r#vlMSd={|BV9`yp`D)|mf=zu=5c zp|{&SpC=IVoJuoKZn+zBPsVr;ftyA-cDo_z5l70`FS>uvo#|>2<8b%7n2)i!Zg+6} z-i7N25NQ0<*r#z%W1hzQI?ywxFlHxX4=X|3au*A83h>d3>lYAzjNhLl%tde_+>Vff z@DxJYaWnKxpFU!7wD=?x~7DjD$~RkW<1-v)s)auX<&?5>^@tut%x_;nVR?< z#SYl~z>Fa3!Yfs~gI013cG7_X-RXj*BAX3b0mI?@B_I{}uZjlyj98a6=323RpZz=C zSus+sb-cTmB4E3V{LR3B5iQ;$ebJ*vI?y!0#Qu9H<}}KAaXNU42&;}SE;A=+^vg=& zuc10JOO=}9V3KcZE<)+s?F;$Sx`nzrlfDI~u-30)Z8k#wKNrx4-n<+3iFG-osbaKJ zA2ju$@>!#@`v>siSDlQ3Hb+~_1)Lw*?lgb2T+&Zn1Ld`x?T+$a76;_bFcFrg2lP|~ zbT3|p{In_K-qiIC_3)72#2LRl3kcLQRgEou?_Vbcr#40fx=M+@QddmCI~crSga zt;dG(3fhvV(K-l7*?=@9;1N5g1^cl7di3(_*UW)`JaQX6eek>IJjDg}uSwJLjX6)4 z-{mId2mYCJM;f1LRYJ=7A+*mFN$+x_G0&fdZ>DD0k*_7ZKd@IkIw{Qb@$@coTFX1$GTN!QL=@E@lv9YC= z${}If!)vb8V(*&p?2=s>AX6lz1RRPyO**J`emSz z1iYt7v~o<%Oh`P~7XlI+_)av3p8W#PP8Up}R>*c_aamd;-lD);n#H?wD$#;V#k+Ey zd|2XRW|yRgH3de<7Bf*VvZdpbR{D(<%c_mV`?=HbMYRDfxKzx@-GX?Dn4U}W?10qb zDXQMgeJ@T9{XXMaR=uCwFVddxl?nr2%4-z0K~GO{bq)8jNV_^TYyxBDap{wOm0PF` zb$JB#^{rRl_5urvIr#|IoDz&_T6mg!u?>;~rri=-fqk zcfzV`FgIocdpSQ7yhhM1v=^_(J1!%1AS_4t6T)9^1_CP(Vv%Rl9l>?2`}k=6>57ez zf<=)Rz0_GkudXZfrv&B1NiD_Ib=uKqfZ`_P=Y;I@!bZXGtyG3q`d%C$-~YfLi8czWXSxIkN7@~)S}Nat zqv!s9cb|7Hk?-#A$+rwd-u%9|Y;gTaPTAITpJf127Wbt*;#w%DRP`*7Qx^24tZ>bf zQy%S^E4TBUzLWx&MNZ*+@?>iTd3`B!Tsd+|Vb3hN$2d8K`ZCL9mQ&{TWXo^M?t3P~ z6$U%&G;dCEcVS%Zz}P0t*?L@0MYx~lIp%dbuJaIR?(>Lg{u52|AOPjGCRlM@iSQJ{ z-w|l7*o)YT_yffGi2sfFON7O^{u=Q)gc4j6J%1S?4|EWZ-xqOx1ECz(G{*+w{wiGS z5kG=>6yi0A4Ty=(Ux(|ti2sUEiR(3pcOX24>lYEznQ1ewk02Hhp2hXIh?@{NZ(dKG6P9Z!F0c1>=5rPsE-<7MYdxJ&zsse_%P+F73)xrNmP_-Otb5TJ;WHqO_j|z!cASSk zGB61&b`0Vg2=6;LSvKWfT>Kb(FS?{JapuetHs&T{cW}Ya0+W2Fup#v81N=&djowLE zmsaBX4%iKL!3TtLRJzc>D^eyq;!4B#A9^wLj<0Lw9nR26`24{>gWL+$z-M`- zbk7#?L3u=~eaY4WNFm%(1L)>t)E}C2Yghad%D*_Lsui~L*DnHKt;?Yb(Oz;4)0!7Ysal{Fa47^5_2 zXb#bwpt(Tvgyuj!0*!f^12pHV@C?z{G*^g6Fa&|l)pVw&_q~JgR|En3G}&HyAA47+ zl405q+7XJv8D_;$hFOmAw zJGzf=JQDUYSwDh(wn;2-JNj(KxEI>Zr<}$3`%h;g{=V!yuVKzU=iH0G>zu{-Yj-AU z3eTKZTh1_(IAJx=lLMQl$r{CreXIE;po*E=wU{&0(C+{80;R1#+AojEca&f7hdi&z zd48ijt-<%C`F`Vb|B$Cn&XW#}pr}xuQOGmn4|z6tS-tW|7%ZU%-xB4UhV*HmV`v1d zmf!Fiw2tUp6Yx&$msTRDy%0+K)jO$Q+MndKr$cG&;OG4QPV@z8_ed!1tT(Y=+B7-M z7E1fftM8XKK~7s3N_!7FFTcNYl$a-Jb)17&priv>XENR)DJ_CSCU11*aOB&*ZR0q2hA;Qvz7L&+B9gF zxTGcz`VltsA92dE+SXOhy>{dg@-38OEwp!B(&xwnt$nrPj18VohBPm<(6dBSz3+i8 zto|H)4E)7uE2Fy+-K?7hAN5HmPQs}Jn{zB=NsRKo<}un?j|_NO z1)KTIq{*#wMjkXxXe~;5pPQca0XHt`J?@bt@LxHmc7T(atP~O_-zFq2X4BXyZ<%5R zreWsD7feH&9=w0t0+fV+qThm+Qyb-muxIKU7}o;y)5kHr%%(`Nj^A9<_OM(HtdW?+V3~w zOh_CI@L9kqkffTc670gLKPd;bFU~$c=S8 z6FPmBi7Lz?P)LS_Y&?0iY{@(stCyr9SINuid910*)>k2ck)(lF_n<9UNKD826ZtUX zYt|UF18e92A1=$(j`tlwIsX6Nc@cKxhX}=31(dA`c6#S8R}5%3dZ)K1kso9nYy01K z-Y>sX1r)$7=#dTjLy11UvWJ(XZ?90Fyk@0dVGWtEs+MK)!iF0?(;`39|BHw5MM-)a z&kRZ&Yy0CfVaJD}e|F$psD-v}(2pZy42~H(t5H9nlG}iC1zSUsp1)!MqL39NNJ9;% z)21uLGw&TcWP2pg{y$8CmnZUac6jwVr&}_>RRClS}__WsK?lfgk6PK`|c`n z_j#Frp%|MCZOs02{i7q6S6-hJS*P7O=atuBy((UPzgQp z0s4FErftzSLVvh6A|oICs}f+_E)XZYDZI^w)&HWCA?p;2^IoYp7BcYQ>bT?tF4s?% z4;jbcwr!TYS8%uWzs~1#pU-;?zQ~`)oqSGRTbamgheh)m;)&&O3t4uja;GGn49wf5 zg|=&4>(dyYcVc`lWOFzPcJAlBQklpCO`8py3gpdwZ_g9R!6FAeElGP|8}f0>#e607 z*$3<&P=_|X`ZxGbE&o4@^F{zI@_F$5ZdcLLw2to@6u z+a8gws;!W(sD$+1M*?LQ1uQabK_uig*?Df<>MM42-9?82vBKm!1;M-7&Mp%p;FcI~Uq-l7XO$r0H zBur3jQ35$g`zGa|jueZ9-b(q#1=^!OmhA|s_3@h7;K;&R;p9jm53aF zMIio{6*1A<4V`!v6d|{VS$vtpnp$t&ybCKoxfcG1(1EYhDQC(ZuE$R`F(R z_A2?=aV<(b+XmgZ@mRxd_z2(50#SR(zl3`-WW!zW*dXII`tP8@{4K~250}XA@Uy_F zy)UvDtuOe&->UmbRJ{2;+ICvk`X@mnp#84Pe6fw&U{R-fMowkI7ffu{X43-U+Z46% zId+V@b7J5JY%(l;^wLHK*sV*bj_f8G+%U(!ZRa$Kvn*^E?R#dIdcfJeSew)WAuCBI zoyAH|Gl)_Lo!q;Pg`KB;&s-2EO$$t8Q<~28-Z?9d%Uy);d#A) zVRAiw5QfUXzZYWU-~SQzZ5slQzQyue$Ocsn_A!-;VZO&WdL3<4f^Y`c4g>*VIl^}c z-yqPqt3V+Bi5-CjL@G!pZvvehr5}px*I| z(julv($}4{OtWTOMY*9*4EqWUc>KUY;6%OMmp!U}hD&6QXO%vEBrb6VZ~~e!ub=ZqC(AoF_#H)^P0-qT+2a{(v|4ANyq>rV<8P*kGlQV$`BwTf*BfMyuIVXfK=OFs)mxZ+{QFv;II z#>)H!$V9-0kl;_@_Z%%anV)owk|haV35vhKvmA#G>@m#cw=ea2#PLZfa=CV(T(1M$ z5b0va>_v{hUm6nPh4`d6*F)$HNvgV}u9u`Vq*1l#WF^SoZ}hySLO+`4d=XYp#yS6k968&D6!gI z(u44%YqO1k{i17*RiLx5+6Pilrd33Pmk*Rji%d^?to9Du*b-U`U6NUTiX@CgwRu33 zedv}zk7v)$F5d*tNkF8gCOH&1p^a&tY2RR9sXK~up0b2BE{rWSRoSvjO!kfT2Gf!j z#z?gI?cf-y563|_d0t>D>X!vdo^&)^ke4&bi!fX1Tk6BGaAmA8(G$v&nI(#ZEYPdV zFC~K;MO+uke+>u(G1T*3DI$PTnGlp@w1~O!uyRZmMnjVQSB7*DtSH%A;`La&P1(KLcSelJ<4jZF zPXIZk*?uiJxb%ca%7pH*R=*o_SD5sr$gwe38gZOWdJP|8l5KdNemE-Mq9&=>q` ztAErmXa44NSCo~)POM|K^Lz#2^%>*$%l7i^%f4T>mUr{hs{RUZO1nH7 z{T}{w#XdaI+NJ@;@~&?W^p96yw(hf%-g55wZhlI&wtE6rk}zR)H9S@@G1(7SZIa9V zu=RCoDj&4`mukgXzxy4}Fy~s?=Ih$3On43VNZWiJcJh|+IJmbWbI#cA=iFDt;9JUl_RuPJXHS{@rnPv}k78`1E-MTu_-Di=;A^gL zJA{^mm*qC{i>+)fE~WFdt;mQoIMebH5UDKk?uAApem6BZ^Ybc=OJEuHacJ0t)rN2X zienNx;s2U)za7f`wsRVIN(!9znC9ti#o%8CuWxH3$0FP?N3awp10@ke0|cAwM8@8_GeOV8d3S`8MoY8QgHgssK3 zwt55iqk7~S9eT9EnPCC)Ifxu^oOF)U$KIX^j<^?mS?sgIPtM@46Ik^a0hTjRZ?cAw zWZ4g0&0yL7OBOPk{mxWS&zGz^ZNb@Kn||yag_eX2A)wGt6*apuvC6;e)yen14%Exi zaoD6pKYig!5vk@(^HQ-yq_$<6tfH+5TGlB09&s_&0y+!K$6EC+!i$q2qe6HV;UYo{ z{hk<%XCply^XMLgmALor1n9fT@8(2TDQITbA^t=)ut+sbZ7e*ChBZpkqb^oI4;t0* zPes}fE%&zVms>>#?bxy6>z1QB>IZ5;NqP`@7l?Z?KF8@{v*UIdbK$UyMZt16-z}z= zu^1!jHX|Rz!b}x+qP5nVcjiIk1JD1Ghn(ny&P@Te1}MO%&KxvloiI}{MtNOtdzg_|An zOafw}d%8ETL%S=y0led=?_!6jheUm+GnUqIsYNt8X9;Vo3pD04Yb!M5Rkdk@^9xX2 z)!cUY)J?=(P&PN}cFkm3cX5BRq!gLC_WWc$?RvFdS-+!Yle3FjRH$;!%PHkC17AE* zVdfT^7xD#Wwl)viiFvRasCq#OI@2W`AOu2(q1-LC1P<~uEo@CLH;6b49P{EWsS@5A z9u$qGW-eKuS~Ly2Jbh`YSF@W**uwq7?B!U)*J5U9Ey(cRA-Bh)enPdf;I=c}4+qX% za7%l7$i6uvcrFCT`fSgn&@*vfTCsxr3awa^eXQaA3th!DV`v86lJ3FjKBYs`NMD4% zbng9%MdrfIjj)>bSM+0pZacSH)L?C+`C{#Y~3x0Cn4Ve+z-Q9 zA|QQ$QvcBwA%{JvVa~&)TaG+oKCN3~S~BCbU8UF7Ji~8+Mn*jRbLh0=&$7nZuzkWz z&_jz*fwN$SRofU_-eLzm>(@3aKsOeagLfiZ?#lF6a$(NXyfSrUE1Qr9t3;%s^9%e_ zW^w&by_Dm-7G|;_JYKyA`-9H(m7@>g)Kc4aXSIvhc1OCjjhdQFeG?yty=&2^h8bSz zPVf8G+TEXnV~SZKyd}#F!i6|ktkVeZf}@r#+>*K>$-URHwt9(Lb(S$2i-_{E6`kkz z@#@+Jj@EO&Exs%P<21dQO;pxyHBD+sK;3`=-yWB>l~X|%g)Gy-hj81z(jV1@XO#_% zfl2u@XsBRYyok1#n-gpe>czP|K6!Sb9bmuAf>wAM9$kCC{2uHLdZcm>?ZV2sFW^;5 zQy)Zj{RjRJe^meH4B<87G+}2ktF-Dfx5E-)FnyE&+A=jlNGL5XrLnUfYqkEiEwTF=Lq{s=qaU&Y47c%TzS-V677D>-Ni_0?pSIK+|(>%W)sRf$@h%O<(CjaFMB z=n}^9CA3~c<74+8SfWmZ4`XJ>Q#`w49lvK*4{RGWtq$TFj$&-m7(a!u1e`6kKopE> zm%_L!JHbkNI?Ei|SKcMuESHV)GiQJ5fVHJWW`aWS@C!_NGw#nV%_mPr3_OKx*!C`vUzI>-*1=hM$hQRVHGt!gUG*++2JJ|F3Yw;LZ7H+!X=oennZ=Tw0qk}E z=+N%AV*Ds@^7z%I-3^-4wvRVJBZKrY_VJa8OR6JqWhf6yWMA`XcTcz6mD6sM``^bJ z4JGwlHLU??izsM4$4;SM?+LVar1P5H!Iohrs2ph_4BIS+cK0NUDsc~gDPMcYn&y(8 zacN(%+SXEQ3Qvm3q1C6!MHn+^SolU`#bS&YW8J!*N^Wt-$k3Z$%cLWoCM4Vf(L=t8nUbV6*M-9|L*ELnq!K_{7u0g{&9 z#MwLZq#gTaxFZ#RW1H=0LrpWi^_iYuW&ByY{FnFwzuT4mLVo(cc=~6N5VY5@2Jb-~ zX#IR1v4B8vfBOCDBLiQK)>qGY2)OVk%w+9iD~+F~e>lyKh{W&W?%Xm+U&Cp-{{c?h z9gYpK+gi(A=T+0$)Px_8l`g?Zea=Z7Sqm&NPul|8x{lqhsGGqtMvv6xQ;qsf00K2N zN7%bfl`z<32ET-Xbs+&f0uOK(?IJxhgL6wS!E@+nSgoMckL1+X-QD7H%ccBk?o&{} z&`9y-19iW`MVh-Nd@mr~iId$Q?IJ9a%fQOthi~T{9_fJZ2t=ZF?lf1Jn#jyF>V45l9g~s4uSSFrg?*c+640hm|m-t@QmQe4qN^GvP$Y53NHWrgE(~ zxx1yxzVVCk0t#U}Y|;nM8n!C>l3%(MAe6ZqGT`3TnS24!w+7)`kBO#G{_Lt}Mb+S41x1#y|95StM`84F%cVi5Pd32`_4x2j=t%uKr) zbf-wq+sKd3BHbyXXrFTvJwhkeCED<_JkL&MTE22V1nHzC-3QKI_sRXT>R3U#inA8rT2{!H99X~B=lj-(J?%rB_;fB9Hst0V(v6G8i zZ%V#v^nE)@bW_@Y?P+o9?5@&e*#a4x(N&ZH&A_Gb`P8Hxf2IsO!aU(8jppox+-j4u zg-Z1m+K>3rsn!JW!g_#RrCb@rFJ&h1srpjzI6E{`&KhL>u`xWeWAtpr3&c5Ncete4 zZl0`>IP%1e^2w0{PaAe=ye5=70@}w0KY15qnx={9m(~p8DWG4rc+;_uo(9eRXYrJw zlKB#Nr|G^%v2sia`>kBxcfh?kCK7M#5d-8Se2}M_hqxGlu<)7iJ4GCw!Hz-TLalQv z^`J_?ZDWDW&V`THc3_WbuKkR02$^c=?m6)2erpPH^gPlrPh_5f@V)C}34uwQq88MA>? z){OdR%yybDnjP95VLQTiMC^#%F7Gg_&Zc*q!y5Oahnc3$&eW3O zjrNj9`Xm!Nff=K#pTX%5jy$B_F>;5hI|7mx9VBRkd-~0#+YyIr;vZke7>uh6+OL1o zNjd0l8}91rD9;$-!O&alO~Zi8!~E~LKREwgg`pWL_~M|y_L*xOczya0X!YL9b)UQr zYP6#@oma`!tsDLpM>RHOe>Wl~E2C&FpT|91lmVV_%$G5z2Y)&EbZldWZA9a-lK95! z#f9FQG-d@3QZr($_y*LPbb`)o5iIAHyF-GbiWOL_a{x$L+i({K6!5RWTJF~ zoxc%>;m6hDupNx?FJdacOS|CApfUQaOt}uv^rXpk7#Z@+Kz+wAp;2Ya5-RClfzIMXLB=G!ANp&rqXN{wB8nV-^Df}99Lw1T4v`!#C z)HWvH!y=VG)ynhZ?RNV?JBRa?x;tN<>vrn|TebG(xPz;NknUpe|6kyq6Y#nCnEa69-dE%NFw_p zvy*H!YDP_JO%9>uxI-;{sAetR{%9&F~`60M+qZ%QdwnTbC)2WRXJUlX*8!yNJq!O|M3|s-}@6t{) z9Nw6Eu=cA)#S0?1crgxRh|`hHwc9j}Bpoo8^Z-i}?4cIWOhGhNBE}TqKRQF+>^zVw zB+DzzZXc~Lw_`2qK0Z`G#LgIDskSa=4Lddh+%0A-EKfL=I|&y>mdBpE@S7X+*px1x)GYM9VaG*el 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�a`J^{p3P`nXNXZwiyjtGRS3m}mWx6JQIt$|< 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 AlK=n! literal 0 HcmV?d00001 diff --git a/jni/Android.mk b/jni/Android.mk index 132540ee..0213c71b 100755 --- a/jni/Android.mk +++ b/jni/Android.mk @@ -16,6 +16,19 @@ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) +POLIPO_SOURCES := util.c event.c io.c chunk.c atom.c object.c log.c diskcache.c main.c \ + config.c local.c http.c client.c server.c auth.c tunnel.c \ + http_parse.c parse_time.c dns.c forbidden.c \ + md5.c fts_compat.c socks.c mingw.c + +LOCAL_MODULE := polipo +LOCAL_SRC_FILES := $(addprefix polipo/, $(POLIPO_SOURCES)) +LOCAL_CFLAGS := -O2 -g -DHAS_STDINT_H -DNO_DISK_CACHE -DNO_SYSLOG -I$(LOCAL_PATH)/polipo + +include $(BUILD_EXECUTABLE) + +include $(CLEAR_VARS) + LOCAL_MODULE := libev LOCAL_CFLAGS += -O2 -DNDEBUG -DHAVE_CONFIG_H LOCAL_SRC_FILES := \ diff --git a/jni/polipo/atom.c b/jni/polipo/atom.c new file mode 100644 index 00000000..bfb33707 --- /dev/null +++ b/jni/polipo/atom.c @@ -0,0 +1,376 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +/* Atoms are interned, read-only reference-counted strings. + + Interned means that equality of atoms is equivalent to structural + equality -- you don't need to strcmp, you just compare the AtomPtrs. + This property is used throughout Polipo, e.g. to speed up the HTTP + parser. + + Polipo's atoms may contain NUL bytes -- you can use internAtomN to + store any random binary data within an atom. However, Polipo always + terminates your data, so if you store textual data in an atom, you + may use the result of atomString as though it were a (read-only) + C string. + +*/ + +static AtomPtr *atomHashTable; +int used_atoms; + +void +initAtoms() +{ + atomHashTable = calloc((1 << LOG2_ATOM_HASH_TABLE_SIZE), + sizeof(AtomPtr)); + + if(atomHashTable == NULL) { + do_log(L_ERROR, "Couldn't allocate atom hash table.\n"); + exit(1); + } + used_atoms = 0; +} + +AtomPtr +internAtomN(const char *string, int n) +{ + AtomPtr atom; + int h; + + if(n < 0 || n >= (1 << (8 * sizeof(unsigned short)))) + return NULL; + + h = hash(0, string, n, LOG2_ATOM_HASH_TABLE_SIZE); + atom = atomHashTable[h]; + while(atom) { + if(atom->length == n && + (n == 0 || memcmp(atom->string, string, n) == 0)) + break; + atom = atom->next; + } + + if(!atom) { + atom = malloc(sizeof(AtomRec) - 1 + n + 1); + if(atom == NULL) { + return NULL; + } + atom->refcount = 0; + atom->length = n; + /* Atoms are used both for binary data and strings. To make + their use as strings more convenient, atoms are always + NUL-terminated. */ + memcpy(atom->string, string, n); + atom->string[n] = '\0'; + atom->next = atomHashTable[h]; + atomHashTable[h] = atom; + used_atoms++; + } + do_log(D_ATOM_REFCOUNT, "A 0x%lx %d++\n", + (unsigned long)atom, atom->refcount); + atom->refcount++; + return atom; +} + +AtomPtr +internAtom(const char *string) +{ + return internAtomN(string, strlen(string)); +} + +AtomPtr +atomCat(AtomPtr atom, const char *string) +{ + char buf[128]; + char *s = buf; + AtomPtr newAtom; + int n = strlen(string); + if(atom->length + n > 128) { + s = malloc(atom->length + n + 1); + if(s == NULL) + return NULL; + } + memcpy(s, atom->string, atom->length); + memcpy(s + atom->length, string, n); + newAtom = internAtomN(s, atom->length + n); + if(s != buf) free(s); + return newAtom; +} + +int +atomSplit(AtomPtr atom, char c, AtomPtr *return1, AtomPtr *return2) +{ + char *p; + AtomPtr atom1, atom2; + p = memchr(atom->string, c, atom->length); + if(p == NULL) + return 0; + atom1 = internAtomN(atom->string, p - atom->string); + if(atom1 == NULL) + return -ENOMEM; + atom2 = internAtomN(p + 1, atom->length - (p + 1 - atom->string)); + if(atom2 == NULL) { + releaseAtom(atom1); + return -ENOMEM; + } + *return1 = atom1; + *return2 = atom2; + return 1; +} + +AtomPtr +internAtomLowerN(const char *string, int n) +{ + char *s; + char buf[100]; + AtomPtr atom; + + if(n < 0 || n >= 50000) + return NULL; + + if(n < 100) { + s = buf; + } else { + s = malloc(n); + if(s == NULL) + return NULL; + } + + lwrcpy(s, string, n); + atom = internAtomN(s, n); + if(s != buf) free(s); + return atom; +} + +AtomPtr +retainAtom(AtomPtr atom) +{ + if(atom == NULL) + return NULL; + + do_log(D_ATOM_REFCOUNT, "A 0x%lx %d++\n", + (unsigned long)atom, atom->refcount); + assert(atom->refcount >= 1 && atom->refcount < LARGE_ATOM_REFCOUNT); + atom->refcount++; + return atom; +} + +void +releaseAtom(AtomPtr atom) +{ + if(atom == NULL) + return; + + do_log(D_ATOM_REFCOUNT, "A 0x%lx %d--\n", + (unsigned long)atom, atom->refcount); + assert(atom->refcount >= 1 && atom->refcount < LARGE_ATOM_REFCOUNT); + + atom->refcount--; + + if(atom->refcount == 0) { + int h = hash(0, atom->string, atom->length, LOG2_ATOM_HASH_TABLE_SIZE); + assert(atomHashTable[h] != NULL); + + if(atom == atomHashTable[h]) { + atomHashTable[h] = atom->next; + free(atom); + } else { + AtomPtr previous = atomHashTable[h]; + while(previous->next) { + if(previous->next == atom) + break; + previous = previous->next; + } + assert(previous->next != NULL); + previous->next = atom->next; + free(atom); + } + used_atoms--; + } +} + +AtomPtr +internAtomF(const char *format, ...) +{ + char *s; + char buf[150]; + int n; + va_list args; + AtomPtr atom = NULL; + + va_start(args, format); + n = vsnprintf(buf, 150, format, args); + va_end(args); + if(n >= 0 && n < 150) { + atom = internAtomN(buf, n); + } else { + va_start(args, format); + s = vsprintf_a(format, args); + va_end(args); + if(s != NULL) { + atom = internAtom(s); + free(s); + } + } + return atom; +} + +static AtomPtr +internAtomErrorV(int e, const char *f, va_list args) +{ + + char *es = pstrerror(e); + AtomPtr atom; + char *s1, *s2; + int n, rc; + + if(f) { + s1 = vsprintf_a(f, args); + if(s1 == NULL) + return NULL; + n = strlen(s1); + } else { + s1 = NULL; + n = 0; + } + + s2 = malloc(n + 70); + if(s2 == NULL) { + free(s1); + return NULL; + } + if(s1) { + strcpy(s2, s1); + free(s1); + } + + rc = snprintf(s2 + n, 69, f ? ": %s" : "%s", es); + if(rc < 0 || rc >= 69) { + free(s2); + return NULL; + } + + atom = internAtomN(s2, n + rc); + free(s2); + return atom; +} + +AtomPtr +internAtomError(int e, const char *f, ...) +{ + AtomPtr atom; + va_list args; + va_start(args, f); + atom = internAtomErrorV(e, f, args); + va_end(args); + return atom; +} + +char * +atomString(AtomPtr atom) +{ + if(atom) + return atom->string; + else + return "(null)"; +} + +AtomListPtr +makeAtomList(AtomPtr *atoms, int n) +{ + AtomListPtr list; + list = malloc(sizeof(AtomListRec)); + if(list == NULL) return NULL; + list->length = 0; + list->size = 0; + list->list = NULL; + if(n > 0) { + int i; + list->list = malloc(n * sizeof(AtomPtr)); + if(list->list == NULL) { + free(list); + return NULL; + } + list->size = n; + for(i = 0; i < n; i++) + list->list[i] = atoms[i]; + list->length = n; + } + return list; +} + +void +destroyAtomList(AtomListPtr list) +{ + int i; + if(list->list) { + for(i = 0; i < list->length; i++) + releaseAtom(list->list[i]); + list->length = 0; + free(list->list); + list->list = NULL; + list->size = 0; + } + assert(list->size == 0); + free(list); +} + +int +atomListMember(AtomPtr atom, AtomListPtr list) +{ + int i; + for(i = 0; i < list->length; i++) { + if(atom == list->list[i]) + return 1; + } + return 0; +} + +void +atomListCons(AtomPtr atom, AtomListPtr list) +{ + if(list->list == NULL) { + assert(list->size == 0); + list->list = malloc(5 * sizeof(AtomPtr)); + if(list->list == NULL) { + do_log(L_ERROR, "Couldn't allocate AtomList\n"); + return; + } + list->size = 5; + } + if(list->size <= list->length) { + AtomPtr *new_list; + int n = (2 * list->length + 1); + new_list = realloc(list->list, n * sizeof(AtomPtr)); + if(new_list == NULL) { + do_log(L_ERROR, "Couldn't realloc AtomList\n"); + return; + } + list->list = new_list; + list->size = n; + } + list->list[list->length] = atom; + list->length++; +} + diff --git a/jni/polipo/atom.h b/jni/polipo/atom.h new file mode 100644 index 00000000..0b9adeb7 --- /dev/null +++ b/jni/polipo/atom.h @@ -0,0 +1,58 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +typedef struct _Atom { + unsigned int refcount; + struct _Atom *next; + unsigned short length; + char string[1]; +} AtomRec, *AtomPtr; + +typedef struct _AtomList { + int length; + int size; + AtomPtr *list; +} AtomListRec, *AtomListPtr; + +#define LOG2_ATOM_HASH_TABLE_SIZE 10 +#define LARGE_ATOM_REFCOUNT 0xFFFFFF00U + +extern int used_atoms; + +void initAtoms(void); +AtomPtr internAtom(const char *string); +AtomPtr internAtomN(const char *string, int n); +AtomPtr internAtomLowerN(const char *string, int n); +AtomPtr atomCat(AtomPtr atom, const char *string); +int atomSplit(AtomPtr atom, char c, AtomPtr *return1, AtomPtr *return2); +AtomPtr retainAtom(AtomPtr atom); +void releaseAtom(AtomPtr atom); +AtomPtr internAtomError(int e, const char *f, ...) + ATTRIBUTE ((format (printf, 2, 3))); +AtomPtr internAtomF(const char *format, ...) + ATTRIBUTE ((format (printf, 1, 2))); +char *atomString(AtomPtr) ATTRIBUTE ((pure)); +AtomListPtr makeAtomList(AtomPtr *atoms, int n); +void destroyAtomList(AtomListPtr list); +int atomListMember(AtomPtr atom, AtomListPtr list) + ATTRIBUTE ((pure)); +void atomListCons(AtomPtr atom, AtomListPtr list); diff --git a/jni/polipo/auth.c b/jni/polipo/auth.c new file mode 100644 index 00000000..0ba5c283 --- /dev/null +++ b/jni/polipo/auth.c @@ -0,0 +1,91 @@ +/* +Copyright (c) 2004-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +int +buildClientAuthHeaders(AtomPtr url, char *word, + AtomPtr *message_return, AtomPtr *headers_return) +{ + int code; + char *h; + AtomPtr message, headers; + if(urlIsLocal(url->string, url->length)) { + code = 401; + message = internAtomF("Server authentication %s", word); + h = "WWW-Authenticate"; + } else { + code = 407; + message = internAtomF("Proxy authentication %s", word); + h = "Proxy-Authenticate"; + } + headers = internAtomF("\r\n%s: Basic realm=\"%s\"", + h, authRealm->string); + if(message_return) + *message_return = message; + else + releaseAtom(message); + *headers_return = headers; + return code; +} + +int +checkClientAuth(AtomPtr auth, AtomPtr url, + AtomPtr *message_return, AtomPtr *headers_return) +{ + int code = 0; + AtomPtr message = NULL, headers = NULL; + + if(authRealm == NULL || authCredentials == NULL) + return 0; + + if(auth == NULL) + code = buildClientAuthHeaders(url, "required", &message, &headers); + else if(auth->length >= 6 || lwrcmp(auth->string, "basic ", 6) == 0) { + if(b64cmp(auth->string + 6, auth->length - 6, + authCredentials->string, authCredentials->length) == 0) + return 0; + code = buildClientAuthHeaders(url, "incorrect", &message, &headers); + } else { + code = buildClientAuthHeaders(url, NULL, NULL, &headers); + message = internAtom("Unexpected authentication scheme"); + } + + *message_return = message; + *headers_return = headers; + return code; +} + +int +buildServerAuthHeaders(char* buf, int n, int size, AtomPtr authCredentials) +{ + char authbuf[4 * 128 + 3]; + int authlen; + + if(authCredentials->length >= 3 * 128) + return -1; + authlen = b64cpy(authbuf, parentAuthCredentials->string, + parentAuthCredentials->length, 0); + n = snnprintf(buf, n, size, "\r\nProxy-Authorization: Basic "); + n = snnprint_n(buf, n, size, authbuf, authlen); + return n; +} diff --git a/jni/polipo/auth.h b/jni/polipo/auth.h new file mode 100644 index 00000000..04d6428c --- /dev/null +++ b/jni/polipo/auth.h @@ -0,0 +1,24 @@ +/* +Copyright (c) 2004-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +int checkClientAuth(AtomPtr, AtomPtr, AtomPtr*, AtomPtr*); +int buildServerAuthHeaders(char*, int, int, AtomPtr); diff --git a/jni/polipo/chunk.c b/jni/polipo/chunk.c new file mode 100644 index 00000000..0a7ed166 --- /dev/null +++ b/jni/polipo/chunk.c @@ -0,0 +1,469 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#define MB (1024 * 1024) +int chunkLowMark = 0, + chunkCriticalMark = 0, + chunkHighMark = 0; + +void +preinitChunks() +{ + CONFIG_VARIABLE(chunkLowMark, CONFIG_INT, + "Low mark for chunk memory (0 = auto)."); + CONFIG_VARIABLE(chunkCriticalMark, CONFIG_INT, + "Critical mark for chunk memory (0 = auto)."); + CONFIG_VARIABLE(chunkHighMark, CONFIG_INT, + "High mark for chunk memory."); +} + +static void +initChunksCommon() +{ +#define ROUND_CHUNKS(a) a = (((unsigned long)(a) + CHUNK_SIZE - 1) / CHUNK_SIZE) * CHUNK_SIZE; + int q; + + if(CHUNK_SIZE != 1 << log2_ceil(CHUNK_SIZE)) { + do_log(L_ERROR, "CHUNK SIZE %d is not a power of two.\n", CHUNK_SIZE); + exit(1); + } + + ROUND_CHUNKS(chunkHighMark); + ROUND_CHUNKS(chunkCriticalMark); + ROUND_CHUNKS(chunkLowMark); + + if(chunkHighMark < 8 * CHUNK_SIZE) { + int mem = physicalMemory(); + if(mem > 0) + chunkHighMark = mem / 4; + else + chunkHighMark = 24 * MB; + chunkHighMark = MIN(chunkHighMark, 24 * MB); + chunkHighMark = MAX(chunkHighMark, 8 * CHUNK_SIZE); + } + + if(chunkHighMark < MB / 2) + fprintf(stderr, + "Warning: little chunk memory (%d bytes)\n", chunkHighMark); + + q = 0; + if(chunkLowMark <= 0) q = 1; + if(chunkLowMark < 4 * CHUNK_SIZE || + chunkLowMark > chunkHighMark - 4 * CHUNK_SIZE) { + chunkLowMark = MIN(chunkHighMark - 4 * CHUNK_SIZE, + chunkHighMark * 3 / 4); + ROUND_CHUNKS(chunkLowMark); + if(!q) do_log(L_WARN, "Inconsistent chunkLowMark -- setting to %d.\n", + chunkLowMark); + } + + q = 0; + if(chunkCriticalMark <= 0) q = 1; + if(chunkCriticalMark >= chunkHighMark - 2 * CHUNK_SIZE || + chunkCriticalMark <= chunkLowMark + 2 * CHUNK_SIZE) { + chunkCriticalMark = + MIN(chunkHighMark - 2 * CHUNK_SIZE, + chunkLowMark + (chunkHighMark - chunkLowMark) * 15 / 16); + ROUND_CHUNKS(chunkCriticalMark); + if(!q) do_log(L_WARN, "Inconsistent chunkCriticalMark -- " + "setting to %d.\n", chunkCriticalMark); + } +#undef ROUND_CHUNKS +} + + +int used_chunks = 0; + +static void +maybe_free_chunks(int arenas, int force) +{ + if(force || used_chunks >= CHUNKS(chunkHighMark)) { + discardObjects(force, force); + } + + if(arenas) + free_chunk_arenas(); + + if(used_chunks >= CHUNKS(chunkLowMark) && !objectExpiryScheduled) { + TimeEventHandlerPtr event; + event = scheduleTimeEvent(1, discardObjectsHandler, 0, NULL); + if(event) + objectExpiryScheduled = 1; + } +} + + + +#ifdef MALLOC_CHUNKS + +void +initChunks(void) +{ + do_log(L_WARN, "Warning: using malloc(3) for chunk allocation.\n"); + used_chunks = 0; + initChunksCommon(); +} + +void +free_chunk_arenas() +{ + return; +} + +void * +get_chunk() +{ + void *chunk; + + if(used_chunks > CHUNKS(chunkHighMark)) + maybe_free_chunks(0, 0); + if(used_chunks > CHUNKS(chunkHighMark)) + return NULL; + chunk = malloc(CHUNK_SIZE); + if(!chunk) { + maybe_free_chunks(1, 1); + chunk = malloc(CHUNK_SIZE); + if(!chunk) + return NULL; + } + used_chunks++; + return chunk; +} + +void * +maybe_get_chunk() +{ + void *chunk; + if(used_chunks > CHUNKS(chunkHighMark)) + return NULL; + chunk = malloc(CHUNK_SIZE); + if(chunk) + used_chunks++; + return chunk; +} + +void +dispose_chunk(void *chunk) +{ + assert(chunk != NULL); + free(chunk); + used_chunks--; +} + +void +free_chunks() +{ + return; +} + +int +totalChunkArenaSize() +{ + return used_chunks * CHUNK_SIZE; +} +#else + +#ifdef WIN32 /*MINGW*/ +#define MAP_FAILED NULL +#define getpagesize() (64 * 1024) +static void * +alloc_arena(size_t size) +{ + return VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); +} +static int +free_arena(void *addr, size_t size) +{ + int rc; + rc = VirtualFree(addr, size, MEM_RELEASE); + if(!rc) + rc = -1; + return rc; +} +#else +#ifndef MAP_FAILED +#define MAP_FAILED ((void*)((long int)-1)) +#endif +static void * +alloc_arena(size_t size) +{ + return mmap(NULL, size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); +} +static int +free_arena(void *addr, size_t size) +{ + return munmap(addr, size); +} +#endif + +/* Memory is organised into a number of chunks of ARENA_CHUNKS chunks + each. Every arena is pointed at by a struct _ChunkArena. */ +/* If currentArena is not NULL, it points at the last arena used, + which gives very fast dispose/get sequences. */ + +#define DEFINE_FFS(type, ffs_name) \ +int \ +ffs_name(type i) \ +{ \ + int n; \ + if(i == 0) return 0; \ + n = 1; \ + while((i & 1) == 0) { \ + i >>= 1; \ + n++; \ + } \ + return n; \ +} + +#if defined(DEFAULT_ARENA_BITMAPS) + defined(LONG_ARENA_BITMAPS) + defined(LONG_LONG_ARENA_BITMAPS) > 1 +#error "Multiple sizes of arenas defined" +#endif + +#if defined(DEFAULT_ARENA_BITMAPS) + defined(LONG_ARENA_BITMAPS) + defined(LONG_LONG_ARENA_BITMAPS) == 0 +#ifdef HAVE_FFSL +/* This gives us 32-bit arena bitmaps on LP32, and 64-bit ones on LP64 */ +#define LONG_ARENA_BITMAPS +#else +#define DEFAULT_ARENA_BITMAPS +#endif +#endif + +#if defined(DEFAULT_ARENA_BITMAPS) + +#ifndef HAVE_FFS +DEFINE_FFS(int, ffs) +#endif +typedef unsigned int ChunkBitmap; +#define BITMAP_FFS(bitmap) (ffs(bitmap)) + +#elif defined(LONG_ARENA_BITMAPS) + +#ifndef HAVE_FFSL +DEFINE_FFS(long, ffsl) +#endif +typedef unsigned long ChunkBitmap; +#define BITMAP_FFS(bitmap) (ffsl(bitmap)) + +#elif defined(LONG_LONG_ARENA_BITMAPS) + +#ifndef HAVE_FFSLL +DEFINE_FFS(long long, ffsll) +#endif +typedef unsigned long long ChunkBitmap; +#define BITMAP_FFS(bitmap) (ffsll(bitmap)) + +#else + +#error "You lose" + +#endif + +#define ARENA_CHUNKS ((unsigned)sizeof(ChunkBitmap) * 8) +#define EMPTY_BITMAP (~(ChunkBitmap)0) +#define BITMAP_BIT(i) (((ChunkBitmap)1) << (i)) + +static int pagesize; +typedef struct _ChunkArena { + ChunkBitmap bitmap; + char *chunks; +} ChunkArenaRec, *ChunkArenaPtr; + +static ChunkArenaPtr chunkArenas, currentArena; +static int numArenas; +#define CHUNK_IN_ARENA(chunk, arena) \ + ((arena)->chunks && \ + (char*)(chunk) >= (arena)->chunks && \ + (char*)(chunk) < (arena)->chunks + (ARENA_CHUNKS * CHUNK_SIZE)) + +#define CHUNK_ARENA_INDEX(chunk, arena) \ + ((unsigned)((unsigned long)(((char*)(chunk) - (arena)->chunks)) / \ + CHUNK_SIZE)) + +void +initChunks(void) +{ + int i; + used_chunks = 0; + initChunksCommon(); + pagesize = getpagesize(); + if((CHUNK_SIZE * ARENA_CHUNKS) % pagesize != 0) { + do_log(L_ERROR, + "The arena size %d (%d x %d) " + "is not a multiple of the page size %d.\n", + ARENA_CHUNKS * CHUNK_SIZE, ARENA_CHUNKS, CHUNK_SIZE, pagesize); + abort(); + } + numArenas = + (CHUNKS(chunkHighMark) + (ARENA_CHUNKS - 1)) / ARENA_CHUNKS; + chunkArenas = malloc(numArenas * sizeof(ChunkArenaRec)); + if(chunkArenas == NULL) { + do_log(L_ERROR, "Couldn't allocate chunk arenas.\n"); + exit (1); + } + for(i = 0; i < numArenas; i++) { + chunkArenas[i].bitmap = EMPTY_BITMAP; + chunkArenas[i].chunks = NULL; + } + currentArena = NULL; +} + +static ChunkArenaPtr +findArena() +{ + ChunkArenaPtr arena = NULL; + int i; + + for(i = 0; i < numArenas; i++) { + arena = &(chunkArenas[i]); + if(arena->bitmap != 0) + break; + else + arena = NULL; + } + + assert(arena != NULL); + + if(!arena->chunks) { + void *p; + p = alloc_arena(CHUNK_SIZE * ARENA_CHUNKS); + if(p == MAP_FAILED) { + do_log_error(L_ERROR, errno, "Couldn't allocate chunk"); + maybe_free_chunks(1, 1); + return NULL; + } + arena->chunks = p; + } + return arena; +} + +void * +get_chunk() +{ + unsigned i; + ChunkArenaPtr arena = NULL; + + if(currentArena && currentArena->bitmap != 0) { + arena = currentArena; + } else { + if(used_chunks >= CHUNKS(chunkHighMark)) + maybe_free_chunks(0, 0); + + if(used_chunks >= CHUNKS(chunkHighMark)) + return NULL; + + arena = findArena(); + if(!arena) + return NULL; + currentArena = arena; + } + i = BITMAP_FFS(arena->bitmap) - 1; + arena->bitmap &= ~BITMAP_BIT(i); + used_chunks++; + return arena->chunks + CHUNK_SIZE * i; +} + +void * +maybe_get_chunk() +{ + unsigned i; + ChunkArenaPtr arena = NULL; + + if(currentArena && currentArena->bitmap != 0) { + arena = currentArena; + } else { + if(used_chunks >= CHUNKS(chunkHighMark)) + return NULL; + + arena = findArena(); + if(!arena) + return NULL; + currentArena = arena; + } + i = BITMAP_FFS(arena->bitmap) - 1; + arena->bitmap &= ~BITMAP_BIT(i); + used_chunks++; + return arena->chunks + CHUNK_SIZE * i; +} + +void +dispose_chunk(void *chunk) +{ + ChunkArenaPtr arena = NULL; + unsigned i; + + assert(chunk != NULL); + + if(currentArena && CHUNK_IN_ARENA(chunk, currentArena)) { + arena = currentArena; + } else { + for(i = 0; i < numArenas; i++) { + arena = &(chunkArenas[i]); + if(CHUNK_IN_ARENA(chunk, arena)) + break; + } + assert(arena != NULL); + currentArena = arena; + } + + i = CHUNK_ARENA_INDEX(chunk, arena); + arena->bitmap |= BITMAP_BIT(i); + used_chunks--; +} + +void +free_chunk_arenas() +{ + ChunkArenaPtr arena; + int i, rc; + + for(i = 0; i < numArenas; i++) { + arena = &(chunkArenas[i]); + if(arena->bitmap == EMPTY_BITMAP && arena->chunks) { + rc = free_arena(arena->chunks, CHUNK_SIZE * ARENA_CHUNKS); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't unmap memory"); + continue; + } + arena->chunks = NULL; + } + } + if(currentArena && currentArena->chunks == NULL) + currentArena = NULL; +} + +int +totalChunkArenaSize() +{ + ChunkArenaPtr arena; + int i, size = 0; + + for(i = 0; i < numArenas; i++) { + arena = &(chunkArenas[i]); + if(arena->chunks) + size += (CHUNK_SIZE * ARENA_CHUNKS); + } + return size; +} +#endif diff --git a/jni/polipo/chunk.h b/jni/polipo/chunk.h new file mode 100644 index 00000000..78d97435 --- /dev/null +++ b/jni/polipo/chunk.h @@ -0,0 +1,51 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* A larger chunk size gets you better I/O throughput, at the cost of + higer memory usage. We assume you've got plenty of memory if you've + got 64-bit longs. */ + +#ifndef CHUNK_SIZE +#ifdef ULONG_MAX +#if ULONG_MAX > 4294967295UL +#define CHUNK_SIZE (8 * 1024) +#else +#define CHUNK_SIZE (4 * 1024) +#endif +#else +#define CHUNK_SIZE (4 * 1024) +#endif +#endif + +#define CHUNKS(bytes) ((unsigned long)(bytes) / CHUNK_SIZE) + +extern int chunkLowMark, chunkHighMark, chunkCriticalMark; +extern int used_chunks; + +void preinitChunks(void); +void initChunks(void); +void *get_chunk(void) ATTRIBUTE ((malloc)); +void *maybe_get_chunk(void) ATTRIBUTE ((malloc)); + +void dispose_chunk(void *chunk); +void free_chunk_arenas(void); +int totalChunkArenaSize(void); diff --git a/jni/polipo/client.c b/jni/polipo/client.c new file mode 100644 index 00000000..7419fc7f --- /dev/null +++ b/jni/polipo/client.c @@ -0,0 +1,2148 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +static int +httpAcceptAgain(TimeEventHandlerPtr event) +{ + FdEventHandlerPtr newevent; + int fd = *(int*)event->data; + + newevent = schedule_accept(fd, httpAccept, NULL); + if(newevent == NULL) { + free_chunk_arenas(); + newevent = schedule_accept(fd, httpAccept, NULL); + if(newevent == NULL) { + do_log(L_ERROR, "Couldn't schedule accept.\n"); + polipoExit(); + } + } + return 1; +} + +int +httpAccept(int fd, FdEventHandlerPtr event, AcceptRequestPtr request) +{ + int rc; + HTTPConnectionPtr connection; + TimeEventHandlerPtr timeout; + + if(fd < 0) { + if(-fd == EINTR || -fd == EAGAIN || -fd == EWOULDBLOCK) + return 0; + do_log_error(L_ERROR, -fd, "Couldn't establish listening socket"); + if(-fd == EMFILE || -fd == ENOMEM || -fd == ENOBUFS) { + TimeEventHandlerPtr again = NULL; + do_log(L_WARN, "Refusing client connections for one second.\n"); + free_chunk_arenas(); + again = scheduleTimeEvent(1, httpAcceptAgain, + sizeof(request->fd), &request->fd); + if(!again) { + do_log(L_ERROR, "Couldn't schedule accept -- sleeping.\n"); + sleep(1); + again = scheduleTimeEvent(1, httpAcceptAgain, + sizeof(request->fd), &request->fd); + if(!again) { + do_log(L_ERROR, "Couldn't schedule accept -- aborting.\n"); + polipoExit(); + } + } + return 1; + } else { + polipoExit(); + return 1; + } + } + + if(allowedNets) { + if(netAddressMatch(fd, allowedNets) != 1) { + do_log(L_WARN, "Refusing connection from unauthorised net\n"); + CLOSE(fd); + return 0; + } + } + + rc = setNonblocking(fd, 1); + if(rc < 0) { + do_log_error(L_WARN, errno, "Couldn't set non blocking mode"); + CLOSE(fd); + return 0; + } + rc = setNodelay(fd, 1); + if(rc < 0) + do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm"); + + connection = httpMakeConnection(); + + timeout = scheduleTimeEvent(clientTimeout, httpTimeoutHandler, + sizeof(connection), &connection); + if(!timeout) { + CLOSE(fd); + free(connection); + return 0; + } + + connection->fd = fd; + connection->timeout = timeout; + + do_log(D_CLIENT_CONN, "Accepted client connection 0x%lx\n", + (unsigned long)connection); + + connection->flags = CONN_READER; + + do_stream_buf(IO_READ | IO_NOTNOW, connection->fd, 0, + &connection->reqbuf, CHUNK_SIZE, + httpClientHandler, connection); + return 0; +} + +/* Abort a client connection. It is only safe to abort the requests + if we know the connection is closed. */ +void +httpClientAbort(HTTPConnectionPtr connection, int closed) +{ + HTTPRequestPtr request = connection->request; + + pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLOUT); + if(closed) { + while(request) { + if(request->chandler) { + request->error_code = 500; + request->error_message = internAtom("Connection finishing"); + abortConditionHandler(request->chandler); + request->chandler = NULL; + } + request = request->next; + } + } +} + +/* s != 0 specifies that the connection must be shut down. It is 1 in + order to linger the connection, 2 to close it straight away. */ +void +httpClientFinish(HTTPConnectionPtr connection, int s) +{ + HTTPRequestPtr request = connection->request; + + assert(!(request && request->request + && request->request->request != request)); + + if(s == 0) { + if(!request || !(request->flags & REQUEST_PERSISTENT)) + s = 1; + } + + httpConnectionDestroyBuf(connection); + + connection->flags &= ~CONN_WRITER; + + if(connection->flags & CONN_SIDE_READER) { + /* We're in POST or PUT and the reader isn't done yet. + Wait for the read side to close the connection. */ + assert(request && (connection->flags & CONN_READER)); + if(s >= 2) { + pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN); + } else { + pokeFdEvent(connection->fd, -EDOGRACEFUL, POLLIN); + } + return; + } + + if(connection->timeout) + cancelTimeEvent(connection->timeout); + connection->timeout = NULL; + + if(request) { + HTTPRequestPtr requestee; + + requestee = request->request; + if(requestee) { + request->request = NULL; + requestee->request = NULL; + } + if(requestee) + httpServerClientReset(requestee); + if(request->chandler) { + request->error_code = 500; + request->error_message = internAtom("Connection finishing"); + abortConditionHandler(request->chandler); + request->chandler = NULL; + } + + if(request->object) { + if(request->object->requestor == request) + request->object->requestor = NULL; + releaseObject(request->object); + request->object = NULL; + } + httpDequeueRequest(connection); + httpDestroyRequest(request); + request = NULL; + + } + + connection->len = -1; + connection->offset = 0; + connection->te = TE_IDENTITY; + + if(!s) { + assert(connection->fd > 0); + connection->serviced++; + httpSetTimeout(connection, clientTimeout); + if(!(connection->flags & CONN_READER)) { + if(connection->reqlen == 0) + httpConnectionDestroyReqbuf(connection); + else if((connection->flags & CONN_BIGREQBUF) && + connection->reqlen < CHUNK_SIZE) + httpConnectionUnbigifyReqbuf(connection); + connection->flags |= CONN_READER; + httpSetTimeout(connection, clientTimeout); + do_stream_buf(IO_READ | IO_NOTNOW | + (connection->reqlen ? IO_IMMEDIATE : 0), + connection->fd, connection->reqlen, + &connection->reqbuf, + (connection->flags & CONN_BIGREQBUF) ? + bigBufferSize : CHUNK_SIZE, + httpClientHandler, connection); + } + /* The request has already been validated when it first got + into the queue */ + if(connection->request) { + if(connection->request->object != NULL) + httpClientNoticeRequest(connection->request, 1); + else + assert(connection->flags & CONN_READER); + } + return; + } + + do_log(D_CLIENT_CONN, "Closing client connection 0x%lx\n", + (unsigned long)connection); + + if(connection->flags & CONN_READER) { + httpSetTimeout(connection, 10); + if(connection->fd < 0) return; + if(s >= 2) { + pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN); + } else { + pokeFdEvent(connection->fd, -EDOGRACEFUL, POLLIN); + } + return; + } + while(1) { + HTTPRequestPtr requestee; + request = connection->request; + if(!request) + break; + requestee = request->request; + request->request = NULL; + if(requestee) { + requestee->request = NULL; + httpServerClientReset(requestee); + } + if(request->chandler) + abortConditionHandler(request->chandler); + request->chandler = NULL; + if(request->object && request->object->requestor == request) + request->object->requestor = NULL; + httpDequeueRequest(connection); + httpDestroyRequest(request); + } + httpConnectionDestroyReqbuf(connection); + if(connection->timeout) + cancelTimeEvent(connection->timeout); + connection->timeout = NULL; + if(connection->fd >= 0) { + if(s >= 2) + CLOSE(connection->fd); + else + lingeringClose(connection->fd); + } + connection->fd = -1; + free(connection); +} + +/* Extremely baroque implementation of close: we need to synchronise + between the writer and the reader. */ + +static char client_shutdown_buffer[17]; + +static int httpClientDelayedShutdownHandler(TimeEventHandlerPtr); + +static int +httpClientDelayedShutdown(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr handler; + + assert(connection->flags & CONN_READER); + handler = scheduleTimeEvent(1, httpClientDelayedShutdownHandler, + sizeof(connection), &connection); + if(!handler) { + do_log(L_ERROR, + "Couldn't schedule delayed shutdown -- freeing memory."); + free_chunk_arenas(); + handler = scheduleTimeEvent(1, httpClientDelayedShutdownHandler, + sizeof(connection), &connection); + if(!handler) { + do_log(L_ERROR, + "Couldn't schedule delayed shutdown -- aborting.\n"); + polipoExit(); + } + } + return 1; +} + +static int +httpClientShutdownHandler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + HTTPConnectionPtr connection = request->data; + + assert(connection->flags & CONN_READER); + + if(!(connection->flags & CONN_WRITER)) { + connection->flags &= ~CONN_READER; + connection->reqlen = 0; + httpConnectionDestroyReqbuf(connection); + if(status && status != -EDOGRACEFUL) + httpClientFinish(connection, 2); + else + httpClientFinish(connection, 1); + return 1; + } + + httpClientDelayedShutdown(connection); + return 1; +} + +static int +httpClientDelayedShutdownHandler(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + assert(connection->flags & CONN_READER); + + if(!(connection->flags & CONN_WRITER)) { + connection->flags &= ~CONN_READER; + connection->reqlen = 0; + httpConnectionDestroyReqbuf(connection); + httpClientFinish(connection, 1); + return 1; + } + do_stream(IO_READ | IO_NOTNOW, connection->fd, + 0, client_shutdown_buffer, 17, + httpClientShutdownHandler, connection); + return 1; +} + +int +httpClientHandler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + HTTPConnectionPtr connection = request->data; + int i, body; + int bufsize = + (connection->flags & CONN_BIGREQBUF) ? bigBufferSize : CHUNK_SIZE; + + assert(connection->flags & CONN_READER); + + /* There's no point trying to do something with this request if + the client has shut the connection down -- HTTP doesn't do + half-open connections. */ + if(status != 0) { + connection->reqlen = 0; + httpConnectionDestroyReqbuf(connection); + if(!(connection->flags & CONN_WRITER)) { + connection->flags &= ~CONN_READER; + if(status > 0 || status == -ECONNRESET || status == -EDOSHUTDOWN) + httpClientFinish(connection, 2); + else + httpClientFinish(connection, 1); + return 1; + } + httpClientAbort(connection, status > 0 || status == -ECONNRESET); + connection->flags &= ~CONN_READER; + return 1; + } + + i = findEndOfHeaders(connection->reqbuf, 0, request->offset, &body); + connection->reqlen = request->offset; + + if(i >= 0) { + connection->reqbegin = i; + httpClientHandlerHeaders(event, request, connection); + return 1; + } + + if(connection->reqlen >= bufsize) { + int rc = 0; + if(!(connection->flags & CONN_BIGREQBUF)) + rc = httpConnectionBigifyReqbuf(connection); + if((connection->flags & CONN_BIGREQBUF) && + connection->reqlen < bigBufferSize) { + do_stream(IO_READ, connection->fd, connection->reqlen, + connection->reqbuf, bigBufferSize, + httpClientHandler, connection); + return 1; + } + connection->reqlen = 0; + httpConnectionDestroyReqbuf(connection); + if(rc < 0) { + do_log(L_ERROR, "Couldn't allocate big buffer.\n"); + httpClientNewError(connection, METHOD_UNKNOWN, 0, 400, + internAtom("Couldn't allocate big buffer")); + } else { + do_log(L_ERROR, "Couldn't find end of client's headers.\n"); + httpClientNewError(connection, METHOD_UNKNOWN, 0, 400, + internAtom("Couldn't find end of headers")); + } + return 1; + } + httpSetTimeout(connection, clientTimeout); + return 0; +} + +int +httpClientRawErrorHeaders(HTTPConnectionPtr connection, + int code, AtomPtr message, + int close, AtomPtr headers) +{ + int fd = connection->fd; + int n; + char *url; int url_len; + char *etag; + + assert(connection->flags & CONN_WRITER); + assert(code != 0); + + if(close >= 0) { + if(connection->request) + close = + close || !(connection->request->flags & REQUEST_PERSISTENT); + else + close = 1; + } + if(connection->request && connection->request->object) { + url = connection->request->object->key; + url_len = connection->request->object->key_size; + etag = connection->request->object->etag; + } else { + url = NULL; + url_len = 0; + etag = NULL; + } + + if(connection->buf == NULL) { + connection->buf = get_chunk(); + if(connection->buf == NULL) { + httpClientFinish(connection, 1); + return 1; + } + } + + n = httpWriteErrorHeaders(connection->buf, CHUNK_SIZE, 0, + connection->request && + connection->request->method != METHOD_HEAD, + code, message, close > 0, headers, + url, url_len, etag); + if(n <= 0) { + shutdown(connection->fd, 1); + if(close >= 0) + httpClientFinish(connection, 1); + return 1; + } + + httpSetTimeout(connection, clientTimeout); + do_stream(IO_WRITE, fd, 0, connection->buf, n, + close > 0 ? httpErrorStreamHandler : + close == 0 ? httpErrorNocloseStreamHandler : + httpErrorNofinishStreamHandler, + connection); + + return 1; +} + +int +httpClientRawError(HTTPConnectionPtr connection, int code, AtomPtr message, + int close) +{ + return httpClientRawErrorHeaders(connection, code, message, close, NULL); +} + +int +httpClientNoticeErrorHeaders(HTTPRequestPtr request, int code, AtomPtr message, + AtomPtr headers) +{ + if(request->error_message) + releaseAtom(request->error_message); + if(request->error_headers) + releaseAtom(request->error_headers); + request->error_code = code; + request->error_message = message; + request->error_headers = headers; + httpClientNoticeRequest(request, 0); + return 1; +} + +int +httpClientNoticeError(HTTPRequestPtr request, int code, AtomPtr message) +{ + return httpClientNoticeErrorHeaders(request, code, message, NULL); +} + +int +httpClientError(HTTPRequestPtr request, int code, AtomPtr message) +{ + if(request->error_message) + releaseAtom(request->error_message); + request->error_code = code; + request->error_message = message; + if(request->chandler) { + abortConditionHandler(request->chandler); + request->chandler = NULL; + } else if(request->object) + notifyObject(request->object); + return 1; +} + +/* This may be called from object handlers. */ +int +httpClientLeanError(HTTPRequestPtr request, int code, AtomPtr message) +{ + if(request->error_message) + releaseAtom(request->error_message); + request->error_code = code; + request->error_message = message; + return 1; +} + + +int +httpClientNewError(HTTPConnectionPtr connection, int method, int persist, + int code, AtomPtr message) +{ + HTTPRequestPtr request; + request = httpMakeRequest(); + if(request == NULL) { + do_log(L_ERROR, "Couldn't allocate error request.\n"); + httpClientFinish(connection, 1); + return 1; + } + request->method = method; + if(persist) + request->flags |= REQUEST_PERSISTENT; + else + request->flags &= ~REQUEST_PERSISTENT; + request->error_code = code; + request->error_message = message; + + httpQueueRequest(connection, request); + httpClientNoticeRequest(request, 0); + return 1; +} + +int +httpErrorStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + + if(status == 0 && !streamRequestDone(srequest)) + return 0; + + httpClientFinish(connection, 1); + return 1; +} + +int +httpErrorNocloseStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + + if(status == 0 && !streamRequestDone(srequest)) + return 0; + + httpClientFinish(connection, 0); + return 1; +} + +int +httpErrorNofinishStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + if(status == 0 && !streamRequestDone(srequest)) + return 0; + + return 1; +} + +int +httpClientHandlerHeaders(FdEventHandlerPtr event, StreamRequestPtr srequest, + HTTPConnectionPtr connection) +{ + HTTPRequestPtr request; + int rc; + int method, version; + AtomPtr url = NULL; + int start; + int code; + AtomPtr message; + + start = 0; + /* Work around clients working around NCSA lossage. */ + if(connection->reqbuf[0] == '\n') + start = 1; + else if(connection->reqbuf[0] == '\r' && connection->reqbuf[1] == '\n') + start = 2; + + httpSetTimeout(connection, -1); + rc = httpParseClientFirstLine(connection->reqbuf, start, + &method, &url, &version); + if(rc <= 0) { + do_log(L_ERROR, "Couldn't parse client's request line\n"); + code = 400; + message = internAtom("Error in request line"); + goto fail; + } + + do_log(D_CLIENT_REQ, "Client request: "); + do_log_n(D_CLIENT_REQ, connection->reqbuf, rc - 1); + do_log(D_CLIENT_REQ, "\n"); + + if(version != HTTP_10 && version != HTTP_11) { + do_log(L_ERROR, "Unknown client HTTP version\n"); + code = 400; + message = internAtom("Error in first request line"); + goto fail; + } + + if(method == METHOD_UNKNOWN) { + code = 501; + message = internAtom("Method not implemented"); + goto fail; + } + + request = httpMakeRequest(); + if(request == NULL) { + do_log(L_ERROR, "Couldn't allocate client request.\n"); + code = 500; + message = internAtom("Couldn't allocate client request"); + goto fail; + } + + if(connection->version != HTTP_UNKNOWN && version != connection->version) { + do_log(L_WARN, "Client version changed!\n"); + } + + connection->version = version; + request->flags = REQUEST_PERSISTENT; + request->method = method; + request->cache_control = no_cache_control; + httpQueueRequest(connection, request); + connection->reqbegin = rc; + return httpClientRequest(request, url); + + fail: + if(url) releaseAtom(url); + shutdown(connection->fd, 0); + connection->reqlen = 0; + connection->reqbegin = 0; + httpConnectionDestroyReqbuf(connection); + connection->flags &= ~CONN_READER; + httpClientNewError(connection, METHOD_UNKNOWN, 0, code, message); + return 1; + +} + +static int +httpClientRequestDelayed(TimeEventHandlerPtr event) +{ + HTTPRequestPtr request = *(HTTPRequestPtr*)event->data; + AtomPtr url; + url = internAtomN(request->object->key, request->object->key_size); + if(url == NULL) { + do_log(L_ERROR, "Couldn't allocate url.\n"); + abortObject(request->object, 503, internAtom("Couldn't allocate url")); + return 1; + } + httpClientRequest(request, url); + return 1; +} + +int +delayedHttpClientRequest(HTTPRequestPtr request) +{ + TimeEventHandlerPtr event; + event = scheduleTimeEvent(-1, httpClientRequestDelayed, + sizeof(request), &request); + if(!event) + return -1; + return 1; +} + +int +httpClientRequest(HTTPRequestPtr request, AtomPtr url) +{ + HTTPConnectionPtr connection = request->connection; + int i, rc; + int body_len, body_te; + AtomPtr headers; + CacheControlRec cache_control; + AtomPtr via, expect, auth; + HTTPConditionPtr condition; + HTTPRangeRec range; + + assert(!request->chandler); + assert(connection->reqbuf); + + i = httpParseHeaders(1, url, + connection->reqbuf, connection->reqbegin, request, + &headers, &body_len, + &cache_control, &condition, &body_te, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, + &expect, &range, NULL, NULL, &via, &auth); + if(i < 0) { + releaseAtom(url); + do_log(L_ERROR, "Couldn't parse client headers.\n"); + shutdown(connection->fd, 0); + request->flags &= ~REQUEST_PERSISTENT; + connection->flags &= ~CONN_READER; + httpClientNoticeError(request, 503, + internAtom("Couldn't parse client headers")); + return 1; + } + + connection->reqbegin = i; + + if(body_len < 0) { + if(request->method == METHOD_GET || request->method == METHOD_HEAD) + body_len = 0; + } + connection->bodylen = body_len; + connection->reqte = body_te; + + if(authRealm) { + AtomPtr message = NULL; + AtomPtr challenge = NULL; + int code = checkClientAuth(auth, url, &message, &challenge); + if(auth) { + releaseAtom(auth); + auth = NULL; + } + if(expect) { + releaseAtom(expect); + expect = NULL; + } + if(code) { + request->flags |= REQUEST_FORCE_ERROR; + httpClientDiscardBody(connection); + httpClientNoticeErrorHeaders(request, code, message, challenge); + return 1; + } + } + + if(auth) { + releaseAtom(auth); + auth = NULL; + } + + if(expect) { + if(expect == atom100Continue && REQUEST_SIDE(request)) { + request->flags |= REQUEST_WAIT_CONTINUE; + } else { + httpClientDiscardBody(connection); + httpClientNoticeError(request, 417, + internAtom("Expectation failed")); + releaseAtom(expect); + return 1; + } + releaseAtom(expect); + } + + request->from = range.from < 0 ? 0 : range.from; + request->to = range.to; + request->cache_control = cache_control; + request->via = via; + request->headers = headers; + request->condition = condition; + request->object = NULL; + + if(connection->serviced > 500) + request->flags &= ~REQUEST_PERSISTENT; + + if(request->method == METHOD_CONNECT) { + if(connection->flags & CONN_WRITER) { + /* For now */ + httpClientDiscardBody(connection); + httpClientNoticeError(request, 500, + internAtom("Pipelined CONNECT " + "not supported")); + return 1; + } + if(connection->flags & CONN_BIGREQBUF) { + /* For now */ + httpClientDiscardBody(connection); + httpClientNoticeError(request, 500, + internAtom("CONNECT over big buffer " + "not supported")); + return 1; + } + connection->flags &= ~CONN_READER; + do_tunnel(connection->fd, connection->reqbuf, + connection->reqbegin, connection->reqlen, url); + connection->fd = -1; + connection->reqbuf = NULL; + connection->reqlen = 0; + connection->reqbegin = 0; + httpClientFinish(connection, 2); + return 1; + } + + rc = urlForbidden(url, httpClientRequestContinue, request); + if(rc < 0) { + do_log(L_ERROR, "Couldn't schedule httpClientRequestContinue.\n"); + httpClientDiscardBody(connection); + httpClientNoticeError(request, 500, + internAtom("Couldn't schedule " + "httpClientRequestContinue")); + return 1; + } + return 1; +} + +int +httpClientRequestContinue(int forbidden_code, AtomPtr url, + AtomPtr forbidden_message, AtomPtr forbidden_headers, + void *closure) +{ + HTTPRequestPtr request = (HTTPRequestPtr)closure; + HTTPConnectionPtr connection = request->connection; + RequestFunction requestfn; + ObjectPtr object = NULL; + + if(forbidden_code < 0) { + releaseAtom(url); + httpClientDiscardBody(connection); + httpClientNoticeError(request, 500, + internAtomError(-forbidden_code, + "Couldn't test for forbidden " + "URL")); + return 1; + } + + if(forbidden_code) { + releaseAtom(url); + httpClientDiscardBody(connection); + httpClientNoticeErrorHeaders(request, + forbidden_code, forbidden_message, + forbidden_headers); + return 1; + } + + requestfn = + urlIsLocal(url->string, url->length) ? + httpLocalRequest : + httpServerRequest; + + if(request->method == METHOD_POST || request->method == METHOD_PUT) { + do { + object = findObject(OBJECT_HTTP, url->string, url->length); + if(object) { + privatiseObject(object, 0); + releaseObject(object); + } + } while(object); + request->object = makeObject(OBJECT_HTTP, url->string, url->length, + 0, 0, requestfn, NULL); + if(request->object == NULL) { + httpClientDiscardBody(connection); + httpClientNoticeError(request, 503, + internAtom("Couldn't allocate object")); + return 1; + } + if(requestfn == httpLocalRequest) + request->object->flags |= OBJECT_LOCAL; + return httpClientSideRequest(request); + } + + if(request->cache_control.flags & CACHE_AUTHORIZATION) { + do { + object = makeObject(OBJECT_HTTP, url->string, url->length, 0, 0, + requestfn, NULL); + if(object && object->flags != OBJECT_INITIAL) { + if(!(object->cache_control & CACHE_PUBLIC)) { + privatiseObject(object, 0); + releaseObject(object); + object = NULL; + } else + break; + } + } while(object == NULL); + if(object) + object->flags |= OBJECT_LINEAR; + } else { + object = findObject(OBJECT_HTTP, url->string, url->length); + if(!object) + object = makeObject(OBJECT_HTTP, url->string, url->length, 1, 1, + requestfn, NULL); + } + releaseAtom(url); + url = NULL; + + if(!object) { + do_log(L_ERROR, "Couldn't allocate object.\n"); + httpClientDiscardBody(connection); + httpClientNoticeError(request, 503, + internAtom("Couldn't allocate object")); + return 1; + } + + if(object->request == httpLocalRequest) { + object->flags |= OBJECT_LOCAL; + } else { + if(disableProxy) { + httpClientDiscardBody(connection); + httpClientNoticeError(request, 403, + internAtom("Proxying disabled")); + releaseObject(object); + return 1; + } + + if(!checkVia(proxyName, request->via)) { + httpClientDiscardBody(connection); + httpClientNoticeError(request, 504, + internAtom("Proxy loop detected")); + releaseObject(object); + return 1; + } + } + + request->object = object; + + httpClientDiscardBody(connection); + httpClientNoticeRequest(request, 0); + return 1; +} + +static int httpClientDelayed(TimeEventHandlerPtr handler); + +int +httpClientDiscardBody(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr handler; + + assert(connection->reqoffset == 0); + assert(connection->flags & CONN_READER); + + if(connection->reqte != TE_IDENTITY) + goto fail; + + if(connection->bodylen < 0) + goto fail; + + if(connection->bodylen < connection->reqlen - connection->reqbegin) { + connection->reqbegin += connection->bodylen; + connection->bodylen = 0; + } else { + connection->bodylen -= connection->reqlen - connection->reqbegin; + connection->reqbegin = 0; + connection->reqlen = 0; + httpConnectionDestroyReqbuf(connection); + } + connection->reqte = TE_UNKNOWN; + + if(connection->bodylen > 0) { + httpSetTimeout(connection, clientTimeout); + do_stream_buf(IO_READ | IO_NOTNOW, + connection->fd, connection->reqlen, + &connection->reqbuf, CHUNK_SIZE, + httpClientDiscardHandler, connection); + return 1; + } + + if(connection->reqlen > connection->reqbegin) { + memmove(connection->reqbuf, connection->reqbuf + connection->reqbegin, + connection->reqlen - connection->reqbegin); + connection->reqlen -= connection->reqbegin; + connection->reqbegin = 0; + } else { + connection->reqlen = 0; + connection->reqbegin = 0; + } + + httpSetTimeout(connection, clientTimeout); + /* We need to delay in order to make sure the previous request + gets queued on the server side. IO_NOTNOW isn't strong enough + for that due to IO_IMMEDIATE. */ + handler = scheduleTimeEvent(-1, httpClientDelayed, + sizeof(connection), &connection); + if(handler == NULL) { + do_log(L_ERROR, "Couldn't schedule reading from client."); + goto fail; + } + return 1; + + fail: + connection->reqlen = 0; + connection->reqbegin = 0; + connection->bodylen = 0; + connection->reqte = TE_UNKNOWN; + shutdown(connection->fd, 2); + handler = scheduleTimeEvent(-1, httpClientDelayed, + sizeof(connection), &connection); + if(handler == NULL) { + do_log(L_ERROR, "Couldn't schedule reading from client."); + connection->flags &= ~CONN_READER; + } + return 1; +} + +static int +httpClientDelayed(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + + /* IO_NOTNOW is unfortunate, but needed to avoid starvation if a + client is pipelining a lot of requests. */ + if(connection->reqlen > 0) { + int bufsize; + if((connection->flags & CONN_BIGREQBUF) && + connection->reqlen < CHUNK_SIZE) + httpConnectionUnbigifyReqbuf(connection); + /* Don't read new requests if buffer is big. */ + bufsize = (connection->flags & CONN_BIGREQBUF) ? + connection->reqlen : CHUNK_SIZE; + do_stream(IO_READ | IO_IMMEDIATE | IO_NOTNOW, + connection->fd, connection->reqlen, + connection->reqbuf, bufsize, + httpClientHandler, connection); + } else { + httpConnectionDestroyReqbuf(connection); + do_stream_buf(IO_READ | IO_NOTNOW, + connection->fd, 0, + &connection->reqbuf, CHUNK_SIZE, + httpClientHandler, connection); + } + return 1; +} + +int +httpClientDiscardHandler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + HTTPConnectionPtr connection = request->data; + + assert(connection->flags & CONN_READER); + if(status) { + if(status < 0 && status != -EPIPE) + do_log_error(L_ERROR, -status, "Couldn't read from client"); + connection->bodylen = -1; + return httpClientDiscardBody(connection); + } + + assert(request->offset > connection->reqlen); + connection->reqlen = request->offset; + + httpClientDiscardBody(connection); + return 1; +} + +int +httpClientNoticeRequest(HTTPRequestPtr request, int novalidate) +{ + HTTPConnectionPtr connection = request->connection; + ObjectPtr object = request->object; + int serveNow = (request == connection->request); + int validate = 0; + int conditional = 0; + int local, haveData; + int rc; + + assert(!request->chandler); + + if(request->error_code) { + if((request->flags & REQUEST_FORCE_ERROR) || REQUEST_SIDE(request) || + request->object == NULL || + (request->object->flags & OBJECT_LOCAL) || + (request->object->flags & OBJECT_ABORTED) || + (relaxTransparency < 1 && !proxyOffline)) { + if(serveNow) { + connection->flags |= CONN_WRITER; + return httpClientRawErrorHeaders(connection, + request->error_code, + retainAtom(request-> + error_message), + 0, request->error_headers); + } else { + return 1; + } + } + } + + if(REQUEST_SIDE(request)) { + assert(!(request->flags & REQUEST_REQUESTED)); + if(serveNow) { + assert(!request->chandler); + request->chandler = + conditionWait(&request->object->condition, + httpClientGetHandler, + sizeof(request), &request); + if(request->chandler == NULL) { + do_log(L_ERROR, "Couldn't register condition handler.\n"); + connection->flags |= CONN_WRITER; + httpClientRawError(connection, 500, + internAtom("Couldn't register " + "condition handler"), + 0); + return 1; + } + connection->flags |= CONN_WRITER; + rc = object->request(request->object, + request->method, + request->from, request->to, + request, + request->object->request_closure); + } + return 1; + } + + local = urlIsLocal(object->key, object->key_size); + objectFillFromDisk(object, request->from, + request->method == METHOD_HEAD ? 0 : 1); + + /* The spec doesn't strictly forbid 206 for non-200 instances, but doing + that breaks some client software. */ + if(object->code && object->code != 200) { + request->from = 0; + request->to = -1; + } + + if(request->condition && request->condition->ifrange) { + if(!object->etag || + strcmp(object->etag, request->condition->ifrange) != 0) { + request->from = 0; + request->to = -1; + } + } + + if(object->flags & OBJECT_DYNAMIC) { + request->from = 0; + request->to = -1; + } + + if(request->method == METHOD_HEAD) + haveData = !(request->object->flags & OBJECT_INITIAL); + else + haveData = + (request->object->length >= 0 && + request->object->length <= request->from) || + (objectHoleSize(request->object, request->from) == 0); + + if(request->flags & REQUEST_REQUESTED) + validate = 0; + else if(novalidate || (!local && proxyOffline)) + validate = 0; + else if(local) + validate = + objectMustRevalidate(request->object, &request->cache_control); + else if(request->cache_control.flags & CACHE_ONLY_IF_CACHED) + validate = 0; + else if((request->object->flags & OBJECT_FAILED) && + !(object->flags & OBJECT_INPROGRESS) && + !relaxTransparency) + validate = 1; + else if(request->method != METHOD_HEAD && + !objectHasData(object, request->from, request->to) && + !(object->flags & OBJECT_INPROGRESS)) + validate = 1; + else if(objectMustRevalidate((relaxTransparency <= 1 ? + request->object : NULL), + &request->cache_control)) + validate = 1; + else + validate = 0; + + if(request->cache_control.flags & CACHE_ONLY_IF_CACHED) { + validate = 0; + if(!haveData) { + if(serveNow) { + connection->flags |= CONN_WRITER; + return httpClientRawError(connection, 504, + internAtom("Object not in cache"), + 0); + } else + return 1; + } + } + + if(!(request->object->flags & OBJECT_VALIDATING) && + ((!validate && haveData) || + (request->object->flags & OBJECT_FAILED))) { + if(serveNow) { + connection->flags |= CONN_WRITER; + lockChunk(request->object, request->from / CHUNK_SIZE); + return httpServeObject(connection); + } else { + return 1; + } + } + + if((request->flags & REQUEST_REQUESTED) && + !(request->object->flags & OBJECT_INPROGRESS)) { + /* This can happen either because the server side ran out of + memory, or because it is using HEAD validation. We mark + the object to be fetched again. */ + request->flags &= ~REQUEST_REQUESTED; + } + + if(serveNow) { + connection->flags |= CONN_WRITER; + if(!local && proxyOffline) + return httpClientRawError(connection, 502, + internAtom("Disconnected operation " + "and object not in cache"), + 0); + request->chandler = + conditionWait(&request->object->condition, httpClientGetHandler, + sizeof(request), &request); + if(request->chandler == NULL) { + do_log(L_ERROR, "Couldn't register condition handler.\n"); + return httpClientRawError(connection, 503, + internAtom("Couldn't register " + "condition handler"), 0); + } + } + + if(request->object->flags & OBJECT_VALIDATING) + return 1; + + conditional = (haveData && request->method == METHOD_GET); + if(!mindlesslyCacheVary && (request->object->cache_control & CACHE_VARY)) + conditional = conditional && (request->object->etag != NULL); + + conditional = + conditional && !(request->object->cache_control & CACHE_MISMATCH); + + if(!(request->object->flags & OBJECT_INPROGRESS)) + request->object->flags |= OBJECT_VALIDATING; + rc = request->object->request(request->object, + conditional ? METHOD_CONDITIONAL_GET : + request->method, + request->from, request->to, request, + request->object->request_closure); + if(rc < 0) { + if(request->chandler) + unregisterConditionHandler(request->chandler); + request->chandler = NULL; + request->object->flags &= ~OBJECT_VALIDATING; + request->object->flags |= OBJECT_FAILED; + if(request->error_message) + releaseAtom(request->error_message); + request->error_code = 503; + request->error_message = internAtom("Couldn't schedule get"); + } + return 1; +} + +static int +httpClientNoticeRequestDelayed(TimeEventHandlerPtr event) +{ + HTTPRequestPtr request = *(HTTPRequestPtr*)event->data; + httpClientNoticeRequest(request, 0); + return 1; +} + +int +delayedHttpClientNoticeRequest(HTTPRequestPtr request) +{ + TimeEventHandlerPtr event; + event = scheduleTimeEvent(-1, httpClientNoticeRequestDelayed, + sizeof(request), &request); + if(!event) + return -1; + return 1; +} + +int +httpClientContinueDelayed(TimeEventHandlerPtr event) +{ + static char httpContinue[] = "HTTP/1.1 100 Continue\r\n\r\n"; + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + + do_stream(IO_WRITE, connection->fd, 0, httpContinue, 25, + httpErrorNofinishStreamHandler, connection); + return 1; +} + +int +delayedHttpClientContinue(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr event; + event = scheduleTimeEvent(-1, httpClientContinueDelayed, + sizeof(connection), &connection); + if(!event) + return -1; + return 1; +} + +int +httpClientGetHandler(int status, ConditionHandlerPtr chandler) +{ + HTTPRequestPtr request = *(HTTPRequestPtr*)chandler->data; + HTTPConnectionPtr connection = request->connection; + ObjectPtr object = request->object; + int rc; + + assert(request == connection->request); + + if(request->request) { + assert(request->object->flags & OBJECT_INPROGRESS); + assert(!request->request->object || + request->request->object == request->object); + } + + if(status < 0) { + object->flags &= ~OBJECT_VALIDATING; /* for now */ + if(request->request && request->request->request == request) + httpServerClientReset(request->request); + lockChunk(object, request->from / CHUNK_SIZE); + request->chandler = NULL; + rc = delayedHttpServeObject(connection); + if(rc < 0) { + unlockChunk(object, request->from / CHUNK_SIZE); + do_log(L_ERROR, "Couldn't schedule serving.\n"); + abortObject(object, 503, internAtom("Couldn't schedule serving")); + } + return 1; + } + + if(object->flags & OBJECT_VALIDATING) + return 0; + + if(request->error_code) { + lockChunk(object, request->from / CHUNK_SIZE); + request->chandler = NULL; + rc = delayedHttpServeObject(connection); + if(rc < 0) { + unlockChunk(object, request->from / CHUNK_SIZE); + do_log(L_ERROR, "Couldn't schedule serving.\n"); + abortObject(object, 503, internAtom("Couldn't schedule serving")); + } + return 1; + } + + if(request->flags & REQUEST_WAIT_CONTINUE) { + if(request->request && + !(request->request->flags & REQUEST_WAIT_CONTINUE)) { + request->flags &= ~REQUEST_WAIT_CONTINUE; + delayedHttpClientContinue(connection); + } + return 0; + } + + /* See httpServerHandlerHeaders */ + if((object->flags & OBJECT_SUPERSEDED) && + request->request && request->request->can_mutate) { + ObjectPtr new_object = retainObject(request->request->can_mutate); + if(object->requestor == request) { + if(new_object->requestor == NULL) + new_object->requestor = request; + object->requestor = NULL; + } + request->chandler = NULL; + releaseObject(object); + request->object = new_object; + request->request->object = new_object; + /* We're handling the wrong object now. It's simpler to + rebuild the whole data structure from scratch rather than + trying to compensate. */ + rc = delayedHttpClientNoticeRequest(request); + if(rc < 0) { + do_log(L_ERROR, "Couldn't schedule noticing of request."); + abortObject(object, 500, + internAtom("Couldn't schedule " + "noticing of request")); + /* We're probably out of memory. What can we do? */ + shutdown(connection->fd, 1); + } + return 1; + } + + if(object->requestor != request && !(object->flags & OBJECT_ABORTED)) { + /* Make sure we don't serve an object that is stale for us + unless we're the requestor. */ + if((object->flags & (OBJECT_LINEAR | OBJECT_MUTATING)) || + objectMustRevalidate(object, &request->cache_control)) { + if(object->flags & OBJECT_INPROGRESS) + return 0; + rc = delayedHttpClientNoticeRequest(request); + if(rc < 0) { + do_log(L_ERROR, "Couldn't schedule noticing of request."); + abortObject(object, 500, + internAtom("Couldn't schedule " + "noticing of request")); + } else { + request->chandler = NULL; + return 1; + } + } + } + + if(object->flags & (OBJECT_INITIAL | OBJECT_VALIDATING)) { + if(object->flags & (OBJECT_INPROGRESS | OBJECT_VALIDATING)) { + return 0; + } else if(object->flags & OBJECT_FAILED) { + if(request->error_code) + abortObject(object, + request->error_code, + retainAtom(request->error_message)); + else { + abortObject(object, 500, + internAtom("Error message lost in transit")); + } + } else { + /* The request was pruned by httpServerDiscardRequests */ + if(chandler == request->chandler) { + int rc; + request->chandler = NULL; + rc = delayedHttpClientNoticeRequest(request); + if(rc < 0) + abortObject(object, 500, + internAtom("Couldn't allocate " + "delayed notice request")); + else + return 1; + } else { + abortObject(object, 500, + internAtom("Wrong request pruned -- " + "this shouldn't happen")); + } + } + } + + if(request->object->flags & OBJECT_DYNAMIC) { + if(objectHoleSize(request->object, 0) == 0) { + request->from = 0; + request->to = -1; + } else { + /* We really should request again if that is not the case */ + } + } + + lockChunk(object, request->from / CHUNK_SIZE); + request->chandler = NULL; + rc = delayedHttpServeObject(connection); + if(rc < 0) { + unlockChunk(object, request->from / CHUNK_SIZE); + do_log(L_ERROR, "Couldn't schedule serving.\n"); + abortObject(object, 503, internAtom("Couldn't schedule serving")); + } + return 1; +} + +int +httpClientSideRequest(HTTPRequestPtr request) +{ + HTTPConnectionPtr connection = request->connection; + + if(request->from < 0 || request->to >= 0) { + httpClientNoticeError(request, 501, + internAtom("Partial requests not implemented")); + httpClientDiscardBody(connection); + return 1; + } + if(connection->reqte != TE_IDENTITY) { + httpClientNoticeError(request, 501, + internAtom("Chunked requests not implemented")); + httpClientDiscardBody(connection); + return 1; + } + if(connection->bodylen < 0) { + httpClientNoticeError(request, 502, + internAtom("POST or PUT without " + "Content-Length")); + httpClientDiscardBody(connection); + return 1; + } + if(connection->reqlen < 0) { + httpClientNoticeError(request, 502, + internAtom("Incomplete POST or PUT")); + httpClientDiscardBody(connection); + return 1; + } + + return httpClientNoticeRequest(request, 0); +} + +int +httpClientSideHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + HTTPRequestPtr requestee; + HTTPConnectionPtr server; + int push; + int code; + AtomPtr message = NULL; + + assert(connection->flags & CONN_SIDE_READER); + + if((request->object->flags & OBJECT_ABORTED) || + !(request->object->flags & OBJECT_INPROGRESS)) { + code = request->object->code; + message = retainAtom(request->object->message); + goto fail; + } + + if(status < 0) { + do_log_error(L_ERROR, -status, "Reading from client"); + code = 502; + message = internAtomError(-status, "Couldn't read from client"); + goto fail; + } + + requestee = request->request; + server = requestee->connection; + + push = MIN(srequest->offset - connection->reqlen, + connection->bodylen - connection->reqoffset); + if(push > 0) { + connection->reqlen += push; + httpServerDoSide(server); + return 1; + } + + if(server->reqoffset >= connection->bodylen) { + connection->flags &= ~(CONN_READER | CONN_SIDE_READER); + return 1; + } + + assert(status); + do_log(L_ERROR, "Incomplete client request.\n"); + code = 502; + message = internAtom("Incomplete client request"); + + fail: + request->error_code = code; + if(request->error_message) + releaseAtom(request->error_message); + request->error_message = message; + if(request->error_headers) + releaseAtom(request->error_headers); + request->error_headers = NULL; + + if(request->request) { + shutdown(request->request->connection->fd, 2); + pokeFdEvent(request->request->connection->fd, -ESHUTDOWN, POLLOUT); + } + notifyObject(request->object); + connection->flags &= ~CONN_SIDE_READER; + httpClientDiscardBody(connection); + return 1; +} + +int +httpServeObject(HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + ObjectPtr object = request->object; + int i = request->from / CHUNK_SIZE; + int j = request->from % CHUNK_SIZE; + int n, len, rc; + int bufsize = CHUNK_SIZE; + int condition_result; + + object->atime = current_time.tv_sec; + objectMetadataChanged(object, 0); + + httpSetTimeout(connection, -1); + + if((request->error_code && relaxTransparency <= 0) || + object->flags & OBJECT_INITIAL) { + object->flags &= ~OBJECT_FAILED; + unlockChunk(object, i); + if(request->error_code) + return httpClientRawError(connection, + request->error_code, + retainAtom(request->error_message), 0); + else + return httpClientRawError(connection, + 500, internAtom("Object vanished."), 0); + } + + if(!(object->flags & OBJECT_INPROGRESS) && object->code == 0) { + if(object->flags & OBJECT_INITIAL) { + unlockChunk(object, i); + return httpClientRawError(connection, 503, + internAtom("Error message lost"), 0); + + } else { + unlockChunk(object, i); + do_log(L_ERROR, "Internal proxy error: object has no code.\n"); + return httpClientRawError(connection, 500, + internAtom("Internal proxy error: " + "object has no code"), 0); + } + } + + condition_result = httpCondition(object, request->condition); + + if(condition_result == CONDITION_FAILED) { + unlockChunk(object, i); + return httpClientRawError(connection, 412, + internAtom("Precondition failed"), 0); + } else if(condition_result == CONDITION_NOT_MODIFIED) { + unlockChunk(object, i); + return httpClientRawError(connection, 304, + internAtom("Not modified"), 0); + } + + objectFillFromDisk(object, request->from, + (request->method == METHOD_HEAD || + condition_result != CONDITION_MATCH) ? 0 : 1); + + if(((object->flags & OBJECT_LINEAR) && + (object->requestor != connection->request)) || + ((object->flags & OBJECT_SUPERSEDED) && + !(object->flags & OBJECT_LINEAR))) { + if(request->request) { + request->request->request = NULL; + request->request = NULL; + request->object->requestor = NULL; + } + object = makeObject(OBJECT_HTTP, + object->key, object->key_size, 1, 0, + object->request, NULL); + if(request->object->requestor == request) + request->object->requestor = NULL; + unlockChunk(request->object, i); + releaseObject(request->object); + request->object = NULL; + if(object == NULL) { + do_log(L_ERROR, "Couldn't allocate object."); + return httpClientRawError(connection, 501, + internAtom("Couldn't allocate object"), + 1); + } + if(urlIsLocal(object->key, object->key_size)) { + object->flags |= OBJECT_LOCAL; + object->request = httpLocalRequest; + } + request->object = object; + connection->flags &= ~CONN_WRITER; + return httpClientNoticeRequest(request, 1); + } + + if(object->flags & OBJECT_ABORTED) { + unlockChunk(object, i); + return httpClientNoticeError(request, object->code, + retainAtom(object->message)); + } + + if(connection->buf == NULL) + connection->buf = get_chunk(); + if(connection->buf == NULL) { + unlockChunk(object, i); + do_log(L_ERROR, "Couldn't allocate client buffer.\n"); + connection->flags &= ~CONN_WRITER; + httpClientFinish(connection, 1); + return 1; + } + + if(object->length >= 0 && request->to >= object->length) + request->to = object->length; + + if(request->from > 0 || request->to >= 0) { + if(request->method == METHOD_HEAD) { + request->to = request->from; + } else if(request->to < 0) { + if(object->length >= 0) + request->to = object->length; + } + } + + again: + + connection->len = 0; + + if((request->from <= 0 && request->to < 0) || + request->method == METHOD_HEAD) { + n = snnprintf(connection->buf, 0, bufsize, + "HTTP/1.1 %d %s", + object->code, atomString(object->message)); + } else { + if((object->length >= 0 && request->from >= object->length) || + (request->to >= 0 && request->from >= request->to)) { + unlockChunk(object, i); + return httpClientRawError(connection, 416, + internAtom("Requested range " + "not satisfiable"), + 0); + } else { + n = snnprintf(connection->buf, 0, bufsize, + "HTTP/1.1 206 Partial content"); + } + } + + n = httpWriteObjectHeaders(connection->buf, n, bufsize, + object, request->from, request->to); + if(n < 0) + goto fail; + + if(request->method != METHOD_HEAD && + condition_result != CONDITION_NOT_MODIFIED && + request->to < 0 && object->length < 0) { + if(connection->version == HTTP_11) { + connection->te = TE_CHUNKED; + n = snnprintf(connection->buf, n, bufsize, + "\r\nTransfer-Encoding: chunked"); + } else { + request->flags &= ~REQUEST_PERSISTENT; + } + } + + if(object->age < current_time.tv_sec) { + n = snnprintf(connection->buf, n, bufsize, + "\r\nAge: %d", + (int)(current_time.tv_sec - object->age)); + } + n = snnprintf(connection->buf, n, bufsize, + "\r\nConnection: %s", + (request->flags & REQUEST_PERSISTENT) ? + "keep-alive" : "close"); + + if(!(object->flags & OBJECT_LOCAL)) { + if((object->flags & OBJECT_FAILED) && !proxyOffline) { + n = snnprintf(connection->buf, n, bufsize, + "\r\nWarning: 111 %s:%d Revalidation failed", + proxyName->string, proxyPort); + if(request->error_code) + n = snnprintf(connection->buf, n, bufsize, + " (%d %s)", + request->error_code, + atomString(request->error_message)); + object->flags &= ~OBJECT_FAILED; + } else if(proxyOffline && + objectMustRevalidate(object, &request->cache_control)) { + n = snnprintf(connection->buf, n, bufsize, + "\r\nWarning: 112 %s:%d Disconnected operation", + proxyName->string, proxyPort); + } else if(objectIsStale(object, &request->cache_control)) { + n = snnprintf(connection->buf, n, bufsize, + "\r\nWarning: 110 %s:%d Object is stale", + proxyName->string, proxyPort); + } else if(object->expires < 0 && object->max_age < 0 && + object->age < current_time.tv_sec - 24 * 3600) { + n = snnprintf(connection->buf, n, bufsize, + "\r\nWarning: 113 %s:%d Heuristic expiration", + proxyName->string, proxyPort); + } + } + + n = snnprintf(connection->buf, n, bufsize, "\r\n\r\n"); + + if(n < 0) + goto fail; + + connection->offset = request->from; + + if(request->method == METHOD_HEAD || + condition_result == CONDITION_NOT_MODIFIED || + (object->flags & OBJECT_ABORTED)) { + len = 0; + } else { + if(i < object->numchunks) { + if(object->chunks[i].size <= j) + len = 0; + else + len = object->chunks[i].size - j; + } else { + len = 0; + } + if(request->to >= 0) + len = MIN(len, request->to - request->from); + } + + connection->offset = request->from; + httpSetTimeout(connection, clientTimeout); + do_log(D_CLIENT_DATA, "Serving on 0x%lx for 0x%lx: offset %d len %d\n", + (unsigned long)connection, (unsigned long)object, + connection->offset, len); + do_stream_h(IO_WRITE | + (connection->te == TE_CHUNKED && len > 0 ? IO_CHUNKED : 0), + connection->fd, 0, + connection->buf, n, + object->chunks[i].data + j, len, + httpServeObjectStreamHandler, connection); + return 1; + + fail: + rc = 0; + connection->len = 0; + if(!(connection->flags & CONN_BIGBUF)) + rc = httpConnectionBigify(connection); + if(rc > 0) { + bufsize = bigBufferSize; + goto again; + } + unlockChunk(object, i); + return httpClientRawError(connection, 500, + rc == 0 ? + internAtom("No space for headers") : + internAtom("Couldn't allocate big buffer"), 0); +} + +static int +httpServeObjectDelayed(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + httpServeObject(connection); + return 1; +} + +int +delayedHttpServeObject(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr event; + + assert(connection->request->object->chunks[connection->request->from / + CHUNK_SIZE].locked > 0); + + event = scheduleTimeEvent(-1, httpServeObjectDelayed, + sizeof(connection), &connection); + if(!event) return -1; + return 1; +} + +static int +httpServeObjectFinishHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + + (void)request; + assert(!request->chandler); + + if(status == 0 && !streamRequestDone(srequest)) + return 0; + + httpSetTimeout(connection, -1); + + if(status < 0) { + do_log(L_ERROR, "Couldn't terminate chunked reply\n"); + httpClientFinish(connection, 1); + } else { + httpClientFinish(connection, 0); + } + return 1; +} + +int +httpServeChunk(HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + ObjectPtr object = request->object; + int i = connection->offset / CHUNK_SIZE; + int j = connection->offset - (i * CHUNK_SIZE); + int to, len, len2, end; + int rc; + + /* This must be called with chunk i locked. */ + assert(object->chunks[i].locked > 0); + + if(object->flags & OBJECT_ABORTED) + goto fail; + + if(object->length >= 0 && request->to >= 0) + to = MIN(request->to, object->length); + else if(object->length >= 0) + to = object->length; + else if(request->to >= 0) + to = request->to; + else + to = -1; + + len = 0; + if(i < object->numchunks) + len = object->chunks[i].size - j; + + if(request->method != METHOD_HEAD && + len < CHUNK_SIZE && connection->offset + len < to) { + objectFillFromDisk(object, connection->offset + len, 2); + len = object->chunks[i].size - j; + } + + if(to >= 0) + len = MIN(len, to - connection->offset); + + if(len <= 0) { + if(to >= 0 && connection->offset >= to) { + if(request->chandler) { + unregisterConditionHandler(request->chandler); + request->chandler = NULL; + } + unlockChunk(object, i); + if(connection->te == TE_CHUNKED) { + httpSetTimeout(connection, clientTimeout); + do_stream(IO_WRITE | IO_CHUNKED | IO_END, + connection->fd, 0, NULL, 0, + httpServeObjectFinishHandler, connection); + } else { + httpClientFinish(connection, + !(object->length >= 0 && + connection->offset >= object->length)); + } + return 1; + } else { + if(!request->chandler) { + request->chandler = + conditionWait(&object->condition, + httpServeObjectHandler, + sizeof(connection), &connection); + if(!request->chandler) { + do_log(L_ERROR, "Couldn't register condition handler\n"); + goto fail; + } + } + if(!(object->flags & OBJECT_INPROGRESS)) { + if(object->flags & OBJECT_SUPERSEDED) { + goto fail; + } + if(REQUEST_SIDE(request)) goto fail; + rc = object->request(object, request->method, + connection->offset, -1, request, + object->request_closure); + if(rc <= 0) goto fail; + } + return 1; + } + } else { + /* len > 0 */ + if(request->method != METHOD_HEAD) + objectFillFromDisk(object, (i + 1) * CHUNK_SIZE, 1); + if(request->chandler) { + unregisterConditionHandler(request->chandler); + request->chandler = NULL; + } + len2 = 0; + if(j + len == CHUNK_SIZE && object->numchunks > i + 1) { + len2 = object->chunks[i + 1].size; + if(to >= 0) + len2 = MIN(len2, to - (i + 1) * CHUNK_SIZE); + } + /* Lock early -- httpServerRequest may get_chunk */ + if(len2 > 0) + lockChunk(object, i + 1); + if(object->length >= 0 && + connection->offset + len + len2 == object->length) + end = 1; + else + end = 0; + /* Prefetch */ + if(!(object->flags & OBJECT_INPROGRESS) && !REQUEST_SIDE(request)) { + if(object->chunks[i].size < CHUNK_SIZE && + to >= 0 && connection->offset + len + 1 < to) + object->request(object, request->method, + connection->offset + len, -1, request, + object->request_closure); + else if(i + 1 < object->numchunks && + object->chunks[i + 1].size == 0 && + to >= 0 && (i + 1) * CHUNK_SIZE + 1 < to) + object->request(object, request->method, + (i + 1) * CHUNK_SIZE, -1, request, + object->request_closure); + } + if(len2 == 0) { + httpSetTimeout(connection, clientTimeout); + do_log(D_CLIENT_DATA, + "Serving on 0x%lx for 0x%lx: offset %d len %d\n", + (unsigned long)connection, (unsigned long)object, + connection->offset, len); + /* IO_NOTNOW in order to give other clients a chance to run. */ + do_stream(IO_WRITE | IO_NOTNOW | + (connection->te == TE_CHUNKED ? IO_CHUNKED : 0) | + (end ? IO_END : 0), + connection->fd, 0, + object->chunks[i].data + j, len, + httpServeObjectStreamHandler, connection); + } else { + httpSetTimeout(connection, clientTimeout); + do_log(D_CLIENT_DATA, + "Serving on 0x%lx for 0x%lx: offset %d len %d + %d\n", + (unsigned long)connection, (unsigned long)object, + connection->offset, len, len2); + do_stream_2(IO_WRITE | IO_NOTNOW | + (connection->te == TE_CHUNKED ? IO_CHUNKED : 0) | + (end ? IO_END : 0), + connection->fd, 0, + object->chunks[i].data + j, len, + object->chunks[i + 1].data, len2, + httpServeObjectStreamHandler2, connection); + } + return 1; + } + + abort(); + + fail: + unlockChunk(object, i); + if(request->chandler) + unregisterConditionHandler(request->chandler); + request->chandler = NULL; + httpClientFinish(connection, 1); + return 1; +} + +static int +httpServeChunkDelayed(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + httpServeChunk(connection); + return 1; +} + +int +delayedHttpServeChunk(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr event; + event = scheduleTimeEvent(-1, httpServeChunkDelayed, + sizeof(connection), &connection); + if(!event) return -1; + return 1; +} + +int +httpServeObjectHandler(int status, ConditionHandlerPtr chandler) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)chandler->data; + HTTPRequestPtr request = connection->request; + int rc; + + if((request->object->flags & OBJECT_ABORTED) || status < 0) { + shutdown(connection->fd, 1); + httpSetTimeout(connection, 10); + /* httpServeChunk will take care of the error. */ + } + + httpSetTimeout(connection, -1); + + request->chandler = NULL; + rc = delayedHttpServeChunk(connection); + if(rc < 0) { + do_log(L_ERROR, "Couldn't schedule serving.\n"); + abortObject(request->object, 503, + internAtom("Couldn't schedule serving")); + } + return 1; +} + +static int +httpServeObjectStreamHandlerCommon(int kind, int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + int condition_result = httpCondition(request->object, request->condition); + int i = connection->offset / CHUNK_SIZE; + + assert(!request->chandler); + + if(status == 0 && !streamRequestDone(srequest)) { + httpSetTimeout(connection, clientTimeout); + return 0; + } + + httpSetTimeout(connection, -1); + + unlockChunk(request->object, i); + if(kind == 2) + unlockChunk(request->object, i + 1); + + if(status) { + if(status < 0) { + do_log_error(status == -ECONNRESET ? D_IO : L_ERROR, + -status, "Couldn't write to client"); + if(status == -EIO || status == -ESHUTDOWN) + httpClientFinish(connection, 2); + else + httpClientFinish(connection, 1); + } else { + do_log(D_IO, "Couldn't write to client: short write.\n"); + httpClientFinish(connection, 2); + } + return 1; + } + + if(srequest->operation & IO_CHUNKED) { + assert(srequest->offset > 2); + connection->offset += srequest->offset - 2; + } else { + connection->offset += srequest->offset; + } + request->flags &= ~REQUEST_REQUESTED; + + if(request->object->flags & OBJECT_ABORTED) { + httpClientFinish(connection, 1); + return 1; + } + + if(connection->request->method == METHOD_HEAD || + condition_result == CONDITION_NOT_MODIFIED) { + httpClientFinish(connection, 0); + return 1; + } + + if(srequest->operation & IO_END) + httpClientFinish(connection, 0); + else { + httpConnectionDestroyBuf(connection); + lockChunk(connection->request->object, + connection->offset / CHUNK_SIZE); + httpServeChunk(connection); + } + return 1; +} + +int +httpServeObjectStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + return httpServeObjectStreamHandlerCommon(1, status, event, srequest); +} + +int +httpServeObjectStreamHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + return httpServeObjectStreamHandlerCommon(2, status, event, srequest); +} diff --git a/jni/polipo/client.h b/jni/polipo/client.h new file mode 100644 index 00000000..b6d6e5d5 --- /dev/null +++ b/jni/polipo/client.h @@ -0,0 +1,64 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +int httpAccept(int, FdEventHandlerPtr, AcceptRequestPtr); +void httpClientFinish(HTTPConnectionPtr connection, int s); +int httpClientHandler(int, FdEventHandlerPtr, StreamRequestPtr); +int httpClientNoticeError(HTTPRequestPtr, int code, struct _Atom *message); +int httpClientError(HTTPRequestPtr, int code, struct _Atom *message); +int httpClientNewError(HTTPConnectionPtr, int method, int persist, + int code, struct _Atom *message); +int httpClientRawError(HTTPConnectionPtr, int, struct _Atom*, int close); +int httpErrorStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int httpErrorNocloseStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int httpErrorNofinishStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int httpClientRequest(HTTPRequestPtr request, AtomPtr url); +int httpClientRequestContinue(int forbidden_code, AtomPtr url, + AtomPtr forbidden_message, + AtomPtr forbidden_headers, + void *closure); +int httpClientDiscardBody(HTTPConnectionPtr connection); +int httpClientDiscardHandler(int, FdEventHandlerPtr, StreamRequestPtr); +int httpClientGetHandler(int, ConditionHandlerPtr); +int httpClientHandlerHeaders(FdEventHandlerPtr event, + StreamRequestPtr request, + HTTPConnectionPtr connection); +int httpClientNoticeRequest(HTTPRequestPtr request, int); +int httpServeObject(HTTPConnectionPtr); +int delayedHttpServeObject(HTTPConnectionPtr connection); +int httpServeObjectStreamHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int httpServeObjectStreamHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int httpServeObjectHandler(int, ConditionHandlerPtr); +int httpClientSideRequest(HTTPRequestPtr request); +int httpClientSideHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest); diff --git a/jni/polipo/config.c b/jni/polipo/config.c new file mode 100644 index 00000000..f96fd90f --- /dev/null +++ b/jni/polipo/config.c @@ -0,0 +1,869 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +ConfigVariablePtr configVariables = NULL; + +static ConfigVariablePtr +findConfigVariable(AtomPtr name) +{ + ConfigVariablePtr var; + var = configVariables; + while(var != NULL) { + if(var->name == name) + break; + var = var->next; + } + return var; +} + +void +declareConfigVariable(AtomPtr name, int type, void *value, + int (*setter)(ConfigVariablePtr, void*), char *help) +{ + ConfigVariablePtr var, previous, next; + + var = findConfigVariable(name); + + if(var) { + do_log(L_ERROR, + "Configuration variable %s declared multiple times.\n", + name->string); + if(var->type != type) { + exit(1); + } + } + + var = malloc(sizeof(ConfigVariableRec)); + if(var == NULL) { + do_log(L_ERROR, "Couldn't allocate config variable.\n"); + exit(1); + } + + var->name = retainAtom(name); + var->type = type; + switch(type) { + case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX: case CONFIG_TIME: + case CONFIG_BOOLEAN: case CONFIG_TRISTATE: case CONFIG_TETRASTATE: + case CONFIG_PENTASTATE: + var->value.i = value; break; + case CONFIG_FLOAT: var->value.f = value; break; + case CONFIG_ATOM: case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD: + var->value.a = value; break; + case CONFIG_INT_LIST: + var->value.il = value; break; + case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER: + var->value.al = value; break; + default: abort(); + } + var->setter = setter; + var->help = help; + + previous = NULL; + next = configVariables; + while(next && strcmp(next->name->string, var->name->string) < 0) { + previous = next; + next = next->next; + } + if(next && strcmp(next->name->string, var->name->string) == 0) { + do_log(L_ERROR, "Variable %s declared multiple times.\n", + next->name->string); + abort(); + } + if(previous == NULL) { + var->next = configVariables; + configVariables = var; + } else { + var->next = next; + previous->next = var; + } +} + +static void +printString(FILE *out, char *string, int html) +{ + if(html) { + char buf[512]; + int i; + i = htmlString(buf, 0, 512, string, strlen(string)); + if(i < 0) { + fprintf(out, "(overflow)"); + return; + } + fwrite(buf, 1, i, out); + } else { + fprintf(out, "%s", string); + } +} + +static void +printVariable(FILE *out, ConfigVariablePtr var, int html, int parseable) +{ + int i; + + switch(var->type) { + case CONFIG_INT: fprintf(out, "%d", *var->value.i); break; + case CONFIG_OCTAL: fprintf(out, "0%o", *var->value.i); break; + case CONFIG_HEX: fprintf(out, "0x%x", *var->value.i); break; + case CONFIG_TIME: + { + int v = *var->value.i; + if(v == 0) { + fprintf(out, "0s"); + } else { + if(v >= 3600 * 24) fprintf(out, "%dd", v/(3600*24)); + v = v % (3600 * 24); + if(v >= 3600) fprintf(out, "%dh", v / 3600); + v = v % 3600; + if(v >= 60) fprintf(out, "%dm", v / 60); + v = v % 60; + if(v > 0) fprintf(out, "%ds", v); + } + } + break; + case CONFIG_BOOLEAN: + switch(*var->value.i) { + case 0: fprintf(out, "false"); break; + case 1: fprintf(out, "true"); break; + default: fprintf(out, "???"); break; + } + break; + case CONFIG_TRISTATE: + switch(*var->value.i) { + case 0: fprintf(out, "false"); break; + case 1: fprintf(out, "maybe"); break; + case 2: fprintf(out, "true"); break; + default: fprintf(out, "???"); break; + } + break; + case CONFIG_TETRASTATE: + switch(*var->value.i) { + case 0: fprintf(out, "false"); break; + case 1: fprintf(out, "reluctantly"); break; + case 2: fprintf(out, "happily"); break; + case 3: fprintf(out, "true"); break; + default: fprintf(out, "???"); break; + } + break; + case CONFIG_PENTASTATE: + switch(*var->value.i) { + case 0: fprintf(out, "no"); break; + case 1: fprintf(out, "reluctantly"); break; + case 2: fprintf(out, "maybe"); break; + case 3: fprintf(out, "happily"); break; + case 4: fprintf(out, "true"); break; + default: fprintf(out, "???"); break; + } + break; + case CONFIG_FLOAT: fprintf(out, "%f", *var->value.f); break; + case CONFIG_ATOM: case CONFIG_ATOM_LOWER: + if(*var->value.a) { + if((*var->value.a)->length > 0) { + printString(out, (*var->value.a)->string, html); + } else { + if(!parseable) + fprintf(out, "(empty)"); + } + } else { + if(!parseable) + fprintf(out, "(none)"); + } + break; + case CONFIG_PASSWORD: + if(!parseable) + fprintf(out, "(hidden)"); + break; + case CONFIG_INT_LIST: + if((*var->value.il) == NULL) { + if(!parseable) + fprintf(out, "(not set)"); + } else if((*var->value.il)->length == 0) { + if(!parseable) + fprintf(out, "(empty list)"); + } else { + for(i = 0; i < (*var->value.il)->length; i++) { + int from = (*var->value.il)->ranges[i].from; + int to = (*var->value.il)->ranges[i].to; + assert(from <= to); + if(from == to) + fprintf(out, "%d", from); + else + fprintf(out, "%d-%d", from, to); + if(i < (*var->value.il)->length - 1) + fprintf(out, ", "); + } + } + break; + case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER: + if((*var->value.al) == NULL) { + if(!parseable) + fprintf(out, "(not set)"); + } else if((*var->value.al)->length == 0) { + if(!parseable) + fprintf(out, "(empty list)"); + } else { + for(i = 0; i < (*var->value.al)->length; i++) { + AtomPtr atom = (*var->value.al)->list[i]; + if(atom) { + if(atom->length > 0) + printString(out, atom->string, html); + else { + if(!parseable) + fprintf(out, "(empty)"); + } + } else { + if(!parseable) + fprintf(out, "(none)"); + } + if(i < (*var->value.al)->length - 1) + fprintf(out, ", "); + } + } + break; + default: abort(); + } +} + +static void +printVariableForm(FILE *out, ConfigVariablePtr var) +{ + char *disabled = ""; + int i; + + if(disableConfiguration || !var->setter) disabled = "disabled=true"; + + fprintf(out, "
"); + + switch(var->type) { + case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX: + case CONFIG_TIME: case CONFIG_FLOAT: case CONFIG_ATOM: + case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD: + case CONFIG_INT_LIST: case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER: + fprintf(out, "\n", + var->type == CONFIG_PASSWORD ? " type=password" : "", + var->name->string, disabled); + break; + + case CONFIG_BOOLEAN: + { + static char *states[] = {"false", "true"}; + + fprintf(out, ""); + if(var->setter) + fprintf(out, ""); + break; + } + + case CONFIG_TRISTATE: + { + static char *states[] = {"false", "maybe", "true"}; + + fprintf(out, ""); + if(var->setter) + fprintf(out, ""); + break; + } + + case CONFIG_TETRASTATE: + { + static char *states[] = + {"false", "reluctantly", "happily", "true"}; + + fprintf(out, ""); + if(var->setter) + fprintf(out, ""); + break; + } + + case CONFIG_PENTASTATE: + { + static char *states[] = + {"no", "reluctantly", "maybe", "happily", "true"}; + + fprintf(out, ""); + if(var->setter) + fprintf(out,""); + break; + } + default: abort(); + } + fprintf(out, "
"); +} + + + + + +void +printConfigVariables(FILE *out, int html) +{ + ConfigVariablePtr var; + int entryno = 0; + +#define PRINT_SEP() \ + do {if(html) fprintf(out, ""); else fprintf(out, " ");} while(0) + + if(html) { + fprintf(out, "\n"); + fprintf(out, "\n"); + } + + if(html) { + alternatingHttpStyle(out, "configlist"); + fprintf(out, + "
\n" + "\n" + "" + "" + "" + "\n" + "\n" +); + } + + /* configFile is not a config variable, for obvious bootstrapping reasons. + CHUNK_SIZE is hardwired for now. */ + + fprintf(out, + html ? + "\n" : + "configFile %s Configuration file.\n", + configFile && configFile->length > 0 ? + configFile->string : "(none)"); + fprintf(out, + html ? + "\n" : + "CHUNK_SIZE %d Unit of chunk memory allocation.\n", CHUNK_SIZE); + + var = configVariables; + while(var != NULL) { + if(html) { + if(entryno % 2) + fprintf(out, ""); + else + fprintf(out, ""); + fprintf(out, "\n"); + else + fprintf(out, "\n"); + + entryno++; + var = var->next; + } + if(html) { + fprintf(out, "\n"); + fprintf(out, "
variable namecurrent valuenew valuedescription
configFile%s" + "Configuration file.
CHUNK_SIZE%d" + "Unit of chunk memory allocation.
"); + } + + fprintf(out, "%s", var->name->string); + + fprintf(out, html ? "
" : " "); + + fprintf(out, html ? "" : ""); + + switch(var->type) { + case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX: + fprintf(out, "integer"); break; + case CONFIG_TIME: fprintf(out, "time"); break; + case CONFIG_BOOLEAN: fprintf(out, "boolean"); break; + case CONFIG_TRISTATE: fprintf(out, "tristate"); break; + case CONFIG_TETRASTATE: fprintf(out, "4-state"); break; + case CONFIG_PENTASTATE: fprintf(out, "5-state"); break; + case CONFIG_FLOAT: fprintf(out, "float"); break; + case CONFIG_ATOM: case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD: + fprintf(out, "atom"); break; + case CONFIG_INT_LIST: fprintf(out, "intlist"); break; + case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER: + fprintf(out, "list"); break; + default: abort(); + } + + fprintf(out, html ? "" : ""); + + PRINT_SEP(); + + printVariable(out, var, html, 0); + + PRINT_SEP(); + + if(html) { + printVariableForm(out, var); + PRINT_SEP(); + } + + fprintf(out, "%s", var->help?var->help:""); + if(html) + fprintf(out, "
\n"); + } + return; +#undef PRINT_SEP +} + +static int +skipWhitespace(char *buf, int i) +{ + while(buf[i] == ' ' || buf[i] == '\t' || buf[i] == '\r') + i++; + return i; +} + +static int +parseInt(char *buf, int offset, int *value_return) +{ + char *p; + int value; + + value = strtol(buf + offset, &p, 0); + if(p <= buf + offset) + return -1; + + *value_return = value; + return p - buf; +} + +static struct config_state { char *name; int value; } +states[] = + { { "false", 0 }, + { "no", 0 }, + { "reluctantly", 1 }, + { "seldom", 1 }, + { "rarely", 1 }, + { "lazily", 1 }, + { "maybe", 2 }, + { "perhaps", 2 }, + { "happily", 3 }, + { "often", 3 }, + { "eagerly", 3 }, + { "true", 4 }, + { "yes", 4 } }; + +static int +parseState(char *buf, int offset, int kind) +{ + int i = offset; + int n; + int state = -1; + + while(letter(buf[i])) + i++; + for(n = 0; n < sizeof(states) / sizeof(states[0]); n++) { + if(strlen(states[n].name) == i - offset && + lwrcmp(buf + offset, states[n].name, i - offset) == 0) { + state = states[n].value; + break; + } + } + if(state < 0) + return -1; + + switch(kind) { + case CONFIG_BOOLEAN: + if(state == 0) return 0; + else if(state == 4) return 1; + else return -1; + break; + case CONFIG_TRISTATE: + if(state == 0) return 0; + else if(state == 2) return 1; + else if(state == 4) return 2; + else return -1; + break; + case CONFIG_TETRASTATE: + if(state == 0) return 0; + else if(state == 1) return 1; + else if(state == 3) return 2; + else if(state == 4) return 3; + else return -1; + break; + case CONFIG_PENTASTATE: + return state; + break; + default: + abort(); + } +} + +static int +parseAtom(char *buf, int offset, AtomPtr *value_return, int insensitive) +{ + int y0, i, j, k; + AtomPtr atom; + int escape = 0; + char *s; + + i = offset; + if(buf[i] == '\"') { + i++; + y0 = i; + while(buf[i] != '\"' && buf[i] != '\n' && buf[i] != '\0') { + if(buf[i] == '\\' && buf[i + 1] != '\0') { + escape = 1; + i += 2; + } else + i++; + } + if(buf[i] != '\"') + return -1; + j = i + 1; + } else { + y0 = i; + while(letter(buf[i]) || digit(buf[i]) || + buf[i] == '_' || buf[i] == '-' || buf[i] == '~' || + buf[i] == '.' || buf[i] == ':' || buf[i] == '/') + i++; + j = i; + } + + if(escape) { + s = malloc(i - y0); + if(s == NULL) return -1; + k = 0; + j = y0; + while(j < i) { + if(buf[j] == '\\' && j <= i - 2) { + s[k++] = buf[j + 1]; + j += 2; + } else + s[k++] = buf[j++]; + } + if(insensitive) + atom = internAtomLowerN(s, k); + else + atom = internAtomN(s, k); + free(s); + j++; + } else { + if(insensitive) + atom = internAtomLowerN(buf + y0, i - y0); + else + atom = internAtomN(buf + y0, i - y0); + } + *value_return = atom; + return j; +} + +static int +parseTime(char *line, int i, int *value_return) +{ + int v = 0, w; + while(1) { + if(!digit(line[i])) + break; + w = atoi(line + i); + while(digit(line[i])) i++; + switch(line[i]) { + case 'd': v += w * 24 * 3600; i++; break; + case 'h': v += w * 3600; i++; break; + case 'm': v += w * 60; i++; break; + case 's': v += w; i++; break; + default: v += w; goto done; + } + } + done: + *value_return = v; + return i; +} + +int +parseConfigLine(char *line, char *filename, int lineno, int set) +{ + int x0, x1; + int i, from, to; + AtomPtr name, value; + ConfigVariablePtr var; + int iv; + float fv; + AtomPtr av; + AtomListPtr alv; + IntListPtr ilv; + + i = skipWhitespace(line, 0); + if(line[i] == '\n' || line[i] == '\0' || line[i] == '#') + return 0; + + x0 = i; + while(letter(line[i]) || digit(line[i])) + i++; + x1 = i; + + i = skipWhitespace(line, i); + if(line[i] != '=') { + goto syntax; + } + i++; + i = skipWhitespace(line, i); + + name = internAtomN(line + x0, x1 - x0); + var = findConfigVariable(name); + releaseAtom(name); + + if(set && var->setter == NULL) + return -2; + + if(var == NULL) { + if(!set) { + do_log(L_ERROR, "%s:%d: unknown config variable ", + filename, lineno); + do_log_n(L_ERROR, line + x0, x1 - x0); + do_log(L_ERROR, "\n"); + } + return -1; + } + + i = skipWhitespace(line, i); + switch(var->type) { + case CONFIG_INT: case CONFIG_OCTAL: case CONFIG_HEX: + i = parseInt(line, i, &iv); + if(i < 0) goto syntax; + if(set) + var->setter(var, &iv); + else + *var->value.i = iv; + break; + case CONFIG_TIME: + i = parseTime(line, i, &iv); + if(i < 0) goto syntax; + i = skipWhitespace(line, i); + if(line[i] != '\n' && line[i] != '\0' && line[i] != '#') + goto syntax; + if(set) + var->setter(var, &iv); + else + *var->value.i = iv; + break; + case CONFIG_BOOLEAN: + case CONFIG_TRISTATE: + case CONFIG_TETRASTATE: + case CONFIG_PENTASTATE: + iv = parseState(line, i, var->type); + if(iv < 0) + goto syntax; + if(set) + var->setter(var, &iv); + else + *var->value.i = iv; + break; + case CONFIG_FLOAT: + if(!digit(line[i]) && line[i] != '.') + goto syntax; + fv = atof(line + i); + if(set) + var->setter(var, &fv); + else + *var->value.f = fv; + break; + case CONFIG_ATOM: case CONFIG_ATOM_LOWER: case CONFIG_PASSWORD: + i = parseAtom(line, i, &av, (var->type == CONFIG_ATOM_LOWER)); + if(i < 0) goto syntax; + if(!av) { + if(!set) + do_log(L_ERROR, "%s:%d: couldn't allocate atom.\n", + filename, lineno); + return -1; + } + i = skipWhitespace(line, i); + if(line[i] != '\n' && line[i] != '\0' && line[i] != '#') { + releaseAtom(av); + goto syntax; + } + if(set) + var->setter(var, &av); + else { + if(*var->value.a) releaseAtom(*var->value.a); + *var->value.a = av; + } + break; + case CONFIG_INT_LIST: + ilv = makeIntList(0); + if(ilv == NULL) { + if(!set) + do_log(L_ERROR, "%s:%d: couldn't allocate int list.\n", + filename, lineno); + return -1; + } + while(1) { + i = parseInt(line, i, &from); + if(i < 0) goto syntax; + to = from; + i = skipWhitespace(line, i); + if(line[i] == '-') { + i = skipWhitespace(line, i + 1); + i = parseInt(line, i, &to); + if(i < 0) { + destroyIntList(ilv); + goto syntax; + } + i = skipWhitespace(line, i); + } + intListCons(from, to, ilv); + if(line[i] == '\n' || line[i] == '\0' || line[i] == '#') + break; + if(line[i] != ',') { + destroyIntList(ilv); + goto syntax; + } + i = skipWhitespace(line, i + 1); + } + if(set) + var->setter(var, &ilv); + else { + if(*var->value.il) destroyIntList(*var->value.il); + *var->value.il = ilv; + } + break; + case CONFIG_ATOM_LIST: case CONFIG_ATOM_LIST_LOWER: + alv = makeAtomList(NULL, 0); + if(alv == NULL) { + if(!set) + do_log(L_ERROR, "%s:%d: couldn't allocate atom list.\n", + filename, lineno); + return -1; + } + while(1) { + i = parseAtom(line, i, &value, + (var->type == CONFIG_ATOM_LIST_LOWER)); + if(i < 0) goto syntax; + if(!value) { + if(!set) + do_log(L_ERROR, "%s:%d: couldn't allocate atom.\n", + filename, lineno); + return -1; + } + atomListCons(value, alv); + i = skipWhitespace(line, i); + if(line[i] == '\n' || line[i] == '\0' || line[i] == '#') + break; + if(line[i] != ',') { + destroyAtomList(alv); + goto syntax; + } + i = skipWhitespace(line, i + 1); + } + if(set) + var->setter(var, &alv); + else { + if(*var->value.al) destroyAtomList(*var->value.al); + *var->value.al = alv; + } + break; + default: abort(); + } + return 1; + + syntax: + if(!set) + do_log(L_ERROR, "%s:%d: parse error.\n", filename, lineno); + return -1; +} + +int +parseConfigFile(AtomPtr filename) +{ + char buf[512]; + int rc, lineno; + FILE *f; + + if(!filename || filename->length == 0) + return 0; + f = fopen(filename->string, "r"); + if(f == NULL) { + do_log_error(L_ERROR, errno, "Couldn't open config file %s", + filename->string); + return -1; + } + + lineno = 1; + while(1) { + char *s; + s = fgets(buf, 512, f); + if(s == NULL) { + fclose(f); + return 1; + } + rc = parseConfigLine(buf, filename->string, lineno, 0); + lineno++; + } +} + +int +configIntSetter(ConfigVariablePtr var, void* value) +{ + assert(var->type <= CONFIG_PENTASTATE); + *var->value.i = *(int*)value; + return 1; +} + +int +configFloatSetter(ConfigVariablePtr var, void* value) +{ + assert(var->type == CONFIG_FLOAT); + *var->value.i = *(float*)value; + return 1; +} + + +int +configAtomSetter(ConfigVariablePtr var, void* value) +{ + assert(var->type == CONFIG_ATOM || var->type == CONFIG_ATOM_LOWER || + var->type == CONFIG_PASSWORD); + if(*var->value.a) + releaseAtom(*var->value.a); + *var->value.a = *(AtomPtr*)value; + return 1; +} diff --git a/jni/polipo/config.h b/jni/polipo/config.h new file mode 100644 index 00000000..ca177a78 --- /dev/null +++ b/jni/polipo/config.h @@ -0,0 +1,68 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#define CONFIG_INT 0 +#define CONFIG_OCTAL 1 +#define CONFIG_HEX 2 +#define CONFIG_TIME 3 +#define CONFIG_BOOLEAN 4 +#define CONFIG_TRISTATE 5 +#define CONFIG_TETRASTATE 6 +#define CONFIG_PENTASTATE 7 +#define CONFIG_FLOAT 8 +#define CONFIG_ATOM 9 +#define CONFIG_ATOM_LOWER 10 +#define CONFIG_PASSWORD 11 +#define CONFIG_INT_LIST 12 +#define CONFIG_ATOM_LIST 13 +#define CONFIG_ATOM_LIST_LOWER 14 + +typedef struct _ConfigVariable { + AtomPtr name; + int type; + union { + int *i; + float *f; + struct _Atom **a; + struct _AtomList **al; + struct _IntList **il; + } value; + int (*setter)(struct _ConfigVariable*, void*); + char *help; + struct _ConfigVariable *next; +} ConfigVariableRec, *ConfigVariablePtr; + +#define CONFIG_VARIABLE(name, type, help) \ + CONFIG_VARIABLE_SETTABLE(name, type, NULL, help) + +#define CONFIG_VARIABLE_SETTABLE(name, type, setter, help) \ + declareConfigVariable(internAtom(#name), type, &name, setter, help) + +void declareConfigVariable(AtomPtr name, int type, void *value, + int (*setter)(ConfigVariablePtr, void*), + char *help); +void printConfigVariables(FILE *out, int html); +int parseConfigLine(char *line, char *filename, int lineno, int set); +int parseConfigFile(AtomPtr); +int configIntSetter(ConfigVariablePtr, void*); +int configFloatSetter(ConfigVariablePtr, void*); +int configAtomSetter(ConfigVariablePtr, void*); diff --git a/jni/polipo/dirent_compat.c b/jni/polipo/dirent_compat.c new file mode 100644 index 00000000..9a37c0b3 --- /dev/null +++ b/jni/polipo/dirent_compat.c @@ -0,0 +1,149 @@ +/* + + Implementation of POSIX directory browsing functions and types for Win32. + + Author: Kevlin Henney (kevlin@acm.org, kevlin@curbralan.com) + History: Created March 1997. Updated June 2003. + Rights: See end of file. + +*/ + +#ifdef WIN32 + +#include "dirent_compat.h" +#include +#include /* _findfirst and _findnext set errno iff they return -1 */ +#include +#include + +#ifdef __cplusplus +extern "C" +{ +#endif + +struct DIR +{ + long handle; /* -1 for failed rewind */ + struct _finddata_t info; + struct dirent result; /* d_name null iff first time */ + char *name; /* null-terminated char string */ +}; + +DIR *opendir(const char *name) +{ + DIR *dir = 0; + + if(name && name[0]) + { + size_t base_length = strlen(name); + const char *all = /* search pattern must end with suitable wildcard */ + strchr("/\\", name[base_length - 1]) ? "*" : "/*"; + + if((dir = (DIR *) malloc(sizeof *dir)) != 0 && + (dir->name = (char *) malloc(base_length + strlen(all) + 1)) != 0) + { + strcat(strcpy(dir->name, name), all); + + if((dir->handle = (long) _findfirst(dir->name, &dir->info)) != -1) + { + dir->result.d_name = 0; + } + else /* rollback */ + { + free(dir->name); + free(dir); + dir = 0; + } + } + else /* rollback */ + { + free(dir); + dir = 0; + errno = ENOMEM; + } + } + else + { + errno = EINVAL; + } + + return dir; +} + +int closedir(DIR *dir) +{ + int result = -1; + + if(dir) + { + if(dir->handle != -1) + { + result = _findclose(dir->handle); + } + + free(dir->name); + free(dir); + } + + if(result == -1) /* map all errors to EBADF */ + { + errno = EBADF; + } + + return result; +} + +struct dirent *readdir(DIR *dir) +{ + struct dirent *result = 0; + + if(dir && dir->handle != -1) + { + if(!dir->result.d_name || _findnext(dir->handle, &dir->info) != -1) + { + result = &dir->result; + result->d_name = dir->info.name; + } + } + else + { + errno = EBADF; + } + + return result; +} + +void rewinddir(DIR *dir) +{ + if(dir && dir->handle != -1) + { + _findclose(dir->handle); + dir->handle = (long) _findfirst(dir->name, &dir->info); + dir->result.d_name = 0; + } + else + { + errno = EBADF; + } +} + +#ifdef __cplusplus +} +#endif + +#endif + +/* + + Copyright Kevlin Henney, 1997, 2003. All rights reserved. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose is hereby granted without fee, provided + that this copyright and permissions notice appear in all copies and + derivatives. + + This software is supplied "as is" without express or implied warranty. + + But that said, if there are any problems please get in touch. + +*/ diff --git a/jni/polipo/dirent_compat.h b/jni/polipo/dirent_compat.h new file mode 100644 index 00000000..a02a0d82 --- /dev/null +++ b/jni/polipo/dirent_compat.h @@ -0,0 +1,50 @@ +#ifndef DIRENT_INCLUDED +#define DIRENT_INCLUDED + +/* + + Declaration of POSIX directory browsing functions and types for Win32. + + Author: Kevlin Henney (kevlin@acm.org, kevlin@curbralan.com) + History: Created March 1997. Updated June 2003. + Rights: See end of file. + +*/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef struct DIR DIR; + +struct dirent +{ + char *d_name; +}; + +DIR *opendir(const char *); +int closedir(DIR *); +struct dirent *readdir(DIR *); +void rewinddir(DIR *); + +/* + + Copyright Kevlin Henney, 1997, 2003. All rights reserved. + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose is hereby granted without fee, provided + that this copyright and permissions notice appear in all copies and + derivatives. + + This software is supplied "as is" without express or implied warranty. + + But that said, if there are any problems please get in touch. + +*/ + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/jni/polipo/diskcache.c b/jni/polipo/diskcache.c new file mode 100644 index 00000000..baaf079c --- /dev/null +++ b/jni/polipo/diskcache.c @@ -0,0 +1,2573 @@ +/* +Copyright (c) 2003-2010 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifndef NO_DISK_CACHE + +#include "md5import.h" + +int maxDiskEntries = 32; + +/* Because the functions in this file can be called during object + expiry, we cannot use get_chunk. */ + +AtomPtr diskCacheRoot; +AtomPtr localDocumentRoot; + +DiskCacheEntryPtr diskEntries = NULL, diskEntriesLast = NULL; +int numDiskEntries = 0; +int diskCacheDirectoryPermissions = 0700; +int diskCacheFilePermissions = 0600; +int diskCacheWriteoutOnClose = (64 * 1024); + +int maxDiskCacheEntrySize = -1; + +int diskCacheUnlinkTime = 32 * 24 * 60 * 60; +int diskCacheTruncateTime = 4 * 24 * 60 * 60 + 12 * 60 * 60; +int diskCacheTruncateSize = 1024 * 1024; +int preciseExpiry = 0; + +static DiskCacheEntryRec negativeEntry = { + NULL, NULL, + -1, -1, -1, -1, 0, 0, NULL, NULL +}; + +#ifndef LOCAL_ROOT +#define LOCAL_ROOT "/usr/share/polipo/www/" +#endif + +#ifndef DISK_CACHE_ROOT +#define DISK_CACHE_ROOT "/var/cache/polipo/" +#endif + +static int maxDiskEntriesSetter(ConfigVariablePtr, void*); +static int atomSetterFlush(ConfigVariablePtr, void*); +static int reallyWriteoutToDisk(ObjectPtr object, int upto, int max); + +void +preinitDiskcache() +{ + diskCacheRoot = internAtom(DISK_CACHE_ROOT); + localDocumentRoot = internAtom(LOCAL_ROOT); + + CONFIG_VARIABLE_SETTABLE(diskCacheDirectoryPermissions, CONFIG_OCTAL, + configIntSetter, + "Access rights for new directories."); + CONFIG_VARIABLE_SETTABLE(diskCacheFilePermissions, CONFIG_OCTAL, + configIntSetter, + "Access rights for new cache files."); + CONFIG_VARIABLE_SETTABLE(diskCacheWriteoutOnClose, CONFIG_INT, + configIntSetter, + "Number of bytes to write out eagerly."); + CONFIG_VARIABLE_SETTABLE(diskCacheRoot, CONFIG_ATOM, atomSetterFlush, + "Root of the disk cache."); + CONFIG_VARIABLE_SETTABLE(localDocumentRoot, CONFIG_ATOM, atomSetterFlush, + "Root of the local tree."); + CONFIG_VARIABLE_SETTABLE(maxDiskEntries, CONFIG_INT, maxDiskEntriesSetter, + "File descriptors used by the on-disk cache."); + CONFIG_VARIABLE(diskCacheUnlinkTime, CONFIG_TIME, + "Time after which on-disk objects are removed."); + CONFIG_VARIABLE(diskCacheTruncateTime, CONFIG_TIME, + "Time after which on-disk objects are truncated."); + CONFIG_VARIABLE(diskCacheTruncateSize, CONFIG_INT, + "Size to which on-disk objects are truncated."); + CONFIG_VARIABLE(preciseExpiry, CONFIG_BOOLEAN, + "Whether to consider all files for purging."); + CONFIG_VARIABLE_SETTABLE(maxDiskCacheEntrySize, CONFIG_INT, + configIntSetter, + "Maximum size of objects cached on disk."); +} + +static int +maxDiskEntriesSetter(ConfigVariablePtr var, void *value) +{ + int i; + assert(var->type == CONFIG_INT && var->value.i == &maxDiskEntries); + i = *(int*)value; + if(i < 0 || i > 1000000) + return -3; + maxDiskEntries = i; + while(numDiskEntries > maxDiskEntries) + destroyDiskEntry(diskEntriesLast->object, 0); + return 1; +} + +static int +atomSetterFlush(ConfigVariablePtr var, void *value) +{ + discardObjects(1, 0); + return configAtomSetter(var, value); +} + +static int +checkRoot(AtomPtr root) +{ + struct stat ss; + int rc; + + if(!root || root->length == 0) + return 0; + +#ifdef WIN32 /* Require "x:/" or "x:\\" */ + rc = isalpha(root->string[0]) && (root->string[1] == ':') && + ((root->string[2] == '/') || (root->string[2] == '\\')); + if(!rc) { + return -2; + } +#else + if(root->string[0] != '/') { + return -2; + } +#endif + + rc = stat(root->string, &ss); + if(rc < 0) + return -1; + else if(!S_ISDIR(ss.st_mode)) { + errno = ENOTDIR; + return -1; + } + return 1; +} + +static AtomPtr +maybeAddSlash(AtomPtr atom) +{ + AtomPtr newAtom = NULL; + if(!atom) return NULL; + if(atom->length > 0 && atom->string[atom->length - 1] != '/') { + newAtom = atomCat(atom, "/"); + releaseAtom(atom); + return newAtom; + } + return atom; +} + +void +initDiskcache() +{ + int rc; + + diskCacheRoot = expandTilde(maybeAddSlash(diskCacheRoot)); + rc = checkRoot(diskCacheRoot); + if(rc <= 0) { + switch(rc) { + case 0: break; + case -1: do_log_error(L_WARN, errno, "Disabling disk cache"); break; + case -2: + do_log(L_WARN, "Disabling disk cache: path %s is not absolute.\n", + diskCacheRoot->string); + break; + default: abort(); + } + releaseAtom(diskCacheRoot); + diskCacheRoot = NULL; + } + + localDocumentRoot = expandTilde(maybeAddSlash(localDocumentRoot)); + rc = checkRoot(localDocumentRoot); + if(rc <= 0) { + switch(rc) { + case 0: break; + case -1: do_log_error(L_WARN, errno, "Disabling local tree"); break; + case -2: + do_log(L_WARN, "Disabling local tree: path is not absolute.\n"); + break; + default: abort(); + } + releaseAtom(localDocumentRoot); + localDocumentRoot = NULL; + } +} + +#ifdef DEBUG_DISK_CACHE +#define CHECK_ENTRY(entry) check_entry((entry)) +static void +check_entry(DiskCacheEntryPtr entry) +{ + if(entry && entry->fd < 0) + assert(entry == &negativeEntry); + if(entry && entry->fd >= 0) { + assert((!entry->previous) == (entry == diskEntries)); + assert((!entry->next) == (entry == diskEntriesLast)); + if(entry->size >= 0) + assert(entry->size + entry->body_offset >= entry->offset); + assert(entry->body_offset >= 0); + if(entry->offset >= 0) { + off_t offset; + offset = lseek(entry->fd, 0, SEEK_CUR); + assert(offset == entry->offset); + } + if(entry->size >= 0) { + int rc; + struct stat ss; + rc = fstat(entry->fd, &ss); + assert(rc >= 0); + assert(ss.st_size == entry->size + entry->body_offset); + } + } +} +#else +#define CHECK_ENTRY(entry) do {} while(0) +#endif + +int +diskEntrySize(ObjectPtr object) +{ + struct stat buf; + int rc; + DiskCacheEntryPtr entry = object->disk_entry; + + if(!entry || entry == &negativeEntry) + return -1; + + if(entry->size >= 0) + return entry->size; + + rc = fstat(entry->fd, &buf); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't stat"); + return -1; + } + + if(buf.st_size <= entry->body_offset) + entry->size = 0; + else + entry->size = buf.st_size - entry->body_offset; + CHECK_ENTRY(entry); + if(object->length >= 0 && entry->size == object->length) + object->flags |= OBJECT_DISK_ENTRY_COMPLETE; + return entry->size; +} + +static int +entrySeek(DiskCacheEntryPtr entry, off_t offset) +{ + off_t rc; + + CHECK_ENTRY(entry); + assert(entry != &negativeEntry); + if(entry->offset == offset) + return 1; + if(offset > entry->body_offset) { + /* Avoid extending the file by mistake */ + if(entry->size < 0) + diskEntrySize(entry->object); + if(entry->size < 0) + return -1; + if(entry->size + entry->body_offset < offset) + return -1; + } + rc = lseek(entry->fd, offset, SEEK_SET); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't seek"); + entry->offset = -1; + return -1; + } + entry->offset = offset; + return 1; +} + +/* Given a local URL, constructs the filename where it can be found. */ + +int +localFilename(char *buf, int n, char *key, int len) +{ + int i, j; + if(len <= 0 || key[0] != '/') return -1; + + if(urlIsSpecial(key, len)) return -1; + + if(localDocumentRoot == NULL || + localDocumentRoot->length <= 0 || localDocumentRoot->string[0] != '/') + return -1; + + if(n <= localDocumentRoot->length) + return -1; + + i = 0; + if(key[i] != '/') + return -1; + + memcpy(buf, localDocumentRoot->string, localDocumentRoot->length); + j = localDocumentRoot->length; + if(buf[j - 1] == '/') + j--; + + while(i < len) { + if(j >= n - 1) + return -1; + if(key[i] == '/' && i < len - 2) + if(key[i + 1] == '.' && + (key[i + 2] == '.' || key[i + 2] == '/')) + return -1; + buf[j++] = key[i++]; + } + + if(buf[j - 1] == '/') { + if(j >= n - 11) + return -1; + memcpy(buf + j, "index.html", 10); + j += 10; + } + + buf[j] = '\0'; + return j; +} + +static void +md5(unsigned char *restrict key, int len, unsigned char *restrict dst) +{ + static MD5_CTX ctx; + MD5Init(&ctx); + MD5Update(&ctx, key, len); + MD5Final(&ctx); + memcpy(dst, ctx.digest, 16); +} + +/* Check whether a character can be stored in a filename. This is + needed since we want to support deficient file systems. */ +static int +fssafe(char c) +{ + if(c <= 31 || c >= 127) + return 0; + if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '.' || c == '-' || c == '_') + return 1; + return 0; +} + +/* Given a URL, returns the directory name within which all files + starting with this URL can be found. */ +static int +urlDirname(char *buf, int n, const char *url, int len) +{ + int i, j; + if(len < 8) + return -1; + if(lwrcmp(url, "http://", 7) != 0) + return -1; + + if(diskCacheRoot == NULL || + diskCacheRoot->length <= 0 +#ifndef WIN32 + || diskCacheRoot->string[0] != '/' +#endif + ) + return -1; + + if(n <= diskCacheRoot->length) + return -1; + + memcpy(buf, diskCacheRoot->string, diskCacheRoot->length); + j = diskCacheRoot->length; + + if(buf[j - 1] != '/') + buf[j++] = '/'; + + for(i = 7; i < len; i++) { + if(i >= len || url[i] == '/') + break; + if(url[i] == '.' && i != len - 1 && url[i + 1] == '.') + return -1; + if(url[i] == '%' || !fssafe(url[i])) { + if(j + 3 >= n) return -1; + buf[j++] = '%'; + buf[j++] = i2h((url[i] & 0xF0) >> 4); + buf[j++] = i2h(url[i] & 0x0F); + } else { + buf[j++] = url[i]; if(j >= n) return -1; + } + } + buf[j++] = '/'; if(j >= n) return -1; + buf[j] = '\0'; + return j; +} + +/* Given a URL, returns the filename where the cached data can be + found. */ +static int +urlFilename(char *restrict buf, int n, const char *url, int len) +{ + int j; + unsigned char md5buf[18]; + j = urlDirname(buf, n, url, len); + if(j < 0 || j + 24 >= n) + return -1; + md5((unsigned char*)url, len, md5buf); + b64cpy(buf + j, (char*)md5buf, 16, 1); + buf[j + 24] = '\0'; + return j + 24; +} + +static char * +dirnameUrl(char *url, int n, char *name, int len) +{ + int i, j, k, c1, c2; + k = diskCacheRoot->length; + if(len < k) + return NULL; + if(memcmp(name, diskCacheRoot->string, k) != 0) + return NULL; + if(n < 8) + return NULL; + memcpy(url, "http://", 7); + if(name[len - 1] == '/') + len --; + j = 7; + for(i = k; i < len; i++) { + if(name[i] == '%') { + if(i >= len - 2) + return NULL; + c1 = h2i(name[i + 1]); + c2 = h2i(name[i + 2]); + if(c1 < 0 || c2 < 0) + return NULL; + url[j++] = c1 * 16 + c2; if(j >= n) goto fail; + i += 2; /* skip extra digits */ + } else if(i < len - 1 && + name[i] == '.' && name[i + 1] == '/') { + return NULL; + } else if(i == len - 1 && name[i] == '.') { + return NULL; + } else { + url[j++] = name[i]; if(j >= n) goto fail; + } + } + url[j++] = '/'; if(j >= n) goto fail; + url[j] = '\0'; + return url; + + fail: + return NULL; +} + +/* Create a file and all intermediate directories. */ +static int +createFile(const char *name, int path_start) +{ + int fd; + char buf[1024]; + int n; + int rc; + + if(name[path_start] == '/') + path_start++; + + if(path_start < 2 || name[path_start - 1] != '/' ) { + do_log(L_ERROR, "Incorrect name %s (%d).\n", name, path_start); + return -1; + } + + fd = open(name, O_RDWR | O_CREAT | O_EXCL | O_BINARY, + diskCacheFilePermissions); + if(fd >= 0) + return fd; + if(errno != ENOENT) { + do_log_error(L_ERROR, errno, "Couldn't create disk file %s", name); + return -1; + } + + n = path_start; + while(name[n] != '\0' && n < 1024) { + while(name[n] != '/' && name[n] != '\0' && n < 512) + n++; + if(name[n] != '/' || n >= 1024) + break; + memcpy(buf, name, n + 1); + buf[n + 1] = '\0'; + rc = mkdir(buf, diskCacheDirectoryPermissions); + if(rc < 0 && errno != EEXIST) { + do_log_error(L_ERROR, errno, "Couldn't create directory %s", buf); + return -1; + } + n++; + } + fd = open(name, O_RDWR | O_CREAT | O_EXCL | O_BINARY, + diskCacheFilePermissions); + if(fd < 0) { + do_log_error(L_ERROR, errno, "Couldn't create file %s", name); + return -1; + } + + return fd; +} + +static int +chooseBodyOffset(int n, ObjectPtr object) +{ + int length = MAX(object->size, object->length); + int body_offset; + + if(object->length >= 0 && object->length + n < 4096 - 4) + return -1; /* no gap for small objects */ + + if(n <= 128) + body_offset = 256; + else if(n <= 192) + body_offset = 384; + else if(n <= 256) + body_offset = 512; + else if(n <= 384) + body_offset = 768; + else if(n <= 512) + body_offset = 1024; + else if(n <= 1024) + body_offset = 2048; + else if(n < 2048) + body_offset = 4096; + else + body_offset = ((n + 32 + 4095) / 4096 + 1) * 4096; + + /* Tweak the gap so that we don't use up a full disk block for + a small tail */ + if(object->length >= 0 && object->length < 64 * 1024) { + int last = (body_offset + object->length) % 4096; + int gap = body_offset - n - 32; + if(last < gap / 2) + body_offset -= last; + } + + /* Rewriting large objects is expensive -- don't use small gaps. + This has the additional benefit of block-aligning large bodies. */ + if(length >= 64 * 1024) { + int min_gap, min_offset; + if(length >= 512 * 1024) + min_gap = 4096; + else if(length >= 256 * 1024) + min_gap = 2048; + else + min_gap = 1024; + + min_offset = ((n + 32 + min_gap - 1) / min_gap + 1) * min_gap; + body_offset = MAX(body_offset, min_offset); + } + + return body_offset; +} + +/* Assumes the file descriptor is at offset 0. Returns -1 on failure, + otherwise the offset at which the file descriptor is left. */ +/* If chunk is not null, it should be the first chunk of the object, + and will be written out in the same operation if possible. */ +static int +writeHeaders(int fd, int *body_offset_return, + ObjectPtr object, char *chunk, int chunk_len) +{ + int n, rc, error = -1; + int body_offset = *body_offset_return; + char *buf = NULL; + int buf_is_chunk = 0; + int bufsize = 0; + + if(object->flags & OBJECT_LOCAL) + return -1; + + if(body_offset > CHUNK_SIZE) + goto overflow; + + /* get_chunk might trigger object expiry */ + bufsize = CHUNK_SIZE; + buf_is_chunk = 1; + buf = maybe_get_chunk(); + if(!buf) { + bufsize = 2048; + buf_is_chunk = 0; + buf = malloc(2048); + if(buf == NULL) { + do_log(L_ERROR, "Couldn't allocate buffer.\n"); + return -1; + } + } + + format_again: + n = snnprintf(buf, 0, bufsize, "HTTP/1.1 %3d %s", + object->code, object->message->string); + + n = httpWriteObjectHeaders(buf, n, bufsize, object, 0, -1); + if(n < 0) + goto overflow; + + n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Location: "); + n = snnprint_n(buf, n, bufsize, object->key, object->key_size); + + if(object->age >= 0 && object->age != object->date) { + n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Date: "); + n = format_time(buf, n, bufsize, object->age); + } + + if(object->atime >= 0) { + n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Access: "); + n = format_time(buf, n, bufsize, object->atime); + } + + if(n < 0) + goto overflow; + + if(body_offset < 0) + body_offset = chooseBodyOffset(n, object); + + if(body_offset > bufsize) + goto overflow; + + if(body_offset > 0 && body_offset != n + 4) + n = snnprintf(buf, n, bufsize, "\r\nX-Polipo-Body-Offset: %d", + body_offset); + + n = snnprintf(buf, n, bufsize, "\r\n\r\n"); + if(n < 0) + goto overflow; + + if(body_offset < 0) + body_offset = n; + if(n > body_offset) { + error = -2; + goto fail; + } + + if(n < body_offset) + memset(buf + n, 0, body_offset - n); + + again: +#ifdef HAVE_READV_WRITEV + if(chunk_len > 0) { + struct iovec iov[2]; + iov[0].iov_base = buf; + iov[0].iov_len = body_offset; + iov[1].iov_base = chunk; + iov[1].iov_len = chunk_len; + rc = writev(fd, iov, 2); + } else +#endif + rc = write(fd, buf, body_offset); + + if(rc < 0 && errno == EINTR) + goto again; + + if(rc < body_offset) + goto fail; + if(object->length >= 0 && + rc - body_offset >= object->length) + object->flags |= OBJECT_DISK_ENTRY_COMPLETE; + + *body_offset_return = body_offset; + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + return rc; + + overflow: + if(bufsize < bigBufferSize) { + char *oldbuf = buf; + buf = malloc(bigBufferSize); + if(!buf) { + do_log(L_ERROR, "Couldn't allocate big buffer.\n"); + goto fail; + } + bufsize = bigBufferSize; + if(oldbuf) { + if(buf_is_chunk) + dispose_chunk(oldbuf); + else + free(oldbuf); + } + buf_is_chunk = 0; + goto format_again; + } + /* fall through */ + + fail: + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + return error; +} + +typedef struct _MimeEntry { + char *extension; + char *mime; +} MimeEntryRec; + +static const MimeEntryRec mimeEntries[] = { + { "html", "text/html" }, + { "htm", "text/html" }, + { "text", "text/plain" }, + { "txt", "text/plain" }, + { "png", "image/png" }, + { "gif", "image/gif" }, + { "jpeg", "image/jpeg" }, + { "jpg", "image/jpeg" }, + { "ico", "image/x-icon" }, + { "pdf", "application/pdf" }, + { "ps", "application/postscript" }, + { "tar", "application/x-tar" }, + { "pac", "application/x-ns-proxy-autoconfig" }, + { "css", "text/css" }, + { "js", "application/x-javascript" }, + { "xml", "text/xml" }, + { "swf", "application/x-shockwave-flash" }, +}; + +static char* +localObjectMimeType(ObjectPtr object, char **encoding_return) +{ + char *name = object->key; + int nlen = object->key_size; + int i; + + assert(nlen >= 1); + + if(name[nlen - 1] == '/') { + *encoding_return = NULL; + return "text/html"; + } + + if(nlen < 3) { + *encoding_return = NULL; + return "application/octet-stream"; + } + + if(memcmp(name + nlen - 3, ".gz", 3) == 0) { + *encoding_return = "x-gzip"; + nlen -= 3; + } else if(memcmp(name + nlen - 2, ".Z", 2) == 0) { + *encoding_return = "x-compress"; + nlen -= 2; + } else { + *encoding_return = NULL; + } + + for(i = 0; i < sizeof(mimeEntries) / sizeof(mimeEntries[0]); i++) { + int len = strlen(mimeEntries[i].extension); + if(nlen > len && + name[nlen - len - 1] == '.' && + memcmp(name + nlen - len, mimeEntries[i].extension, len) == 0) + return mimeEntries[i].mime; + } + + return "application/octet-stream"; +} + +/* Same interface as validateEntry -- see below */ +int +validateLocalEntry(ObjectPtr object, int fd, + int *body_offset_return, off_t *offset_return) +{ + struct stat ss; + char buf[512]; + int n, rc; + char *encoding; + + rc = fstat(fd, &ss); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't stat"); + return -1; + } + + if(S_ISREG(ss.st_mode)) { + if(!(ss.st_mode & S_IROTH) || + (object->length >= 0 && object->length != ss.st_size) || + (object->last_modified >= 0 && + object->last_modified != ss.st_mtime)) + return -1; + } else { + notifyObject(object); + return -1; + } + + n = snnprintf(buf, 0, 512, "%lx-%lx-%lx", + (unsigned long)ss.st_ino, + (unsigned long)ss.st_size, + (unsigned long)ss.st_mtime); + if(n >= 512) + n = -1; + + if(n > 0 && object->etag) { + if(strlen(object->etag) != n || + memcmp(object->etag, buf, n) != 0) + return -1; + } + + if(!(object->flags & OBJECT_INITIAL)) { + if(!object->last_modified && !object->etag) + return -1; + } + + if(object->flags & OBJECT_INITIAL) { + object->length = ss.st_size; + object->last_modified = ss.st_mtime; + object->date = current_time.tv_sec; + object->age = current_time.tv_sec; + object->code = 200; + if(n > 0) + object->etag = strdup(buf); /* okay if fails */ + object->message = internAtom("Okay"); + n = snnprintf(buf, 0, 512, + "\r\nServer: Polipo" + "\r\nContent-Type: %s", + localObjectMimeType(object, &encoding)); + if(encoding != NULL) + n = snnprintf(buf, n, 512, + "\r\nContent-Encoding: %s", encoding); + if(n < 0) + return -1; + object->headers = internAtomN(buf, n); + if(object->headers == NULL) + return -1; + object->flags &= ~OBJECT_INITIAL; + } + + if(body_offset_return) + *body_offset_return = 0; + if(offset_return) + *offset_return = 0; + return 0; +} + +/* Assumes fd is at offset 0. + Returns -1 if not valid, 1 if metadata should be written out, 0 + otherwise. */ +int +validateEntry(ObjectPtr object, int fd, + int *body_offset_return, off_t *offset_return) +{ + char *buf; + int buf_is_chunk, bufsize; + int rc, n; + int dummy; + int code; + AtomPtr headers; + time_t date, last_modified, expires, polipo_age, polipo_access; + int length; + off_t offset = -1; + int body_offset; + char *etag; + AtomPtr via; + CacheControlRec cache_control; + char *location; + AtomPtr message; + int dirty = 0; + + if(object->flags & OBJECT_LOCAL) + return validateLocalEntry(object, fd, + body_offset_return, offset_return); + + if(!(object->flags & OBJECT_PUBLIC) && (object->flags & OBJECT_INITIAL)) + return 0; + + /* get_chunk might trigger object expiry */ + bufsize = CHUNK_SIZE; + buf_is_chunk = 1; + buf = maybe_get_chunk(); + if(!buf) { + bufsize = 2048; + buf_is_chunk = 0; + buf = malloc(2048); + if(buf == NULL) { + do_log(L_ERROR, "Couldn't allocate buffer.\n"); + return -1; + } + } + + again: + rc = read(fd, buf, bufsize); + if(rc < 0) { + if(errno == EINTR) + goto again; + do_log_error(L_ERROR, errno, "Couldn't read disk entry"); + goto fail; + } + offset = rc; + + parse_again: + n = findEndOfHeaders(buf, 0, rc, &dummy); + if(n < 0) { + char *oldbuf = buf; + if(bufsize < bigBufferSize) { + buf = malloc(bigBufferSize); + if(!buf) { + do_log(L_ERROR, "Couldn't allocate big buffer.\n"); + goto fail; + } + bufsize = bigBufferSize; + memcpy(buf, oldbuf, offset); + if(buf_is_chunk) + dispose_chunk(oldbuf); + else + free(oldbuf); + buf_is_chunk = 0; + again2: + rc = read(fd, buf + offset, bufsize - offset); + if(rc < 0) { + if(errno == EINTR) + goto again2; + do_log_error(L_ERROR, errno, "Couldn't read disk entry"); + goto fail; + } + offset += rc; + goto parse_again; + } + do_log(L_ERROR, "Couldn't parse disk entry.\n"); + goto fail; + } + + rc = httpParseServerFirstLine(buf, &code, &dummy, &message); + if(rc < 0) { + do_log(L_ERROR, "Couldn't parse disk entry.\n"); + goto fail; + } + + if(object->code != 0 && object->code != code) { + releaseAtom(message); + goto fail; + } + + rc = httpParseHeaders(0, NULL, buf, rc, NULL, + &headers, &length, &cache_control, NULL, NULL, + &date, &last_modified, &expires, &polipo_age, + &polipo_access, &body_offset, + NULL, &etag, NULL, + NULL, NULL, &location, &via, NULL); + if(rc < 0) { + releaseAtom(message); + goto fail; + } + if(body_offset < 0) + body_offset = n; + + if(!location || strlen(location) != object->key_size || + memcmp(location, object->key, object->key_size) != 0) { + do_log(L_ERROR, "Inconsistent cache file for %s.\n", scrub(location)); + goto invalid; + } + + if(polipo_age < 0) + polipo_age = date; + + if(polipo_age < 0) { + do_log(L_ERROR, "Undated disk entry for %s.\n", scrub(location)); + goto invalid; + } + + if(!(object->flags & OBJECT_INITIAL)) { + if((last_modified >= 0) != (object->last_modified >= 0)) + goto invalid; + + if((object->cache_control & CACHE_MISMATCH) || + (cache_control.flags & CACHE_MISMATCH)) + goto invalid; + + if(last_modified >= 0 && object->last_modified >= 0 && + last_modified != object->last_modified) + goto invalid; + + if(length >= 0 && object->length >= 0) + if(length != object->length) + goto invalid; + + if(!!etag != !!object->etag) + goto invalid; + + if(etag && object->etag && strcmp(etag, object->etag) != 0) + goto invalid; + + /* If we don't have a usable ETag, and either CACHE_VARY or we + don't have a last-modified date, we validate disk entries by + using their date. */ + if(!(etag && object->etag) && + (!(last_modified >= 0 && object->last_modified >= 0) || + ((cache_control.flags & CACHE_VARY) || + (object->cache_control & CACHE_VARY)))) { + if(date >= 0 && date != object->date) + goto invalid; + if(polipo_age >= 0 && polipo_age != object->age) + goto invalid; + } + if((object->cache_control & CACHE_VARY) && dontTrustVaryETag >= 1) { + /* Check content-type to work around mod_gzip bugs */ + if(!httpHeaderMatch(atomContentType, object->headers, headers) || + !httpHeaderMatch(atomContentEncoding, object->headers, headers)) + goto invalid; + } + } + + if(location) + free(location); + + if(headers) { + if(!object->headers) + object->headers = headers; + else + releaseAtom(headers); + } + + if(object->code == 0) { + object->code = code; + object->message = retainAtom(message); + } + if(object->date <= date) + object->date = date; + else + dirty = 1; + if(object->last_modified < 0) + object->last_modified = last_modified; + if(object->expires < 0) + object->expires = expires; + else if(object->expires > expires) + dirty = 1; + if(object->age < 0) + object->age = polipo_age; + else if(object->age > polipo_age) + dirty = 1; + if(object->atime <= polipo_access) + object->atime = polipo_access; + else + dirty = 1; + + object->cache_control |= cache_control.flags; + + if(object->age < 0) object->age = object->date; + if(object->age < 0) object->age = 0; /* a long time ago */ + if(object->length < 0) object->length = length; + if(!object->etag) + object->etag = etag; + else { + if(etag) + free(etag); + } + releaseAtom(message); + + if(object->flags & OBJECT_INITIAL) object->via = via; + object->flags &= ~OBJECT_INITIAL; + if(offset > body_offset) { + /* We need to make sure we don't invoke object expiry recursively */ + objectSetChunks(object, 1); + if(object->numchunks >= 1) { + if(object->chunks[0].data == NULL) + object->chunks[0].data = maybe_get_chunk(); + if(object->chunks[0].data) + objectAddData(object, buf + body_offset, + 0, MIN(offset - body_offset, CHUNK_SIZE)); + } + } + + httpTweakCachability(object); + + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + if(body_offset_return) *body_offset_return = body_offset; + if(offset_return) *offset_return = offset; + return dirty; + + invalid: + releaseAtom(message); + if(etag) free(etag); + if(location) free(location); + if(via) releaseAtom(via); + /* fall through */ + + fail: + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + return -1; +} + +void +dirtyDiskEntry(ObjectPtr object) +{ + DiskCacheEntryPtr entry = object->disk_entry; + if(entry && entry != &negativeEntry) entry->metadataDirty = 1; +} + +int +revalidateDiskEntry(ObjectPtr object) +{ + DiskCacheEntryPtr entry = object->disk_entry; + int rc; + int body_offset; + + if(!entry || entry == &negativeEntry) + return 1; + + CHECK_ENTRY(entry); + rc = entrySeek(entry, 0); + if(rc < 0) return 0; + + rc = validateEntry(object, entry->fd, &body_offset, &entry->offset); + if(rc < 0) { + destroyDiskEntry(object, 0); + return 0; + } + if(body_offset != entry->body_offset) { + do_log(L_WARN, "Inconsistent body offset (%d != %d).\n", + body_offset, entry->body_offset); + destroyDiskEntry(object, 0); + return 0; + } + + entry->metadataDirty |= !!rc; + CHECK_ENTRY(entry); + return 1; +} + +static inline int +objectHasDiskEntry(ObjectPtr object) +{ + return object->disk_entry && object->disk_entry != &negativeEntry; +} + +static DiskCacheEntryPtr +makeDiskEntry(ObjectPtr object, int create) +{ + DiskCacheEntryPtr entry = NULL; + char buf[1024]; + int fd = -1; + int negative = 0, size = -1, name_len = -1; + char *name = NULL; + off_t offset = -1; + int body_offset = -1; + int rc; + int local = (object->flags & OBJECT_LOCAL) != 0; + int dirty = 0; + + if(local && create) + return NULL; + + if(!local && !(object->flags & OBJECT_PUBLIC)) + return NULL; + + if(maxDiskCacheEntrySize >= 0) { + if(object->length > 0) { + if(object->length > maxDiskCacheEntrySize) + return NULL; + } else { + if(object->size > maxDiskCacheEntrySize) + return NULL; + } + } + + if(object->disk_entry) { + entry = object->disk_entry; + CHECK_ENTRY(entry); + if(entry != &negativeEntry) { + /* We'll keep the entry -- put it at the front. */ + if(entry != diskEntries && entry != &negativeEntry) { + entry->previous->next = entry->next; + if(entry->next) + entry->next->previous = entry->previous; + else + diskEntriesLast = entry->previous; + entry->next = diskEntries; + diskEntries->previous = entry; + entry->previous = NULL; + diskEntries = entry; + } + return entry; + } else { + if(entry == &negativeEntry) { + negative = 1; + if(!create) return NULL; + object->disk_entry = NULL; + } + entry = NULL; + destroyDiskEntry(object, 0); + } + } + + if(numDiskEntries > maxDiskEntries) + destroyDiskEntry(diskEntriesLast->object, 0); + + if(!local) { + if(diskCacheRoot == NULL || diskCacheRoot->length <= 0) + return NULL; + name_len = urlFilename(buf, 1024, object->key, object->key_size); + if(name_len < 0) return NULL; + if(!negative) + fd = open(buf, O_RDWR | O_BINARY); + if(fd >= 0) { + rc = validateEntry(object, fd, &body_offset, &offset); + if(rc >= 0) { + dirty = rc; + } else { + close(fd); + fd = -1; + rc = unlink(buf); + if(rc < 0 && errno != ENOENT) { + do_log_error(L_WARN, errno, + "Couldn't unlink stale disk entry %s", + scrub(buf)); + /* But continue -- it's okay to have stale entries. */ + } + } + } + + if(fd < 0 && create && name_len > 0 && + !(object->flags & OBJECT_INITIAL)) { + fd = createFile(buf, diskCacheRoot->length); + if(fd < 0) + return NULL; + + if(fd >= 0) { + char *data = NULL; + int dsize = 0; + if(object->numchunks > 0) { + data = object->chunks[0].data; + dsize = object->chunks[0].size; + } + rc = writeHeaders(fd, &body_offset, object, data, dsize); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't write headers"); + rc = unlink(buf); + if(rc < 0 && errno != ENOENT) + do_log_error(L_ERROR, errno, + "Couldn't unlink truncated entry %s", + scrub(buf)); + close(fd); + return NULL; + } + assert(rc >= body_offset); + size = rc - body_offset; + offset = rc; + dirty = 0; + } + } + } else { + /* local */ + if(localDocumentRoot == NULL || localDocumentRoot->length == 0) + return NULL; + + name_len = + localFilename(buf, 1024, object->key, object->key_size); + if(name_len < 0) + return NULL; + fd = open(buf, O_RDONLY | O_BINARY); + if(fd >= 0) { + if(validateEntry(object, fd, &body_offset, NULL) < 0) { + close(fd); + fd = -1; + } + } + offset = 0; + } + + if(fd < 0) { + object->disk_entry = &negativeEntry; + return NULL; + } + assert(body_offset >= 0); + + name = strdup_n(buf, name_len); + if(name == NULL) { + do_log(L_ERROR, "Couldn't allocate name.\n"); + close(fd); + fd = -1; + return NULL; + } + + entry = malloc(sizeof(DiskCacheEntryRec)); + if(entry == NULL) { + do_log(L_ERROR, "Couldn't allocate entry.\n"); + free(name); + close(fd); + return NULL; + } + + entry->filename = name; + entry->object = object; + entry->fd = fd; + entry->body_offset = body_offset; + entry->local = local; + entry->offset = offset; + entry->size = size; + entry->metadataDirty = dirty; + + entry->next = diskEntries; + if(diskEntries) + diskEntries->previous = entry; + diskEntries = entry; + if(diskEntriesLast == NULL) + diskEntriesLast = entry; + entry->previous = NULL; + numDiskEntries++; + + object->disk_entry = entry; + + CHECK_ENTRY(entry); + return entry; +} + +/* Rewrite a disk cache entry, used when the body offset needs to change. */ +static int +rewriteEntry(ObjectPtr object) +{ + int old_body_offset = object->disk_entry->body_offset; + int fd, rc, n; + DiskCacheEntryPtr entry; + char* buf; + int buf_is_chunk, bufsize; + int offset; + + fd = dup(object->disk_entry->fd); + if(fd < 0) { + do_log_error(L_ERROR, errno, "Couldn't duplicate file descriptor"); + return -1; + } + + rc = destroyDiskEntry(object, 1); + if(rc < 0) { + close(fd); + return -1; + } + entry = makeDiskEntry(object, 1); + if(!entry) { + close(fd); + return -1; + } + + offset = diskEntrySize(object); + if(offset < 0) { + close(fd); + return -1; + } + + bufsize = CHUNK_SIZE; + buf_is_chunk = 1; + buf = maybe_get_chunk(); + if(!buf) { + bufsize = 2048; + buf_is_chunk = 0; + buf = malloc(2048); + if(buf == NULL) { + do_log(L_ERROR, "Couldn't allocate buffer.\n"); + close(fd); + return -1; + } + } + + rc = lseek(fd, old_body_offset + offset, SEEK_SET); + if(rc < 0) + goto done; + + while(1) { + CHECK_ENTRY(entry); + n = read(fd, buf, bufsize); + if(n < 0 && errno == EINTR) + continue; + if(n <= 0) + goto done; + rc = entrySeek(entry, entry->body_offset + offset); + if(rc < 0) + goto done; + write_again: + rc = write(entry->fd, buf, n); + if(rc >= 0) { + entry->offset += rc; + entry->size += rc; + } else if(errno == EINTR) { + goto write_again; + } + if(rc < n) + goto done; + } + + done: + CHECK_ENTRY(entry); + if(object->length >= 0 && entry->size == object->length) + object->flags |= OBJECT_DISK_ENTRY_COMPLETE; + close(fd); + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + return 1; +} + +int +destroyDiskEntry(ObjectPtr object, int d) +{ + DiskCacheEntryPtr entry = object->disk_entry; + int rc, urc = 1; + + assert(!entry || !entry->local || !d); + + if(d && !entry) + entry = makeDiskEntry(object, 0); + + CHECK_ENTRY(entry); + + if(!entry || entry == &negativeEntry) { + return 1; + } + + assert(entry->object == object); + + if(maxDiskCacheEntrySize >= 0 && object->size > maxDiskCacheEntrySize) { + /* See writeoutToDisk */ + d = 1; + } + + if(d) { + entry->object->flags &= ~OBJECT_DISK_ENTRY_COMPLETE; + if(entry->filename) { + urc = unlink(entry->filename); + if(urc < 0) + do_log_error(L_WARN, errno, + "Couldn't unlink %s", scrub(entry->filename)); + } + } else { + if(entry && entry->metadataDirty) + writeoutMetadata(object); + makeDiskEntry(object, 0); + /* rewriteDiskEntry may change the disk entry */ + entry = object->disk_entry; + if(entry == NULL || entry == &negativeEntry) + return 0; + if(diskCacheWriteoutOnClose > 0) { + reallyWriteoutToDisk(object, -1, diskCacheWriteoutOnClose); + entry = object->disk_entry; + if(entry == NULL || entry == &negativeEntry) + return 0; + } + } + again: + rc = close(entry->fd); + if(rc < 0 && errno == EINTR) + goto again; + + entry->fd = -1; + + if(entry->filename) + free(entry->filename); + entry->filename = NULL; + + if(entry->previous) + entry->previous->next = entry->next; + else + diskEntries = entry->next; + if(entry->next) + entry->next->previous = entry->previous; + else + diskEntriesLast = entry->previous; + + numDiskEntries--; + assert(numDiskEntries >= 0); + + free(entry); + object->disk_entry = NULL; + if(urc < 0) + return -1; + else + return 1; +} + +ObjectPtr +objectGetFromDisk(ObjectPtr object) +{ + DiskCacheEntryPtr entry = makeDiskEntry(object, 0); + if(!entry) return NULL; + return object; +} + + +int +objectFillFromDisk(ObjectPtr object, int offset, int chunks) +{ + DiskCacheEntryPtr entry; + int rc, result; + int i, j, k; + int complete; + + if(object->type != OBJECT_HTTP) + return 0; + + if(object->flags & OBJECT_LINEAR) + return 0; + + if(object->length >= 0) { + chunks = MIN(chunks, + (object->length - offset + CHUNK_SIZE - 1) / CHUNK_SIZE); + } + + rc = objectSetChunks(object, offset / CHUNK_SIZE + chunks); + if(rc < 0) + return 0; + + complete = 1; + if(object->flags & OBJECT_INITIAL) { + complete = 0; + } else if((object->length < 0 || object->size < object->length) && + object->size < (offset / CHUNK_SIZE + chunks) * CHUNK_SIZE) { + complete = 0; + } else { + for(k = 0; k < chunks; k++) { + int s; + i = offset / CHUNK_SIZE + k; + s = MIN(CHUNK_SIZE, object->size - i * CHUNK_SIZE); + if(object->chunks[i].size < s) { + complete = 0; + break; + } + } + } + + if(complete) + return 1; + + /* This has the side-effect of revalidating the entry, which is + what makes HEAD requests work. */ + entry = makeDiskEntry(object, 0); + if(!entry) + return 0; + + for(k = 0; k < chunks; k++) { + i = offset / CHUNK_SIZE + k; + if(!object->chunks[i].data) + object->chunks[i].data = get_chunk(); + if(!object->chunks[i].data) { + chunks = k; + break; + } + lockChunk(object, i); + } + + result = 0; + + for(k = 0; k < chunks; k++) { + int o; + i = offset / CHUNK_SIZE + k; + j = object->chunks[i].size; + o = i * CHUNK_SIZE + j; + + if(object->chunks[i].size == CHUNK_SIZE) + continue; + + if(entry->size >= 0 && entry->size <= o) + break; + + if(entry->offset != entry->body_offset + o) { + rc = entrySeek(entry, entry->body_offset + o); + if(rc < 0) { + result = 0; + break; + } + } + + CHECK_ENTRY(entry); + again: + rc = read(entry->fd, object->chunks[i].data + j, CHUNK_SIZE - j); + if(rc < 0) { + if(errno == EINTR) + goto again; + entry->offset = -1; + do_log_error(L_ERROR, errno, "Couldn't read"); + break; + } + + entry->offset += rc; + object->chunks[i].size += rc; + if(object->size < o + rc) + object->size = o + rc; + + if(entry->object->length >= 0 && entry->size < 0 && + entry->offset - entry->body_offset == entry->object->length) + entry->size = entry->object->length; + + if(rc < CHUNK_SIZE - j) { + /* Paranoia: the read may have been interrupted half-way. */ + if(entry->size < 0) { + if(rc == 0 || + (entry->object->length >= 0 && + entry->object->length == + entry->offset - entry->body_offset)) + entry->size = entry->offset - entry->body_offset; + break; + } else if(entry->size != entry->offset - entry->body_offset) { + if(rc == 0 || + entry->size < entry->offset - entry->body_offset) { + do_log(L_WARN, + "Disk entry size changed behind our back: " + "%ld -> %ld (%d).\n", + (long)entry->size, + (long)entry->offset - entry->body_offset, + object->size); + entry->size = -1; + } + } + break; + } + + CHECK_ENTRY(entry); + result = 1; + } + + CHECK_ENTRY(object->disk_entry); + for(k = 0; k < chunks; k++) { + i = offset / CHUNK_SIZE + k; + unlockChunk(object, i); + } + + if(result > 0) { + notifyObject(object); + return 1; + } else { + return 0; + } +} + +int +writeoutToDisk(ObjectPtr object, int upto, int max) +{ + if(maxDiskCacheEntrySize >= 0 && object->size > maxDiskCacheEntrySize) { + /* An object was created with an unknown length, and then grew + beyond maxDiskCacheEntrySize. Destroy the disk entry. */ + destroyDiskEntry(object, 1); + return 0; + } + + return reallyWriteoutToDisk(object, upto, max); +} + +static int +reallyWriteoutToDisk(ObjectPtr object, int upto, int max) +{ + DiskCacheEntryPtr entry; + int rc; + int i, j; + int offset; + int bytes = 0; + + if(upto < 0) + upto = object->size; + + if((object->cache_control & CACHE_NO_STORE) || + (object->flags & OBJECT_LOCAL)) + return 0; + + if((object->flags & OBJECT_DISK_ENTRY_COMPLETE) && !object->disk_entry) + return 0; + + entry = makeDiskEntry(object, 1); + if(!entry) return 0; + + assert(!entry->local); + + if(object->flags & OBJECT_DISK_ENTRY_COMPLETE) + goto done; + + diskEntrySize(object); + if(entry->size < 0) + return 0; + + if(object->length >= 0 && entry->size >= object->length) { + object->flags |= OBJECT_DISK_ENTRY_COMPLETE; + goto done; + } + + if(entry->size >= upto) + goto done; + + offset = entry->size; + + /* Avoid a seek in case we start writing at the beginning */ + if(offset == 0 && entry->metadataDirty) { + writeoutMetadata(object); + /* rewriteDiskEntry may change the entry */ + entry = makeDiskEntry(object, 0); + if(entry == NULL) + return 0; + } + + rc = entrySeek(entry, offset + entry->body_offset); + if(rc < 0) return 0; + + do { + if(max >= 0 && bytes >= max) + break; + CHECK_ENTRY(entry); + assert(entry->offset == offset + entry->body_offset); + i = offset / CHUNK_SIZE; + j = offset % CHUNK_SIZE; + if(i >= object->numchunks) + break; + if(object->chunks[i].size <= j) + break; + again: + rc = write(entry->fd, object->chunks[i].data + j, + object->chunks[i].size - j); + if(rc < 0) { + if(errno == EINTR) + goto again; + do_log_error(L_ERROR, errno, "Couldn't write disk entry"); + break; + } + entry->offset += rc; + offset += rc; + bytes += rc; + if(entry->size < offset) + entry->size = offset; + } while(j + rc >= CHUNK_SIZE); + + done: + CHECK_ENTRY(entry); + if(entry->metadataDirty) + writeoutMetadata(object); + + return bytes; +} + +int +writeoutMetadata(ObjectPtr object) +{ + DiskCacheEntryPtr entry; + int rc; + + if((object->cache_control & CACHE_NO_STORE) || + (object->flags & OBJECT_LOCAL)) + return 0; + + entry = makeDiskEntry(object, 0); + if(entry == NULL || entry == &negativeEntry) + goto fail; + + assert(!entry->local); + + rc = entrySeek(entry, 0); + if(rc < 0) goto fail; + + rc = writeHeaders(entry->fd, &entry->body_offset, object, NULL, 0); + if(rc == -2) { + rc = rewriteEntry(object); + if(rc < 0) return 0; + return 1; + } + if(rc < 0) goto fail; + entry->offset = rc; + entry->metadataDirty = 0; + return 1; + + fail: + /* We need this in order to avoid trying to write this entry out + multiple times. */ + if(entry && entry != &negativeEntry) + entry->metadataDirty = 0; + return 0; +} + +static void +mergeDobjects(DiskObjectPtr dst, DiskObjectPtr src) +{ + if(dst->filename == NULL) { + dst->filename = src->filename; + dst->body_offset = src->body_offset; + } else + free(src->filename); + free(src->location); + if(dst->length < 0) + dst->length = src->length; + if(dst->size < 0) + dst->size = src->size; + if(dst->age < 0) + dst->age = src->age; + if(dst->date < 0) + dst->date = src->date; + if(dst->last_modified < 0) + dst->last_modified = src->last_modified; + free(src); +} + +DiskObjectPtr +readDiskObject(char *filename, struct stat *sb) +{ + int fd, rc, n, dummy, code; + int length, size; + time_t date, last_modified, age, atime, expires; + char *location = NULL, *fn = NULL; + DiskObjectPtr dobject; + char *buf; + int buf_is_chunk, bufsize; + int body_offset; + struct stat ss; + + fd = -1; + + if(sb == NULL) { + rc = stat(filename, &ss); + if(rc < 0) { + do_log_error(L_WARN, errno, "Couldn't stat %s", scrub(filename)); + return NULL; + } + sb = &ss; + } + + buf_is_chunk = 1; + bufsize = CHUNK_SIZE; + buf = get_chunk(); + if(buf == NULL) { + do_log(L_ERROR, "Couldn't allocate buffer.\n"); + return NULL; + } + + if(S_ISREG(sb->st_mode)) { + fd = open(filename, O_RDONLY | O_BINARY); + if(fd < 0) + goto fail; + again: + rc = read(fd, buf, bufsize); + if(rc < 0) + goto fail; + + n = findEndOfHeaders(buf, 0, rc, &dummy); + if(n < 0) { + long lrc; + if(buf_is_chunk) { + dispose_chunk(buf); + buf_is_chunk = 0; + bufsize = bigBufferSize; + buf = malloc(bigBufferSize); + if(buf == NULL) + goto fail2; + lrc = lseek(fd, 0, SEEK_SET); + if(lrc < 0) + goto fail; + goto again; + } + goto fail; + } + + rc = httpParseServerFirstLine(buf, &code, &dummy, NULL); + if(rc < 0) + goto fail; + + rc = httpParseHeaders(0, NULL, buf, rc, NULL, + NULL, &length, NULL, NULL, NULL, + &date, &last_modified, &expires, &age, + &atime, &body_offset, NULL, + NULL, NULL, NULL, NULL, &location, NULL, NULL); + if(rc < 0 || location == NULL) + goto fail; + if(body_offset < 0) + body_offset = n; + + size = sb->st_size - body_offset; + if(size < 0) + size = 0; + } else if(S_ISDIR(sb->st_mode)) { + char *n; + n = dirnameUrl(buf, 512, (char*)filename, strlen(filename)); + if(n == NULL) + goto fail; + location = strdup(n); + if(location == NULL) + goto fail; + length = -1; + size = -1; + body_offset = -1; + age = -1; + atime = -1; + date = -1; + last_modified = -1; + expires = -1; + } else { + goto fail; + } + + dobject = malloc(sizeof(DiskObjectRec)); + if(!dobject) + goto fail; + + fn = strdup(filename); + if(!fn) + goto fail; + + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + + dobject->location = location; + dobject->filename = fn; + dobject->length = length; + dobject->body_offset = body_offset; + dobject->size = size; + dobject->age = age; + dobject->access = atime; + dobject->date = date; + dobject->last_modified = last_modified; + dobject->expires = expires; + if(fd >= 0) close(fd); + return dobject; + + fail: + if(buf_is_chunk) + dispose_chunk(buf); + else + free(buf); + fail2: + if(fd >= 0) close(fd); + if(location) free(location); + return NULL; +} + + +DiskObjectPtr +processObject(DiskObjectPtr dobjects, char *filename, struct stat *sb) +{ + DiskObjectPtr dobject = NULL; + int c = 0; + + dobject = readDiskObject((char*)filename, sb); + if(dobject == NULL) + return dobjects; + + if(!dobjects || + (c = strcmp(dobject->location, dobjects->location)) <= 0) { + if(dobjects && c == 0) { + mergeDobjects(dobjects, dobject); + } else { + dobject->next = dobjects; + dobjects = dobject; + } + } else { + DiskObjectPtr other = dobjects; + while(other->next) { + c = strcmp(dobject->location, other->next->location); + if(c < 0) + break; + other = other->next; + } + if(strcmp(dobject->location, other->location) == 0) { + mergeDobjects(other, dobject); + } else { + dobject->next = other->next; + other->next = dobject; + } + } + return dobjects; +} + +/* Determine whether p is below root */ +static int +filter(DiskObjectPtr p, const char *root, int n, int recursive) +{ + char *cp; + int m = strlen(p->location); + if(m < n) + return 0; + if(memcmp(root, p->location, n) != 0) + return 0; + if(recursive) + return 1; + if(m == 0 || p->location[m - 1] == '/') + return 1; + cp = strchr(p->location + n, '/'); + if(cp && cp - p->location != m - 1) + return 0; + return 1; +} + +/* Filter out all disk objects that are not under root */ +DiskObjectPtr +filterDiskObjects(DiskObjectPtr from, const char *root, int recursive) +{ + int n = strlen(root); + DiskObjectPtr p, q; + + while(from && !filter(from, root, n, recursive)) { + p = from; + from = p->next; + free(p->location); + free(p); + } + + p = from; + while(p && p->next) { + if(!filter(p->next, root, n, recursive)) { + q = p->next; + p->next = q->next; + free(q->location); + free(q); + } else { + p = p->next; + } + } + return from; +} + +DiskObjectPtr +insertRoot(DiskObjectPtr from, const char *root) +{ + DiskObjectPtr p; + + p = from; + while(p) { + if(strcmp(root, p->location) == 0) + return from; + p = p->next; + } + + p = malloc(sizeof(DiskObjectRec)); + if(!p) return from; + p->location = strdup(root); + if(p->location == NULL) { + free(p); + return from; + } + p->filename = NULL; + p->length = -1; + p->size = -1; + p->age = -1; + p->access = -1; + p->last_modified = -1; + p->expires = -1; + p->next = from; + return p; +} + +/* Insert all missing directories in a sorted list of dobjects */ +DiskObjectPtr +insertDirs(DiskObjectPtr from) +{ + DiskObjectPtr p, q, new; + int n, m; + char *cp; + + p = NULL; q = from; + while(q) { + n = strlen(q->location); + if(n > 0 && q->location[n - 1] != '/') { + cp = strrchr(q->location, '/'); + m = cp - q->location + 1; + if(cp && (!p || strlen(p->location) < m || + memcmp(p->location, q->location, m) != 0)) { + new = malloc(sizeof(DiskObjectRec)); + if(!new) break; + new->location = strdup_n(q->location, m); + if(new->location == NULL) { + free(new); + break; + } + new->filename = NULL; + new->length = -1; + new->size = -1; + new->age = -1; + new->access = -1; + new->last_modified = -1; + new->expires = -1; + new->next = q; + if(p) + p->next = new; + else + from = new; + } + } + p = q; + q = q->next; + } + return from; +} + +void +indexDiskObjects(FILE *out, const char *root, int recursive) +{ + int n, i, isdir; + DIR *dir; + struct dirent *dirent; + char buf[1024]; + char *fts_argv[2]; + FTS *fts; + FTSENT *fe; + DiskObjectPtr dobjects = NULL; + char *of = root[0] == '\0' ? "" : " of "; + + fprintf(out, "\n" + "\n" + "%s%s%s\n" + "\n" + "

%s%s%s

\n", + recursive ? "Recursive index" : "Index", of, root, + recursive ? "Recursive index" : "Index", of, root); + + if(diskCacheRoot == NULL || diskCacheRoot->length <= 0) { + fprintf(out, "

No diskCacheRoot.

\n"); + goto trailer; + } + + if(diskCacheRoot->length >= 1024) { + fprintf(out, + "

The value of diskCacheRoot is " + "too long (%d).

\n", + diskCacheRoot->length); + goto trailer; + } + + if(strlen(root) < 8) { + memcpy(buf, diskCacheRoot->string, diskCacheRoot->length); + buf[diskCacheRoot->length] = '\0'; + n = diskCacheRoot->length; + } else { + n = urlDirname(buf, 1024, root, strlen(root)); + } + if(n > 0) { + if(recursive) { + dir = NULL; + fts_argv[0] = buf; + fts_argv[1] = NULL; + fts = fts_open(fts_argv, FTS_LOGICAL, NULL); + if(fts) { + while(1) { + fe = fts_read(fts); + if(!fe) break; + if(fe->fts_info != FTS_DP) + dobjects = + processObject(dobjects, + fe->fts_path, + fe->fts_info == FTS_NS || + fe->fts_info == FTS_NSOK ? + fe->fts_statp : NULL); + } + fts_close(fts); + } + } else { + dir = opendir(buf); + if(dir) { + while(1) { + dirent = readdir(dir); + if(!dirent) break; + if(n + strlen(dirent->d_name) < 1024) { + strcpy(buf + n, dirent->d_name); + } else { + continue; + } + dobjects = processObject(dobjects, buf, NULL); + } + closedir(dir); + } else { + fprintf(out, "

Couldn't open directory: %s (%d).

\n", + strerror(errno), errno); + goto trailer; + } + } + } + + if(dobjects) { + DiskObjectPtr dobject; + int entryno; + dobjects = insertRoot(dobjects, root); + dobjects = insertDirs(dobjects); + dobjects = filterDiskObjects(dobjects, root, recursive); + dobject = dobjects; + buf[0] = '\0'; + alternatingHttpStyle(out, "diskcachelist"); + fprintf(out, "\n"); + fprintf(out, "\n"); + entryno = 0; + while(dobjects) { + dobject = dobjects; + i = strlen(dobject->location); + isdir = (i == 0 || dobject->location[i - 1] == '/'); + if(entryno % 2) + fprintf(out, ""); + else + fprintf(out, ""); + if(dobject->size >= 0) { + fprintf(out, " "); + if(dobject->length >= 0) { + if(dobject->size == dobject->length) + fprintf(out, " ", dobject->length); + else + fprintf(out, " ", + dobject->size, dobject->length); + } else { + /* Avoid a trigraph. */ + fprintf(out, " ", dobject->size); + } + if(dobject->last_modified >= 0) { + struct tm *tm = gmtime(&dobject->last_modified); + if(tm == NULL) + n = -1; + else + n = strftime(buf, 1024, "%d.%m.%Y", tm); + } else + n = -1; + if(n > 0) { + buf[n] = '\0'; + fprintf(out, " ", buf); + } else { + fprintf(out, ""); + } + + if(dobject->date >= 0) { + struct tm *tm = gmtime(&dobject->date); + if(tm == NULL) + n = -1; + else + n = strftime(buf, 1024, "%d.%m.%Y", tm); + } else + n = -1; + if(n > 0) { + buf[n] = '\0'; + fprintf(out, "", buf); + } else { + fprintf(out, ""); + } + } else { + fprintf(out, ""); + } + if(isdir) { + fprintf(out, "" + "", + dobject->location, dobject->location); + } + fprintf(out, "\n"); + entryno++; + dobjects = dobject->next; + free(dobject->location); + free(dobject->filename); + free(dobject); + } + fprintf(out, "\n"); + fprintf(out, "
", + dobject->location); + htmlPrint(out, + dobject->location, strlen(dobject->location)); + fprintf(out, "%d%d/%d%d/??" "?%s%s"); + htmlPrint(out, dobject->location, + strlen(dobject->location)); + fprintf(out, "plain" + "recursive
\n"); + } + + trailer: + fprintf(out, "

back

\n"); + fprintf(out, "\n"); + return; +} + +static int +checkForZeroes(char *buf, int n) +{ + int i, j; + unsigned long *lbuf = (unsigned long *)buf; + assert(n % sizeof(unsigned long) == 0); + + for(i = 0; i * sizeof(unsigned long) < n; i++) { + if(lbuf[i] != 0L) + return i * sizeof(unsigned long); + } + for(j = 0; i * sizeof(unsigned long) + j < n; j++) { + if(buf[i * sizeof(unsigned long) + j] != 0) + break; + } + + return i * sizeof(unsigned long) + j; +} + +static int +copyFile(int from, char *filename, int n) +{ + char *buf; + int to, offset, nread, nzeroes, rc; + + buf = malloc(CHUNK_SIZE); + if(buf == NULL) + return -1; + + to = open(filename, O_RDWR | O_CREAT | O_EXCL | O_BINARY, + diskCacheFilePermissions); + if(to < 0) { + free(buf); + return -1; + } + + offset = 0; + while(offset < n) { + nread = read(from, buf, MIN(CHUNK_SIZE, n - offset)); + if(nread <= 0) + break; + nzeroes = checkForZeroes(buf, nread & -8); + if(nzeroes > 0) { + /* I like holes */ + rc = lseek(to, nzeroes, SEEK_CUR); + if(rc != offset + nzeroes) { + if(rc < 0) + do_log_error(L_ERROR, errno, "Couldn't extend file"); + else + do_log(L_ERROR, + "Couldn't extend file: " + "unexpected offset %d != %d + %d.\n", + rc, offset, nread); + break; + } + } + if(nread > nzeroes) { + rc = write(to, buf + nzeroes, nread - nzeroes); + if(rc != nread - nzeroes) { + if(rc < 0) + do_log_error(L_ERROR, errno, "Couldn't write"); + else + do_log(L_ERROR, "Short write.\n"); + break; + } + } + offset += nread; + } + free(buf); + close(to); + if(offset <= 0) + unlink(filename); /* something went wrong straight away */ + return 1; +} + +static long int +expireFile(char *filename, struct stat *sb, + int *considered, int *unlinked, int *truncated) +{ + DiskObjectPtr dobject = NULL; + time_t t; + int fd, rc; + long int ret = sb->st_size; + + if(!preciseExpiry) { + t = sb->st_mtime; + if(t > current_time.tv_sec + 1) { + do_log(L_WARN, "File %s has access time in the future.\n", + filename); + t = current_time.tv_sec; + } + + if(t > current_time.tv_sec - diskCacheUnlinkTime && + (sb->st_size < diskCacheTruncateSize || + t > current_time.tv_sec - diskCacheTruncateTime)) + return ret; + } + + (*considered)++; + + dobject = readDiskObject(filename, sb); + if(!dobject) { + do_log(L_ERROR, "Incorrect disk entry %s -- removing.\n", + scrub(filename)); + rc = unlink(filename); + if(rc < 0) { + do_log_error(L_ERROR, errno, + "Couldn't unlink %s", scrub(filename)); + return ret; + } else { + (*unlinked)++; + return 0; + } + } + + t = dobject->access; + if(t < 0) t = dobject->age; + if(t < 0) t = dobject->date; + + if(t > current_time.tv_sec) + do_log(L_WARN, + "Disk entry %s (%s) has access time in the future.\n", + scrub(dobject->location), scrub(dobject->filename)); + + if(t < current_time.tv_sec - diskCacheUnlinkTime) { + rc = unlink(dobject->filename); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't unlink %s", + scrub(filename)); + } else { + (*unlinked)++; + ret = 0; + } + } else if(dobject->size > + diskCacheTruncateSize + 4 * dobject->body_offset && + t < current_time.tv_sec - diskCacheTruncateTime) { + /* We need to copy rather than simply truncate in place: the + latter would confuse a running polipo. */ + fd = open(dobject->filename, O_RDONLY | O_BINARY, 0); + rc = unlink(dobject->filename); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't unlink %s", + scrub(filename)); + close(fd); + fd = -1; + } else { + (*unlinked)++; + copyFile(fd, dobject->filename, + dobject->body_offset + diskCacheTruncateSize); + close(fd); + (*unlinked)--; + (*truncated)++; + ret = sb->st_size - dobject->body_offset + diskCacheTruncateSize; + } + } + free(dobject->location); + free(dobject->filename); + free(dobject); + return ret; +} + +void +expireDiskObjects() +{ + int rc; + char *fts_argv[2]; + FTS *fts; + FTSENT *fe; + int files = 0, considered = 0, unlinked = 0, truncated = 0; + int dirs = 0, rmdirs = 0; + long left = 0, total = 0; + + if(diskCacheRoot == NULL || + diskCacheRoot->length <= 0 || diskCacheRoot->string[0] != '/') + return; + + fts_argv[0] = diskCacheRoot->string; + fts_argv[1] = NULL; + fts = fts_open(fts_argv, FTS_LOGICAL, NULL); + if(fts == NULL) { + do_log_error(L_ERROR, errno, "Couldn't fts_open disk cache"); + } else { + while(1) { + gettimeofday(¤t_time, NULL); + + fe = fts_read(fts); + if(!fe) break; + + if(fe->fts_info == FTS_D) + continue; + + if(fe->fts_info == FTS_DP || fe->fts_info == FTS_DC || + fe->fts_info == FTS_DNR) { + if(fe->fts_accpath[0] == '/' && + strlen(fe->fts_accpath) <= diskCacheRoot->length) + continue; + dirs++; + rc = rmdir(fe->fts_accpath); + if(rc >= 0) + rmdirs++; + else if(errno != ENOTEMPTY && errno != EEXIST) + do_log_error(L_ERROR, errno, + "Couldn't remove directory %s", + scrub(fe->fts_accpath)); + continue; + } else if(fe->fts_info == FTS_NS) { + do_log_error(L_ERROR, fe->fts_errno, "Couldn't stat file %s", + scrub(fe->fts_accpath)); + continue; + } else if(fe->fts_info == FTS_ERR) { + do_log_error(L_ERROR, fe->fts_errno, + "Couldn't fts_read disk cache"); + break; + } + + if(!S_ISREG(fe->fts_statp->st_mode)) { + do_log(L_ERROR, "Unexpected file %s type 0%o.\n", + fe->fts_accpath, (unsigned int)fe->fts_statp->st_mode); + continue; + } + + files++; + left += expireFile(fe->fts_accpath, fe->fts_statp, + &considered, &unlinked, &truncated); + total += fe->fts_statp->st_size; + } + fts_close(fts); + } + + printf("Disk cache purged.\n"); + printf("%d files, %d considered, %d removed, %d truncated " + "(%ldkB -> %ldkB).\n", + files, considered, unlinked, truncated, total/1024, left/1024); + printf("%d directories, %d removed.\n", dirs, rmdirs); + return; +} + +#else + +void +preinitDiskcache() +{ + return; +} + +void +initDiskcache() +{ + return; +} + +int +writeoutToDisk(ObjectPtr object, int upto, int max) +{ + return 0; +} + +int +destroyDiskEntry(ObjectPtr object, int d) +{ + return 0; +} + +ObjectPtr +objectGetFromDisk(ObjectPtr object) +{ + return NULL; +} + +int +objectFillFromDisk(ObjectPtr object, int offset, int chunks) +{ + return 0; +} + +int +revalidateDiskEntry(ObjectPtr object) +{ + return 0; +} + +void +dirtyDiskEntry(ObjectPtr object) +{ + return; +} + +void +expireDiskObjects() +{ + do_log(L_ERROR, "Disk cache not supported in this version.\n"); +} + +int +diskEntrySize(ObjectPtr object) +{ + return -1; +} +#endif diff --git a/jni/polipo/diskcache.h b/jni/polipo/diskcache.h new file mode 100644 index 00000000..4c9b87f4 --- /dev/null +++ b/jni/polipo/diskcache.h @@ -0,0 +1,71 @@ +/* +Copyright (c) 2003-2010 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +extern int maxDiskEntries; + +extern AtomPtr diskCacheRoot; +extern AtomPtr additionalDiskCacheRoot; + +typedef struct _DiskCacheEntry { + char *filename; + ObjectPtr object; + int fd; + off_t offset; + off_t size; + int body_offset; + short local; + short metadataDirty; + struct _DiskCacheEntry *next; + struct _DiskCacheEntry *previous; +} *DiskCacheEntryPtr, DiskCacheEntryRec; + +typedef struct _DiskObject { + char *location; + char *filename; + int body_offset; + int length; + int size; + time_t age; + time_t access; + time_t date; + time_t last_modified; + time_t expires; + struct _DiskObject *next; +} DiskObjectRec, *DiskObjectPtr; + +struct stat; + +extern int maxDiskCacheEntrySize; + +void preinitDiskcache(void); +void initDiskcache(void); +int destroyDiskEntry(ObjectPtr object, int); +int diskEntrySize(ObjectPtr object); +ObjectPtr objectGetFromDisk(ObjectPtr); +int objectFillFromDisk(ObjectPtr object, int offset, int chunks); +int writeoutMetadata(ObjectPtr object); +int writeoutToDisk(ObjectPtr object, int upto, int max); +void dirtyDiskEntry(ObjectPtr object); +int revalidateDiskEntry(ObjectPtr object); +DiskObjectPtr readDiskObject(char *filename, struct stat *sb); +void indexDiskObjects(FILE *out, const char *root, int r); +void expireDiskObjects(void); diff --git a/jni/polipo/dns.c b/jni/polipo/dns.c new file mode 100644 index 00000000..06103e09 --- /dev/null +++ b/jni/polipo/dns.c @@ -0,0 +1,1747 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifndef NO_STANDARD_RESOLVER +#ifndef NO_FANCY_RESOLVER +int dnsUseGethostbyname = 1; +#else +const int dnsUseGethostbyname = 3; +#endif +#else +#ifndef NO_FANCY_RESOLVER +const int dnsUseGethostbyname = 0; +#else +#error use no resolver at all? +#endif +#endif + +#ifndef NO_FANCY_RESOLVER +AtomPtr dnsNameServer = NULL; +int dnsMaxTimeout = 60; +#endif + +#ifndef NO_STANDARD_RESOLVER +int dnsGethostbynameTtl = 1200; +#endif + +int dnsNegativeTtl = 120; + +#ifdef HAVE_IPv6 +int dnsQueryIPv6 = 2; +#else +const int dnsQueryIPv6 = 0; +#endif + +typedef struct _DnsQuery { + unsigned id; + AtomPtr name; + ObjectPtr object; + AtomPtr inet4, inet6; + time_t ttl4, ttl6; + time_t time; + int timeout; + TimeEventHandlerPtr timeout_handler; + struct _DnsQuery *next; +} DnsQueryRec, *DnsQueryPtr; + +union { + struct sockaddr sa; + struct sockaddr_in sin; +#ifdef HAVE_IPv6 + struct sockaddr_in6 sin6; +#endif +} nameserverAddress_storage; + +#ifndef NO_FANCY_RESOLVER +static AtomPtr atomLocalhost, atomLocalhostDot; + +#define nameserverAddress nameserverAddress_storage.sa + +static DnsQueryPtr inFlightDnsQueries; +static DnsQueryPtr inFlightDnsQueriesLast; +#endif + +static int really_do_gethostbyname(AtomPtr name, ObjectPtr object); +static int really_do_dns(AtomPtr name, ObjectPtr object); + +#ifndef NO_FANCY_RESOLVER +static int stringToLabels(char *buf, int offset, int n, char *string); +static int labelsToString(char *buf, int offset, int n, char *d, + int m, int *j_return); +static int dnsBuildQuery(int id, char *buf, int offset, int n, + AtomPtr name, int af); +static int dnsReplyHandler(int abort, FdEventHandlerPtr event); +static int dnsReplyId(char *buf, int offset, int n, int *id_return); +static int dnsDecodeReply(char *buf, int offset, int n, + int *id_return, + AtomPtr *name_return, AtomPtr *value_return, + int *af_return, unsigned *ttl_return); +static int dnsHandler(int status, ConditionHandlerPtr chandler); +static int dnsGethostbynameFallback(int id, AtomPtr message); +static int sendQuery(DnsQueryPtr query); + +static int idSeed; +#endif + +#ifndef NO_FANCY_RESOLVER +static int +parseResolvConf(char *filename) +{ + FILE *f; + char buf[512]; + char *p, *q; + int n; + AtomPtr nameserver = NULL; + + f = fopen(filename, "r"); + if(f == NULL) { + do_log_error(L_ERROR, errno, "DNS: couldn't open %s", filename); + return 0; + } + + while(1) { + p = fgets(buf, 512, f); + if(p == NULL) + break; + + n = strlen(buf); + if(buf[n - 1] != '\n') { + int c; + do_log(L_WARN, "DNS: overly long line in %s -- skipping.\n", + filename); + do { + c = fgetc(f); + if(c == EOF) + break; + } while(c != '\n'); + if(c == EOF) + break; + } + + while(*p == ' ' || *p == '\t') + p++; + if(strcasecmp_n("nameserver", p, 10) != 0) + continue; + p += 10; + while(*p == ' ' || *p == '\t') + p++; + q = p; + while(*q == '.' || *q == ':' || digit(*q) || letter(*q)) + q++; + if(*q != ' ' && *q != '\t' && *q != '\r' && *q != '\n') { + do_log(L_WARN, "DNS: couldn't parse line in %s -- skipping.\n", + filename); + continue; + } + nameserver = internAtomLowerN(p, q - p); + break; + } + + fclose(f); + if(nameserver) { + dnsNameServer = nameserver; + return 1; + } else { + return 0; + } +} +#endif + +void +preinitDns() +{ +#ifdef HAVE_IPv6 + int fd; +#endif + + assert(sizeof(struct in_addr) == 4); +#ifdef HAVE_IPv6 + assert(sizeof(struct in6_addr) == 16); +#endif + +#ifndef NO_STANDARD_RESOLVER + CONFIG_VARIABLE(dnsGethostbynameTtl, CONFIG_TIME, + "TTL for gethostbyname addresses."); +#endif + +#ifdef HAVE_IPv6 + fd = socket(PF_INET6, SOCK_STREAM, 0); + if(fd < 0) { + if(errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT) { + dnsQueryIPv6 = 0; + } else { + do_log_error(L_WARN, errno, "DNS: couldn't create socket"); + } + } else { + close(fd); + } +#endif + +#ifndef NO_FANCY_RESOLVER + parseResolvConf("/etc/resolv.conf"); + if(dnsNameServer == NULL || dnsNameServer->string[0] == '\0') + dnsNameServer = internAtom("127.0.0.1"); + CONFIG_VARIABLE(dnsMaxTimeout, CONFIG_TIME, + "Max timeout for DNS queries."); + CONFIG_VARIABLE(dnsNegativeTtl, CONFIG_TIME, + "TTL for negative DNS replies with no TTL."); + CONFIG_VARIABLE(dnsNameServer, CONFIG_ATOM_LOWER, + "The name server to use."); +#ifndef NO_STANDARD_RESOLVER + CONFIG_VARIABLE(dnsUseGethostbyname, CONFIG_TETRASTATE, + "Use the system resolver."); +#endif +#endif + +#ifdef HAVE_IPv6 + CONFIG_VARIABLE(dnsQueryIPv6, CONFIG_TETRASTATE, + "Query for IPv6 addresses."); +#endif +} + +void +initDns() +{ +#ifndef NO_FANCY_RESOLVER + int rc; + struct timeval t; + struct sockaddr_in *sin = (struct sockaddr_in*)&nameserverAddress; +#ifdef HAVE_IPv6 + struct sockaddr_in6 *sin6 = (struct sockaddr_in6*)&nameserverAddress; +#endif + + atomLocalhost = internAtom("localhost"); + atomLocalhostDot = internAtom("localhost."); + inFlightDnsQueries = NULL; + inFlightDnsQueriesLast = NULL; + + gettimeofday(&t, NULL); + idSeed = t.tv_usec & 0xFFFF; + sin->sin_family = AF_INET; + sin->sin_port = htons(53); + rc = inet_aton(dnsNameServer->string, &sin->sin_addr); +#ifdef HAVE_IPv6 + if(rc != 1) { + sin6->sin6_family = AF_INET6; + sin6->sin6_port = htons(53); + rc = inet_pton(AF_INET6, dnsNameServer->string, &sin6->sin6_addr); + } +#endif + if(rc != 1) { + do_log(L_ERROR, "DNS: couldn't parse name server %s.\n", + dnsNameServer->string); + exit(1); + } +#endif +} + +int +do_gethostbyname(char *origname, + int count, + int (*handler)(int, GethostbynameRequestPtr), + void *data) +{ + ObjectPtr object; + int n = strlen(origname); + AtomPtr name; + GethostbynameRequestRec request; + int done, rc; + + memset(&request, 0, sizeof(request)); + request.name = NULL; + request.addr = NULL; + request.error_message = NULL; + request.count = count; + request.handler = handler; + request.data = data; + + if(n <= 0 || n > 131) { + if(n <= 0) { + request.error_message = internAtom("empty name"); + do_log(L_ERROR, "Empty DNS name.\n"); + done = handler(-EINVAL, &request); + } else { + request.error_message = internAtom("name too long"); + do_log(L_ERROR, "DNS name too long.\n"); + done = handler(-ENAMETOOLONG, &request); + } + assert(done); + releaseAtom(request.error_message); + return 1; + } + + if(origname[n - 1] == '.') + n--; + + name = internAtomLowerN(origname, n); + + if(name == NULL) { + request.error_message = internAtom("couldn't allocate name"); + do_log(L_ERROR, "Couldn't allocate DNS name.\n"); + done = handler(-ENOMEM, &request); + assert(done); + releaseAtom(request.error_message); + return 1; + } + + request.name = name; + request.addr = NULL; + request.error_message = NULL; + request.count = count; + request.object = NULL; + request.handler = handler; + request.data = data; + + object = findObject(OBJECT_DNS, name->string, name->length); + if(object == NULL || objectMustRevalidate(object, NULL)) { + if(object) { + privatiseObject(object, 0); + releaseObject(object); + } + object = makeObject(OBJECT_DNS, name->string, name->length, 1, 0, + NULL, NULL); + if(object == NULL) { + request.error_message = internAtom("Couldn't allocate object"); + do_log(L_ERROR, "Couldn't allocate DNS object.\n"); + done = handler(-ENOMEM, &request); + assert(done); + releaseAtom(name); + releaseAtom(request.error_message); + return 1; + } + } + + if((object->flags & (OBJECT_INITIAL | OBJECT_INPROGRESS)) == + OBJECT_INITIAL) { + if(dnsUseGethostbyname >= 3) + rc = really_do_gethostbyname(name, object); + else + rc = really_do_dns(name, object); + if(rc < 0) { + assert(!(object->flags & (OBJECT_INITIAL | OBJECT_INPROGRESS))); + goto fail; + } + } + + if(dnsUseGethostbyname >= 3) + assert(!(object->flags & OBJECT_INITIAL)); + +#ifndef NO_FANCY_RESOLVER + if(object->flags & OBJECT_INITIAL) { + ConditionHandlerPtr chandler; + assert(object->flags & OBJECT_INPROGRESS); + request.object = object; + chandler = conditionWait(&object->condition, dnsHandler, + sizeof(request), &request); + if(chandler == NULL) + goto fail; + return 1; + } +#endif + + if(object->headers && object->headers->length > 0) { + if(object->headers->string[0] == DNS_A) + assert(((object->headers->length - 1) % + sizeof(HostAddressRec)) == 0); + else + assert(object->headers->string[0] == DNS_CNAME); + request.addr = retainAtom(object->headers); + } else if(object->message) { + request.error_message = retainAtom(object->message); + } + + releaseObject(object); + + if(request.addr && request.addr->length > 0) + done = handler(1, &request); + else + done = handler(-EDNS_HOST_NOT_FOUND, &request); + assert(done); + + releaseAtom(request.addr); request.addr = NULL; + releaseAtom(request.name); request.name = NULL; + releaseAtom(request.error_message); request.error_message = NULL; + return 1; + + fail: + releaseNotifyObject(object); + done = handler(-errno, &request); + assert(done); + releaseAtom(name); + return 1; +} + +static int +dnsDelayedErrorNotifyHandler(TimeEventHandlerPtr event) +{ + int done; + GethostbynameRequestRec request = + *(GethostbynameRequestPtr)event->data; + done = request.handler(-EDNS_HOST_NOT_FOUND, &request); + assert(done); + releaseAtom(request.name); request.name = NULL; + releaseAtom(request.addr); request.addr = NULL; + releaseAtom(request.error_message); request.error_message = NULL; + return 1; +} + +static int +dnsDelayedDoneNotifyHandler(TimeEventHandlerPtr event) +{ + int done; + GethostbynameRequestRec request = *(GethostbynameRequestPtr)event->data; + done = request.handler(1, &request); + assert(done); + releaseAtom(request.name); request.name = NULL; + releaseAtom(request.addr); request.addr = NULL; + releaseAtom(request.error_message); request.error_message = NULL; + return 1; +} + +static int +dnsDelayedNotify(int error, GethostbynameRequestPtr request) +{ + TimeEventHandlerPtr handler; + + if(error) + handler = scheduleTimeEvent(0, + dnsDelayedErrorNotifyHandler, + sizeof(*request), request); + else + handler = scheduleTimeEvent(0, + dnsDelayedDoneNotifyHandler, + sizeof(*request), request); + if(handler == NULL) { + do_log(L_ERROR, "Couldn't schedule DNS notification.\n"); + return -1; + } + return 1; +} + +#ifdef HAVE_IPv6 +AtomPtr +rfc2732(AtomPtr name) +{ + char buf[40]; /* 8*4 (hexdigits) + 7 (colons) + 1 ('\0') */ + int rc; + AtomPtr a = NULL; + + if(name->length < 40+2 && + name->string[0] == '[' && name->string[name->length - 1] == ']') { + struct in6_addr in6a; + memcpy(buf, name->string + 1, name->length - 2); + buf[name->length - 2] = '\0'; + rc = inet_pton(AF_INET6, buf, &in6a); + if(rc == 1) { + char s[1 + sizeof(HostAddressRec)]; + memset(s, 0, sizeof(s)); + s[0] = DNS_A; + s[1] = 6; + memcpy(s + 2, &in6a, 16); + a = internAtomN(s, 1 + sizeof(HostAddressRec)); + if(a == NULL) + return NULL; + } + } + return a; +} + +/* Used for sorting host addresses depending on the value of dnsQueryIPv6 */ +int +compare_hostaddr(const void *av, const void *bv) +{ + const HostAddressRec *a = av, *b = bv; + int r; + if(a->af == 4) { + if(b->af == 4) + r = 0; + else + r = -1; + } else { + if(b->af == 6) + r = 0; + else + r = 1; + } + if(dnsQueryIPv6 >= 2) + return -r; + else + return r; +} + +#ifndef NO_STANDARD_RESOLVER +static int +really_do_gethostbyname(AtomPtr name, ObjectPtr object) +{ + struct addrinfo *ai, *entry, hints; + int rc; + int error, i; + char buf[1024]; + AtomPtr a; + + a = rfc2732(name); + if(a) { + object->headers = a; + object->age = current_time.tv_sec; + object->expires = current_time.tv_sec + 240; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + notifyObject(object); + return 0; + } + + memset(&hints, 0, sizeof(hints)); + hints.ai_protocol = IPPROTO_TCP; + if(dnsQueryIPv6 <= 0) + hints.ai_family = AF_INET; + else if(dnsQueryIPv6 >= 3) + hints.ai_family = AF_INET6; + + rc = getaddrinfo(name->string, NULL, &hints, &ai); + + switch(rc) { + case 0: error = 0; break; + case EAI_FAMILY: +#ifdef EAI_ADDRFAMILY + case EAI_ADDRFAMILY: +#endif + case EAI_SOCKTYPE: + error = EAFNOSUPPORT; break; + case EAI_BADFLAGS: error = EINVAL; break; + case EAI_SERVICE: error = EDNS_NO_RECOVERY; break; +#ifdef EAI_NONAME + case EAI_NONAME: +#endif +#ifdef EAI_NODATA + case EAI_NODATA: +#endif + error = EDNS_NO_ADDRESS; break; + case EAI_FAIL: error = EDNS_NO_RECOVERY; break; + case EAI_AGAIN: error = EDNS_TRY_AGAIN; break; +#ifdef EAI_MEMORY + case EAI_MEMORY: error = ENOMEM; break; +#endif + case EAI_SYSTEM: error = errno; break; + default: error = EUNKNOWN; + } + + if(error == EDNS_NO_ADDRESS) { + object->headers = NULL; + object->age = current_time.tv_sec; + object->expires = current_time.tv_sec + dnsNegativeTtl; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + notifyObject(object); + return 0; + } else if(error) { + do_log_error(L_ERROR, error, "Getaddrinfo failed"); + object->flags &= ~OBJECT_INPROGRESS; + abortObject(object, 404, + internAtomError(error, "Getaddrinfo failed")); + notifyObject(object); + return 0; + } + + entry = ai; + buf[0] = DNS_A; + i = 0; + while(entry) { + HostAddressRec host; + int host_valid = 0; + if(entry->ai_family == AF_INET && entry->ai_protocol == IPPROTO_TCP) { + if(dnsQueryIPv6 < 3) { + host.af = 4; + memset(host.data, 0, sizeof(host.data)); + memcpy(&host.data, + &((struct sockaddr_in*)entry->ai_addr)->sin_addr, + 4); + host_valid = 1; + } + } else if(entry->ai_family == AF_INET6 && + entry->ai_protocol == IPPROTO_TCP) { + if(dnsQueryIPv6 > 0) { + host.af = 6; + memset(&host.data, 0, sizeof(host.data)); + memcpy(&host.data, + &((struct sockaddr_in6*)entry->ai_addr)->sin6_addr, + 16); + host_valid = 1; + } + } + if(host_valid) { + if(i >= 1024 / sizeof(HostAddressRec) - 2) { + do_log(L_ERROR, "Too many addresses for host %s\n", + name->string); + break; + } + memcpy(buf + 1 + i * sizeof(HostAddressRec), + &host, sizeof(HostAddressRec)); + i++; + } + entry = entry->ai_next; + } + freeaddrinfo(ai); + if(i == 0) { + do_log(L_ERROR, "Getaddrinfo returned no useful addresses\n"); + object->flags &= ~OBJECT_INPROGRESS; + abortObject(object, 404, + internAtom("Getaddrinfo returned no useful addresses")); + notifyObject(object); + return 0; + } + + if(1 <= dnsQueryIPv6 && dnsQueryIPv6 <= 2) + qsort(buf + 1, i, sizeof(HostAddressRec), compare_hostaddr); + + a = internAtomN(buf, 1 + i * sizeof(HostAddressRec)); + if(a == NULL) { + object->flags &= ~OBJECT_INPROGRESS; + abortObject(object, 501, internAtom("Couldn't allocate address")); + notifyObject(object); + return 0; + } + object->headers = a; + object->age = current_time.tv_sec; + object->expires = current_time.tv_sec + dnsGethostbynameTtl; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + notifyObject(object); + return 0; +} +#endif + +#else + +#ifndef NO_STANDARD_RESOLVER +static int +really_do_gethostbyname(AtomPtr name, ObjectPtr object) +{ + struct hostent *host; + char *s; + AtomPtr a; + int i, j; + int error; + + host = gethostbyname(name->string); + if(host == NULL) { + switch(h_errno) { + case HOST_NOT_FOUND: error = EDNS_HOST_NOT_FOUND; break; + case NO_ADDRESS: error = EDNS_NO_ADDRESS; break; + case NO_RECOVERY: error = EDNS_NO_RECOVERY; break; + case TRY_AGAIN: error = EDNS_TRY_AGAIN; break; + default: error = EUNKNOWN; break; + } + if(error == EDNS_HOST_NOT_FOUND) { + object->headers = NULL; + object->age = current_time.tv_sec; + object->expires = current_time.tv_sec + dnsNegativeTtl; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + object->flags &= ~OBJECT_INPROGRESS; + notifyObject(object); + return 0; + } else { + do_log_error(L_ERROR, error, "Gethostbyname failed"); + abortObject(object, 404, + internAtomError(error, "Gethostbyname failed")); + object->flags &= ~OBJECT_INPROGRESS; + notifyObject(object); + return 0; + } + } + if(host->h_addrtype != AF_INET) { + do_log(L_ERROR, "Address is not AF_INET.\n"); + object->flags &= ~OBJECT_INPROGRESS; + abortObject(object, 404, internAtom("Address is not AF_INET")); + notifyObject(object); + return -1; + } + if(host->h_length != sizeof(struct in_addr)) { + do_log(L_ERROR, "Address size inconsistent.\n"); + object->flags &= ~OBJECT_INPROGRESS; + abortObject(object, 404, internAtom("Address size inconsistent")); + notifyObject(object); + return 0; + } + i = 0; + while(host->h_addr_list[i] != NULL) i++; + s = malloc(1 + i * sizeof(HostAddressRec)); + if(s == NULL) { + a = NULL; + } else { + memset(s, 0, 1 + i * sizeof(HostAddressRec)); + s[0] = DNS_A; + for(j = 0; j < i; j++) { + s[j * sizeof(HostAddressRec) + 1] = 4; + memcpy(&s[j * sizeof(HostAddressRec) + 2], host->h_addr_list[j], + sizeof(struct in_addr)); + } + a = internAtomN(s, i * sizeof(HostAddressRec) + 1); + free(s); + } + if(!a) { + object->flags &= ~OBJECT_INPROGRESS; + abortObject(object, 501, internAtom("Couldn't allocate address")); + notifyObject(object); + return 0; + } + object->headers = a; + object->age = current_time.tv_sec; + object->expires = current_time.tv_sec + dnsGethostbynameTtl; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + notifyObject(object); + return 0; + +} +#endif + +#endif + +#ifdef NO_STANDARD_RESOLVER +static int +really_do_gethostbyname(AtomPtr name, ObjectPtr object) +{ + abort(); +} +#endif + +#ifndef NO_FANCY_RESOLVER + +static int dnsSocket = -1; +static FdEventHandlerPtr dnsSocketHandler = NULL; + +static int +dnsHandler(int status, ConditionHandlerPtr chandler) +{ + GethostbynameRequestRec request = *(GethostbynameRequestPtr)chandler->data; + ObjectPtr object = request.object; + + assert(!(object->flags & OBJECT_INPROGRESS)); + + if(object->headers) { + request.addr = retainAtom(object->headers); + dnsDelayedNotify(0, &request); + } else { + if(object->message) + request.error_message = retainAtom(object->message); + dnsDelayedNotify(1, &request); + } + releaseObject(object); + return 1; +} + +static int +queryInFlight(DnsQueryPtr query) +{ + DnsQueryPtr other; + other = inFlightDnsQueries; + while(other) { + if(other == query) + return 1; + other = other->next; + } + return 0; +} + +static void +removeQuery(DnsQueryPtr query) +{ + DnsQueryPtr previous; + if(query == inFlightDnsQueries) { + inFlightDnsQueries = query->next; + if(inFlightDnsQueries == NULL) + inFlightDnsQueriesLast = NULL; + } else { + previous = inFlightDnsQueries; + while(previous->next) { + if(previous->next == query) + break; + previous = previous->next; + } + assert(previous->next != NULL); + previous->next = query->next; + if(previous->next == NULL) + inFlightDnsQueriesLast = previous; + } +} + +static void +insertQuery(DnsQueryPtr query) +{ + if(inFlightDnsQueriesLast) + inFlightDnsQueriesLast->next = query; + else + inFlightDnsQueries = query; + inFlightDnsQueriesLast = query; +} + +static DnsQueryPtr +findQuery(int id, AtomPtr name) +{ + DnsQueryPtr query; + query = inFlightDnsQueries; + while(query) { + if(query->id == id && (name == NULL || query->name == name)) + return query; + query = query->next; + } + return NULL; +} + +static int +dnsTimeoutHandler(TimeEventHandlerPtr event) +{ + DnsQueryPtr query = *(DnsQueryPtr*)event->data; + ObjectPtr object = query->object; + int rc; + + /* People are reporting that this does happen. And I have no idea why. */ + if(!queryInFlight(query)) { + do_log(L_ERROR, "BUG: timing out martian query (%s, flags: 0x%x).\n", + scrub(query->name->string), (unsigned)object->flags); + return 1; + } + + query->timeout = MAX(10, query->timeout * 2); + + if(query->timeout > dnsMaxTimeout) { + abortObject(object, 501, internAtom("Timeout")); + goto fail; + } else { + rc = sendQuery(query); + if(rc < 0) { + if(rc != -EWOULDBLOCK && rc != -EAGAIN && rc != -ENOBUFS) { + abortObject(object, 501, + internAtomError(-rc, + "Couldn't send DNS query")); + goto fail; + } + /* else let it timeout */ + } + query->timeout_handler = + scheduleTimeEvent(query->timeout, dnsTimeoutHandler, + sizeof(query), &query); + if(query->timeout_handler == NULL) { + do_log(L_ERROR, "Couldn't schedule DNS timeout handler.\n"); + abortObject(object, 501, + internAtom("Couldn't schedule DNS timeout handler")); + goto fail; + } + return 1; + } + + fail: + removeQuery(query); + object->flags &= ~OBJECT_INPROGRESS; + if(query->inet4) releaseAtom(query->inet4); + if(query->inet6) releaseAtom(query->inet6); + free(query); + releaseNotifyObject(object); + return 1; +} + +static int +establishDnsSocket() +{ + int rc; +#ifdef HAVE_IPv6 + int inet6 = (nameserverAddress.sa_family == AF_INET6); + int pf = inet6 ? PF_INET6 : PF_INET; + int sa_size = + inet6 ? sizeof(struct sockaddr_in6) : sizeof(struct sockaddr_in); +#else + int pf = PF_INET; + int sa_size = sizeof(struct sockaddr_in); +#endif + + if(dnsSocket < 0) { + assert(!dnsSocketHandler); + dnsSocket = socket(pf, SOCK_DGRAM, 0); + if(dnsSocket < 0) { + do_log_error(L_ERROR, errno, "Couldn't create DNS socket"); + return -errno; + } + + rc = connect(dnsSocket, &nameserverAddress, sa_size); + if(rc < 0) { + CLOSE(dnsSocket); + dnsSocket = -1; + do_log_error(L_ERROR, errno, "Couldn't create DNS \"connection\""); + return -errno; + } + } + + if(!dnsSocketHandler) { + dnsSocketHandler = + registerFdEvent(dnsSocket, POLLIN, dnsReplyHandler, 0, NULL); + if(dnsSocketHandler == NULL) { + do_log(L_ERROR, "Couldn't register DNS socket handler.\n"); + CLOSE(dnsSocket); + dnsSocket = -1; + return -ENOMEM; + } + } + + return 1; +} + +static int +sendQuery(DnsQueryPtr query) +{ + char buf[512]; + int buflen; + int rc; + int af[2]; + int i; + + if(dnsSocket < 0) + return -1; + + if(dnsQueryIPv6 <= 0) { + af[0] = 4; af[1] = 0; + } else if(dnsQueryIPv6 <= 2) { + af[0] = 4; af[1] = 6; + } else { + af[0] = 6; af[1] = 0; + } + + for(i = 0; i < 2; i++) { + if(af[i] == 0) + continue; + if(af[i] == 4 && query->inet4) + continue; + else if(af[i] == 6 && query->inet6) + continue; + + buflen = dnsBuildQuery(query->id, buf, 0, 512, query->name, af[i]); + if(buflen <= 0) { + do_log(L_ERROR, "Couldn't build DNS query.\n"); + return buflen; + } + + rc = send(dnsSocket, buf, buflen, 0); + if(rc < buflen) { + if(rc >= 0) { + do_log(L_ERROR, "Couldn't send DNS query: partial send.\n"); + return -EAGAIN; + } else { + do_log_error(L_ERROR, errno, "Couldn't send DNS query"); + return -errno; + } + } + } + return 1; +} + +static int +really_do_dns(AtomPtr name, ObjectPtr object) +{ + int rc; + DnsQueryPtr query; + AtomPtr message = NULL; + int id; + AtomPtr a = NULL; + + if(a == NULL) { + if(name == atomLocalhost || name == atomLocalhostDot) { + char s[1 + sizeof(HostAddressRec)]; + memset(s, 0, sizeof(s)); + s[0] = DNS_A; + s[1] = 4; + s[2] = 127; + s[3] = 0; + s[4] = 0; + s[5] = 1; + a = internAtomN(s, 1 + sizeof(HostAddressRec)); + if(a == NULL) { + abortObject(object, 501, + internAtom("Couldn't allocate address")); + notifyObject(object); + errno = ENOMEM; + return -1; + } + } + } + + if(a == NULL) { + struct in_addr ina; + rc = inet_aton(name->string, &ina); + if(rc == 1) { + char s[1 + sizeof(HostAddressRec)]; + memset(s, 0, sizeof(s)); + s[0] = DNS_A; + s[1] = 4; + memcpy(s + 2, &ina, 4); + a = internAtomN(s, 1 + sizeof(HostAddressRec)); + if(a == NULL) { + abortObject(object, 501, + internAtom("Couldn't allocate address")); + notifyObject(object); + errno = ENOMEM; + return -1; + } + } + } +#ifdef HAVE_IPv6 + if(a == NULL) + a = rfc2732(name); +#endif + + if(a) { + object->headers = a; + object->age = current_time.tv_sec; + object->expires = current_time.tv_sec + 240; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + notifyObject(object); + return 0; + } + + rc = establishDnsSocket(); + if(rc < 0) { + do_log_error(L_ERROR, -rc, "Couldn't establish DNS socket.\n"); + message = internAtomError(-rc, "Couldn't establish DNS socket"); + goto fallback; + } + + /* The id is used to speed up detecting replies to queries that + are no longer current -- see dnsReplyHandler. */ + id = (idSeed++) & 0xFFFF; + + query = malloc(sizeof(DnsQueryRec)); + if(query == NULL) { + do_log(L_ERROR, "Couldn't allocate DNS query.\n"); + message = internAtom("Couldn't allocate DNS query"); + goto fallback; + } + query->id = id; + query->inet4 = NULL; + query->inet6 = NULL; + query->name = name; + query->time = current_time.tv_sec; + query->object = retainObject(object); + query->timeout = 4; + query->timeout_handler = NULL; + query->next = NULL; + + query->timeout_handler = + scheduleTimeEvent(query->timeout, dnsTimeoutHandler, + sizeof(query), &query); + if(query->timeout_handler == NULL) { + do_log(L_ERROR, "Couldn't schedule DNS timeout handler.\n"); + message = internAtom("Couldn't schedule DNS timeout handler"); + goto free_fallback; + } + insertQuery(query); + + object->flags |= OBJECT_INPROGRESS; + rc = sendQuery(query); + if(rc < 0) { + if(rc != -EWOULDBLOCK && rc != -EAGAIN && rc != -ENOBUFS) { + object->flags &= ~OBJECT_INPROGRESS; + message = internAtomError(-rc, "Couldn't send DNS query"); + goto remove_fallback; + } + /* else let it timeout */ + } + releaseAtom(message); + return 1; + + remove_fallback: + removeQuery(query); + free_fallback: + releaseObject(query->object); + cancelTimeEvent(query->timeout_handler); + free(query); + fallback: + if(dnsUseGethostbyname >= 1) { + releaseAtom(message); + do_log(L_WARN, "Falling back on gethostbyname.\n"); + return really_do_gethostbyname(name, object); + } else { + abortObject(object, 501, message); + notifyObject(object); + return 1; + } +} + +static int +dnsReplyHandler(int abort, FdEventHandlerPtr event) +{ + int fd = event->fd; + char buf[2048]; + int len, rc; + ObjectPtr object; + unsigned ttl = 0; + AtomPtr name, value, message = NULL; + int id; + int af; + DnsQueryPtr query; + AtomPtr cname = NULL; + + if(abort) { + dnsSocketHandler = NULL; + rc = establishDnsSocket(); + if(rc < 0) { + do_log(L_ERROR, "Couldn't reestablish DNS socket.\n"); + /* At this point, we should abort all in-flight + DNS requests. Oh, well, they'll timeout anyway. */ + } + return 1; + } + + len = recv(fd, buf, 2048, 0); + if(len <= 0) { + if(errno == EINTR || errno == EAGAIN) return 0; + /* This is where we get ECONNREFUSED for an ICMP port unreachable */ + do_log_error(L_ERROR, errno, "DNS: recv failed"); + dnsGethostbynameFallback(-1, message); + return 0; + } + + /* This could be a late reply to a query that timed out and was + resent, a reply to a query that timed out, or a reply to an + AAAA query when we already got a CNAME reply to the associated + A. We filter such replies straight away, without trying to + parse them. */ + rc = dnsReplyId(buf, 0, len, &id); + if(rc < 0) { + do_log(L_WARN, "Short DNS reply.\n"); + return 0; + } + if(!findQuery(id, NULL)) { + return 0; + } + + rc = dnsDecodeReply(buf, 0, len, &id, &name, &value, &af, &ttl); + if(rc < 0) { + assert(value == NULL); + /* We only want to fallback on gethostbyname if we received a + reply that we could not understand. What about truncated + replies? */ + if(rc < 0) { + do_log_error(L_WARN, -rc, "DNS"); + if(dnsUseGethostbyname >= 2 || + (dnsUseGethostbyname && + (rc != -EDNS_HOST_NOT_FOUND && rc != -EDNS_NO_RECOVERY && + rc != -EDNS_FORMAT))) { + dnsGethostbynameFallback(id, message); + return 0; + } else { + message = internAtom(pstrerror(-rc)); + } + } else { + assert(name != NULL && id >= 0 && af >= 0); + } + } + + query = findQuery(id, name); + if(query == NULL) { + /* Duplicate id ? */ + releaseAtom(value); + releaseAtom(name); + return 0; + } + + /* We're going to use the information in this reply. If it was an + error, construct an empty atom to distinguish it from information + we're still waiting for. */ + if(value == NULL) + value = internAtom(""); + + again: + if(af == 4) { + if(query->inet4 == NULL) { + query->inet4 = value; + query->ttl4 = current_time.tv_sec + ttl; + } else + releaseAtom(value); + } else if(af == 6) { + if(query->inet6 == NULL) { + query->inet6 = value; + query->ttl6 = current_time.tv_sec + ttl; + } else + releaseAtom(value); + } else if(af == 0) { + /* Ignore errors in this case. */ + if(query->inet4 && query->inet4->length == 0) { + releaseAtom(query->inet4); + query->inet4 = NULL; + } + if(query->inet6 && query->inet6->length == 0) { + releaseAtom(query->inet6); + query->inet6 = NULL; + } + if(query->inet4 || query->inet6) { + do_log(L_WARN, "Host %s has both %s and CNAME -- " + "ignoring CNAME.\n", scrub(query->name->string), + query->inet4 ? "A" : "AAAA"); + releaseAtom(value); + value = internAtom(""); + af = query->inet4 ? 4 : 6; + goto again; + } else { + cname = value; + } + } + + if(rc >= 0 && !cname && + ((dnsQueryIPv6 < 3 && query->inet4 == NULL) || + (dnsQueryIPv6 > 0 && query->inet6 == NULL))) + return 0; + + /* This query is complete */ + + cancelTimeEvent(query->timeout_handler); + object = query->object; + + if(object->flags & OBJECT_INITIAL) { + assert(!object->headers); + if(cname) { + assert(query->inet4 == NULL && query->inet6 == NULL); + object->headers = cname; + object->expires = current_time.tv_sec + ttl; + } else if((!query->inet4 || query->inet4->length == 0) && + (!query->inet6 || query->inet6->length == 0)) { + releaseAtom(query->inet4); + releaseAtom(query->inet6); + object->expires = current_time.tv_sec + dnsNegativeTtl; + abortObject(object, 500, retainAtom(message)); + } else if(!query->inet4 || query->inet4->length == 0) { + object->headers = query->inet6; + object->expires = query->ttl6; + releaseAtom(query->inet4); + } else if(!query->inet6 || query->inet6->length == 0) { + object->headers = query->inet4; + object->expires = query->ttl4; + releaseAtom(query->inet6); + } else { + /* need to merge results */ + char buf[1024]; + if(query->inet4->length + query->inet6->length > 1024) { + releaseAtom(query->inet4); + releaseAtom(query->inet6); + abortObject(object, 500, internAtom("DNS reply too long")); + } else { + if(dnsQueryIPv6 <= 1) { + memcpy(buf, query->inet4->string, query->inet4->length); + memcpy(buf + query->inet4->length, + query->inet6->string + 1, query->inet6->length - 1); + } else { + memcpy(buf, query->inet6->string, query->inet6->length); + memcpy(buf + query->inet6->length, + query->inet4->string + 1, query->inet4->length - 1); + } + object->headers = + internAtomN(buf, + query->inet4->length + + query->inet6->length - 1); + if(object->headers == NULL) + abortObject(object, 500, + internAtom("Couldn't allocate DNS atom")); + } + object->expires = MIN(query->ttl4, query->ttl6); + } + object->age = current_time.tv_sec; + object->flags &= ~(OBJECT_INITIAL | OBJECT_INPROGRESS); + } else { + do_log(L_WARN, "DNS object ex nihilo for %s.\n", + scrub(query->name->string)); + } + + removeQuery(query); + free(query); + + releaseAtom(name); + releaseAtom(message); + releaseNotifyObject(object); + return 0; +} + +static int +dnsGethostbynameFallback(int id, AtomPtr message) +{ + DnsQueryPtr query, previous; + ObjectPtr object; + + if(inFlightDnsQueries == NULL) { + releaseAtom(message); + return 1; + } + + query = NULL; + if(id < 0 || inFlightDnsQueries->id == id) { + previous = NULL; + query = inFlightDnsQueries; + } else { + previous = inFlightDnsQueries; + while(previous->next) { + if(previous->next->id == id) { + query = previous->next; + break; + } + previous = previous->next; + } + if(!query) { + previous = NULL; + query = inFlightDnsQueries; + } + } + + if(previous == NULL) { + inFlightDnsQueries = query->next; + if(inFlightDnsQueries == NULL) + inFlightDnsQueriesLast = NULL; + } else { + previous->next = query->next; + if(query->next == NULL) + inFlightDnsQueriesLast = NULL; + } + + object = makeObject(OBJECT_DNS, query->name->string, query->name->length, + 1, 0, NULL, NULL); + if(!object) { + do_log(L_ERROR, "Couldn't make DNS object.\n"); + releaseAtom(query->name); + releaseAtom(message); + releaseObject(query->object); + cancelTimeEvent(query->timeout_handler); + free(query); + return -1; + } + if(dnsUseGethostbyname >= 1) { + releaseAtom(message); + do_log(L_WARN, "Falling back to using system resolver.\n"); + really_do_gethostbyname(retainAtom(query->name), object); + } else { + releaseAtom(object->message); + object->message = message; + object->flags &= ~OBJECT_INPROGRESS; + releaseNotifyObject(object); + } + cancelTimeEvent(query->timeout_handler); + releaseAtom(query->name); + if(query->inet4) releaseAtom(query->inet4); + if(query->inet6) releaseAtom(query->inet6); + releaseObject(query->object); + free(query); + return 1; +} + +static int +stringToLabels(char *buf, int offset, int n, char *string) +{ + int i = offset; + int j = 0, k = 0; + while(1) { + while(string[k] != '.' && string[k] != '\0') + k++; + if(k >= j + 256) return -1; + buf[i] = (unsigned char)(k - j); i++; if(i >= n) return -1; + while(j < k) { + buf[i] = string[j]; i++; j++; if(i >= n) return -1; + } + if(string[j] == '\0') { + buf[i] = '\0'; + i++; if(i >= n) return -1; + break; + } + j++; k++; + } + + return i; +} + +#ifdef UNALIGNED_ACCESS +#define DO_NTOHS(_d, _s) _d = ntohs(*(short*)(_s)); +#define DO_NTOHL(_d, _s) _d = ntohl(*(unsigned*)(_s)) +#define DO_HTONS(_d, _s) *(short*)(_d) = htons(_s); +#define DO_HTONL(_d, _s) *(unsigned*)(_d) = htonl(_s) +#else +#define DO_NTOHS(_d, _s) \ + do { short _dd; \ + memcpy(&(_dd), (_s), sizeof(short)); \ + _d = ntohs(_dd); } while(0) +#define DO_NTOHL(_d, _s) \ + do { unsigned _dd; \ + memcpy(&(_dd), (_s), sizeof(unsigned)); \ + _d = ntohl(_dd); } while(0) +#define DO_HTONS(_d, _s) \ + do { unsigned short _dd; \ + _dd = htons(_s); \ + memcpy((_d), &(_dd), sizeof(unsigned short)); } while(0); +#define DO_HTONL(_d, _s) \ + do { unsigned _dd; \ + _dd = htonl(_s); \ + memcpy((_d), &(_dd), sizeof(unsigned)); } while(0); +#endif + +static int +labelsToString(char *buf, int offset, int n, char *d, int m, int *j_return) +{ + int i = offset, j, k; + int ll; + + j = 0; + while(1) { + if(i >= n) return -1; + ll = *(unsigned char*)&buf[i]; i++; + if(ll == 0) { + break; + } + if((ll & (3 << 6)) == (3 << 6)) { + /* RFC 1035, 4.1.4 */ + int o; + if(i >= n) return -1; + o = (ll & ~(3 << 6)) << 8 | *(unsigned char*)&buf[i]; + i++; + labelsToString(buf, o, n, &d[j], m - j, &k); + j += k; + break; + } else if((ll & (3 << 6)) == 0) { + for(k = 0; k < ll; k++) { + if(i >= n || j >= m) return -1; + d[j++] = buf[i++]; + } + if(i >= n) return -1; + if(buf[i] != '\0') { + if(j >= m) return -1; + d[j++] = '.'; + } + } else { + return -1; + } + } + *j_return = j; + return i; +} + +static int +dnsBuildQuery(int id, char *buf, int offset, int n, AtomPtr name, int af) +{ + int i = offset; + int type; + switch(af) { + case 4: type = 1; break; + case 6: type = 28; break; + default: return -EINVAL; + } + + if(i + 12 >= n) return -1; + DO_HTONS(&buf[i], id); i += 2; + DO_HTONS(&buf[i], 1<<8); i += 2; + DO_HTONS(&buf[i], 1); i += 2; + DO_HTONS(&buf[i], 0); i += 2; + DO_HTONS(&buf[i], 0); i += 2; + DO_HTONS(&buf[i], 0); i += 2; + + i = stringToLabels(buf, i, n, name->string); + if(i < 0) return -ENAMETOOLONG; + + if(i + 4 >= n) return -ENAMETOOLONG; + DO_HTONS(&buf[i], type); i += 2; + DO_HTONS(&buf[i], 1); i += 2; + return i; +} + +static int +dnsReplyId(char *buf, int offset, int n, int *id_return) +{ + if(n - offset < 12) + return -1; + DO_NTOHS(*id_return, &buf[offset]); + return 1; +} + +static int +dnsDecodeReply(char *buf, int offset, int n, int *id_return, + AtomPtr *name_return, AtomPtr *value_return, + int *af_return, unsigned *ttl_return) +{ + int i = offset, j, m; + int id = -1, b23, qdcount, ancount, nscount, arcount, rdlength; + int class, type; + unsigned int ttl; + char b[2048]; + int af = -1; + AtomPtr name = NULL, value; + char addresses[1024]; + int addr_index = 0; + int error = EDNS_NO_ADDRESS; + unsigned final_ttl = 7 * 24 * 3600; + int dnserror; + + if(n - i < 12) { + error = EDNS_INVALID; + goto fail; + } + + DO_NTOHS(id, &buf[i]); i += 2; + DO_NTOHS(b23, &buf[i]); i += 2; + DO_NTOHS(qdcount, &buf[i]); i += 2; + DO_NTOHS(ancount, &buf[i]); i += 2; + DO_NTOHS(nscount, &buf[i]); i += 2; + DO_NTOHS(arcount, &buf[i]); i += 2; + + do_log(D_DNS, + "DNS id %d, b23 0x%x, qdcount %d, ancount %d, " + "nscount %d, arcount %d\n", + id, b23, qdcount, ancount, nscount, arcount); + + if((b23 & (0xF870)) != 0x8000) { + do_log(L_ERROR, "Incorrect DNS reply (b23 = 0x%x).\n", b23); + error = EDNS_INVALID; + goto fail; + } + + dnserror = b23 & 0xF; + + if(b23 & 0x200) { + do_log(L_WARN, "Truncated DNS reply (b23 = 0x%x).\n", b23); + } + + if(dnserror || qdcount != 1) { + if(!dnserror) + do_log(L_ERROR, + "Unexpected number %d of DNS questions.\n", qdcount); + if(dnserror == 1) + error = EDNS_FORMAT; + else if(dnserror == 2) + error = EDNS_NO_RECOVERY; + else if(dnserror == 3) + error = EDNS_HOST_NOT_FOUND; + else if(dnserror == 4 || dnserror == 5) + error = EDNS_REFUSED; + else if(dnserror == 0) + error = EDNS_INVALID; + else + error = EUNKNOWN; + goto fail; + } + + /* We do this early, so that we can return the address family to + the caller in case of error. */ + i = labelsToString(buf, i, n, b, 2048, &m); + if(i < 0) { + error = EDNS_FORMAT; + goto fail; + } + DO_NTOHS(type, &buf[i]); i += 2; + DO_NTOHS(class, &buf[i]); i += 2; + + if(type == 1) + af = 4; + else if(type == 28) + af = 6; + else { + error = EDNS_FORMAT; + goto fail; + } + + do_log(D_DNS, "DNS q: "); + do_log_n(D_DNS, b, m); + do_log(D_DNS, " (%d, %d)\n", type, class); + name = internAtomLowerN(b, m); + if(name == NULL) { + error = ENOMEM; + goto fail; + } + + if(class != 1) { + error = EDNS_FORMAT; + goto fail; + } + +#define PARSE_ANSWER(kind, label) \ +do { \ + i = labelsToString(buf, i, 1024, b, 2048, &m); \ + if(i < 0) goto label; \ + DO_NTOHS(type, &buf[i]); i += 2; if(i > 1024) goto label; \ + DO_NTOHS(class, &buf[i]); i += 2; if(i > 1024) goto label; \ + DO_NTOHL(ttl, &buf[i]); i += 4; if(i > 1024) goto label; \ + DO_NTOHS(rdlength, &buf[i]); i += 2; if(i > 1024) goto label; \ + do_log(D_DNS, "DNS " kind ": "); \ + do_log_n(D_DNS, b, m); \ + do_log(D_DNS, " (%d, %d): %d bytes, ttl %u\n", \ + type, class, rdlength, ttl); \ + } while(0) + + + for(j = 0; j < ancount; j++) { + PARSE_ANSWER("an", fail); + if(strcasecmp_n(name->string, b, m) == 0) { + if(class != 1) { + do_log(D_DNS, "DNS: %s: unknown class %d.\n", + name->string, class); + error = EDNS_UNSUPPORTED; + goto cont; + } + if(type == 1 || type == 28) { + if((type == 1 && rdlength != 4) || + (type == 28 && rdlength != 16)) { + do_log(L_ERROR, + "DNS: %s: unexpected length %d of %s record.\n", + scrub(name->string), + rdlength, type == 1 ? "A" : "AAAA"); + error = EDNS_INVALID; + if(rdlength <= 0 || rdlength >= 32) + goto fail; + goto cont; + } + if(af == 0) { + do_log(L_WARN, "DNS: %s: host has both A and CNAME -- " + "ignoring CNAME.\n", scrub(name->string)); + addr_index = 0; + af = -1; + } + if(type == 1) { + if(af < 0) + af = 4; + else if(af == 6) { + do_log(L_WARN, "Unexpected AAAA reply.\n"); + goto cont; + } + } else { + if(af < 0) + af = 6; + else if(af == 4) { + do_log(L_WARN, "Unexpected A reply.\n"); + goto cont; + } + } + + if(addr_index == 0) { + addresses[0] = DNS_A; + addr_index++; + } else { + if(addr_index > 1000) { + error = EDNS_INVALID; + goto fail; + } + } + assert(addresses[0] == DNS_A); + if(final_ttl > ttl) + final_ttl = ttl; + memset(&addresses[addr_index], 0, sizeof(HostAddressRec)); + if(type == 1) { + addresses[addr_index] = 4; + memcpy(addresses + addr_index + 1, buf + i, 4); + } else { + addresses[addr_index] = 6; + memcpy(addresses + addr_index + 1, buf + i, 16); + } + addr_index += sizeof(HostAddressRec); + } else if(type == 5) { + int j, k; + if(af != 0 && addr_index > 0) { + do_log(L_WARN, "DNS: host has both CNAME and A -- " + "ignoring CNAME.\n"); + goto cont; + } + af = 0; + + if(addr_index != 0) { + /* Only warn if the CNAMEs are not identical */ + char tmp[512]; int jj, kk; + assert(addresses[0] == DNS_CNAME); + jj = labelsToString(buf, i, n, + tmp, 512, &kk); + if(jj < 0 || + kk != strlen(addresses + 1) || + memcmp(addresses + 1, tmp, kk) != 0) { + do_log(L_WARN, "DNS: " + "%s: host has multiple CNAMEs -- " + "ignoring subsequent.\n", + scrub(name->string)); + + } + goto cont; + } + addresses[0] = DNS_CNAME; + addr_index++; + j = labelsToString(buf, i, n, + addresses + 1, 1020, &k); + if(j < 0) { + addr_index = 0; + error = ENAMETOOLONG; + continue; + } + addr_index = k + 1; + } else { + error = EDNS_NO_ADDRESS; + i += rdlength; + continue; + } + + } + cont: + i += rdlength; + } + +#if (LOGGING_MAX & D_DNS) + for(j = 0; j < nscount; j++) { + PARSE_ANSWER("ns", nofail); + i += rdlength; + } + + for(j = 0; j < arcount; j++) { + PARSE_ANSWER("ar", nofail); + i += rdlength; + } + + nofail: +#endif + +#undef PARSE_ANSWER + + do_log(D_DNS, "DNS: %d bytes\n", addr_index); + if(af < 0) + goto fail; + + value = internAtomN(addresses, addr_index); + if(value == NULL) { + error = ENOMEM; + goto fail; + } + + assert(af >= 0); + *id_return = id; + *name_return = name; + *value_return = value; + *af_return = af; + *ttl_return = final_ttl; + return 1; + + fail: + *id_return = id; + *name_return = name; + *value_return = NULL; + *af_return = af; + return -error; +} + +#else + +static int +really_do_dns(AtomPtr name, ObjectPtr object) +{ + abort(); +} + +#endif diff --git a/jni/polipo/dns.h b/jni/polipo/dns.h new file mode 100644 index 00000000..a54a55b6 --- /dev/null +++ b/jni/polipo/dns.h @@ -0,0 +1,48 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +extern char *nameServer; +extern int useGethostbyname; + +#define DNS_A 0 +#define DNS_CNAME 1 + +typedef struct _GethostbynameRequest { + AtomPtr name; + AtomPtr addr; + AtomPtr error_message; + int count; + ObjectPtr object; + int (*handler)(int, struct _GethostbynameRequest*); + void *data; +} GethostbynameRequestRec, *GethostbynameRequestPtr; + +/* Note that this requires no alignment */ +typedef struct _HostAddress { + char af; /* 4 or 6 */ + char data[16]; +} HostAddressRec, *HostAddressPtr; + +void preinitDns(void); +void initDns(void); +int do_gethostbyname(char *name, int count, + int (*handler)(int, GethostbynameRequestPtr), void *data); diff --git a/jni/polipo/event.c b/jni/polipo/event.c new file mode 100644 index 00000000..21e6c9eb --- /dev/null +++ b/jni/polipo/event.c @@ -0,0 +1,834 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifdef HAVE_FORK +static volatile sig_atomic_t exitFlag = 0; +#else +static int exitFlag = 0; +#endif +static int in_signalCondition = 0; + +static TimeEventHandlerPtr timeEventQueue; +static TimeEventHandlerPtr timeEventQueueLast; + +struct timeval current_time; +struct timeval null_time = {0,0}; + +static int fdEventSize = 0; +static int fdEventNum = 0; +static struct pollfd *poll_fds = NULL; +static FdEventHandlerPtr *fdEvents = NULL, *fdEventsLast = NULL; +int diskIsClean = 1; + +static int fds_invalid = 0; + +static inline int +timeval_cmp(struct timeval *t1, struct timeval *t2) +{ + if(t1->tv_sec < t2->tv_sec) + return -1; + else if(t1->tv_sec > t2->tv_sec) + return +1; + else if(t1->tv_usec < t2->tv_usec) + return -1; + else if(t1->tv_usec > t2->tv_usec) + return +1; + else + return 0; +} + +static inline void +timeval_minus(struct timeval *d, + const struct timeval *s1, const struct timeval *s2) +{ + if(s1->tv_usec >= s2->tv_usec) { + d->tv_usec = s1->tv_usec - s2->tv_usec; + d->tv_sec = s1->tv_sec - s2->tv_sec; + } else { + d->tv_usec = s1->tv_usec + 1000000 - s2->tv_usec; + d->tv_sec = s1->tv_sec - s2->tv_sec - 1; + } +} + +int +timeval_minus_usec(const struct timeval *s1, const struct timeval *s2) +{ + return (s1->tv_sec - s2->tv_sec) * 1000000 + s1->tv_usec - s2->tv_usec; +} + +#ifdef HAVE_FORK +static void +sigexit(int signo) +{ + if(signo == SIGUSR1) + exitFlag = 1; + else if(signo == SIGUSR2) + exitFlag = 2; + else + exitFlag = 3; +} +#endif + +void +initEvents() +{ +#ifdef HAVE_FORK + struct sigaction sa; + sigset_t ss; + + sigemptyset(&ss); + sa.sa_handler = SIG_IGN; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGPIPE, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = sigexit; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGTERM, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = sigexit; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGHUP, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = sigexit; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGINT, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = sigexit; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGUSR1, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = sigexit; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGUSR2, &sa, NULL); +#endif + + timeEventQueue = NULL; + timeEventQueueLast = NULL; + fdEventSize = 0; + fdEventNum = 0; + poll_fds = NULL; + fdEvents = NULL; + fdEventsLast = NULL; +} + +void +uninitEvents(void) +{ +#ifdef HAVE_FORK + struct sigaction sa; + sigset_t ss; + + sigemptyset(&ss); + sa.sa_handler = SIG_DFL; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGTERM, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = SIG_DFL; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGHUP, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = SIG_DFL; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGINT, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = SIG_DFL; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGUSR1, &sa, NULL); + + sigemptyset(&ss); + sa.sa_handler = SIG_DFL; + sa.sa_mask = ss; + sa.sa_flags = 0; + sigaction(SIGUSR2, &sa, NULL); +#endif +} + +#ifdef HAVE_FORK +void +interestingSignals(sigset_t *ss) +{ + sigemptyset(ss); + sigaddset(ss, SIGTERM); + sigaddset(ss, SIGHUP); + sigaddset(ss, SIGINT); + sigaddset(ss, SIGUSR1); + sigaddset(ss, SIGUSR2); +} +#endif + +void +timeToSleep(struct timeval *time) +{ + if(!timeEventQueue) { + time->tv_sec = ~0L; + time->tv_usec = ~0L; + } else { + *time = timeEventQueue->time; + } +} + +static TimeEventHandlerPtr +enqueueTimeEvent(TimeEventHandlerPtr event) +{ + TimeEventHandlerPtr otherevent; + + /* We try to optimise two cases -- the event happens very soon, or + it happens after most of the other events. */ + if(timeEventQueue == NULL || + timeval_cmp(&event->time, &timeEventQueue->time) < 0) { + /* It's the first event */ + event->next = timeEventQueue; + event->previous = NULL; + if(timeEventQueue) { + timeEventQueue->previous = event; + } else { + timeEventQueueLast = event; + } + timeEventQueue = event; + } else if(timeval_cmp(&event->time, &timeEventQueueLast->time) >= 0) { + /* It's the last one */ + event->next = NULL; + event->previous = timeEventQueueLast; + timeEventQueueLast->next = event; + timeEventQueueLast = event; + } else { + /* Walk from the end */ + otherevent = timeEventQueueLast; + while(otherevent->previous && + timeval_cmp(&event->time, &otherevent->previous->time) < 0) { + otherevent = otherevent->previous; + } + event->next = otherevent; + event->previous = otherevent->previous; + if(otherevent->previous) { + otherevent->previous->next = event; + } else { + timeEventQueue = event; + } + otherevent->previous = event; + } + return event; +} + +TimeEventHandlerPtr +scheduleTimeEvent(int seconds, + int (*handler)(TimeEventHandlerPtr), int dsize, void *data) +{ + struct timeval when; + TimeEventHandlerPtr event; + + if(seconds >= 0) { + when = current_time; + when.tv_sec += seconds; + } else { + when.tv_sec = 0; + when.tv_usec = 0; + } + + event = malloc(sizeof(TimeEventHandlerRec) - 1 + dsize); + if(event == NULL) { + do_log(L_ERROR, "Couldn't allocate time event handler -- " + "discarding all objects.\n"); + exitFlag = 2; + return NULL; + } + + event->time = when; + event->handler = handler; + /* Let the compiler optimise the common case */ + if(dsize == sizeof(void*)) + memcpy(event->data, data, sizeof(void*)); + else if(dsize > 0) + memcpy(event->data, data, dsize); + + return enqueueTimeEvent(event); +} + +void +cancelTimeEvent(TimeEventHandlerPtr event) +{ + if(event == timeEventQueue) + timeEventQueue = event->next; + if(event == timeEventQueueLast) + timeEventQueueLast = event->previous; + if(event->next) + event->next->previous = event->previous; + if(event->previous) + event->previous->next = event->next; + free(event); +} + +int +allocateFdEventNum(int fd) +{ + int i; + if(fdEventNum < fdEventSize) { + i = fdEventNum; + fdEventNum++; + } else { + struct pollfd *new_poll_fds; + FdEventHandlerPtr *new_fdEvents, *new_fdEventsLast; + int new_size = 3 * fdEventSize / 2 + 1; + + new_poll_fds = realloc(poll_fds, new_size * sizeof(struct pollfd)); + if(!new_poll_fds) + return -1; + new_fdEvents = realloc(fdEvents, new_size * sizeof(FdEventHandlerPtr)); + if(!new_fdEvents) + return -1; + new_fdEventsLast = realloc(fdEventsLast, + new_size * sizeof(FdEventHandlerPtr)); + if(!new_fdEventsLast) + return -1; + + poll_fds = new_poll_fds; + fdEvents = new_fdEvents; + fdEventsLast = new_fdEventsLast; + fdEventSize = new_size; + i = fdEventNum; + fdEventNum++; + } + + poll_fds[i].fd = fd; + poll_fds[i].events = POLLERR | POLLHUP | POLLNVAL; + poll_fds[i].revents = 0; + fdEvents[i] = NULL; + fdEventsLast[i] = NULL; + fds_invalid = 1; + return i; +} + +void +deallocateFdEventNum(int i) +{ + if(i < fdEventNum - 1) { + memmove(&poll_fds[i], &poll_fds[i + 1], + (fdEventNum - i - 1) * sizeof(struct pollfd)); + memmove(&fdEvents[i], &fdEvents[i + 1], + (fdEventNum - i - 1) * sizeof(FdEventHandlerPtr)); + memmove(&fdEventsLast[i], &fdEventsLast[i + 1], + (fdEventNum - i - 1) * sizeof(FdEventHandlerPtr)); + } + fdEventNum--; + fds_invalid = 1; +} + +FdEventHandlerPtr +makeFdEvent(int fd, int poll_events, + int (*handler)(int, FdEventHandlerPtr), int dsize, void *data) +{ + FdEventHandlerPtr event; + + event = malloc(sizeof(FdEventHandlerRec) - 1 + dsize); + if(event == NULL) { + do_log(L_ERROR, "Couldn't allocate fd event handler -- " + "discarding all objects.\n"); + exitFlag = 2; + return NULL; + } + event->fd = fd; + event->poll_events = poll_events; + event->handler = handler; + /* Let the compiler optimise the common cases */ + if(dsize == sizeof(void*)) + memcpy(event->data, data, sizeof(void*)); + else if(dsize == sizeof(StreamRequestRec)) + memcpy(event->data, data, sizeof(StreamRequestRec)); + else if(dsize > 0) + memcpy(event->data, data, dsize); + return event; +} + +FdEventHandlerPtr +registerFdEventHelper(FdEventHandlerPtr event) +{ + int i; + int fd = event->fd; + + for(i = 0; i < fdEventNum; i++) + if(poll_fds[i].fd == fd) + break; + + if(i >= fdEventNum) + i = allocateFdEventNum(fd); + if(i < 0) { + free(event); + return NULL; + } + + event->next = NULL; + event->previous = fdEventsLast[i]; + if(fdEvents[i] == NULL) { + fdEvents[i] = event; + } else { + fdEventsLast[i]->next = event; + } + fdEventsLast[i] = event; + poll_fds[i].events |= event->poll_events; + + return event; +} + +FdEventHandlerPtr +registerFdEvent(int fd, int poll_events, + int (*handler)(int, FdEventHandlerPtr), int dsize, void *data) +{ + FdEventHandlerPtr event; + + event = makeFdEvent(fd, poll_events, handler, dsize, data); + if(event == NULL) + return NULL; + + return registerFdEventHelper(event); +} + +static int +recomputePollEvents(FdEventHandlerPtr event) +{ + int pe = 0; + while(event) { + pe |= event->poll_events; + event = event->next; + } + return pe | POLLERR | POLLHUP | POLLNVAL; +} + +static void +unregisterFdEventI(FdEventHandlerPtr event, int i) +{ + assert(i < fdEventNum && poll_fds[i].fd == event->fd); + + if(fdEvents[i] == event) { + assert(!event->previous); + fdEvents[i] = event->next; + } else { + event->previous->next = event->next; + } + + if(fdEventsLast[i] == event) { + assert(!event->next); + fdEventsLast[i] = event->previous; + } else { + event->next->previous = event->previous; + } + + free(event); + + if(fdEvents[i] == NULL) { + deallocateFdEventNum(i); + } else { + poll_fds[i].events = recomputePollEvents(fdEvents[i]) | + POLLERR | POLLHUP | POLLNVAL; + } +} + +void +unregisterFdEvent(FdEventHandlerPtr event) +{ + int i; + + for(i = 0; i < fdEventNum; i++) { + if(poll_fds[i].fd == event->fd) { + unregisterFdEventI(event, i); + return; + } + } + abort(); +} + +void +runTimeEventQueue() +{ + TimeEventHandlerPtr event; + int done; + + while(timeEventQueue && + timeval_cmp(&timeEventQueue->time, ¤t_time) <= 0) { + event = timeEventQueue; + timeEventQueue = event->next; + if(timeEventQueue) + timeEventQueue->previous = NULL; + else + timeEventQueueLast = NULL; + done = event->handler(event); + assert(done); + free(event); + } +} + +static FdEventHandlerPtr +findEventHelper(int revents, FdEventHandlerPtr events) +{ + FdEventHandlerPtr event = events; + while(event) { + if(revents & event->poll_events) + return event; + event = event->next; + } + return NULL; +} + + + +static FdEventHandlerPtr +findEvent(int revents, FdEventHandlerPtr events) +{ + FdEventHandlerPtr event; + + assert(!(revents & POLLNVAL)); + + if((revents & POLLHUP) || (revents & POLLERR)) { + event = findEventHelper(POLLOUT, events); + if(event) return event; + + event = findEventHelper(POLLIN, events); + if(event) return event; + return NULL; + } + + if(revents & POLLOUT) { + event = findEventHelper(POLLOUT, events); + if(event) return event; + } + + if(revents & POLLIN) { + event = findEventHelper(POLLIN, events); + if(event) return event; + } + return NULL; +} + +typedef struct _FdEventHandlerPoke { + int fd; + int what; + int status; +} FdEventHandlerPokeRec, *FdEventHandlerPokePtr; + +static int +pokeFdEventHandler(TimeEventHandlerPtr tevent) +{ + FdEventHandlerPokePtr poke = (FdEventHandlerPokePtr)tevent->data; + int fd = poke->fd; + int what = poke->what; + int status = poke->status; + int done; + FdEventHandlerPtr event, next; + int i; + + for(i = 0; i < fdEventNum; i++) { + if(poll_fds[i].fd == fd) + break; + } + + if(i >= fdEventNum) + return 1; + + event = fdEvents[i]; + while(event) { + next = event->next; + if(event->poll_events & what) { + done = event->handler(status, event); + if(done) { + if(fds_invalid) + unregisterFdEvent(event); + else + unregisterFdEventI(event, i); + } + if(fds_invalid) + break; + } + event = next; + } + return 1; +} + +void +pokeFdEvent(int fd, int status, int what) +{ + TimeEventHandlerPtr handler; + FdEventHandlerPokeRec poke; + + poke.fd = fd; + poke.status = status; + poke.what = what; + + handler = scheduleTimeEvent(0, pokeFdEventHandler, + sizeof(poke), &poke); + if(!handler) { + do_log(L_ERROR, "Couldn't allocate handler.\n"); + } +} + +int +workToDo() +{ + struct timeval sleep_time; + int rc; + + if(exitFlag) + return 1; + + timeToSleep(&sleep_time); + gettimeofday(¤t_time, NULL); + if(timeval_cmp(&sleep_time, ¤t_time) <= 0) + return 1; + rc = poll(poll_fds, fdEventNum, 0); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't poll"); + return 1; + } + return(rc >= 1); +} + +void +eventLoop() +{ + struct timeval sleep_time, timeout; + int rc, i, done, n; + FdEventHandlerPtr event; + int fd0; + + gettimeofday(¤t_time, NULL); + + while(1) { + again: + if(exitFlag) { + if(exitFlag < 3) + reopenLog(); + if(exitFlag >= 2) { + discardObjects(1, 0); + if(exitFlag >= 3) + return; + free_chunk_arenas(); + } else { + writeoutObjects(1); + } + initForbidden(); + exitFlag = 0; + } + + timeToSleep(&sleep_time); + if(sleep_time.tv_sec == -1) { + rc = poll(poll_fds, fdEventNum, + diskIsClean ? -1 : idleTime * 1000); + } else if(timeval_cmp(&sleep_time, ¤t_time) <= 0) { + runTimeEventQueue(); + continue; + } else { + gettimeofday(¤t_time, NULL); + if(timeval_cmp(&sleep_time, ¤t_time) <= 0) { + runTimeEventQueue(); + continue; + } else { + int t; + timeval_minus(&timeout, &sleep_time, ¤t_time); + t = timeout.tv_sec * 1000 + (timeout.tv_usec + 999) / 1000; + rc = poll(poll_fds, fdEventNum, + diskIsClean ? t : MIN(idleTime * 1000, t)); + } + } + + gettimeofday(¤t_time, NULL); + + if(rc < 0) { + if(errno == EINTR) { + continue; + } else if(errno == ENOMEM) { + free_chunk_arenas(); + do_log(L_ERROR, + "Couldn't poll: out of memory. " + "Sleeping for one second.\n"); + sleep(1); + } else { + do_log_error(L_ERROR, errno, "Couldn't poll"); + exitFlag = 3; + } + continue; + } + + if(rc == 0) { + if(!diskIsClean) { + timeToSleep(&sleep_time); + if(timeval_cmp(&sleep_time, ¤t_time) > 0) + writeoutObjects(0); + } + continue; + } + + /* Rather than tracking all changes to the in-memory cache, we + assume that something changed whenever we see any activity. */ + diskIsClean = 0; + + fd0 = + (current_time.tv_usec ^ (current_time.tv_usec >> 16)) % fdEventNum; + n = rc; + for(i = 0; i < fdEventNum; i++) { + int j = (i + fd0) % fdEventNum; + if(n <= 0) + break; + if(poll_fds[j].revents) { + n--; + event = findEvent(poll_fds[j].revents, fdEvents[j]); + if(!event) + continue; + done = event->handler(0, event); + if(done) { + if(fds_invalid) + unregisterFdEvent(event); + else + unregisterFdEventI(event, j); + } + if(fds_invalid) { + fds_invalid = 0; + goto again; + } + } + } + } +} + +void +initCondition(ConditionPtr condition) +{ + condition->handlers = NULL; +} + +ConditionPtr +makeCondition(void) +{ + ConditionPtr condition; + condition = malloc(sizeof(ConditionRec)); + if(condition == NULL) + return NULL; + initCondition(condition); + return condition; +} + +ConditionHandlerPtr +conditionWait(ConditionPtr condition, + int (*handler)(int, ConditionHandlerPtr), + int dsize, void *data) +{ + ConditionHandlerPtr chandler; + + assert(!in_signalCondition); + + chandler = malloc(sizeof(ConditionHandlerRec) - 1 + dsize); + if(!chandler) + return NULL; + + chandler->condition = condition; + chandler->handler = handler; + /* Let the compiler optimise the common case */ + if(dsize == sizeof(void*)) + memcpy(chandler->data, data, sizeof(void*)); + else if(dsize > 0) + memcpy(chandler->data, data, dsize); + + if(condition->handlers) + condition->handlers->previous = chandler; + chandler->next = condition->handlers; + chandler->previous = NULL; + condition->handlers = chandler; + return chandler; +} + +void +unregisterConditionHandler(ConditionHandlerPtr handler) +{ + ConditionPtr condition = handler->condition; + + assert(!in_signalCondition); + + if(condition->handlers == handler) + condition->handlers = condition->handlers->next; + if(handler->next) + handler->next->previous = handler->previous; + if(handler->previous) + handler->previous->next = handler->next; + + free(handler); +} + +void +abortConditionHandler(ConditionHandlerPtr handler) +{ + int done; + done = handler->handler(-1, handler); + assert(done); + unregisterConditionHandler(handler); +} + +void +signalCondition(ConditionPtr condition) +{ + ConditionHandlerPtr handler; + int done; + + assert(!in_signalCondition); + in_signalCondition++; + + handler = condition->handlers; + while(handler) { + ConditionHandlerPtr next = handler->next; + done = handler->handler(0, handler); + if(done) { + if(handler == condition->handlers) + condition->handlers = next; + if(next) + next->previous = handler->previous; + if(handler->previous) + handler->previous->next = next; + else + condition->handlers = next; + free(handler); + } + handler = next; + } + in_signalCondition--; +} + +void +polipoExit() +{ + exitFlag = 3; +} diff --git a/jni/polipo/event.h b/jni/polipo/event.h new file mode 100644 index 00000000..c94aa3c3 --- /dev/null +++ b/jni/polipo/event.h @@ -0,0 +1,90 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +extern struct timeval current_time; +extern struct timeval null_time; +extern int diskIsClean; + +typedef struct _TimeEventHandler { + struct timeval time; + struct _TimeEventHandler *previous, *next; + int (*handler)(struct _TimeEventHandler*); + char data[1]; +} TimeEventHandlerRec, *TimeEventHandlerPtr; + +typedef struct _FdEventHandler { + short fd; + short poll_events; + struct _FdEventHandler *previous, *next; + int (*handler)(int, struct _FdEventHandler*); + char data[1]; +} FdEventHandlerRec, *FdEventHandlerPtr; + +typedef struct _ConditionHandler { + struct _Condition *condition; + struct _ConditionHandler *previous, *next; + int (*handler)(int, struct _ConditionHandler*); + char data[1]; +} ConditionHandlerRec, *ConditionHandlerPtr; + +typedef struct _Condition { + ConditionHandlerPtr handlers; +} ConditionRec, *ConditionPtr; + +void initEvents(void); +void uninitEvents(void); +#ifdef HAVE_FORK +void interestingSignals(sigset_t *ss); +#endif + +TimeEventHandlerPtr scheduleTimeEvent(int seconds, + int (*handler)(TimeEventHandlerPtr), + int dsize, void *data); + +int timeval_minus_usec(const struct timeval *s1, const struct timeval *s2) + ATTRIBUTE((pure)); +void cancelTimeEvent(TimeEventHandlerPtr); +int allocateFdEventNum(int fd); +void deallocateFdEventNum(int i); +void timeToSleep(struct timeval *); +void runTimeEventQueue(void); +FdEventHandlerPtr makeFdEvent(int fd, int poll_events, + int (*handler)(int, FdEventHandlerPtr), + int dsize, void *data); +FdEventHandlerPtr registerFdEvent(int fd, int poll_events, + int (*handler)(int, FdEventHandlerPtr), + int dsize, void *data); +FdEventHandlerPtr registerFdEventHelper(FdEventHandlerPtr event); +void unregisterFdEvent(FdEventHandlerPtr event); +void pokeFdEvent(int fd, int status, int what); +int workToDo(void); +void eventLoop(void); +ConditionPtr makeCondition(void); +void initCondition(ConditionPtr); +void signalCondition(ConditionPtr condition); +ConditionHandlerPtr +conditionWait(ConditionPtr condition, + int (*handler)(int, ConditionHandlerPtr), + int dsize, void *data); +void unregisterConditionHandler(ConditionHandlerPtr); +void abortConditionHandler(ConditionHandlerPtr); +void polipoExit(void); diff --git a/jni/polipo/forbidden.c b/jni/polipo/forbidden.c new file mode 100644 index 00000000..4b9fa368 --- /dev/null +++ b/jni/polipo/forbidden.c @@ -0,0 +1,835 @@ +/* +Copyright (c) 2003-2010 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifndef NO_FORBIDDEN + +#include +#include + +typedef struct _Domain { + int length; + char domain[1]; +} DomainRec, *DomainPtr; + +AtomPtr forbiddenFile = NULL; +AtomPtr forbiddenUrl = NULL; +int forbiddenRedirectCode = 302; + +AtomPtr redirector = NULL; +int redirectorRedirectCode = 302; + +DomainPtr *forbiddenDomains = NULL; +regex_t *forbiddenRegex = NULL; + +AtomPtr uncachableFile = NULL; +DomainPtr *uncachableDomains = NULL; +regex_t *uncachableRegex = NULL; + +AtomPtr forbiddenTunnelsFile = NULL; +DomainPtr *forbiddenTunnelsDomains = NULL; +regex_t *forbiddenTunnelsRegex = NULL; + + +/* these three are only used internally by {parse,read}DomainFile */ +/* to avoid having to pass it all as parameters */ +static DomainPtr *domains; +static char *regexbuf; +static int rlen, rsize, dlen, dsize; + +#ifndef NO_REDIRECTOR +static pid_t redirector_pid = 0; +static int redirector_read_fd = -1, redirector_write_fd = -1; +#define REDIRECTOR_BUFFER_SIZE 1024 +static char *redirector_buffer = NULL; +RedirectRequestPtr redirector_request_first = NULL, + redirector_request_last = NULL; +#endif + +static int atomSetterForbidden(ConfigVariablePtr, void*); + +void +preinitForbidden(void) +{ + CONFIG_VARIABLE_SETTABLE(forbiddenUrl, CONFIG_ATOM, configAtomSetter, + "URL to which forbidden requests " + "should be redirected."); + CONFIG_VARIABLE_SETTABLE(forbiddenRedirectCode, CONFIG_INT, + configIntSetter, + "Redirect code, 301 or 302."); + CONFIG_VARIABLE_SETTABLE(forbiddenFile, CONFIG_ATOM, atomSetterForbidden, + "File specifying forbidden URLs."); +#ifndef NO_REDIRECTOR + CONFIG_VARIABLE_SETTABLE(redirector, CONFIG_ATOM, atomSetterForbidden, + "Squid-style redirector."); + CONFIG_VARIABLE_SETTABLE(redirectorRedirectCode, CONFIG_INT, + configIntSetter, + "Redirect code to use with redirector."); +#endif + CONFIG_VARIABLE_SETTABLE(uncachableFile, CONFIG_ATOM, atomSetterForbidden, + "File specifying uncachable URLs."); + + CONFIG_VARIABLE_SETTABLE(forbiddenTunnelsFile, CONFIG_ATOM, atomSetterForbidden, + "File specifying forbidden tunnels."); +} + +static int +atomSetterForbidden(ConfigVariablePtr var, void *value) +{ + initForbidden(); + return configAtomSetter(var, value); +} + +int +readDomainFile(char *filename) +{ + FILE *in; + char buf[512]; + char *rs; + int i, j, is_regex, start; + + in = fopen(filename, "r"); + if(in == NULL) { + if(errno != ENOENT) + do_log_error(L_ERROR, errno, "Couldn't open file %s", filename); + return -1; + } + + while(1) { + rs = fgets(buf, 512, in); + if(rs == NULL) + break; + for(i = 0; i < 512; i++) { + if(buf[i] != ' ' && buf[i] != '\t') + break; + } + start = i; + for(i = start; i < 512; i++) { + if(buf[i] == '#' || buf[i] == '\r' || buf[i] == '\n') + break; + } + while(i > start) { + if(buf[i - 1] != ' ' && buf[i - 1] != '\t') + break; + i--; + } + + if(i <= start) + continue; + + /* The significant part of the line is now between start and i */ + + is_regex = 0; + for(j = start; j < i; j++) { + if(buf[j] == '\\' || buf[j] == '*' || buf[j] == '/') { + is_regex = 1; + break; + } + } + + if(is_regex) { + while(rlen + i - start + 8 >= rsize) { + char *new_regexbuf; + new_regexbuf = realloc(regexbuf, rsize * 2 + 1); + if(new_regexbuf == NULL) { + do_log(L_ERROR, "Couldn't reallocate regex.\n"); + fclose(in); + return -1; + } + regexbuf = new_regexbuf; + rsize = rsize * 2 + 1; + } + if(rlen != 0) + rlen = snnprintf(regexbuf, rlen, rsize, "|"); + rlen = snnprintf(regexbuf, rlen, rsize, "("); + rlen = snnprint_n(regexbuf, rlen, rsize, buf + start, i - start); + rlen = snnprintf(regexbuf, rlen, rsize, ")"); + } else { + DomainPtr new_domain; + if(dlen >= dsize - 1) { + DomainPtr *new_domains; + new_domains = realloc(domains, (dsize * 2 + 1) * + sizeof(DomainPtr)); + if(new_domains == NULL) { + do_log(L_ERROR, + "Couldn't reallocate domain list.\n"); + fclose(in); + return -1; + } + domains = new_domains; + dsize = dsize * 2 + 1; + } + new_domain = malloc(sizeof(DomainRec) - 1 + i - start); + if(new_domain == NULL) { + do_log(L_ERROR, "Couldn't allocate domain.\n"); + fclose(in); + return -1; + } + new_domain->length = i - start; + memcpy(new_domain->domain, buf + start, i - start); + domains[dlen++] = new_domain; + } + } + fclose(in); + return 1; +} + +void +parseDomainFile(AtomPtr file, + DomainPtr **domains_return, regex_t **regex_return) +{ + struct stat ss; + int rc; + + if(*domains_return) { + DomainPtr *domain = *domains_return; + while(*domain) { + free(*domain); + domain++; + } + free(*domains_return); + *domains_return = NULL; + } + + if(*regex_return) { + regfree(*regex_return); + *regex_return = NULL; + } + + if(!file || file->length == 0) + return; + + domains = malloc(64 * sizeof(DomainPtr)); + if(domains == NULL) { + do_log(L_ERROR, "Couldn't allocate domain list.\n"); + return; + } + dlen = 0; + dsize = 64; + + regexbuf = malloc(512); + if(regexbuf == NULL) { + do_log(L_ERROR, "Couldn't allocate regex.\n"); + free(domains); + return; + } + rlen = 0; + rsize = 512; + + rc = stat(file->string, &ss); + if(rc < 0) { + if(errno != ENOENT) + do_log_error(L_WARN, errno, "Couldn't stat file %s", file->string); + } else { + if(!S_ISDIR(ss.st_mode)) + readDomainFile(file->string); + else { + char *fts_argv[2]; + FTS *fts; + FTSENT *fe; + fts_argv[0] = file->string; + fts_argv[1] = NULL; + fts = fts_open(fts_argv, FTS_LOGICAL, NULL); + if(fts) { + while(1) { + fe = fts_read(fts); + if(!fe) break; + if(fe->fts_info != FTS_D && fe->fts_info != FTS_DP && + fe->fts_info != FTS_DC && fe->fts_info != FTS_DNR) + readDomainFile(fe->fts_accpath); + } + fts_close(fts); + } else { + do_log_error(L_ERROR, errno, + "Couldn't scan directory %s", file->string); + } + } + } + + if(dlen > 0) { + domains[dlen] = NULL; + } else { + free(domains); + domains = NULL; + } + + regex_t *regex; + + if(rlen > 0) { + regex = malloc(sizeof(regex_t)); + rc = regcomp(regex, regexbuf, REG_EXTENDED | REG_NOSUB); + if(rc != 0) { + char errbuf[100]; + regerror(rc, regex, errbuf, 100); + do_log(L_ERROR, "Couldn't compile regex: %s.\n", errbuf); + free(regex); + regex = NULL; + } + } else { + regex = NULL; + } + free(regexbuf); + + *domains_return = domains; + *regex_return = regex; + + return; +} + +void +initForbidden(void) +{ + redirectorKill(); + + if(forbiddenFile) + forbiddenFile = expandTilde(forbiddenFile); + + if(forbiddenFile == NULL) { + forbiddenFile = expandTilde(internAtom("~/.polipo-forbidden")); + if(forbiddenFile) { + if(access(forbiddenFile->string, F_OK) < 0) { + releaseAtom(forbiddenFile); + forbiddenFile = NULL; + } + } + } + + if(forbiddenFile == NULL) { + if(access("/etc/polipo/forbidden", F_OK) >= 0) + forbiddenFile = internAtom("/etc/polipo/forbidden"); + } + + parseDomainFile(forbiddenFile, &forbiddenDomains, &forbiddenRegex); + + + if(uncachableFile) + uncachableFile = expandTilde(uncachableFile); + + if(uncachableFile == NULL) { + uncachableFile = expandTilde(internAtom("~/.polipo-uncachable")); + if(uncachableFile) { + if(access(uncachableFile->string, F_OK) < 0) { + releaseAtom(uncachableFile); + uncachableFile = NULL; + } + } + } + + if(uncachableFile == NULL) { + if(access("/etc/polipo/uncachable", F_OK) >= 0) + uncachableFile = internAtom("/etc/polipo/uncachable"); + } + + parseDomainFile(uncachableFile, &uncachableDomains, &uncachableRegex); + + if(forbiddenTunnelsFile) + forbiddenTunnelsFile = expandTilde(forbiddenTunnelsFile); + + if(forbiddenTunnelsFile == NULL) { + forbiddenTunnelsFile = expandTilde(internAtom("~/.polipo-forbiddenTunnels")); + if(forbiddenTunnelsFile) { + if(access(forbiddenTunnelsFile->string, F_OK) < 0) { + releaseAtom(forbiddenTunnelsFile); + forbiddenTunnelsFile = NULL; + } + } + } + + if(forbiddenTunnelsFile == NULL) { + if(access("/etc/polipo/forbiddenTunnels", F_OK) >= 0) + forbiddenTunnelsFile = internAtom("/etc/polipo/forbiddenTunnels"); + } + + parseDomainFile(forbiddenTunnelsFile, &forbiddenTunnelsDomains, &forbiddenTunnelsRegex); + // + + return; +} + +int +tunnelIsMatched(char *url, int lurl, char *hostname, int lhost) +{ + DomainPtr *domain, *domains; + + domains=forbiddenTunnelsDomains; + if (domains) { + domain = domains; + while(*domain) { + if (lhost == (*domain)->length && + memcmp(hostname, (*domain)->domain, lhost)==0) + return 1; + domain++; + } + } + + if(forbiddenTunnelsRegex) { + if(!regexec(forbiddenTunnelsRegex, url, 0, NULL, 0)) + return 1; + } + return 0; +} + +int +urlIsMatched(char *url, int length, DomainPtr *domains, regex_t *regex) +{ + /* This requires url to be NUL-terminated. */ + assert(url[length] == '\0'); + + if(length < 8) + return 0; + + if(lwrcmp(url, "http://", 7) != 0) + return 0; + + if(domains) { + int i; + DomainPtr *domain; + for(i = 8; i < length; i++) { + if(url[i] == '/') + break; + } + domain = domains; + while(*domain) { + if((*domain)->length <= (i - 7) && + (url[i - (*domain)->length - 1] == '.' || + url[i - (*domain)->length - 1] == '/') && + memcmp(url + i - (*domain)->length, + (*domain)->domain, + (*domain)->length) == 0) + return 1; + domain++; + } + } + + if(regex) + return !regexec(regex, url, 0, NULL, 0); + + return 0; +} + +int +urlIsUncachable(char *url, int length) +{ + return urlIsMatched(url, length, uncachableDomains, uncachableRegex); +} + +int +urlForbidden(AtomPtr url, + int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*), + void *closure) +{ + int forbidden = urlIsMatched(url->string, url->length, + forbiddenDomains, forbiddenRegex); + int code = 0; + AtomPtr message = NULL, headers = NULL; + + + if(forbidden) { + message = internAtomF("Forbidden URL %s", url->string); + if(forbiddenUrl) { + code = forbiddenRedirectCode; + headers = internAtomF("\r\nLocation: %s", forbiddenUrl->string); + } else { + code = 403; + } + } + +#ifndef NO_REDIRECTOR + if(code == 0 && redirector) { + RedirectRequestPtr request; + request = malloc(sizeof(RedirectRequestRec)); + if(request == NULL) { + do_log(L_ERROR, "Couldn't allocate redirect request.\n"); + goto done; + } + request->url = url; + request->handler = handler; + request->data = closure; + if(redirector_request_first == NULL) + redirector_request_first = request; + else + redirector_request_last->next = request; + redirector_request_last = request; + request->next = NULL; + if(request == redirector_request_first) + redirectorTrigger(); + return 1; + } + +#endif + + done: + handler(code, url, message, headers, closure); + return 1; +} + +#ifndef NO_REDIRECTOR +static void +logExitStatus(int status) +{ + if(WIFEXITED(status) && WEXITSTATUS(status) == 142) + /* See child code in runRedirector */ + do_log(L_ERROR, "Couldn't start redirector.\n"); + else { + char *reason = + WIFEXITED(status) ? "with status" : + WIFSIGNALED(status) ? "on signal" : + "with unknown status"; + int value = + WIFEXITED(status) ? WEXITSTATUS(status) : + WIFSIGNALED(status) ? WTERMSIG(status) : + status; + do_log(L_ERROR, + "Redirector exited %s %d.\n", reason, value); + } +} + +void +redirectorKill(void) +{ + int rc, status, dead; + + if(redirector_read_fd >= 0) { + rc = waitpid(redirector_pid, &status, WNOHANG); + dead = (rc > 0); + close(redirector_read_fd); + redirector_read_fd = -1; + close(redirector_write_fd); + redirector_write_fd = -1; + if(!dead) { + rc = kill(redirector_pid, SIGTERM); + if(rc < 0 && errno != ESRCH) { + do_log_error(L_ERROR, errno, "Couldn't kill redirector"); + redirector_pid = -1; + return; + } + do { + rc = waitpid(redirector_pid, &status, 0); + } while(rc < 0 && errno == EINTR); + if(rc < 0) + do_log_error(L_ERROR, errno, + "Couldn't wait for redirector's death"); + } else + logExitStatus(status); + redirector_pid = -1; + } +} + +static void +redirectorDestroyRequest(RedirectRequestPtr request) +{ + assert(redirector_request_first == request); + redirector_request_first = request->next; + if(redirector_request_first == NULL) + redirector_request_last = NULL; + free(request); +} + +void +redirectorTrigger(void) +{ + RedirectRequestPtr request = redirector_request_first; + int rc; + + if(!request) + return; + + if(redirector_read_fd < 0) { + rc = runRedirector(&redirector_pid, + &redirector_read_fd, &redirector_write_fd); + if(rc < 0) { + request->handler(rc, request->url, NULL, NULL, request->data); + redirectorDestroyRequest(request); + return; + } + } + do_stream_2(IO_WRITE, redirector_write_fd, 0, + request->url->string, request->url->length, + "\n", 1, + redirectorStreamHandler1, request); +} + +int +redirectorStreamHandler1(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + RedirectRequestPtr request = (RedirectRequestPtr)srequest->data; + + if(status) { + if(status >= 0) + status = -EPIPE; + do_log_error(L_ERROR, -status, "Write to redirector failed"); + goto fail; + } + + if(!streamRequestDone(srequest)) + return 0; + + do_stream(IO_READ, redirector_read_fd, 0, + redirector_buffer, REDIRECTOR_BUFFER_SIZE, + redirectorStreamHandler2, request); + return 1; + + fail: + request->handler(status < 0 ? status : -EPIPE, + request->url, NULL, NULL, request->data); + redirectorDestroyRequest(request); + redirectorKill(); + return 1; +} + +int +redirectorStreamHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + RedirectRequestPtr request = (RedirectRequestPtr)srequest->data; + char *c; + AtomPtr message; + AtomPtr headers; + int code; + + if(status < 0) { + do_log_error(L_ERROR, -status, "Read from redirector failed"); + request->handler(status, request->url, NULL, NULL, request->data); + goto kill; + } + c = memchr(redirector_buffer, '\n', srequest->offset); + if(!c) { + if(!status && srequest->offset < REDIRECTOR_BUFFER_SIZE) + return 0; + do_log(L_ERROR, "Redirector returned incomplete reply.\n"); + request->handler(-EREDIRECTOR, request->url, NULL, NULL, request->data); + goto kill; + } + *c = '\0'; + + if(srequest->offset > c + 1 - redirector_buffer) + do_log(L_WARN, "Stray bytes in redirector output.\n"); + + if(c > redirector_buffer + 1 && + (c - redirector_buffer != request->url->length || + memcmp(redirector_buffer, request->url->string, + request->url->length) != 0)) { + code = redirectorRedirectCode; + message = internAtom("Redirected by external redirector"); + if(message == NULL) { + request->handler(-ENOMEM, request->url, NULL, NULL, request->data); + goto kill; + } + + headers = internAtomF("\r\nLocation: %s", redirector_buffer); + if(headers == NULL) { + releaseAtom(message); + request->handler(-ENOMEM, request->url, NULL, NULL, request->data); + goto kill; + } + } else { + code = 0; + message = NULL; + headers = NULL; + } + request->handler(code, request->url, + message, headers, request->data); + goto cont; + + cont: + redirectorDestroyRequest(request); + redirectorTrigger(); + return 1; + + kill: + redirectorKill(); + goto cont; +} + +int +runRedirector(pid_t *pid_return, int *read_fd_return, int *write_fd_return) +{ + int rc, rc2, status; + pid_t pid; + int filedes1[2], filedes2[2]; + sigset_t ss, old_mask; + + assert(redirector); + + if(redirector_buffer == NULL) { + redirector_buffer = malloc(REDIRECTOR_BUFFER_SIZE); + if(redirector_buffer == NULL) + return -errno; + } + + rc = pipe(filedes1); + if(rc < 0) { + rc = -errno; + goto fail1; + } + + + rc = pipe(filedes2); + if(rc < 0) { + rc = -errno; + goto fail2; + } + + fflush(stdout); + fflush(stderr); + flushLog(); + + interestingSignals(&ss); + do { + rc = sigprocmask(SIG_BLOCK, &ss, &old_mask); + } while (rc < 0 && errno == EINTR); + if(rc < 0) { + rc = -errno; + goto fail3; + } + + pid = fork(); + if(pid < 0) { + rc = -errno; + goto fail4; + } + + if(pid > 0) { + do { + rc = sigprocmask(SIG_SETMASK, &old_mask, NULL); + } while(rc < 0 && errno == EINTR); + + if(rc < 0) { + rc = -errno; + goto fail4; + } + + rc = setNonblocking(filedes1[1], 1); + if(rc >= 0) + rc = setNonblocking(filedes2[0], 1); + if(rc < 0) { + rc = -errno; + goto fail4; + } + + /* This is completely unnecesary -- if the redirector cannot be + started, redirectorStreamHandler1 will get EPIPE straight away --, + but it improves error messages somewhat. */ + rc = waitpid(pid, &status, WNOHANG); + if(rc > 0) { + logExitStatus(status); + rc = -EREDIRECTOR; + goto fail4; + } else if(rc < 0) { + rc = -errno; + goto fail4; + } + + *read_fd_return = filedes2[0]; + *write_fd_return = filedes1[1]; + + *pid_return = pid; + /* This comes at the end so that the fail* labels can work */ + close(filedes1[0]); + close(filedes2[1]); + } else { + close(filedes1[1]); + close(filedes2[0]); + uninitEvents(); + do { + rc = sigprocmask(SIG_SETMASK, &old_mask, NULL); + } while (rc < 0 && errno == EINTR); + if(rc < 0) + exit(142); + + if(filedes1[0] != 0) + dup2(filedes1[0], 0); + if(filedes2[1] != 1) + dup2(filedes2[1], 1); + + execlp(redirector->string, redirector->string, NULL); + exit(142); + /* NOTREACHED */ + } + return 1; + + fail4: + do { + rc2 = sigprocmask(SIG_SETMASK, &old_mask, NULL); + } while(rc2 < 0 && errno == EINTR); + fail3: + close(filedes2[0]); + close(filedes2[1]); + fail2: + close(filedes1[0]); + close(filedes1[1]); + fail1: + free(redirector_buffer); + redirector_buffer = NULL; + return rc; +} + +#else + +void +redirectorKill(void) +{ + return; +} + +#endif + +#else + +void +preinitForbidden() +{ + return; +} + +void +initForbidden() +{ + return; +} + +int +tunnelIsMatched(char *url, int lurl, char *hostname, int lhost) +{ + return 0; +} + +int +urlIsUncachable(char *url, int length) +{ + return 0; +} + +int +urlForbidden(AtomPtr url, + int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*), + void *closure) +{ + handler(0, url, NULL, NULL, closure); + return 1; +} + +#endif diff --git a/jni/polipo/forbidden.h b/jni/polipo/forbidden.h new file mode 100644 index 00000000..fefc9454 --- /dev/null +++ b/jni/polipo/forbidden.h @@ -0,0 +1,50 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +extern AtomPtr forbiddenUrl; +extern int forbiddenRedirectCode; + +typedef struct _RedirectRequest { + AtomPtr url; + struct _RedirectRequest *next; + int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*); + void *data; +} RedirectRequestRec, *RedirectRequestPtr; + +void preinitForbidden(void); +void initForbidden(void); +int urlIsUncachable(char *url, int length); +int urlForbidden(AtomPtr url, + int (*handler)(int, AtomPtr, AtomPtr, AtomPtr, void*), + void *closure); +void redirectorKill(void); +int redirectorStreamHandler1(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest); +int redirectorStreamHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest); +void redirectorTrigger(void); +int +runRedirector(pid_t *pid_return, int *read_fd_return, int *write_fd_return); + +int tunnelIsMatched(char *url, int lurl, char *hostname, int lhost); diff --git a/jni/polipo/fts_compat.c b/jni/polipo/fts_compat.c new file mode 100644 index 00000000..16a1b6d1 --- /dev/null +++ b/jni/polipo/fts_compat.c @@ -0,0 +1,365 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* This file implements just enough of the fts family of functions + to make Polipo happy. */ + +#include +#include +#include +#ifndef _WIN32 +#include +#include +#else +#include "dirent_compat.h" +#endif +#include +#include +#include + +#include "fts_compat.h" + +static char * +getcwd_a() +{ + char buf[256]; + char *ret; + ret = getcwd(buf, 256); + if(ret == NULL) + return NULL; + return strdup(buf); +} + +static char * +mkfilename(const char *path, char *filename) +{ + int n = strlen(path); + char *buf = malloc(n + 1 + strlen(filename) + 1); + if(buf == NULL) + return NULL; + memcpy(buf, path, n); + if(buf[n - 1] != '/') + buf[n++] = '/'; + strcpy(buf + n, filename); + return buf; +} + +static int +split(const char *path, int *slash_return, int *dlen, int *blen) +{ + int len; int slash; + len = strlen(path); + while(len > 0 && path[len - 1] == '/') + len--; + if(len == 0) + return -1; + slash = len - 1; + while(slash >= 0 && path[slash] != '/') + slash--; + + if(slash_return) *slash_return = slash; + if(dlen) *dlen = slash + 1; + if(blen) *blen = len - slash - 1; + return 1; +} + +static char * +basename_a(const char *path) +{ + int blen, slash; + char *b; + int rc; + + rc = split(path, &slash, NULL, &blen); + if(rc < 0 || blen == 0) + return NULL; + + b = malloc(blen + 1); + if(b == NULL) + return NULL; + memcpy(b, path + slash + 1, blen); + b[blen] = '\0'; + return b; +} + +static char * +dirname_a(const char *path) +{ + int dlen; + int rc; + char *d; + + rc = split(path, NULL, &dlen, NULL); + if(rc < 0) + return NULL; + + d = malloc(dlen + 1); + if(d == NULL) + return NULL; + memcpy(d, path, dlen); + d[dlen] = '\0'; + return d; +} + +#if defined(__svr4__) || defined(SVR4) +static int +dirfd(DIR *dir) +{ + return dir->dd_fd; +} +#endif + +/* + * Make the directory identified by the argument the current directory. + */ +#ifdef WIN32 /*MINGW*/ +int +change_to_dir(DIR *dir) +{ + errno = ENOSYS; + return -1; +} +#else +int +change_to_dir(DIR *dir) +{ + return fchdir(dirfd(dir)); +} +#endif + +FTS* +fts_open(char * const *path_argv, int options, + int (*compar)(const FTSENT **, const FTSENT **)) +{ + FTS *fts; + DIR *dir; + char *cwd; + int rc; + + if(options != FTS_LOGICAL || compar != NULL || path_argv[1] != NULL) { + errno = ENOSYS; + return NULL; + } + + dir = opendir(path_argv[0]); + if(dir == NULL) + return NULL; + + fts = calloc(sizeof(FTS), 1); + if(fts == NULL) { + int save = errno; + closedir(dir); + errno = save; + return NULL; + } + + cwd = getcwd_a(); + if(cwd == NULL) { + int save = errno; + free(fts); + closedir(dir); + errno = save; + return NULL; + } + + rc = change_to_dir(dir); + if(rc < 0) { + int save = errno; + free(cwd); + free(fts); + closedir(dir); + errno = save; + return NULL; + } + + fts->depth = 0; + fts->dir[0] = dir; + fts->cwd0 = cwd; + fts->cwd = strdup(path_argv[0]); + return fts; +} + +int +fts_close(FTS *fts) +{ + int save = 0; + int rc; + + if(fts->ftsent.fts_path) { + free(fts->ftsent.fts_path); + fts->ftsent.fts_path = NULL; + } + + if(fts->dname) { + free(fts->dname); + fts->dname = NULL; + } + + rc = chdir(fts->cwd0); + if(rc < 0) + save = errno; + + while(fts->depth >= 0) { + closedir(fts->dir[fts->depth]); + fts->depth--; + } + + free(fts->cwd0); + if(fts->cwd) free(fts->cwd); + free(fts); + + if(rc < 0) { + errno = save; + return -1; + } + return 0; +} + +FTSENT * +fts_read(FTS *fts) +{ + struct dirent *dirent; + int rc; + char *name; + char buf[1024]; + + if(fts->ftsent.fts_path) { + free(fts->ftsent.fts_path); + fts->ftsent.fts_path = NULL; + } + + if(fts->dname) { + free(fts->dname); + fts->dname = NULL; + } + + again: + dirent = readdir(fts->dir[fts->depth]); + if(dirent == NULL) { + char *newcwd = NULL; + closedir(fts->dir[fts->depth]); + fts->dir[fts->depth] = NULL; + fts->depth--; + if(fts->depth >= 0) { + fts->dname = basename_a(fts->cwd); + if(fts->dname == NULL) + goto error; + newcwd = dirname_a(fts->cwd); + if(newcwd == NULL) + goto error; + } + if(fts->cwd) free(fts->cwd); + fts->cwd = NULL; + if(fts->depth < 0) + return NULL; + rc = change_to_dir(fts->dir[fts->depth]); + if(rc < 0) { + free(newcwd); + goto error; + } + fts->cwd = newcwd; + name = fts->dname; + fts->ftsent.fts_info = FTS_DP; + goto done; + } + + name = dirent->d_name; + + again2: + rc = stat(name, &fts->ftstat); + if(rc < 0) { + fts->ftsent.fts_info = FTS_NS; + goto error2; + } + + if(S_ISDIR(fts->ftstat.st_mode)) { + char *newcwd; + DIR *dir; + + if(strcmp(name, ".") == 0 || strcmp(name, "..") == 0) + goto again; + + if(fts->depth >= FTS_MAX_DEPTH) { + errno = ENFILE; + goto error; + } + dir = opendir(dirent->d_name); + if(dir == NULL) { + if(errno == EACCES) { + fts->ftsent.fts_info = FTS_DNR; + goto error2; + } else + goto error; + } + newcwd = mkfilename(fts->cwd, dirent->d_name); + rc = change_to_dir(dir); + if(rc < 0) { + free(newcwd); + goto error; + } + free(fts->cwd); + fts->cwd = newcwd; + fts->ftsent.fts_info = FTS_D; + fts->depth++; + fts->dir[fts->depth] = dir; + goto done; + } else if(S_ISREG(fts->ftstat.st_mode)) { + fts->ftsent.fts_info = FTS_F; + goto done; +#ifdef S_ISLNK + } else if(S_ISLNK(fts->ftstat.st_mode)) { + int rc; + rc = readlink(name, buf, 1024); + if(rc < 0) + goto error; + if(rc >= 1023) { + errno = ENAMETOOLONG; + goto error; + } + buf[rc] = '\0'; + name = buf; + if(access(buf, F_OK) >= 0) + goto again2; + fts->ftsent.fts_info = FTS_SLNONE; + goto done; +#endif + } else { + fts->ftsent.fts_info = FTS_DEFAULT; + goto done; + } + + done: + if(fts->cwd == NULL) + fts->cwd = getcwd_a(); + if(fts->cwd == NULL) goto error; + fts->ftsent.fts_path = mkfilename(fts->cwd, name); + if(fts->ftsent.fts_path == NULL) goto error; + fts->ftsent.fts_accpath = name; + fts->ftsent.fts_statp = &fts->ftstat; + return &fts->ftsent; + + error: + fts->ftsent.fts_info = FTS_ERR; + error2: + fts->ftsent.fts_errno = errno; + return &fts->ftsent; +} diff --git a/jni/polipo/fts_compat.h b/jni/polipo/fts_compat.h new file mode 100644 index 00000000..ba2e6c46 --- /dev/null +++ b/jni/polipo/fts_compat.h @@ -0,0 +1,69 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#ifndef _FTS_COMPAT_H +#define _FTS_COMPAT_H + +#ifndef FTS_MAX_DEPTH +#define FTS_MAX_DEPTH 4 +#endif + +#define FTS_LOGICAL 1 + +#define FTS_F 1 +#define FTS_D 2 +#define FTS_DP 3 +#define FTS_DC 4 +#define FTS_NS 5 +#define FTS_NSOK 6 +#define FTS_DNR 7 +#define FTS_SLNONE 8 +#define FTS_DEFAULT 9 +#define FTS_ERR 10 + +struct _FTSENT { + unsigned short fts_info; + char *fts_path; + char *fts_accpath; + struct stat *fts_statp; + int fts_errno; +}; + +typedef struct _FTSENT FTSENT; + +struct _FTS { + int depth; + DIR *dir[FTS_MAX_DEPTH]; + char *cwd0, *cwd; + struct _FTSENT ftsent; + struct stat ftstat; + char *dname; +}; + +typedef struct _FTS FTS; + +FTS* fts_open(char * const *path_argv, int options, + int (*compar)(const FTSENT **, const FTSENT **)); +int fts_close(FTS *fts); +FTSENT * fts_read(FTS *fts); + +#endif diff --git a/jni/polipo/ftsimport.c b/jni/polipo/ftsimport.c new file mode 100644 index 00000000..324c0fac --- /dev/null +++ b/jni/polipo/ftsimport.c @@ -0,0 +1,12 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include "polipo.h" + +#ifndef HAVE_FTS +#include "fts_compat.c" +#else +static int dummy ATTRIBUTE((unused)); +#endif + diff --git a/jni/polipo/ftsimport.h b/jni/polipo/ftsimport.h new file mode 100644 index 00000000..44d6433c --- /dev/null +++ b/jni/polipo/ftsimport.h @@ -0,0 +1,6 @@ +#ifdef HAVE_FTS +#include +#else +#include "fts_compat.h" +#endif + diff --git a/jni/polipo/http.c b/jni/polipo/http.c new file mode 100644 index 00000000..cc207f8b --- /dev/null +++ b/jni/polipo/http.c @@ -0,0 +1,1087 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +int disableProxy = 0; +AtomPtr proxyName = NULL; +int proxyPort = 8123; + +int clientTimeout = 120; +int serverTimeout = 90; +int serverIdleTimeout = 45; + +int bigBufferSize = (32 * 1024); + +AtomPtr displayName = NULL; + +AtomPtr authRealm = NULL; +AtomPtr authCredentials = NULL; + +AtomPtr parentAuthCredentials = NULL; + +AtomListPtr allowedClients = NULL; +NetAddressPtr allowedNets = NULL; + +IntListPtr allowedPorts = NULL; +IntListPtr tunnelAllowedPorts = NULL; +int expectContinue = 1; +int dontTrustVaryETag = 1; + +AtomPtr atom100Continue; + +int disableVia = 1; + +/* 0 means that all failures lead to errors. 1 means that failures to + connect are reported in a Warning header when stale objects are + served. 2 means that only missing data is fetched from the net, + stale data is served without revalidation (browser-side + Cache-Control directives are still honoured). 3 means that no + connections are ever attempted. */ + +int proxyOffline = 0; +int relaxTransparency = 0; +AtomPtr proxyAddress = NULL; + +static int timeoutSetter(ConfigVariablePtr var, void *value); + +void +preinitHttp() +{ + proxyAddress = internAtom("127.0.0.1"); + CONFIG_VARIABLE_SETTABLE(disableProxy, CONFIG_BOOLEAN, configIntSetter, + "Whether to be a web server only."); + CONFIG_VARIABLE_SETTABLE(proxyOffline, CONFIG_BOOLEAN, configIntSetter, + "Avoid contacting remote servers."); + CONFIG_VARIABLE_SETTABLE(relaxTransparency, CONFIG_TRISTATE, + configIntSetter, + "Avoid contacting remote servers."); + CONFIG_VARIABLE(proxyPort, CONFIG_INT, + "The TCP port on which the proxy listens."); + CONFIG_VARIABLE(proxyAddress, CONFIG_ATOM_LOWER, + "The IP address on which the proxy listens."); + CONFIG_VARIABLE_SETTABLE(proxyName, CONFIG_ATOM_LOWER, configAtomSetter, + "The name by which the proxy is known."); + CONFIG_VARIABLE_SETTABLE(clientTimeout, CONFIG_TIME, + timeoutSetter, "Client-side timeout."); + CONFIG_VARIABLE_SETTABLE(serverTimeout, CONFIG_TIME, + timeoutSetter, "Server-side timeout."); + CONFIG_VARIABLE_SETTABLE(serverIdleTimeout, CONFIG_TIME, + timeoutSetter, "Server-side idle timeout."); + CONFIG_VARIABLE(authRealm, CONFIG_ATOM, + "Authentication realm."); + CONFIG_VARIABLE(displayName, CONFIG_ATOM, + "Server name displayed on error pages."); + CONFIG_VARIABLE(authCredentials, CONFIG_PASSWORD, + "username:password."); + CONFIG_VARIABLE(parentAuthCredentials, CONFIG_PASSWORD, + "username:password."); + CONFIG_VARIABLE(allowedClients, CONFIG_ATOM_LIST_LOWER, + "Networks from which clients are allowed to connect."); + CONFIG_VARIABLE(tunnelAllowedPorts, CONFIG_INT_LIST, + "Ports to which tunnelled connections are allowed."); + CONFIG_VARIABLE(allowedPorts, CONFIG_INT_LIST, + "Ports to which connections are allowed."); + CONFIG_VARIABLE(expectContinue, CONFIG_TRISTATE, + "Send Expect-Continue to servers."); + CONFIG_VARIABLE(bigBufferSize, CONFIG_INT, + "Size of big buffers (max size of headers)."); + CONFIG_VARIABLE_SETTABLE(disableVia, CONFIG_BOOLEAN, configIntSetter, + "Don't use Via headers."); + CONFIG_VARIABLE(dontTrustVaryETag, CONFIG_TRISTATE, + "Whether to trust the ETag when there's Vary."); + preinitHttpParser(); +} + +static int +timeoutSetter(ConfigVariablePtr var, void *value) +{ + configIntSetter(var, value); + if(clientTimeout <= serverTimeout) + clientTimeout = serverTimeout + 1; + return 1; +} + +void +initHttp() +{ + char *buf = NULL; + int namelen; + int n; + struct hostent *host; + + initHttpParser(); + + atom100Continue = internAtom("100-continue"); + + if(clientTimeout <= serverTimeout) { + clientTimeout = serverTimeout + 1; + do_log(L_WARN, "Value of clientTimeout too small -- setting to %d.\n", + clientTimeout); + } + + if(displayName == NULL) + displayName = internAtom("Polipo"); + + if(authCredentials != NULL && authRealm == NULL) + authRealm = internAtom("Polipo"); + + if(allowedClients) { + allowedNets = parseNetAddress(allowedClients); + if(allowedNets == NULL) + exit(1); + } + + if(allowedPorts == NULL) { + allowedPorts = makeIntList(0); + if(allowedPorts == NULL) { + do_log(L_ERROR, "Couldn't allocate allowedPorts.\n"); + exit(1); + } + intListCons(80, 100, allowedPorts); + intListCons(1024, 0xFFFF, allowedPorts); + } + + if(tunnelAllowedPorts == NULL) { + tunnelAllowedPorts = makeIntList(0); + if(tunnelAllowedPorts == NULL) { + do_log(L_ERROR, "Couldn't allocate tunnelAllowedPorts.\n"); + exit(1); + } + intListCons(22, 22, tunnelAllowedPorts); /* ssh */ + intListCons(80, 80, tunnelAllowedPorts); /* HTTP */ + intListCons(109, 110, tunnelAllowedPorts); /* POP 2 and 3*/ + intListCons(143, 143, tunnelAllowedPorts); /* IMAP 2/4 */ + intListCons(443, 443, tunnelAllowedPorts); /* HTTP/SSL */ + intListCons(873, 873, tunnelAllowedPorts); /* rsync */ + intListCons(993, 993, tunnelAllowedPorts); /* IMAP/SSL */ + intListCons(995, 995, tunnelAllowedPorts); /* POP/SSL */ + intListCons(2401, 2401, tunnelAllowedPorts); /* CVS */ + intListCons(5222, 5223, tunnelAllowedPorts); /* Jabber */ + intListCons(9418, 9418, tunnelAllowedPorts); /* Git */ + } + + if(proxyName) + return; + + buf = get_chunk(); + if(buf == NULL) { + do_log(L_ERROR, "Couldn't allocate chunk for host name.\n"); + goto fail; + } + + n = gethostname(buf, CHUNK_SIZE); + if(n != 0) { + do_log_error(L_WARN, errno, "Gethostname"); + strcpy(buf, "polipo"); + goto success; + } + /* gethostname doesn't necessarily NUL-terminate on overflow */ + buf[CHUNK_SIZE - 1] = '\0'; + + if(strcmp(buf, "(none)") == 0 || + strcmp(buf, "localhost") == 0 || + strcmp(buf, "localhost.localdomain") == 0) { + do_log(L_WARN, "Couldn't determine host name -- using ``polipo''.\n"); + strcpy(buf, "polipo"); + goto success; + } + + if(strchr(buf, '.') != NULL) + goto success; + + host = gethostbyname(buf); + if(host == NULL) { + goto success; + } + + if(host->h_addrtype != AF_INET) + goto success; + + host = gethostbyaddr(host->h_addr_list[0], host->h_length, AF_INET); + + if(!host || !host->h_name || strcmp(host->h_name, "localhost") == 0 || + strcmp(host->h_name, "localhost.localdomain") == 0) + goto success; + + namelen = strlen(host->h_name); + if(namelen >= CHUNK_SIZE) { + do_log(L_ERROR, "Host name too long.\n"); + goto success; + } + + memcpy(buf, host->h_name, namelen + 1); + + success: + proxyName = internAtom(buf); + if(proxyName == NULL) { + do_log(L_ERROR, "Couldn't allocate proxy name.\n"); + goto fail; + } + dispose_chunk(buf); + return; + + fail: + if(buf) + dispose_chunk(buf); + exit(1); + return; +} + +int +httpSetTimeout(HTTPConnectionPtr connection, int secs) +{ + TimeEventHandlerPtr new; + + if(connection->timeout) + cancelTimeEvent(connection->timeout); + connection->timeout = NULL; + + if(secs > 0) { + new = scheduleTimeEvent(secs, httpTimeoutHandler, + sizeof(connection), &connection); + if(!new) { + do_log(L_ERROR, "Couldn't schedule timeout for connection 0x%lx\n", + (unsigned long)connection); + return -1; + } + } else { + new = NULL; + } + + connection->timeout = new; + return 1; +} + +int +httpTimeoutHandler(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + + if(connection->fd >= 0) { + int rc; + rc = shutdown(connection->fd, 2); + if(rc < 0 && errno != ENOTCONN) + do_log_error(L_ERROR, errno, "Timeout: shutdown failed"); + pokeFdEvent(connection->fd, -EDOTIMEOUT, POLLIN | POLLOUT); + } + connection->timeout = NULL; + return 1; +} + +int +httpWriteObjectHeaders(char *buf, int offset, int len, + ObjectPtr object, int from, int to) +{ + int n = offset; + + if(from <= 0 && to < 0) { + if(object->length >= 0) { + n = snnprintf(buf, n, len, + "\r\nContent-Length: %d", object->length); + } + } else { + if(to >= 0) { + n = snnprintf(buf, n, len, + "\r\nContent-Length: %d", to - from); + } + } + + if(from > 0 || to > 0) { + if(object->length >= 0) { + if(from >= to) { + n = snnprintf(buf, n, len, + "\r\nContent-Range: bytes */%d", + object->length); + } else { + n = snnprintf(buf, n, len, + "\r\nContent-Range: bytes %d-%d/%d", + from, to - 1, + object->length); + } + } else { + if(to >= 0) { + n = snnprintf(buf, n, len, + "\r\nContent-Range: bytes %d-/*", + from); + } else { + n = snnprintf(buf, n, len, + "\r\nContent-Range: bytes %d-%d/*", + from, to); + } + } + } + + if(object->etag) { + n = snnprintf(buf, n, len, "\r\nETag: \"%s\"", object->etag); + } + if((object->flags & OBJECT_LOCAL) || object->date >= 0) { + n = snnprintf(buf, n, len, "\r\nDate: "); + n = format_time(buf, n, len, + (object->flags & OBJECT_LOCAL) ? + current_time.tv_sec : object->date); + if(n < 0) + goto fail; + } + + if(object->last_modified >= 0) { + n = snnprintf(buf, n, len, "\r\nLast-Modified: "); + n = format_time(buf, n, len, object->last_modified); + if(n < 0) + goto fail; + } + + if(object->expires >= 0) { + n = snnprintf(buf, n, len, "\r\nExpires: "); + n = format_time(buf, n, len, object->expires); + if(n < 0) + goto fail; + } + + n = httpPrintCacheControl(buf, n, len, + object->cache_control, NULL); + if(n < 0) + goto fail; + + if(!disableVia && object->via) + n = snnprintf(buf, n, len, "\r\nVia: %s", object->via->string); + + if(object->headers) + n = snnprint_n(buf, n, len, object->headers->string, + object->headers->length); + + if(n < len) + return n; + else + return -1; + + fail: + return -1; +} + +static int +cachePrintSeparator(char *buf, int offset, int len, + int subsequent) +{ + int n = offset; + if(subsequent) + n = snnprintf(buf, offset, len, ", "); + else + n = snnprintf(buf, offset, len, "\r\nCache-Control: "); + return n; +} + +int +httpPrintCacheControl(char *buf, int offset, int len, + int flags, CacheControlPtr cache_control) +{ + int n = offset; + int sub = 0; + +#define PRINT_SEP() \ + do {\ + n = cachePrintSeparator(buf, n, len, sub); \ + sub = 1; \ + } while(0) + + if(cache_control) + flags |= cache_control->flags; + + if(flags & CACHE_NO) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "no-cache"); + } + if(flags & CACHE_PUBLIC) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "public"); + } + if(flags & CACHE_PRIVATE) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "private"); + } + if(flags & CACHE_NO_STORE) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "no-store"); + } + if(flags & CACHE_NO_TRANSFORM) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "no-transform"); + } + if(flags & CACHE_MUST_REVALIDATE) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "must-revalidate"); + } + if(flags & CACHE_PROXY_REVALIDATE) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "proxy-revalidate"); + } + if(flags & CACHE_ONLY_IF_CACHED) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "only-if-cached"); + } + if(cache_control) { + if(cache_control->max_age >= 0) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "max-age=%d", + cache_control->max_age); + } + if(cache_control->s_maxage >= 0) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "s-maxage=%d", + cache_control->s_maxage); + } + if(cache_control->min_fresh > 0) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "min-fresh=%d", + cache_control->min_fresh); + } + if(cache_control->max_stale > 0) { + PRINT_SEP(); + n = snnprintf(buf, n, len, "max-stale=%d", + cache_control->min_fresh); + } + } + return n; +#undef PRINT_SEP +} + +char * +httpMessage(int code) +{ + switch(code) { + case 200: + return "Okay"; + case 206: + return "Partial content"; + case 300: + return "Multiple choices"; + case 301: + return "Moved permanently"; + case 302: + return "Found"; + case 303: + return "See other"; + case 304: + return "Not changed"; + case 307: + return "Temporary redirect"; + case 401: + return "Authentication Required"; + case 403: + return "Forbidden"; + case 404: + return "Not found"; + case 405: + return "Method not allowed"; + case 407: + return "Proxy authentication required"; + default: + return "Unknown error code"; + } +} + +int +htmlString(char *buf, int n, int len, char *s, int slen) +{ + int i = 0; + while(i < slen && n + 5 < len) { + switch(s[i]) { + case '&': + buf[n++] = '&'; buf[n++] = 'a'; buf[n++] = 'm'; buf[n++] = 'p'; + buf[n++] = ';'; + break; + case '<': + buf[n++] = '&'; buf[n++] = 'l'; buf[n++] = 't'; buf[n++] = ';'; + break; + case '>': + buf[n++] = '&'; buf[n++] = 'g'; buf[n++] = 't'; buf[n++] = ';'; + break; + case '"': + buf[n++] = '&'; buf[n++] = 'q'; buf[n++] = 'u'; buf[n++] = 'o'; + buf[n++] = 't'; buf[n++] = ';'; + break; + case '\0': + break; + default: + buf[n++] = s[i]; + } + i++; + } + return n; +} + +void +htmlPrint(FILE *out, char *s, int slen) +{ + int i; + for(i = 0; i < slen; i++) { + switch(s[i]) { + case '&': + fputs("&", out); + break; + case '<': + fputs("<", out); + break; + case '>': + fputs(">", out); + break; + default: + fputc(s[i], out); + } + } +} + +HTTPConnectionPtr +httpMakeConnection() +{ + HTTPConnectionPtr connection; + connection = malloc(sizeof(HTTPConnectionRec)); + if(connection == NULL) + return NULL; + connection->flags = 0; + connection->fd = -1; + connection->buf = NULL; + connection->len = 0; + connection->offset = 0; + connection->request = NULL; + connection->request_last = NULL; + connection->serviced = 0; + connection->version = HTTP_UNKNOWN; + connection->time = current_time.tv_sec; + connection->timeout = NULL; + connection->te = TE_IDENTITY; + connection->reqbuf = NULL; + connection->reqlen = 0; + connection->reqbegin = 0; + connection->reqoffset = 0; + connection->bodylen = -1; + connection->reqte = TE_IDENTITY; + connection->chunk_remaining = 0; + connection->server = NULL; + connection->pipelined = 0; + connection->connecting = 0; + connection->server = NULL; + return connection; +} + +void +httpDestroyConnection(HTTPConnectionPtr connection) +{ + assert(connection->flags == 0); + httpConnectionDestroyBuf(connection); + assert(!connection->request); + assert(!connection->request_last); + httpConnectionDestroyReqbuf(connection); + assert(!connection->timeout); + assert(!connection->server); + free(connection); +} + +void +httpConnectionDestroyBuf(HTTPConnectionPtr connection) +{ + if(connection->buf) { + if(connection->flags & CONN_BIGBUF) + free(connection->buf); + else + dispose_chunk(connection->buf); + } + connection->flags &= ~CONN_BIGBUF; + connection->buf = NULL; +} + +void +httpConnectionDestroyReqbuf(HTTPConnectionPtr connection) +{ + if(connection->reqbuf) { + if(connection->flags & CONN_BIGREQBUF) + free(connection->reqbuf); + else + dispose_chunk(connection->reqbuf); + } + connection->flags &= ~CONN_BIGREQBUF; + connection->reqbuf = NULL; +} + +HTTPRequestPtr +httpMakeRequest() +{ + HTTPRequestPtr request; + request = malloc(sizeof(HTTPRequestRec)); + if(request == NULL) + return NULL; + request->flags = 0; + request->connection = NULL; + request->object = NULL; + request->method = METHOD_UNKNOWN; + request->from = 0; + request->to = -1; + request->cache_control = no_cache_control; + request->condition = NULL; + request->via = NULL; + request->chandler = NULL; + request->can_mutate = NULL; + request->error_code = 0; + request->error_message = NULL; + request->error_headers = NULL; + request->headers = NULL; + request->time0 = null_time; + request->time1 = null_time; + request->request = NULL; + request->next = NULL; + return request; +} + +void +httpDestroyRequest(HTTPRequestPtr request) +{ + if(request->object) + releaseObject(request->object); + if(request->condition) + httpDestroyCondition(request->condition); + releaseAtom(request->via); + assert(request->chandler == NULL); + releaseAtom(request->error_message); + releaseAtom(request->headers); + releaseAtom(request->error_headers); + assert(request->request == NULL); + assert(request->next == NULL); + free(request); +} + +void +httpQueueRequest(HTTPConnectionPtr connection, HTTPRequestPtr request) +{ + assert(request->next == NULL && request->connection == NULL); + request->connection = connection; + if(connection->request_last) { + assert(connection->request); + connection->request_last->next = request; + connection->request_last = request; + } else { + assert(!connection->request_last); + connection->request = request; + connection->request_last = request; + } +} + +HTTPRequestPtr +httpDequeueRequest(HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + if(request) { + assert(connection->request_last); + connection->request = request->next; + if(!connection->request) connection->request_last = NULL; + request->next = NULL; + } + return request; +} + +int +httpConnectionBigify(HTTPConnectionPtr connection) +{ + char *bigbuf; + assert(!(connection->flags & CONN_BIGBUF)); + + if(bigBufferSize <= CHUNK_SIZE) + return 0; + + bigbuf = malloc(bigBufferSize); + if(bigbuf == NULL) + return -1; + if(connection->len > 0) + memcpy(bigbuf, connection->buf, connection->len); + if(connection->buf) + dispose_chunk(connection->buf); + connection->buf = bigbuf; + connection->flags |= CONN_BIGBUF; + return 1; +} + +int +httpConnectionBigifyReqbuf(HTTPConnectionPtr connection) +{ + char *bigbuf; + assert(!(connection->flags & CONN_BIGREQBUF)); + + if(bigBufferSize <= CHUNK_SIZE) + return 0; + + bigbuf = malloc(bigBufferSize); + if(bigbuf == NULL) + return -1; + if(connection->reqlen > 0) + memcpy(bigbuf, connection->reqbuf, connection->reqlen); + if(connection->reqbuf) + dispose_chunk(connection->reqbuf); + connection->reqbuf = bigbuf; + connection->flags |= CONN_BIGREQBUF; + return 1; +} + +int +httpConnectionUnbigify(HTTPConnectionPtr connection) +{ + char *buf; + assert(connection->flags & CONN_BIGBUF); + assert(connection->len < CHUNK_SIZE); + + buf = get_chunk(); + if(buf == NULL) + return -1; + if(connection->len > 0) + memcpy(buf, connection->buf, connection->len); + free(connection->buf); + connection->buf = buf; + connection->flags &= ~CONN_BIGBUF; + return 1; +} + +int +httpConnectionUnbigifyReqbuf(HTTPConnectionPtr connection) +{ + char *buf; + assert(connection->flags & CONN_BIGREQBUF); + assert(connection->reqlen < CHUNK_SIZE); + + buf = get_chunk(); + if(buf == NULL) + return -1; + if(connection->reqlen > 0) + memcpy(buf, connection->reqbuf, connection->reqlen); + free(connection->reqbuf); + connection->reqbuf = buf; + connection->flags &= ~CONN_BIGREQBUF; + return 1; +} + +HTTPConditionPtr +httpMakeCondition() +{ + HTTPConditionPtr condition; + condition = malloc(sizeof(HTTPConditionRec)); + if(condition == NULL) + return NULL; + condition->ims = -1; + condition->inms = -1; + condition->im = NULL; + condition->inm = NULL; + condition->ifrange = NULL; + return condition; +} + +void +httpDestroyCondition(HTTPConditionPtr condition) +{ + if(condition->inm) + free(condition->inm); + if(condition->im) + free(condition->im); + if(condition->ifrange) + free(condition->ifrange); + free(condition); +} + +int +httpCondition(ObjectPtr object, HTTPConditionPtr condition) +{ + int rc = CONDITION_MATCH; + + assert(!(object->flags & OBJECT_INITIAL)); + + if(!condition) return CONDITION_MATCH; + + if(condition->ims >= 0) { + if(object->last_modified < 0 || + condition->ims < object->last_modified) + return rc; + else + rc = CONDITION_NOT_MODIFIED; + } + + if(condition->inms >= 0) { + if(object->last_modified < 0 || + condition->inms >= object->last_modified) + return rc; + else + rc = CONDITION_FAILED; + } + + if(condition->inm) { + if(!object->etag || strcmp(object->etag, condition->inm) != 0) + return rc; + else + rc = CONDITION_NOT_MODIFIED; + } + + if(condition->im) { + if(!object->etag || strcmp(object->etag, condition->im) != 0) + rc = CONDITION_FAILED; + else + return rc; + } + + return rc; +} + +int +httpWriteErrorHeaders(char *buf, int size, int offset, int do_body, + int code, AtomPtr message, int close, AtomPtr headers, + char *url, int url_len, char *etag) +{ + int n, m, i; + char *body; + char htmlMessage[100]; + char timeStr[100]; + + assert(code != 0); + + i = htmlString(htmlMessage, 0, 100, message->string, message->length); + if(i < 0) + strcpy(htmlMessage, "(Couldn't format message)"); + else + htmlMessage[MIN(i, 99)] = '\0'; + + if(code != 304) { + body = get_chunk(); + if(!body) { + do_log(L_ERROR, "Couldn't allocate body buffer.\n"); + return -1; + } + m = snnprintf(body, 0, CHUNK_SIZE, + "" + "\n" + "\nProxy %s: %3d %s." + "\n" + "\n

%3d %s

" + "\n

The following %s", + code >= 400 ? "error" : "result", + code, htmlMessage, + code, htmlMessage, + code >= 400 ? + "error occurred" : + "status was returned"); + if(url_len > 0) { + m = snnprintf(body, m, CHUNK_SIZE, + " while trying to access "); + m = htmlString(body, m, CHUNK_SIZE, url, url_len); + m = snnprintf(body, m, CHUNK_SIZE, ""); + } + + { + /* On BSD systems, tv_sec is a long. */ + const time_t ct = current_time.tv_sec; + /*Mon, 24 Sep 2004 17:46:35 GMT*/ + strftime(timeStr, sizeof(timeStr), "%a, %d %b %Y %H:%M:%S %Z", + localtime(&ct)); + } + + m = snnprintf(body, m, CHUNK_SIZE, + ":

" + "\n%3d %s

" + "\n
Generated %s by %s on %s:%d." + "\n\r\n", + code, htmlMessage, + timeStr, displayName->string, proxyName->string, proxyPort); + if(m <= 0 || m >= CHUNK_SIZE) { + do_log(L_ERROR, "Couldn't write error body.\n"); + dispose_chunk(body); + return -1; + } + } else { + body = NULL; + m = 0; + } + + n = snnprintf(buf, 0, size, + "HTTP/1.1 %3d %s" + "\r\nConnection: %s" + "\r\nDate: ", + code, atomString(message), + close ? "close" : "keep-alive"); + n = format_time(buf, n, size, current_time.tv_sec); + if(code != 304) { + n = snnprintf(buf, n, size, + "\r\nContent-Type: text/html" + "\r\nContent-Length: %d", m); + } else { + if(etag) + n = snnprintf(buf, n, size, "\r\nETag: \"%s\"", etag); + } + + if(code != 304 && code != 412) { + n = snnprintf(buf, n, size, + "\r\nExpires: 0" + "\r\nCache-Control: no-cache" + "\r\nPragma: no-cache"); + } + + if(headers) + n = snnprint_n(buf, n, size, + headers->string, headers->length); + + n = snnprintf(buf, n, size, "\r\n\r\n"); + + if(n < 0 || n >= size) { + do_log(L_ERROR, "Couldn't write error.\n"); + dispose_chunk(body); + return -1; + } + + if(code != 304 && do_body) { + if(m > 0) memcpy(buf + n, body, m); + n += m; + } + + if(body) + dispose_chunk(body); + + return n; +} + +AtomListPtr +urlDecode(char *buf, int n) +{ + char mybuf[500]; + int i, j = 0; + AtomListPtr list; + AtomPtr atom; + + list = makeAtomList(NULL, 0); + if(list == NULL) + return NULL; + + i = 0; + while(i < n) { + if(buf[i] == '%') { + int a, b; + if(i + 3 > n) + goto fail; + a = h2i(buf[i + 1]); + b = h2i(buf[i + 2]); + if(a < 0 || b < 0) + goto fail; + mybuf[j++] = (char)((a << 4) | b); + i += 3; + if(j >= 500) goto fail; + } else if(buf[i] == '&') { + atom = internAtomN(mybuf, j); + if(atom == NULL) + goto fail; + atomListCons(atom, list); + j = 0; + i++; + } else { + mybuf[j++] = buf[i++]; + if(j >= 500) goto fail; + } + } + + atom = internAtomN(mybuf, j); + if(atom == NULL) + goto fail; + atomListCons(atom, list); + return list; + + fail: + destroyAtomList(list); + return NULL; +} + +void +httpTweakCachability(ObjectPtr object) +{ + int code = object->code; + + if((object->cache_control & CACHE_AUTHORIZATION) && + !(object->cache_control & CACHE_PUBLIC)) { + object->cache_control |= CACHE_NO_HIDDEN; + object->flags |= OBJECT_LINEAR; + } + + /* This is not required by RFC 2616 -- but see RFC 3143 2.1.1. We + manically avoid caching replies that we don't know how to + handle, even if Expires or Cache-Control says otherwise. As to + known uncacheable replies, we obey Cache-Control and default to + allowing sharing but not caching. */ + if(code != 200 && code != 206 && + code != 300 && code != 301 && code != 302 && code != 303 && + code != 304 && code != 307 && + code != 403 && code != 404 && code != 405 && code != 416) { + object->cache_control |= (CACHE_NO_HIDDEN | CACHE_MISMATCH); + object->flags |= OBJECT_LINEAR; + } else if(code != 200 && code != 206 && + code != 300 && code != 301 && code != 304 && + code != 410) { + if(object->expires < 0 && !(object->cache_control & CACHE_PUBLIC)) { + object->cache_control |= CACHE_NO_HIDDEN; + } + } else if(dontCacheRedirects && (code == 301 || code == 302)) { + object->cache_control |= CACHE_NO_HIDDEN; + } + + if(urlIsUncachable(object->key, object->key_size)) { + object->cache_control |= CACHE_NO_HIDDEN; + } + + if((object->cache_control & CACHE_NO_STORE) != 0) { + object->cache_control |= CACHE_NO_HIDDEN; + } + + if(object->cache_control & CACHE_VARY) { + if(!object->etag || dontTrustVaryETag >= 2) { + object->cache_control |= CACHE_MISMATCH; + } + } +} + +int +httpHeaderMatch(AtomPtr header, AtomPtr headers1, AtomPtr headers2) +{ + int rc1, b1, e1, rc2, b2, e2; + + /* Short cut if both sets of headers are identical */ + if(headers1 == headers2) + return 1; + + rc1 = httpFindHeader(header, headers1->string, headers1->length, + &b1, &e1); + rc2 = httpFindHeader(header, headers2->string, headers2->length, + &b2, &e2); + + if(rc1 == 0 && rc2 == 0) + return 1; + + if(rc1 == 0 || rc2 == 0) + return 0; + + if(e1 - b1 != e2 - b2) + return 0; + + if(memcmp(headers1->string + b1, headers2->string + b2, e1 - b1) != 0) + return 0; + + return 1; +} diff --git a/jni/polipo/http.h b/jni/polipo/http.h new file mode 100644 index 00000000..d5628160 --- /dev/null +++ b/jni/polipo/http.h @@ -0,0 +1,175 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +typedef struct _HTTPCondition { + time_t ims; + time_t inms; + char *im; + char *inm; + char *ifrange; +} HTTPConditionRec, *HTTPConditionPtr; + +typedef struct _HTTPRequest { + int flags; + struct _HTTPConnection *connection; + ObjectPtr object; + int method; + int from; + int to; + CacheControlRec cache_control; + HTTPConditionPtr condition; + AtomPtr via; + struct _ConditionHandler *chandler; + ObjectPtr can_mutate; + int error_code; + struct _Atom *error_message; + struct _Atom *error_headers; + AtomPtr headers; + struct timeval time0, time1; + struct _HTTPRequest *request; + struct _HTTPRequest *next; +} HTTPRequestRec, *HTTPRequestPtr; + +/* request->flags */ +#define REQUEST_PERSISTENT 1 +#define REQUEST_REQUESTED 2 +#define REQUEST_WAIT_CONTINUE 4 +#define REQUEST_FORCE_ERROR 8 +#define REQUEST_PIPELINED 16 + +typedef struct _HTTPConnection { + int flags; + int fd; + char *buf; + int len; + int offset; + HTTPRequestPtr request; + HTTPRequestPtr request_last; + int serviced; + int version; + int time; + TimeEventHandlerPtr timeout; + int te; + char *reqbuf; + int reqlen; + int reqbegin; + int reqoffset; + int bodylen; + int reqte; + /* For server connections */ + int chunk_remaining; + struct _HTTPServer *server; + int pipelined; + int connecting; +} HTTPConnectionRec, *HTTPConnectionPtr; + +/* connection->flags */ +#define CONN_READER 1 +#define CONN_WRITER 2 +#define CONN_SIDE_READER 4 +#define CONN_BIGBUF 8 +#define CONN_BIGREQBUF 16 + +/* request->method */ +#define METHOD_UNKNOWN -1 +#define METHOD_NONE -1 +#define METHOD_GET 0 +#define METHOD_HEAD 1 +#define METHOD_CONDITIONAL_GET 2 +#define METHOD_CONNECT 3 +#define METHOD_POST 4 +#define METHOD_PUT 5 + +#define REQUEST_SIDE(request) ((request)->method >= METHOD_POST) + +/* server->version */ +#define HTTP_10 0 +#define HTTP_11 1 +#define HTTP_UNKNOWN -1 + +/* connection->te */ +#define TE_IDENTITY 0 +#define TE_CHUNKED 1 +#define TE_UNKNOWN -1 + +/* connection->connecting */ +#define CONNECTING_DNS 1 +#define CONNECTING_CONNECT 2 +#define CONNECTING_SOCKS 3 + +/* the results of a conditional request. 200, 304 and 412. */ +#define CONDITION_MATCH 0 +#define CONDITION_NOT_MODIFIED 1 +#define CONDITION_FAILED 2 + +extern int disableProxy; +extern AtomPtr proxyName; +extern int proxyPort; +extern int clientTimeout, serverTimeout, serverIdleTimeout; +extern int bigBufferSize; +extern AtomPtr proxyAddress; +extern int proxyOffline; +extern int relaxTransparency; +extern AtomPtr authRealm; +extern AtomPtr authCredentials; +extern AtomPtr parentAuthCredentials; +extern AtomListPtr allowedClients; +extern NetAddressPtr allowedNets; +extern IntListPtr allowedPorts; +extern IntListPtr tunnelAllowedPorts; +extern int expectContinue; +extern AtomPtr atom100Continue; +extern int disableVia; +extern int dontTrustVaryETag; + +void preinitHttp(void); +void initHttp(void); + +int httpTimeoutHandler(TimeEventHandlerPtr); +int httpSetTimeout(HTTPConnectionPtr connection, int secs); +int httpWriteObjectHeaders(char *buf, int offset, int len, + ObjectPtr object, int from, int to); +int httpPrintCacheControl(char*, int, int, int, CacheControlPtr); +char *httpMessage(int) ATTRIBUTE((pure)); +int htmlString(char *buf, int n, int len, char *s, int slen); +void htmlPrint(FILE *out, char *s, int slen); +HTTPConnectionPtr httpMakeConnection(void); +void httpDestroyConnection(HTTPConnectionPtr connection); +void httpConnectionDestroyBuf(HTTPConnectionPtr connection); +void httpConnectionDestroyReqbuf(HTTPConnectionPtr connection); +HTTPRequestPtr httpMakeRequest(void); +void httpDestroyRequest(HTTPRequestPtr request); +void httpQueueRequest(HTTPConnectionPtr, HTTPRequestPtr); +HTTPRequestPtr httpDequeueRequest(HTTPConnectionPtr connection); +int httpConnectionBigify(HTTPConnectionPtr); +int httpConnectionBigifyReqbuf(HTTPConnectionPtr); +int httpConnectionUnbigify(HTTPConnectionPtr); +int httpConnectionUnbigifyReqbuf(HTTPConnectionPtr); +HTTPConditionPtr httpMakeCondition(void); +void httpDestroyCondition(HTTPConditionPtr condition); +int httpCondition(ObjectPtr, HTTPConditionPtr); +int httpWriteErrorHeaders(char *buf, int size, int offset, int do_body, + int code, AtomPtr message, int close, AtomPtr, + char *url, int url_len, char *etag); +AtomListPtr urlDecode(char*, int); +void httpTweakCachability(ObjectPtr); +int httpHeaderMatch(AtomPtr header, AtomPtr headers1, AtomPtr headers2); diff --git a/jni/polipo/http_parse.c b/jni/polipo/http_parse.c new file mode 100644 index 00000000..a5940ff3 --- /dev/null +++ b/jni/polipo/http_parse.c @@ -0,0 +1,1548 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +static int getNextWord(const char *buf, int i, int *x_return, int *y_return); +static int getNextToken(const char *buf, int i, int *x_return, int *y_return); +static int getNextTokenInList(const char *buf, int i, + int *x_return, int *y_return, + int *z_return, int *t_return, + int *end_return); + +static AtomPtr atomConnection, atomProxyConnection, atomContentLength, + atomHost, atomAcceptRange, atomTE, + atomReferer, atomProxyAuthenticate, atomProxyAuthorization, + atomKeepAlive, atomTrailer, atomUpgrade, atomDate, atomExpires, + atomIfModifiedSince, atomIfUnmodifiedSince, atomIfRange, atomLastModified, + atomIfMatch, atomIfNoneMatch, atomAge, atomTransferEncoding, + atomETag, atomCacheControl, atomPragma, atomContentRange, atomRange, + atomVia, atomVary, atomExpect, atomAuthorization, + atomSetCookie, atomCookie, atomCookie2, + atomXPolipoDate, atomXPolipoAccess, atomXPolipoLocation, + atomXPolipoBodyOffset; + +AtomPtr atomContentType, atomContentEncoding; + +int censorReferer = 0; +int laxHttpParser = 1; + +static AtomListPtr censoredHeaders; + +void +preinitHttpParser() +{ + CONFIG_VARIABLE_SETTABLE(censorReferer, CONFIG_TRISTATE, configIntSetter, + "Censor referer headers."); + censoredHeaders = makeAtomList(NULL, 0); + if(censoredHeaders == NULL) { + do_log(L_ERROR, "Couldn't allocate censored atoms.\n"); + exit(1); + } + CONFIG_VARIABLE(censoredHeaders, CONFIG_ATOM_LIST_LOWER, + "Headers to censor."); + CONFIG_VARIABLE_SETTABLE(laxHttpParser, CONFIG_BOOLEAN, configIntSetter, + "Ignore unknown HTTP headers."); +} + +void +initHttpParser() +{ +#define A(name, value) name = internAtom(value); if(!name) goto fail; + /* These must be in lower-case */ + A(atomConnection, "connection"); + A(atomProxyConnection, "proxy-connection"); + A(atomContentLength, "content-length"); + A(atomHost, "host"); + A(atomAcceptRange, "accept-range"); + A(atomTE, "te"); + A(atomReferer, "referer"); + A(atomProxyAuthenticate, "proxy-authenticate"); + A(atomProxyAuthorization, "proxy-authorization"); + A(atomKeepAlive, "keep-alive"); + A(atomTrailer, "trailer"); + A(atomUpgrade, "upgrade"); + A(atomDate, "date"); + A(atomExpires, "expires"); + A(atomIfModifiedSince, "if-modified-since"); + A(atomIfUnmodifiedSince, "if-unmodified-since"); + A(atomIfRange, "if-range"); + A(atomLastModified, "last-modified"); + A(atomIfMatch, "if-match"); + A(atomIfNoneMatch, "if-none-match"); + A(atomAge, "age"); + A(atomTransferEncoding, "transfer-encoding"); + A(atomETag, "etag"); + A(atomCacheControl, "cache-control"); + A(atomPragma, "pragma"); + A(atomContentRange, "content-range"); + A(atomRange, "range"); + A(atomVia, "via"); + A(atomContentType, "content-type"); + A(atomContentEncoding, "content-encoding"); + A(atomVary, "vary"); + A(atomExpect, "expect"); + A(atomAuthorization, "authorization"); + A(atomSetCookie, "set-cookie"); + A(atomCookie, "cookie"); + A(atomCookie2, "cookie2"); + A(atomXPolipoDate, "x-polipo-date"); + A(atomXPolipoAccess, "x-polipo-access"); + A(atomXPolipoLocation, "x-polipo-location"); + A(atomXPolipoBodyOffset, "x-polipo-body-offset"); +#undef A + return; + + fail: + do_log(L_ERROR, "Couldn't allocate atom.\n"); + exit(1); +} + +static int +getNextWord(const char *restrict buf, int i, int *x_return, int *y_return) +{ + int x, y; + while(buf[i] == ' ') i++; + if(buf[i] == '\n' || buf[i] == '\r') return -1; + x = i; + while(buf[i] > 32 && buf[i] < 127) i++; + y = i; + + *x_return = x; + *y_return = y; + + return 0; +} + +static int +skipComment(const char *restrict buf, int i) +{ + assert(buf[i] == '('); + + i++; + while(1) { + if(buf[i] == '\\' && buf[i + 1] == ')') i+=2; + else if(buf[i] == ')') return i + 1; + else if(buf[i] == '\n') { + if(buf[i + 1] == ' ' || buf[i + 1] == '\t') + i += 2; + else + return -1; + } else if(buf[i] == '\r') { + if(buf[i + 1] != '\n') return -1; + if(buf[i + 2] == ' ' || buf[i + 2] == '\t') + i += 3; + else + return -1; + } else { + i++; + } + } + return i; +} + + +static int +skipWhitespace(const char *restrict buf, int i) +{ + while(1) { + if(buf[i] == ' ' || buf[i] == '\t') + i++; + else if(buf[i] == '(') { + i = skipComment(buf, i); + if(i < 0) return -1; + } else if(buf[i] == '\n') { + if(buf[i + 1] == ' ' || buf[i + 1] == '\t') + i += 2; + else + return i; + } else if(buf[i] == '\r' && buf[i + 1] == '\n') { + if(buf[i + 2] == ' ' || buf[i + 2] == '\t') + i += 3; + else + return i; + } else + return i; + } +} + +static int +getNextToken(const char *restrict buf, int i, int *x_return, int *y_return) +{ + int x, y; + again: + while(buf[i] == ' ' || buf[i] == '\t') + i++; + if(buf[i] == '(') { + i++; + while(buf[i] != ')') { + if(buf[i] == '\n' || buf[i] == '\r') + return -1; + if(buf[i] == '\\' && buf[i + 1] != '\n' && buf[i + 1] != '\r') + buf += 2; + else + buf++; + } + goto again; + } + if(buf[i] == '\n') { + if(buf[i + 1] == ' ' || buf[i + 1] == '\t') { + i += 2; + goto again; + } else { + return -1; + } + } + if(buf[i] == '\r') { + if(buf[i + 1] == '\n' && (buf[i + 2] == ' ' || buf[i + 2] == '\t')) { + i += 3; + goto again; + } else { + return -1; + } + } + x = i; + while(buf[i] > 32 && buf[i] < 127) { + switch(buf[i]) { + case '(': case ')': case '<': case '>': case '@': + case ',': case ';': case ':': case '\\': case '/': + case '[': case ']': case '?': case '=': + case '{': case '}': case ' ': case '\t': + goto out; + default: + i++; + } + } + out: + y = i; + + *x_return = x; + *y_return = y; + + return y; +} + +static int +getNextETag(const char * restrict buf, int i, + int *x_return, int *y_return, int *weak_return) +{ + int weak = 0; + int x, y; + while(buf[i] == ' ' || buf[i] == '\t') + i++; + if(buf[i] == 'W' && buf[i + 1] == '/') { + weak = 1; + i += 2; + } + if(buf[i] == '"') + i++; + else + return -1; + + x = i; + while(buf[i] != '"') { + if(buf[i] == '\r' || buf[i] == '\n') + return -1; + i++; + } + y = i; + i++; + + *x_return = x; + *y_return = y; + *weak_return = weak; + return i; +} + +static int +getNextTokenInList(const char *restrict buf, int i, + int *x_return, int *y_return, + int *z_return, int *t_return, + int *end_return) +{ + int j, x, y, z = -1, t = -1, end; + j = getNextToken(buf, i, &x, &y); + if(j < 0) + return -1; + while(buf[j] == ' ' || buf[j] == '\t') + j++; + + if(buf[j] == '=') { + j++; + while(buf[j] == ' ' || buf[j] == '\t') + j++; + z = j; + while(buf[j] != ',' && buf[j] != '\n' && buf[j] != '\r') + j++; + } + + if(buf[j] == '\n' || buf[j] == '\r') { + if(buf[j] == '\r') { + if(buf[j + 1] != '\n') + return -1; + j += 2; + } else + j++; + end = 1; + if(buf[j] == ' ' || buf[j] == '\t') { + while(buf[j] == ' ' || buf[j] == '\t') + j++; + end = 0; + } + } else if(buf[j] == ',') { + j++; + while(buf[j] == ' ' || buf[j] == '\t') + j++; + end = 0; + } else { + return -1; + } + + *x_return = x; + *y_return = y; + if(z_return) + *z_return = z; + if(t_return) + *t_return = t; + *end_return = end; + return j; +} + +static inline int +token_compare(const char *buf, int start, int end, const char *s) +{ + return (strcasecmp_n(s, buf + start, end - start) == 0); +} + +static int +skipEol(const char *restrict buf, int i) +{ + while(buf[i] == ' ') + i++; + if(buf[i] == '\n') + return i + 1; + else if(buf[i] == '\r') { + if(buf[i + 1] == '\n') + return i + 2; + else + return -1; + } else { + return -1; + } +} + +static int +skipToEol(const char *restrict buf, int i, int *start_return) +{ + while(buf[i] != '\n' && buf[i] != '\r') + i++; + if(buf[i] == '\n') { + *start_return = i; + return i + 1; + } else if(buf[i] == '\r') { + if(buf[i + 1] == '\n') { + *start_return = i; + return i + 2; + } else { + return -1; + } + } + return -1; +} + +static int +getHeaderValue(const char *restrict buf, int start, + int *value_start_return, int *value_end_return) +{ + int i, j, k; + + while(buf[start] == ' ' || buf[start] == '\t') + start++; + i = start; + again: + j = skipToEol(buf, i, &k); + if(j < 0) + return -1; + if(buf[j] == ' ' || buf[j] == '\t') { + i = j + 1; + goto again; + } + *value_start_return = start; + *value_end_return = k; + return j; +} + +int +httpParseClientFirstLine(const char *restrict buf, int offset, + int *method_return, + AtomPtr *url_return, + int *version_return) +{ + int i = 0; + int x, y; + int method; + AtomPtr url; + int version = HTTP_UNKNOWN; + int eol; + + i = offset; + i = getNextWord(buf, i, &x, &y); + if(i < 0) return -1; + if(y == x + 3 && memcmp(buf + x, "GET", 3) == 0) + method = METHOD_GET; + else if(y == x + 4 && memcmp(buf + x, "HEAD", 4) == 0) + method = METHOD_HEAD; + else if(y == x + 4 && memcmp(buf + x, "POST", 4) == 0) + method = METHOD_POST; + else if(y == x + 3 && memcmp(buf + x, "PUT", 3) == 0) + method = METHOD_PUT; + else if(y == x + 7 && memcmp(buf + x, "CONNECT", 7) == 0) + method = METHOD_CONNECT; + else + method = METHOD_UNKNOWN; + + i = getNextWord(buf, y + 1, &x, &y); + if(i < 0) return -1; + + url = internAtomN(buf + x, y - x); + + i = getNextWord(buf, y + 1, &x, &y); + if(i < 0) { + releaseAtom(url); + return -1; + } + + if(y == x + 8) { + if(memcmp(buf + x, "HTTP/1.", 7) != 0) + version = HTTP_UNKNOWN; + else if(buf[x + 7] == '0') + version = HTTP_10; + else if(buf[x + 7] >= '1' && buf[x + 7] <= '9') + version = HTTP_11; + else + version = HTTP_UNKNOWN; + } + + eol = skipEol(buf, y); + if(eol < 0) return -1; + + *method_return = method; + if(url_return) + *url_return = url; + else + releaseAtom(url); + *version_return = version; + return eol; +} + +int +httpParseServerFirstLine(const char *restrict buf, + int *status_return, + int *version_return, + AtomPtr *message_return) +{ + int i = 0; + int x, y, eol; + int status; + int version = HTTP_UNKNOWN; + + i = getNextWord(buf, 0, &x, &y); + if(i < 0) + return -1; + if(y == x + 8 && memcmp(buf + x, "HTTP/1.0", 8) == 0) + version = HTTP_10; + else if(y >= x + 8 && memcmp(buf + x, "HTTP/1.", 7) == 0) + version = HTTP_11; + else + version = HTTP_UNKNOWN; + + i = getNextWord(buf, y + 1, &x, &y); + if(i < 0) return -1; + if(y == x + 3) + status = atol(buf + x); + else return -1; + + i = skipToEol(buf, y, &eol); + if(i < 0) return -1; + + *status_return = status; + *version_return = version; + if(message_return) { + /* Netscape enterprise bug */ + if(eol > y) + *message_return = internAtomN(buf + y + 1, eol - y - 1); + else + *message_return = internAtom("No message"); + } + return i; +} + +static int +parseInt(const char *restrict buf, int start, int *val_return) +{ + int i = start, val = 0; + if(!digit(buf[i])) + return -1; + while(digit(buf[i])) { + val = val * 10 + (buf[i] - '0'); + i++; + } + *val_return = val; + return i; +} + +/* Returned *name_start_return is -1 at end of headers, -2 if the line + couldn't be parsed. */ +static int +parseHeaderLine(const char *restrict buf, int start, + int *name_start_return, int *name_end_return, + int *value_start_return, int *value_end_return) +{ + int i; + int name_start, name_end, value_start, value_end; + + if(buf[start] == '\n') { + *name_start_return = -1; + return start + 1; + } + if(buf[start] == '\r' && buf[start + 1] == '\n') { + *name_start_return = -1; + return start + 2; + } + + i = getNextToken(buf, start, &name_start, &name_end); + if(i < 0 || buf[i] != ':') + goto syntax; + i++; + while(buf[i] == ' ' || buf[i] == '\t') + i++; + + i = getHeaderValue(buf, i, &value_start, &value_end); + if(i < 0) + goto syntax; + + *name_start_return = name_start; + *name_end_return = name_end; + *value_start_return = value_start; + *value_end_return = value_end; + return i; + + syntax: + i = start; + while(1) { + if(buf[i] == '\n') { + i++; + break; + } + if(buf[i] == '\r' && buf[i + 1] == '\n') { + i += 2; + break; + } + i++; + } + *name_start_return = -2; + return i; +} + +int +findEndOfHeaders(const char *restrict buf, int from, int to, int *body_return) +{ + int i = from; + int eol = 0; + while(i < to) { + if(buf[i] == '\n') { + if(eol) { + *body_return = i + 1; + return eol; + } + eol = i; + i++; + } else if(buf[i] == '\r') { + if(i < to - 1 && buf[i + 1] == '\n') { + if(eol) { + *body_return = eol; + return i + 2; + } + eol = i; + i += 2; + } else { + eol = 0; + i++; + } + } else { + eol = 0; + i++; + } + } + return -1; +} + +static int +parseContentRange(const char *restrict buf, int i, + int *from_return, int *to_return, int *full_len_return) +{ + int j; + int from, to, full_len; + + i = skipWhitespace(buf, i); + if(i < 0) return -1; + if(!token_compare(buf, i, i + 5, "bytes")) { + do_log(L_WARN, "Incorrect Content-Range header -- chugging along.\n"); + } else { + i += 5; + } + i = skipWhitespace(buf, i); + if(buf[i] == '*') { + from = 0; + to = -1; + i++; + } else { + i = parseInt(buf, i, &from); + if(i < 0) return -1; + if(buf[i] != '-') return -1; + i++; + i = parseInt(buf, i, &to); + if(i < 0) return -1; + to = to + 1; + } + if(buf[i] != '/') + return -1; + i++; + if(buf[i] == '*') + full_len = -1; + else { + i = parseInt(buf, i, &full_len); + if(i < 0) return -1; + } + j = skipEol(buf, i); + if(j < 0) + return -1; + + *from_return = from; + *to_return = to; + *full_len_return = full_len; + return i; +} + +static int +parseRange(const char *restrict buf, int i, + int *from_return, int *to_return) +{ + int j; + int from, to; + + i = skipWhitespace(buf, i); + if(i < 0) + return -1; + if(!token_compare(buf, i, i + 6, "bytes=")) + return -1; + i += 6; + i = skipWhitespace(buf, i); + if(buf[i] == '-') { + from = 0; + } else { + i = parseInt(buf, i, &from); + if(i < 0) return -1; + } + if(buf[i] != '-') + return -1; + i++; + j = parseInt(buf, i, &to); + if(j < 0) + to = -1; + else { + to = to + 1; + i = j; + } + j = skipEol(buf, i); + if(j < 0) return -1; + *from_return = from; + *to_return = to; + return i; +} + +static void +parseCacheControl(const char *restrict buf, + int token_start, int token_end, + int v_start, int v_end, int *age_return) +{ + if(v_start <= 0 || !digit(buf[v_start])) { + do_log(L_WARN, "Couldn't parse Cache-Control: "); + do_log_n(L_WARN, buf + token_start, + (v_end >= 0 ? v_end : token_end) - + token_start); + do_log(L_WARN, "\n"); + } else + *age_return = atoi(buf + v_start); +} + +static int +urlSameHost(const char *url1, int len1, const char *url2, int len2) +{ + int i; + if(len1 < 7 || len2 < 7) + return 0; + if(memcmp(url1 + 4, "://", 3) != 0 || memcmp(url2 + 4, "://", 3) != 0) + return 0; + + i = 7; + while(i < len1 && i < len2 && url1[i] != '/' && url2[i] != '/') { + if((url1[i] | 0x20) != (url2[i] | 0x20)) + break; + i++; + } + + if((i == len1 || url1[i] == '/') && ((i == len2 || url2[i] == '/'))) + return 1; + return 0; +} + +static char * +resize_hbuf(char *hbuf, int *size, char *hbuf_small) +{ + int new_size = 2 * *size; + char *new_hbuf; + + if(new_size <= *size) + goto fail; + + if(hbuf == hbuf_small) { + new_hbuf = malloc(new_size); + if(new_hbuf == NULL) goto fail; + memcpy(new_hbuf, hbuf, *size); + } else { + new_hbuf = realloc(hbuf, new_size); + if(new_hbuf == NULL) goto fail; + } + *size = new_size; + return new_hbuf; + + fail: + if(hbuf != hbuf_small) + free(hbuf); + *size = 0; + return NULL; +} + +int +httpParseHeaders(int client, AtomPtr url, + const char *buf, int start, HTTPRequestPtr request, + AtomPtr *headers_return, + int *len_return, CacheControlPtr cache_control_return, + HTTPConditionPtr *condition_return, int *te_return, + time_t *date_return, time_t *last_modified_return, + time_t *expires_return, time_t *polipo_age_return, + time_t *polipo_access_return, int *polipo_body_offset_return, + int *age_return, char **etag_return, AtomPtr *expect_return, + HTTPRangePtr range_return, HTTPRangePtr content_range_return, + char **location_return, AtomPtr *via_return, + AtomPtr *auth_return) +{ + int local = url ? urlIsLocal(url->string, url->length) : 0; + char hbuf_small[512]; + char *hbuf = hbuf_small; + int hbuf_size = 512, hbuf_length = 0; + int i, j, + name_start, name_end, value_start, value_end, + token_start, token_end, end; + AtomPtr name = NULL; + time_t date = -1, last_modified = -1, expires = -1, polipo_age = -1, + polipo_access = -1, polipo_body_offset = -1; + int len = -1; + CacheControlRec cache_control; + char *endptr; + int te = TE_IDENTITY; + int age = -1; + char *etag = NULL, *ifrange = NULL; + int persistent = (!request || (request->connection->version != HTTP_10)); + char *location = NULL; + AtomPtr via = NULL; + AtomPtr auth = NULL; + AtomPtr expect = NULL; + HTTPConditionPtr condition; + time_t ims = -1, inms = -1; + char *im = NULL, *inm = NULL; + AtomListPtr hopToHop = NULL; + HTTPRangeRec range = {-1, -1, -1}, content_range = {-1, -1, -1}; + int haveCacheControl = 0; + +#define RESIZE_HBUF() \ + do { \ + hbuf = resize_hbuf(hbuf, &hbuf_size, hbuf_small); \ + if(hbuf == NULL) \ + goto fail; \ + } while(0) + + cache_control.flags = 0; + cache_control.max_age = -1; + cache_control.s_maxage = -1; + cache_control.min_fresh = -1; + cache_control.max_stale = -1; + + i = start; + + while(1) { + i = parseHeaderLine(buf, i, + &name_start, &name_end, &value_start, &value_end); + if(i < 0) { + do_log(L_ERROR, "Couldn't find end of header line.\n"); + goto fail; + } + + if(name_start == -1) + break; + + if(name_start < 0) + continue; + + name = internAtomLowerN(buf + name_start, name_end - name_start); + + if(name == atomConnection) { + j = getNextTokenInList(buf, value_start, + &token_start, &token_end, NULL, NULL, + &end); + while(1) { + if(j < 0) { + do_log(L_ERROR, "Couldn't parse Connection: "); + do_log_n(L_ERROR, buf + value_start, + value_end - value_start); + do_log(L_ERROR, ".\n"); + goto fail; + } + if(token_compare(buf, token_start, token_end, "close")) { + persistent = 0; + } else if(token_compare(buf, token_start, token_end, + "keep-alive")) { + persistent = 1; + } else { + if(hopToHop == NULL) + hopToHop = makeAtomList(NULL, 0); + if(hopToHop == NULL) { + do_log(L_ERROR, "Couldn't allocate atom list.\n"); + goto fail; + } + atomListCons(internAtomLowerN(buf + token_start, + token_end - token_start), + hopToHop); + } + if(end) + break; + j = getNextTokenInList(buf, j, + &token_start, &token_end, NULL, NULL, + &end); + } + } else if(name == atomCacheControl) + haveCacheControl = 1; + + releaseAtom(name); + name = NULL; + } + + i = start; + + while(1) { + i = parseHeaderLine(buf, i, + &name_start, &name_end, &value_start, &value_end); + if(i < 0) { + do_log(L_ERROR, "Couldn't find end of header line.\n"); + goto fail; + } + + if(name_start == -1) + break; + + if(name_start < 0) { + do_log(L_WARN, "Couldn't parse header line.\n"); + if(laxHttpParser) + continue; + else + goto fail; + } + + name = internAtomLowerN(buf + name_start, name_end - name_start); + + if(name == atomProxyConnection) { + j = getNextTokenInList(buf, value_start, + &token_start, &token_end, NULL, NULL, + &end); + while(1) { + if(j < 0) { + do_log(L_WARN, "Couldn't parse Proxy-Connection:"); + do_log_n(L_WARN, buf + value_start, + value_end - value_start); + do_log(L_WARN, ".\n"); + persistent = 0; + break; + } + if(token_compare(buf, token_start, token_end, "close")) { + persistent = 0; + } else if(token_compare(buf, token_start, token_end, + "keep-alive")) { + persistent = 1; + } + if(end) + break; + j = getNextTokenInList(buf, j, + &token_start, &token_end, NULL, NULL, + &end); + } + } else if(name == atomContentLength) { + j = skipWhitespace(buf, value_start); + if(j < 0) { + do_log(L_WARN, "Couldn't parse Content-Length: \n"); + do_log_n(L_WARN, buf + value_start, value_end - value_start); + do_log(L_WARN, ".\n"); + len = -1; + } else { + errno = 0; + len = strtol(buf + value_start, &endptr, 10); + if(errno == ERANGE || endptr <= buf + value_start) { + do_log(L_WARN, "Couldn't parse Content-Length: \n"); + do_log_n(L_WARN, buf + value_start, + value_end - value_start); + do_log(L_WARN, ".\n"); + len = -1; + } + } + } else if((!local && name == atomProxyAuthorization) || + (local && name == atomAuthorization)) { + if(auth_return) { + auth = internAtomN(buf + value_start, value_end - value_start); + if(auth == NULL) { + do_log(L_ERROR, "Couldn't allocate authorization.\n"); + goto fail; + } + } + } else if(name == atomReferer) { + int h; + if(censorReferer == 0 || + (censorReferer == 1 && url != NULL && + urlSameHost(url->string, url->length, + buf + value_start, value_end - value_start))) { + while(hbuf_length > hbuf_size - 2) + RESIZE_HBUF(); + hbuf[hbuf_length++] = '\r'; + hbuf[hbuf_length++] = '\n'; + do { + h = snnprint_n(hbuf, hbuf_length, hbuf_size, + buf + name_start, value_end - name_start); + if(h < 0) RESIZE_HBUF(); + } while(h < 0); + hbuf_length = h; + } + } else if(name == atomTrailer || name == atomUpgrade) { + do_log(L_ERROR, "Trailers or upgrade present.\n"); + goto fail; + } else if(name == atomDate || name == atomExpires || + name == atomIfModifiedSince || + name == atomIfUnmodifiedSince || + name == atomLastModified || + name == atomXPolipoDate || name == atomXPolipoAccess) { + time_t t; + j = parse_time(buf, value_start, value_end, &t); + if(j < 0) { + if(name != atomExpires) { + do_log(L_WARN, "Couldn't parse %s: ", name->string); + do_log_n(L_WARN, buf + value_start, + value_end - value_start); + do_log(L_WARN, "\n"); + } + t = -1; + } + if(name == atomDate) { + if(t >= 0) + date = t; + } else if(name == atomExpires) { + if(t >= 0) + expires = t; + else + expires = 0; + } else if(name == atomLastModified) + last_modified = t; + else if(name == atomIfModifiedSince) + ims = t; + else if(name == atomIfUnmodifiedSince) + inms = t; + else if(name == atomXPolipoDate) + polipo_age = t; + else if(name == atomXPolipoAccess) + polipo_access = t; + } else if(name == atomAge) { + j = skipWhitespace(buf, value_start); + if(j < 0) { + age = -1; + } else { + errno = 0; + age = strtol(buf + value_start, &endptr, 10); + if(errno == ERANGE || endptr <= buf + value_start) + age = -1; + } + if(age < 0) { + do_log(L_WARN, "Couldn't parse age: \n"); + do_log_n(L_WARN, buf + value_start, value_end - value_start); + do_log(L_WARN, " -- ignored.\n"); + } + } else if(name == atomXPolipoBodyOffset) { + j = skipWhitespace(buf, value_start); + if(j < 0) { + do_log(L_ERROR, "Couldn't parse body offset.\n"); + goto fail; + } else { + errno = 0; + polipo_body_offset = strtol(buf + value_start, &endptr, 10); + if(errno == ERANGE || endptr <= buf + value_start) { + do_log(L_ERROR, "Couldn't parse body offset.\n"); + goto fail; + } + } + } else if(name == atomTransferEncoding) { + if(token_compare(buf, value_start, value_end, "identity")) + te = TE_IDENTITY; + else if(token_compare(buf, value_start, value_end, "chunked")) + te = TE_CHUNKED; + else + te = TE_UNKNOWN; + } else if(name == atomETag || + name == atomIfNoneMatch || name == atomIfMatch || + name == atomIfRange) { + int x, y; + int weak; + char *e; + j = getNextETag(buf, value_start, &x, &y, &weak); + if(j < 0) { + if(buf[value_start] != '\r' && buf[value_start] != '\n') + do_log(L_ERROR, "Couldn't parse ETag.\n"); + } else if(weak) { + do_log(L_WARN, "Server returned weak ETag -- ignored.\n"); + } else { + e = strdup_n(buf + x, y - x); + if(e == NULL) goto fail; + if(name == atomETag) { + if(!etag) + etag = e; + else + free(e); + } else if(name == atomIfNoneMatch) { + if(!inm) + inm = e; + else + free(e); + } else if(name == atomIfMatch) { + if(!im) + im = e; + else + free(e); + } else if(name == atomIfRange) { + if(!ifrange) + ifrange = e; + else + free(e); + } else { + abort(); + } + } + } else if(name == atomCacheControl) { + int v_start, v_end; + j = getNextTokenInList(buf, value_start, + &token_start, &token_end, + &v_start, &v_end, + &end); + while(1) { + if(j < 0) { + do_log(L_WARN, "Couldn't parse Cache-Control.\n"); + cache_control.flags |= CACHE_NO; + break; + } + if(token_compare(buf, token_start, token_end, "no-cache")) { + cache_control.flags |= CACHE_NO; + } else if(token_compare(buf, token_start, token_end, + "public")) { + cache_control.flags |= CACHE_PUBLIC; + } else if(token_compare(buf, token_start, token_end, + "private")) { + cache_control.flags |= CACHE_PRIVATE; + } else if(token_compare(buf, token_start, token_end, + "no-store")) { + cache_control.flags |= CACHE_NO_STORE; + } else if(token_compare(buf, token_start, token_end, + "no-transform")) { + cache_control.flags |= CACHE_NO_TRANSFORM; + } else if(token_compare(buf, token_start, token_end, + "must-revalidate") || + token_compare(buf, token_start, token_end, + "must-validate")) { /* losers */ + cache_control.flags |= CACHE_MUST_REVALIDATE; + } else if(token_compare(buf, token_start, token_end, + "proxy-revalidate")) { + cache_control.flags |= CACHE_PROXY_REVALIDATE; + } else if(token_compare(buf, token_start, token_end, + "only-if-cached")) { + cache_control.flags |= CACHE_ONLY_IF_CACHED; + } else if(token_compare(buf, token_start, token_end, + "max-age") || + token_compare(buf, token_start, token_end, + "maxage") || /* losers */ + token_compare(buf, token_start, token_end, + "s-maxage") || + token_compare(buf, token_start, token_end, + "min-fresh")) { + parseCacheControl(buf, token_start, token_end, + v_start, v_end, + &cache_control.max_age); + } else if(token_compare(buf, token_start, token_end, + "max-stale")) { + parseCacheControl(buf, token_start, token_end, + v_start, v_end, + &cache_control.max_stale); + } else { + do_log(L_WARN, "Unsupported Cache-Control directive "); + do_log_n(L_WARN, buf + token_start, + (v_end >= 0 ? v_end : token_end) - token_start); + do_log(L_WARN, " -- ignored.\n"); + } + if(end) + break; + j = getNextTokenInList(buf, j, + &token_start, &token_end, + &v_start, &v_end, + &end); + } + } else if(name == atomContentRange) { + if(!client) { + j = parseContentRange(buf, value_start, + &content_range.from, &content_range.to, + &content_range.full_length); + if(j < 0) { + do_log(L_ERROR, "Couldn't parse Content-Range: "); + do_log_n(L_ERROR, buf + value_start, + value_end - value_start); + do_log(L_ERROR, "\n"); + goto fail; + } + } else { + do_log(L_ERROR, "Content-Range from client.\n"); + goto fail; + } + } else if(name == atomRange) { + if(client) { + j = parseRange(buf, value_start, &range.from, &range.to); + if(j < 0) { + do_log(L_WARN, "Couldn't parse Range -- ignored.\n"); + range.from = -1; + range.to = -1; + } + } else { + do_log(L_WARN, "Range from server -- ignored\n"); + } + } else if(name == atomXPolipoLocation) { + if(location_return) { + location = + strdup_n(buf + value_start, value_end - value_start); + if(location == NULL) { + do_log(L_ERROR, "Couldn't allocate location.\n"); + goto fail; + } + } + } else if(name == atomVia) { + if(via_return) { + AtomPtr new_via, full_via; + new_via = + internAtomN(buf + value_start, value_end - value_start); + if(new_via == NULL) { + do_log(L_ERROR, "Couldn't allocate via.\n"); + goto fail; + } + if(via) { + full_via = + internAtomF("%s, %s", via->string, new_via->string); + releaseAtom(new_via); + if(full_via == NULL) { + do_log(L_ERROR, "Couldn't allocate via"); + goto fail; + } + releaseAtom(via); + via = full_via; + } else { + via = new_via; + } + } + } else if(name == atomExpect) { + if(expect_return) { + expect = internAtomLowerN(buf + value_start, + value_end - value_start); + if(expect == NULL) { + do_log(L_ERROR, "Couldn't allocate expect.\n"); + goto fail; + } + } + } else { + if(!client && name == atomContentType) { + if(token_compare(buf, value_start, value_end, + "multipart/byteranges")) { + do_log(L_ERROR, + "Server returned multipart/byteranges -- yuck!\n"); + goto fail; + } + } + if(name == atomVary) { + if(!token_compare(buf, value_start, value_end, "host") && + !token_compare(buf, value_start, value_end, "*")) { + /* What other vary headers should be ignored? */ + do_log(L_VARY, "Vary header present ("); + do_log_n(L_VARY, + buf + value_start, value_end - value_start); + do_log(L_VARY, ").\n"); + } + cache_control.flags |= CACHE_VARY; + } else if(name == atomAuthorization) { + cache_control.flags |= CACHE_AUTHORIZATION; + } + + if(name == atomPragma) { + /* Pragma is only defined for the client, and the only + standard value is no-cache (RFC 1945, 10.12). + However, we honour a Pragma: no-cache for both the client + and the server when there's no Cache-Control header. In + all cases, we pass the Pragma header to the next hop. */ + if(!haveCacheControl) { + j = getNextTokenInList(buf, value_start, + &token_start, &token_end, NULL, NULL, + &end); + while(1) { + if(j < 0) { + do_log(L_WARN, "Couldn't parse Pragma.\n"); + cache_control.flags |= CACHE_NO; + break; + } + if(token_compare(buf, token_start, token_end, + "no-cache")) + cache_control.flags = CACHE_NO; + if(end) + break; + j = getNextTokenInList(buf, j, &token_start, &token_end, + NULL, NULL, &end); + } + } + } + if(!client && + (name == atomSetCookie || + name == atomCookie || name == atomCookie2)) + cache_control.flags |= CACHE_COOKIE; + + if(hbuf) { + if(name != atomConnection && name != atomHost && + name != atomAcceptRange && name != atomTE && + name != atomProxyAuthenticate && + name != atomKeepAlive && + (!hopToHop || !atomListMember(name, hopToHop)) && + !atomListMember(name, censoredHeaders)) { + int h; + while(hbuf_length > hbuf_size - 2) + RESIZE_HBUF(); + hbuf[hbuf_length++] = '\r'; + hbuf[hbuf_length++] = '\n'; + do { + h = snnprint_n(hbuf, hbuf_length, hbuf_size, + buf + name_start, + value_end - name_start); + if(h < 0) RESIZE_HBUF(); + } while(h < 0); + hbuf_length = h; + } + } + } + releaseAtom(name); + name = NULL; + } + + if(headers_return) { + AtomPtr pheaders = NULL; + pheaders = internAtomN(hbuf, hbuf_length); + if(!pheaders) + goto fail; + *headers_return = pheaders; + } + if(hbuf != hbuf_small) + free(hbuf); + hbuf = NULL; + hbuf_size = 0; + + if(request) + if(!persistent) + request->flags &= ~REQUEST_PERSISTENT; + + if(te != TE_IDENTITY) len = -1; + if(len_return) *len_return = len; + if(cache_control_return) *cache_control_return = cache_control; + if(condition_return) { + if(ims >= 0 || inms >= 0 || im || inm || ifrange) { + condition = httpMakeCondition(); + if(condition) { + condition->ims = ims; + condition->inms = inms; + condition->im = im; + condition->inm = inm; + condition->ifrange = ifrange; + } else { + do_log(L_ERROR, "Couldn't allocate condition.\n"); + if(im) free(im); + if(inm) free(inm); + } + } else { + condition = NULL; + } + *condition_return = condition; + } else { + assert(!im && !inm); + } + + if(te_return) *te_return = te; + if(date_return) *date_return = date; + if(last_modified_return) *last_modified_return = last_modified; + if(expires_return) *expires_return = expires; + if(polipo_age_return) *polipo_age_return = polipo_age; + if(polipo_access_return) *polipo_access_return = polipo_access; + if(polipo_body_offset_return) + *polipo_body_offset_return = polipo_body_offset; + if(age_return) *age_return = age; + if(etag_return) + *etag_return = etag; + else { + if(etag) free(etag); + } + if(range_return) *range_return = range; + if(content_range_return) *content_range_return = content_range; + if(location_return) { + *location_return = location; + } else { + if(location) + free(location); + } + if(via_return) + *via_return = via; + else { + if(via) + releaseAtom(via); + } + if(expect_return) + *expect_return = expect; + else { + if(expect) + releaseAtom(expect); + } + if(auth_return) + *auth_return = auth; + else { + if(auth) + releaseAtom(auth); + } + if(hopToHop) destroyAtomList(hopToHop); + return i; + + fail: + if(hbuf && hbuf != hbuf_small) free(hbuf); + if(name) releaseAtom(name); + if(etag) free(etag); + if(location) free(location); + if(via) releaseAtom(via); + if(expect) releaseAtom(expect); + if(auth) releaseAtom(auth); + if(hopToHop) destroyAtomList(hopToHop); + + return -1; +#undef RESIZE_HBUF +} + +int +httpFindHeader(AtomPtr header, const char *headers, int hlen, + int *value_begin_return, int *value_end_return) +{ + int len = header->length; + int i = 0; + + while(i + len + 1 < hlen) { + if(headers[i + len] == ':' && + lwrcmp(headers + i, header->string, len) == 0) { + int j = i + len + 1, k; + while(j < hlen && headers[j] == ' ') + j++; + k = j; + while(k < hlen && headers[k] != '\n' && headers[k] != '\r') + k++; + *value_begin_return = j; + *value_end_return = k; + return 1; + } else { + while(i < hlen && headers[i] != '\n' && headers[i] != '\r') + i++; + i++; + if(i < hlen && headers[i] == '\n') + i++; + } + } + return 0; +} + +int +parseUrl(const char *url, int len, + int *x_return, int *y_return, int *port_return, int *z_return) +{ + int x, y, z, port = -1, i = 0; + + if(len >= 7 && lwrcmp(url, "http://", 7) == 0) { + x = 7; + if(x < len && url[x] == '[') { + /* RFC 2732 */ + for(i = x + 1; i < len; i++) { + if(url[i] == ']') { + i++; + break; + } + if((url[i] != ':') && !letter(url[i]) && !digit(url[i])) + break; + } + } else { + for(i = x; i < len; i++) + if(url[i] == ':' || url[i] == '/') + break; + } + y = i; + + if(i < len && url[i] == ':') { + int j; + j = atoi_n(url, i + 1, len, &port); + if(j < 0) { + port = 80; + } else { + i = j; + } + } else { + port = 80; + } + } else { + x = -1; + y = -1; + } + + z = i; + + *x_return = x; + *y_return = y; + *port_return = port; + *z_return = z; + return 0; +} + +int +urlIsLocal(const char *url, int len) +{ + return (len > 0 && url[0] == '/'); +} + +int +urlIsSpecial(const char *url, int len) +{ + return (len >= 8 && memcmp(url, "/polipo/", 8) == 0); +} + +int +parseChunkSize(const char *restrict buf, int i, int end, + int *chunk_size_return) +{ + int v, d; + v = h2i(buf[i]); + if(v < 0) + return -1; + + i++; + + while(i < end) { + d = h2i(buf[i]); + if(d < 0) + break; + v = v * 16 + d; + i++; + } + + while(i < end) { + if(buf[i] == ' ' || buf[i] == '\t') + i++; + else + break; + } + + if(i >= end - 1) + return 0; + + if(buf[i] != '\r' || buf[i + 1] != '\n') + return -1; + + i += 2; + + if(v == 0) { + if(i >= end - 1) + return 0; + if(buf[i] != '\r') { + do_log(L_ERROR, "Trailers present!\n"); + return -1; + } + i++; + if(buf[i] != '\n') + return -1; + i++; + } + + *chunk_size_return = v; + return i; +} + + +int +checkVia(AtomPtr name, AtomPtr via) +{ + int i; + char *v; + if(via == NULL || via->length == 0) + return 1; + + v = via->string; + + i = 0; + while(i < via->length) { + while(v[i] == ' ' || v[i] == '\t' || v[i] == ',' || + v[i] == '\r' || v[i] == '\n' || + digit(v[i]) || v[i] == '.') + i++; + if(i + name->length > via->length) + break; + if(memcmp(v + i, name->string, name->length) == 0) { + char c = v[i + name->length]; + if(c == '\0' || c == ' ' || c == '\t' || c == ',' || + c == '\r' || c == '\n') + return 0; + } + i++; + while(letter(v[i]) || digit(v[i]) || v[i] == '.') + i++; + } + return 1; +} diff --git a/jni/polipo/http_parse.h b/jni/polipo/http_parse.h new file mode 100644 index 00000000..952fcf52 --- /dev/null +++ b/jni/polipo/http_parse.h @@ -0,0 +1,58 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +typedef struct HTTPRange { + int from; + int to; + int full_length; +} HTTPRangeRec, *HTTPRangePtr; + +extern int censorReferer; +extern AtomPtr atomContentType, atomContentEncoding; + +void preinitHttpParser(void); +void initHttpParser(void); +int httpParseClientFirstLine(const char *buf, int offset, + int *method_return, + AtomPtr *url_return, + int *version_return); +int httpParseServerFirstLine(const char *buf, + int *status_return, + int *version_return, + AtomPtr *message_return); + +int findEndOfHeaders(const char *buf, int from, int to, int *body_return); + +int httpParseHeaders(int, AtomPtr, const char *, int, HTTPRequestPtr, + AtomPtr*, int*, CacheControlPtr, + HTTPConditionPtr *, int*, + time_t*, time_t*, time_t*, time_t*, time_t*, + int*, int*, char**, AtomPtr*, + HTTPRangePtr, HTTPRangePtr, char**, AtomPtr*, AtomPtr*); +int httpFindHeader(AtomPtr header, const char *headers, int hlen, + int *value_begin_return, int *value_end_return); +int parseUrl(const char *url, int len, + int *x_return, int *y_return, int *port_return, int *z_return); +int urlIsLocal(const char *url, int len); +int urlIsSpecial(const char *url, int len); +int parseChunkSize(const char *buf, int i, int end, int *chunk_size_return); +int checkVia(AtomPtr, AtomPtr); diff --git a/jni/polipo/io.c b/jni/polipo/io.c new file mode 100644 index 00000000..fb58d416 --- /dev/null +++ b/jni/polipo/io.c @@ -0,0 +1,1140 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifdef HAVE_IPv6 +#ifdef IPV6_PREFER_TEMPADDR +#define HAVE_IPV6_PREFER_TEMPADDR 1 +#endif +#endif + +#ifdef HAVE_IPV6_PREFER_TEMPADDR +int useTemporarySourceAddress = 1; +#endif + +void +preinitIo() +{ +#ifdef HAVE_IPV6_PREFER_TEMPADDR + CONFIG_VARIABLE_SETTABLE(useTemporarySourceAddress, CONFIG_TRISTATE, + configIntSetter, + "Prefer IPv6 temporary source address."); +#endif + +#ifdef HAVE_WINSOCK + /* Load the winsock dll */ + WSADATA wsaData; + WORD wVersionRequested = MAKEWORD(2, 2); + int err = WSAStartup( wVersionRequested, &wsaData ); + if (err != 0) { + do_log_error(L_ERROR, err, "Couldn't load winsock dll"); + exit(-1); + } +#endif + return; +} + +void +initIo() +{ + return; +} + +FdEventHandlerPtr +do_stream(int operation, int fd, int offset, char *buf, int len, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + assert(len > offset || (operation & (IO_END | IO_IMMEDIATE))); + return schedule_stream(operation, fd, offset, + NULL, 0, buf, len, NULL, 0, NULL, 0, NULL, + handler, data); +} + +FdEventHandlerPtr +do_stream_2(int operation, int fd, int offset, + char *buf, int len, char *buf2, int len2, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + assert(len + len2 > offset || (operation & (IO_END | IO_IMMEDIATE))); + return schedule_stream(operation, fd, offset, + NULL, 0, buf, len, buf2, len2, NULL, 0, NULL, + handler, data); +} + +FdEventHandlerPtr +do_stream_3(int operation, int fd, int offset, + char *buf, int len, char *buf2, int len2, char *buf3, int len3, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + assert(len + len2 > offset || (operation & (IO_END | IO_IMMEDIATE))); + return schedule_stream(operation, fd, offset, + NULL, 0, buf, len, buf2, len2, buf3, len3, NULL, + handler, data); +} + +FdEventHandlerPtr +do_stream_h(int operation, int fd, int offset, + char *header, int hlen, char *buf, int len, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + assert(hlen + len > offset || (operation & (IO_END | IO_IMMEDIATE))); + return schedule_stream(operation, fd, offset, + header, hlen, buf, len, NULL, 0, NULL, 0, NULL, + handler, data); +} + +FdEventHandlerPtr +do_stream_buf(int operation, int fd, int offset, char **buf_location, int len, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + assert((len > offset || (operation & (IO_END | IO_IMMEDIATE))) + && len <= CHUNK_SIZE); + return schedule_stream(operation, fd, offset, + NULL, 0, *buf_location, len, + NULL, 0, NULL, 0, buf_location, + handler, data); +} + +static int +chunkHeaderLen(int i) +{ + if(i <= 0) + return 0; + if(i < 0x10) + return 3; + else if(i < 0x100) + return 4; + else if(i < 0x1000) + return 5; + else if(i < 0x10000) + return 6; + else + abort(); +} + +static int +chunkHeader(char *buf, int buflen, int i) +{ + int n; + if(i <= 0) + return 0; + n = snprintf(buf, buflen, "%x\r\n", i); + return n; +} + + +FdEventHandlerPtr +schedule_stream(int operation, int fd, int offset, + char *header, int hlen, + char *buf, int len, char *buf2, int len2, char *buf3, int len3, + char **buf_location, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + StreamRequestRec request; + FdEventHandlerPtr event; + int done; + + request.operation = operation; + request.fd = fd; + if(len3) { + assert(hlen == 0 && buf_location == NULL); + request.u.b.len3 = len3; + request.u.b.buf3 = buf3; + request.operation |= IO_BUF3; + } else if(buf_location) { + assert(hlen == 0); + request.u.l.buf_location = buf_location; + request.operation |= IO_BUF_LOCATION; + } else { + request.u.h.hlen = hlen; + request.u.h.header = header; + } + request.buf = buf; + request.len = len; + request.buf2 = buf2; + request.len2 = len2; + if((operation & IO_CHUNKED) || + (!(request.operation & (IO_BUF3 | IO_BUF_LOCATION)) && hlen > 0)) { + assert(offset == 0); + request.offset = -hlen; + if(operation & IO_CHUNKED) + request.offset += -chunkHeaderLen(len + len2); + } else { + request.offset = offset; + } + request.handler = handler; + request.data = data; + event = makeFdEvent(fd, + (operation & IO_MASK) == IO_WRITE ? + POLLOUT : POLLIN, + do_scheduled_stream, + sizeof(StreamRequestRec), &request); + if(!event) { + done = (*handler)(-ENOMEM, NULL, &request); + assert(done); + return NULL; + } + + if(!(operation & IO_NOTNOW)) { + done = event->handler(0, event); + if(done) { + free(event); + return NULL; + } + } + + if(operation & IO_IMMEDIATE) { + assert(hlen == 0 && !(operation & IO_CHUNKED)); + done = (*handler)(0, event, &request); + if(done) { + free(event); + return NULL; + } + } + event = registerFdEventHelper(event); + return event; +} + +static const char *endChunkTrailer = "\r\n0\r\n\r\n"; + +int +do_scheduled_stream(int status, FdEventHandlerPtr event) +{ + StreamRequestPtr request = (StreamRequestPtr)&event->data; + int rc, done, i; + struct iovec iov[6]; + int chunk_header_len; + char chunk_header[10]; + int len12 = request->len + request->len2; + int len123 = + request->len + request->len2 + + ((request->operation & IO_BUF3) ? request->u.b.len3 : 0); + + if(status) { + done = request->handler(status, event, request); + return done; + } + + i = 0; + + if(request->offset < 0) { + assert((request->operation & (IO_MASK | IO_BUF3 | IO_BUF_LOCATION)) == + IO_WRITE); + if(request->operation & IO_CHUNKED) { + chunk_header_len = chunkHeaderLen(len123); + } else { + chunk_header_len = 0; + } + + if(request->offset < -chunk_header_len) { + assert(request->offset >= -(request->u.h.hlen + chunk_header_len)); + iov[i].iov_base = request->u.h.header; + iov[i].iov_len = -request->offset - chunk_header_len; + i++; + } + + if(chunk_header_len > 0) { + chunkHeader(chunk_header, 10, len123); + if(request->offset < -chunk_header_len) { + iov[i].iov_base = chunk_header; + iov[i].iov_len = chunk_header_len; + } else { + iov[i].iov_base = chunk_header + + chunk_header_len + request->offset; + iov[i].iov_len = -request->offset; + } + i++; + } + } + + if(request->len > 0) { + if(request->buf == NULL && + (request->operation & IO_BUF_LOCATION)) { + assert(*request->u.l.buf_location == NULL); + request->buf = *request->u.l.buf_location = get_chunk(); + if(request->buf == NULL) { + done = request->handler(-ENOMEM, event, request); + return done; + } + } + if(request->offset <= 0) { + iov[i].iov_base = request->buf; + iov[i].iov_len = request->len; + i++; + } else if(request->offset < request->len) { + iov[i].iov_base = request->buf + request->offset; + iov[i].iov_len = request->len - request->offset; + i++; + } + } + + if(request->len2 > 0) { + if(request->offset <= request->len) { + iov[i].iov_base = request->buf2; + iov[i].iov_len = request->len2; + i++; + } else if(request->offset < request->len + request->len2) { + iov[i].iov_base = request->buf2 + request->offset - request->len; + iov[i].iov_len = request->len2 - request->offset + request->len; + i++; + } + } + + if((request->operation & IO_BUF3) && request->u.b.len3 > 0) { + if(request->offset <= len12) { + iov[i].iov_base = request->u.b.buf3; + iov[i].iov_len = request->u.b.len3; + i++; + } else if(request->offset < len12 + request->u.b.len3) { + iov[i].iov_base = request->u.b.buf3 + request->offset - len12; + iov[i].iov_len = request->u.b.len3 - request->offset + len12; + i++; + } + } + + if((request->operation & IO_CHUNKED)) { + int l; + const char *trailer; + if(request->operation & IO_END) { + if(len123 == 0) { + trailer = endChunkTrailer + 2; + l = 5; + } else { + trailer = endChunkTrailer; + l = 7; + } + } else { + trailer = endChunkTrailer; + l = 2; + } + + if(request->offset <= len123) { + iov[i].iov_base = (char*)trailer; + iov[i].iov_len = l; + i++; + } else if(request->offset < len123 + l) { + iov[i].iov_base = + (char*)endChunkTrailer + request->offset - len123; + iov[i].iov_len = l - request->offset + len123; + i++; + } + } + + assert(i > 0); + + if((request->operation & IO_MASK) == IO_WRITE) { + if(i > 1) + rc = WRITEV(request->fd, iov, i); + else + rc = WRITE(request->fd, iov[0].iov_base, iov[0].iov_len); + } else { + if(i > 1) + rc = READV(request->fd, iov, i); + else + rc = READ(request->fd, iov[0].iov_base, iov[0].iov_len); + } + + if(rc > 0) { + request->offset += rc; + if(request->offset < 0) return 0; + done = request->handler(0, event, request); + return done; + } else if(rc == 0 || errno == EPIPE) { + done = request->handler(1, event, request); + } else if(errno == EAGAIN || errno == EINTR) { + return 0; + } else if(errno == EFAULT || errno == EBADF) { + abort(); + } else { + done = request->handler(-errno, event, request); + } + assert(done); + return done; +} + +int +streamRequestDone(StreamRequestPtr request) +{ + int len123 = + request->len + request->len2 + + ((request->operation & IO_BUF3) ? request->u.b.len3 : 0); + + if(request->offset < 0) + return 0; + else if(request->offset < len123) + return 0; + else if(request->operation & IO_CHUNKED) { + if(request->operation & IO_END) { + if(request->offset < len123 + (len123 ? 7 : 5)) + return 0; + } else { + if(request->offset < len123 + 2) + return 0; + } + } + + return 1; +} + +static int +serverSocket(int af) +{ + int fd, rc; + if(af == 4) { + fd = socket(PF_INET, SOCK_STREAM, 0); + } else if(af == 6) { +#ifdef HAVE_IPv6 + fd = socket(PF_INET6, SOCK_STREAM, 0); +#else + fd = -1; + errno = EAFNOSUPPORT; +#endif + } else { + abort(); + } + + if(fd >= 0) { + rc = setNonblocking(fd, 1); + if(rc < 0) { + int errno_save = errno; + CLOSE(fd); + errno = errno_save; + return -1; + } +#ifdef HAVE_IPV6_PREFER_TEMPADDR + if (af == 6 && useTemporarySourceAddress != 1) { + int value; + value = (useTemporarySourceAddress == 2) ? 1 : 0; + rc = setsockopt(fd, IPPROTO_IPV6, IPV6_PREFER_TEMPADDR, + &value, sizeof(value)); + if (rc < 0) { + /* no error, warning only */ + do_log_error(L_WARN, errno, "Couldn't set IPV6CTL_USETEMPADDR"); + } + } + +#endif + } + return fd; +} + +FdEventHandlerPtr +do_connect(AtomPtr addr, int index, int port, + int (*handler)(int, FdEventHandlerPtr, ConnectRequestPtr), + void *data) +{ + ConnectRequestRec request; + FdEventHandlerPtr event; + int done, fd, af; + + assert(addr->length > 0 && addr->string[0] == DNS_A); + assert(addr->length % sizeof(HostAddressRec) == 1); + + if(index >= (addr->length - 1)/ sizeof(HostAddressRec)) + index = 0; + + request.firstindex = index; + request.port = port; + request.handler = handler; + request.data = data; + again: + af = addr->string[1 + index * sizeof(HostAddressRec)]; + fd = serverSocket(af); + + request.fd = fd; + request.af = af; + request.addr = addr; + request.index = index; + + if(fd < 0) { + int n = (addr->length - 1) / sizeof(HostAddressRec); + if(errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT) { + if((index + 1) % n != request.firstindex) { + index = (index + 1) % n; + goto again; + } + } + do_log_error(L_ERROR, errno, "Couldn't create socket"); + done = (*handler)(-errno, NULL, &request); + assert(done); + return NULL; + } + + /* POLLIN is apparently needed on Windows */ + event = registerFdEvent(fd, POLLIN | POLLOUT, + do_scheduled_connect, + sizeof(ConnectRequestRec), &request); + if(event == NULL) { + done = (*handler)(-ENOMEM, NULL, &request); + assert(done); + return NULL; + } + + done = event->handler(0, event); + if(done) { + unregisterFdEvent(event); + return NULL; + } + return event; +} + +int +do_scheduled_connect(int status, FdEventHandlerPtr event) +{ + ConnectRequestPtr request = (ConnectRequestPtr)&event->data; + AtomPtr addr = request->addr; + int done; + int rc; + HostAddressPtr host; + struct sockaddr_in servaddr; +#ifdef HAVE_IPv6 + struct sockaddr_in6 servaddr6; +#endif + + assert(addr->length > 0 && addr->string[0] == DNS_A); + assert(addr->length % sizeof(HostAddressRec) == 1); + assert(request->index < (addr->length - 1) / sizeof(HostAddressRec)); + + if(status) { + done = request->handler(status, event, request); + if(done) { + releaseAtom(addr); + request->addr = NULL; + return 1; + } + return 0; + } + + again: + host = (HostAddressPtr)&addr->string[1 + + request->index * + sizeof(HostAddressRec)]; + if(host->af != request->af) { + int newfd; + /* Ouch. Our socket has a different protocol than the host + address. */ + CLOSE(request->fd); + newfd = serverSocket(host->af); + if(newfd < 0) { + if(errno == EAFNOSUPPORT || errno == EPROTONOSUPPORT) { + int n = request->addr->length / sizeof(HostAddressRec); + if((request->index + 1) % n != request->firstindex) { + request->index = (request->index + 1) % n; + goto again; + } + } + request->fd = -1; + done = request->handler(-errno, event, request); + assert(done); + return 1; + } + if(newfd != request->fd) { + request->fd = dup2(newfd, request->fd); + CLOSE(newfd); + if(request->fd < 0) { + done = request->handler(-errno, event, request); + assert(done); + return 1; + } + } + request->af = host->af; + } + switch(host->af) { + case 4: + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_port = htons(request->port); + memcpy(&servaddr.sin_addr, &host->data, sizeof(struct in_addr)); + rc = connect(request->fd, + (struct sockaddr*)&servaddr, sizeof(servaddr)); + break; + case 6: +#ifdef HAVE_IPv6 + memset(&servaddr6, 0, sizeof(servaddr6)); + servaddr6.sin6_family = AF_INET6; + servaddr6.sin6_port = htons(request->port); + memcpy(&servaddr6.sin6_addr, &host->data, sizeof(struct in6_addr)); + rc = connect(request->fd, + (struct sockaddr*)&servaddr6, sizeof(servaddr6)); +#else + rc = -1; + errno = EAFNOSUPPORT; +#endif + break; + default: + abort(); + } + + if(rc >= 0 || errno == EISCONN) { + done = request->handler(1, event, request); + assert(done); + releaseAtom(request->addr); + request->addr = NULL; + return 1; + } + + if(errno == EINPROGRESS || errno == EINTR) { + return 0; + } else if(errno == EFAULT || errno == EBADF) { + abort(); + } else { + int n = request->addr->length / sizeof(HostAddressRec); + if((request->index + 1) % n != request->firstindex) { + request->index = (request->index + 1) % n; + goto again; + } + done = request->handler(-errno, event, request); + assert(done); + releaseAtom(request->addr); + request->addr = NULL; + return 1; + } +} + +FdEventHandlerPtr +do_accept(int fd, + int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr), + void *data) +{ + FdEventHandlerPtr event; + int done; + + event = schedule_accept(fd, handler, data); + if(event == NULL) { + done = (*handler)(-ENOMEM, NULL, NULL); + assert(done); + } + + /* But don't invoke it now - this will delay accept if under load. */ + return event; +} + +FdEventHandlerPtr +schedule_accept(int fd, + int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr), + void *data) +{ + FdEventHandlerPtr event; + AcceptRequestRec request; + int done; + + request.fd = fd; + request.handler = handler; + request.data = data; + event = registerFdEvent(fd, POLLOUT|POLLIN, + do_scheduled_accept, sizeof(request), &request); + if(!event) { + done = (*handler)(-ENOMEM, NULL, NULL); + assert(done); + } + return event; +} + +int +do_scheduled_accept(int status, FdEventHandlerPtr event) +{ + AcceptRequestPtr request = (AcceptRequestPtr)&event->data; + int rc, done; + unsigned len; + struct sockaddr_in addr; + + if(status) { + done = request->handler(status, event, request); + if(done) return done; + } + + len = sizeof(struct sockaddr_in); + + rc = accept(request->fd, (struct sockaddr*)&addr, &len); + + if(rc >= 0) + done = request->handler(rc, event, request); + else + done = request->handler(-errno, event, request); + return done; +} + +FdEventHandlerPtr +create_listener(char *address, int port, + int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr), + void *data) +{ + int fd, rc; + int one = 1; + int done; + struct sockaddr_in addr; +#ifdef HAVE_IPv6 + int inet6 = 1; + struct sockaddr_in6 addr6; +#else + int inet6 = 0; +#endif + + if(inet6 && address) { + struct in_addr buf; + rc = inet_aton(address, &buf); + if(rc == 1) + inet6 = 0; + } + fd = -1; + errno = EAFNOSUPPORT; + +#ifdef HAVE_IPv6 + if(inet6) { + fd = socket(PF_INET6, SOCK_STREAM, 0); + } +#endif + + if(fd < 0 && (errno == EPROTONOSUPPORT || errno == EAFNOSUPPORT)) { + inet6 = 0; + fd = socket(PF_INET, SOCK_STREAM, 0); + } + + if(fd < 0) { + done = (*handler)(-errno, NULL, NULL); + assert(done); + return NULL; + } + + rc = setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&one, sizeof(one)); + if(rc < 0) do_log_error(L_WARN, errno, "Couldn't set SO_REUSEADDR"); + + if(inet6) { +#ifdef HAVE_IPv6 + rc = setV6only(fd, 0); + if(rc < 0) + /* Reportedly OpenBSD returns an error for that. So only + log it as a debugging message. */ + do_log_error(D_CLIENT_CONN, errno, "Couldn't reset IPV6_V6ONLY"); + + memset(&addr6, 0, sizeof(addr6)); + rc = inet_pton(AF_INET6, address, &addr6.sin6_addr); + if(rc != 1) { + done = (*handler)(rc == 0 ? -ESYNTAX : -errno, NULL, NULL); + assert(done); + return NULL; + } + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(port); + rc = bind(fd, (struct sockaddr*)&addr6, sizeof(addr6)); +#else + rc = -1; + errno = EAFNOSUPPORT; +#endif + } else { + memset(&addr, 0, sizeof(addr)); + rc = inet_aton(address, &addr.sin_addr); + if(rc != 1) { + done = (*handler)(rc == 0 ? -ESYNTAX : -errno, NULL, NULL); + assert(done); + return NULL; + } + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + rc = bind(fd, (struct sockaddr*)&addr, sizeof(addr)); + } + + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't bind"); + CLOSE(fd); + done = (*handler)(-errno, NULL, NULL); + assert(done); + return NULL; + } + + rc = setNonblocking(fd, 1); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't set non blocking mode"); + CLOSE(fd); + done = (*handler)(-errno, NULL, NULL); + assert(done); + return NULL; + } + + rc = listen(fd, 1024); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't listen"); + CLOSE(fd); + done = (*handler)(-errno, NULL, NULL); + assert(done); + return NULL; + } + + do_log(L_INFO, "Established listening socket on port %d.\n", port); + + return schedule_accept(fd, handler, data); +} + +#ifndef SOL_TCP +/* BSD */ +#define SOL_TCP IPPROTO_TCP +#endif + +int +setNonblocking(int fd, int nonblocking) +{ +#ifdef WIN32 /*MINGW*/ + return win32_setnonblocking(fd, nonblocking); +#else + int rc; + rc = fcntl(fd, F_GETFL, 0); + if(rc < 0) + return -1; + + rc = fcntl(fd, F_SETFL, nonblocking?(rc | O_NONBLOCK):(rc & ~O_NONBLOCK)); + if(rc < 0) + return -1; + + return 0; +#endif +} + +int +setNodelay(int fd, int nodelay) +{ + int val = nodelay ? 1 : 0; + int rc; + rc = setsockopt(fd, SOL_TCP, TCP_NODELAY, (char *)&val, sizeof(val)); + if(rc < 0) + return -1; + return 0; +} + +#ifdef IPV6_V6ONLY +int +setV6only(int fd, int v6only) +{ + int val = v6only ? 1 : 0; + int rc; + rc = setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&val, sizeof(val)); + if(rc < 0) + return -1; + return 0; +} +#else +int +setV6only(int fd, int v6only) +{ + return 0; +} +#endif + +typedef struct _LingeringClose { + int fd; + FdEventHandlerPtr handler; + TimeEventHandlerPtr timeout; +} LingeringCloseRec, *LingeringClosePtr; + +static int +lingeringCloseTimeoutHandler(TimeEventHandlerPtr event) +{ + LingeringClosePtr l = *(LingeringClosePtr*)event->data; + assert(l->timeout == event); + l->timeout = NULL; + if(l->handler) + pokeFdEvent(l->fd, -ESHUTDOWN, POLLIN | POLLOUT); + else { + CLOSE(l->fd); + free(l); + } + return 1; +} + +static int +lingeringCloseHandler(int status, FdEventHandlerPtr event) +{ + LingeringClosePtr l = *(LingeringClosePtr*)event->data; + char buf[17]; + int rc; + + assert(l->handler == event); + + l->handler = NULL; + if(status && status != -EDOGRACEFUL) + goto done; + + rc = READ(l->fd, &buf, 17); + if(rc == 0 || (rc < 0 && errno != EAGAIN && errno != EINTR)) + goto done; + + /* The client is still sending data. Ignore it in order to let + TCP's flow control do its work. The timeout will close the + connection. */ + return 1; + + done: + if(l->timeout) { + cancelTimeEvent(l->timeout); + l->timeout = NULL; + } + CLOSE(l->fd); + free(l); + return 1; +} + +int +lingeringClose(int fd) +{ + int rc; + LingeringClosePtr l; + + rc = shutdown(fd, 1); + if(rc < 0) { + if(errno != ENOTCONN) { + do_log_error(L_ERROR, errno, "Shutdown failed"); + } else if(errno == EFAULT || errno == EBADF) { + abort(); + } + CLOSE(fd); + return 1; + } + + l = malloc(sizeof(LingeringCloseRec)); + if(l == NULL) + goto fail; + l->fd = fd; + l->handler = NULL; + l->timeout = NULL; + + l->timeout = scheduleTimeEvent(10, lingeringCloseTimeoutHandler, + sizeof(LingeringClosePtr), &l); + if(l->timeout == NULL) { + free(l); + goto fail; + } + + l->handler = registerFdEvent(fd, POLLIN, + lingeringCloseHandler, + sizeof(LingeringClosePtr), &l); + if(l->handler == NULL) { + do_log(L_ERROR, "Couldn't schedule lingering close handler.\n"); + /* But don't close -- the timeout will do its work. */ + } + return 1; + + fail: + do_log(L_ERROR, "Couldn't schedule lingering close.\n"); + CLOSE(fd); + return 1; +} + +NetAddressPtr +parseNetAddress(AtomListPtr list) +{ + NetAddressPtr nl; + int i, rc, rc6; + char buf[100]; + struct in_addr ina; +#ifdef HAVE_IPv6 + struct in6_addr ina6; +#endif + + nl = malloc((list->length + 1) * sizeof(NetAddressRec)); + if(nl == NULL) { + do_log(L_ERROR, "Couldn't allocate network list.\n"); + return NULL; + } + + for(i = 0; i < list->length; i++) { + int prefix; + char *s = list->list[i]->string, *p; + int n = list->list[i]->length; + char *suffix; + + while(*s == ' ' || *s == '\t') { + s++; + n--; + } + + if(n >= 100) { + do_log(L_ERROR, "Network name too long.\n"); + goto fail; + } + p = memchr(s, '/', n); + if(p) { + memcpy(buf, s, p - s); + buf[p - s] = '\0'; + prefix = strtol(p + 1, &suffix, 10); + } else { + char *s1, *s2; + prefix = -1; + strcpy(buf, s); + s1 = strchr(s, ' '); + s2 = strchr(s, '\t'); + if(s1 == NULL) suffix = s2; + else if(s2 == NULL) suffix = s1; + else if(s1 < s2) suffix = s1; + else suffix = s2; + if(suffix == NULL) + suffix = s + n; + } + + if(!isWhitespace(suffix)) { + do_log(L_ERROR, "Couldn't parse network %s.\n", buf); + goto fail; + } + + rc = 0; rc6 = 0; + rc = inet_aton(buf, &ina); +#ifdef HAVE_IPv6 + if(rc == 0) { + rc6 = inet_pton(AF_INET6, buf, &ina6); + } +#endif + if(rc == 0 && rc6 == 0) { + do_log(L_ERROR, "Couldn't parse network %s.\n", buf); + goto fail; + } + nl[i].prefix = prefix; + if(rc) { + nl[i].af = 4; + memcpy(nl[i].data, &ina, 4); + } else { +#ifdef HAVE_IPv6 + nl[i].af = 6; + memcpy(nl[i].data, &ina6, 16); +#else + abort(); +#endif + } + } + nl[i].af = 0; + return nl; + + fail: + free(nl); + return NULL; +} + +/* Returns 1 if the first n bits of a and b are equal */ +static int +bitmatch(const unsigned char *a, const unsigned char *b, int n) +{ + if(n >= 8) { + if(memcmp(a, b, n / 8) != 0) + return 0; + } + + if(n % 8 != 0) { + int mask = (~0) << (8 - n % 8); + if((a[n / 8] & mask) != (b[n / 8] & mask)) + return 0; + } + + return 1; +} + +/* Returns 1 if the address in data is in list */ +static int +match(int af, unsigned char *data, NetAddressPtr list) +{ + int i; +#ifdef HAVE_IPv6 + static const unsigned char v6mapped[] = + { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF }; +#endif + + i = 0; + while(list[i].af != 0) { + if(af == 4 && list[i].af == 4) { + if(bitmatch(data, list[i].data, + list[i].prefix >= 0 ? list[i].prefix : 32)) + return 1; +#ifdef HAVE_IPv6 + } else if(af == 6 && list[i].af == 6) { + if(bitmatch(data, list[i].data, + list[i].prefix >= 0 ? list[i].prefix : 128)) + return 1; + } else if(af == 6 && list[i].af == 4) { + if(bitmatch(data, v6mapped, 96)) { + if(bitmatch(data + 12, list[i].data, + list[i].prefix >= 0 ? list[i].prefix : 32)) + return 1; + } + } else if(af == 4 && list[i].af == 6) { + if(bitmatch(list[i].data, v6mapped, 96)) { + if(bitmatch(data, list[i].data + 12, + list[i].prefix >= 96 ? + list[i].prefix - 96 : 32)) + return 1; + } +#endif + } else { + abort(); + } + i++; + } + return 0; +} + +int +netAddressMatch(int fd, NetAddressPtr list) +{ + int rc; + unsigned int len; + struct sockaddr_in sain; +#ifdef HAVE_IPv6 + struct sockaddr_in6 sain6; +#endif + + len = sizeof(sain); + rc = getpeername(fd, (struct sockaddr*)&sain, &len); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't get peer name"); + return -1; + } + + if(sain.sin_family == AF_INET) { + return match(4, (unsigned char*)&sain.sin_addr, list); +#ifdef HAVE_IPv6 + } else if(sain.sin_family == AF_INET6) { + len = sizeof(sain6); + rc = getpeername(fd, (struct sockaddr*)&sain6, &len); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't get peer name"); + return -1; + } + if(sain6.sin6_family != AF_INET6) { + do_log(L_ERROR, "Inconsistent peer name"); + return -1; + } + return match(6, (unsigned char*)&sain6.sin6_addr, list); +#endif + } else { + do_log(L_ERROR, "Unknown address family %d\n", sain.sin_family); + return -1; + } + return 0; +} + + + + + + diff --git a/jni/polipo/io.h b/jni/polipo/io.h new file mode 100644 index 00000000..79f5e95e --- /dev/null +++ b/jni/polipo/io.h @@ -0,0 +1,161 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* request->operation */ +#define IO_READ 0 +#define IO_WRITE 1 +#define IO_MASK 0xFF +/* Do not initiate operation now -- wait for the poll loop. */ +#define IO_NOTNOW 0x100 +/* Call the progress handler once if no data arrives immediately. */ +#define IO_IMMEDIATE 0x200 +/* Emit a chunk length before every write operation */ +#define IO_CHUNKED 0x400 +/* Emit a zero-length chunk at the end if chunked */ +#define IO_END 0x800 + +/* Internal -- header is really buf3 */ +#define IO_BUF3 0x1000 +/* Internal -- header is really buf_location */ +#define IO_BUF_LOCATION 0x2000 + +typedef struct _StreamRequest { + short operation; + short fd; + int offset; + int len; + int len2; + union { + struct { + int hlen; + char *header; + } h; + struct { + int len3; + char *buf3; + } b; + struct { + char **buf_location; + } l; + } u; + char *buf; + char *buf2; + int (*handler)(int, FdEventHandlerPtr, struct _StreamRequest*); + void *data; +} StreamRequestRec, *StreamRequestPtr; + +typedef struct _ConnectRequest { + int fd; + int af; + struct _Atom *addr; + int firstindex; + int index; + int port; + int (*handler)(int, FdEventHandlerPtr, struct _ConnectRequest*); + void *data; +} ConnectRequestRec, *ConnectRequestPtr; + +typedef struct _AcceptRequest { + int fd; + int (*handler)(int, FdEventHandlerPtr, struct _AcceptRequest*); + void *data; +} AcceptRequestRec, *AcceptRequestPtr; + +void preinitIo(); +void initIo(); + +FdEventHandlerPtr +do_stream(int operation, int fd, int offset, char *buf, int len, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data); + +FdEventHandlerPtr +do_stream_h(int operation, int fd, int offset, + char *header, int hlen, char *buf, int len, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data); + +FdEventHandlerPtr +do_stream_2(int operation, int fd, int offset, + char *buf, int len, char *buf2, int len2, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data); + +FdEventHandlerPtr +do_stream_3(int operation, int fd, int offset, + char *buf, int len, char *buf2, int len2, char *buf3, int len3, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data); + +FdEventHandlerPtr +do_stream_buf(int operation, int fd, int offset, char **buf_location, int len, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data); + +FdEventHandlerPtr +schedule_stream(int operation, int fd, int offset, + char *header, int hlen, + char *buf, int len, char *buf2, int len2, char *buf3, int len3, + char **buf_location, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data); + +int do_scheduled_stream(int, FdEventHandlerPtr); +int streamRequestDone(StreamRequestPtr); + +FdEventHandlerPtr +do_connect(struct _Atom *addr, int index, int port, + int (*handler)(int, FdEventHandlerPtr, ConnectRequestPtr), + void *data); + +int do_scheduled_connect(int, FdEventHandlerPtr event); + +FdEventHandlerPtr +do_accept(int fd, + int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr), + void* data); + +FdEventHandlerPtr +schedule_accept(int fd, + int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr), + void* data); + +int do_scheduled_accept(int, FdEventHandlerPtr event); + +FdEventHandlerPtr +create_listener(char *address, int port, + int (*handler)(int, FdEventHandlerPtr, AcceptRequestPtr), + void *data); +int setNonblocking(int fd, int nonblocking); +int setNodelay(int fd, int nodelay); +int setV6only(int fd, int v6only); +int lingeringClose(int fd); + +typedef struct _NetAddress { + int prefix; + int af; + unsigned char data[16]; +} NetAddressRec, *NetAddressPtr; + +NetAddressPtr parseNetAddress(AtomListPtr list); +int netAddressMatch(int fd, NetAddressPtr list) ATTRIBUTE ((pure)); + diff --git a/jni/polipo/local.c b/jni/polipo/local.c new file mode 100644 index 00000000..82106c4e --- /dev/null +++ b/jni/polipo/local.c @@ -0,0 +1,743 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +int disableLocalInterface = 0; +int disableConfiguration = 0; +int disableIndexing = 1; +int disableServersList = 1; + +AtomPtr atomInitForbidden; +AtomPtr atomReopenLog; +AtomPtr atomDiscardObjects; +AtomPtr atomWriteoutObjects; +AtomPtr atomFreeChunkArenas; + +void +preinitLocal() +{ + atomInitForbidden = internAtom("init-forbidden"); + atomReopenLog = internAtom("reopen-log"); + atomDiscardObjects = internAtom("discard-objects"); + atomWriteoutObjects = internAtom("writeout-objects"); + atomFreeChunkArenas = internAtom("free-chunk-arenas"); + + /* These should not be settable for obvious reasons */ + CONFIG_VARIABLE(disableLocalInterface, CONFIG_BOOLEAN, + "Disable the local configuration pages."); + CONFIG_VARIABLE(disableConfiguration, CONFIG_BOOLEAN, + "Disable reconfiguring Polipo at runtime."); + CONFIG_VARIABLE(disableIndexing, CONFIG_BOOLEAN, + "Disable indexing of the local cache."); + CONFIG_VARIABLE(disableServersList, CONFIG_BOOLEAN, + "Disable the list of known servers."); +} + +static void fillSpecialObject(ObjectPtr, void (*)(FILE*, char*), void*); + +int +httpLocalRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr requestor, void *closure) +{ + if(object->requestor == NULL) + object->requestor = requestor; + + if(!disableLocalInterface && urlIsSpecial(object->key, object->key_size)) + return httpSpecialRequest(object, method, from, to, + requestor, closure); + + if(method >= METHOD_POST) { + httpClientError(requestor, 405, internAtom("Method not allowed")); + requestor->connection->flags &= ~CONN_READER; + return 1; + } + + /* objectFillFromDisk already did the real work but we have to + make sure we don't get into an infinite loop. */ + if(object->flags & OBJECT_INITIAL) { + abortObject(object, 404, internAtom("Not found")); + } + object->age = current_time.tv_sec; + object->date = current_time.tv_sec; + + object->flags &= ~OBJECT_VALIDATING; + notifyObject(object); + return 1; +} + +void +alternatingHttpStyle(FILE *out, char *id) +{ + fprintf(out, + "\n", id, id); +} + +static void +printConfig(FILE *out, char *dummy) +{ + fprintf(out, + "\n" + "\n" + "Polipo configuration\n" + "\n" + "

Polipo configuration

\n"); + printConfigVariables(out, 1); + fprintf(out, "

back

"); + fprintf(out, "\n"); +} + +#ifndef NO_DISK_CACHE + +static void +recursiveIndexDiskObjects(FILE *out, char *root) +{ + indexDiskObjects(out, root, 1); +} + +static void +plainIndexDiskObjects(FILE *out, char *root) +{ + indexDiskObjects(out, root, 0); +} +#endif + +static void +serversList(FILE *out, char *dummy) +{ + listServers(out); +} + +static int +matchUrl(char *base, ObjectPtr object) +{ + int n = strlen(base); + if(object->key_size < n) + return 0; + if(memcmp(base, object->key, n) != 0) + return 0; + return (object->key_size == n) || (((char*)object->key)[n] == '?'); +} + +int +httpSpecialRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr requestor, void *closure) +{ + char buffer[1024]; + int hlen; + + if(method >= METHOD_POST) { + return httpSpecialSideRequest(object, method, from, to, + requestor, closure); + } + + if(!(object->flags & OBJECT_INITIAL)) { + privatiseObject(object, 0); + supersedeObject(object); + object->flags &= ~(OBJECT_VALIDATING | OBJECT_INPROGRESS); + notifyObject(object); + return 1; + } + + hlen = snnprintf(buffer, 0, 1024, + "\r\nServer: polipo" + "\r\nContent-Type: text/html"); + object->date = current_time.tv_sec; + object->age = current_time.tv_sec; + object->headers = internAtomN(buffer, hlen); + object->code = 200; + object->message = internAtom("Okay"); + object->flags &= ~OBJECT_INITIAL; + object->flags |= OBJECT_DYNAMIC; + + if(object->key_size == 8 && memcmp(object->key, "/polipo/", 8) == 0) { + objectPrintf(object, 0, + "\n" + "\n" + "Polipo\n" + "\n" + "

Polipo

\n" + "

Status report.

\n" + "

Current configuration.

\n" + "

Known servers.

\n" +#ifndef NO_DISK_CACHE + "

Disk cache index.

\n" +#endif + "\n"); + object->length = object->size; + } else if(matchUrl("/polipo/status", object)) { + objectPrintf(object, 0, + "\n" + "\n" + "Polipo status report\n" + "\n" + "

Polipo proxy on %s:%d: status report

\n" + "

The %s proxy on %s:%d is %s.

\n" + "

There are %d public and %d private objects " + "currently in memory using %d KB in %d chunks " + "(%d KB allocated).

\n" + "

There are %d atoms.

" + "

" + "
\n" + "
" + "
\n" + "
" + "
\n" + "
" + "
\n" + "
" + "

\n" + "

back

" + "\n", + proxyName->string, proxyPort, + cacheIsShared ? "shared" : "private", + proxyName->string, proxyPort, + proxyOffline ? "off line" : + (relaxTransparency ? + "on line (transparency relaxed)" : + "on line"), + publicObjectCount, privateObjectCount, + used_chunks * CHUNK_SIZE / 1024, used_chunks, + totalChunkArenaSize() / 1024, + used_atoms); + object->expires = current_time.tv_sec; + object->length = object->size; + } else if(matchUrl("/polipo/config", object)) { + fillSpecialObject(object, printConfig, NULL); + object->expires = current_time.tv_sec + 5; +#ifndef NO_DISK_CACHE + } else if(matchUrl("/polipo/index", object)) { + int len; + char *root; + if(disableIndexing) { + abortObject(object, 403, internAtom("Action not allowed")); + notifyObject(object); + return 1; + } + len = MAX(0, object->key_size - 14); + root = strdup_n((char*)object->key + 14, len); + if(root == NULL) { + abortObject(object, 503, internAtom("Couldn't allocate root")); + notifyObject(object); + return 1; + } + writeoutObjects(1); + fillSpecialObject(object, plainIndexDiskObjects, root); + free(root); + object->expires = current_time.tv_sec + 5; + } else if(matchUrl("/polipo/recursive-index", object)) { + int len; + char *root; + if(disableIndexing) { + abortObject(object, 403, internAtom("Action not allowed")); + notifyObject(object); + return 1; + } + len = MAX(0, object->key_size - 24); + root = strdup_n((char*)object->key + 24, len); + if(root == NULL) { + abortObject(object, 503, internAtom("Couldn't allocate root")); + notifyObject(object); + return 1; + } + writeoutObjects(1); + fillSpecialObject(object, recursiveIndexDiskObjects, root); + free(root); + object->expires = current_time.tv_sec + 20; +#endif + } else if(matchUrl("/polipo/servers", object)) { + if(disableServersList) { + abortObject(object, 403, internAtom("Action not allowed")); + notifyObject(object); + return 1; + } + fillSpecialObject(object, serversList, NULL); + object->expires = current_time.tv_sec + 2; + } else { + abortObject(object, 404, internAtom("Not found")); + } + + object->flags &= ~OBJECT_VALIDATING; + notifyObject(object); + return 1; +} + +int +httpSpecialSideRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr requestor, void *closure) +{ + HTTPConnectionPtr client = requestor->connection; + + assert(client->request == requestor); + + if(method != METHOD_POST) { + httpClientError(requestor, 405, internAtom("Method not allowed")); + requestor->connection->flags &= ~CONN_READER; + return 1; + } + + return httpSpecialDoSide(requestor); +} + +int +httpSpecialDoSide(HTTPRequestPtr requestor) +{ + HTTPConnectionPtr client = requestor->connection; + + if(client->reqlen - client->reqbegin >= client->bodylen) { + AtomPtr data; + data = internAtomN(client->reqbuf + client->reqbegin, + client->reqlen - client->reqbegin); + client->reqbegin = 0; + client->reqlen = 0; + if(data == NULL) { + do_log(L_ERROR, "Couldn't allocate data.\n"); + httpClientError(requestor, 500, + internAtom("Couldn't allocate data")); + return 1; + } + httpSpecialDoSideFinish(data, requestor); + return 1; + } + + if(client->reqlen - client->reqbegin >= CHUNK_SIZE) { + httpClientError(requestor, 500, internAtom("POST too large")); + return 1; + } + + if(client->reqbegin > 0 && client->reqlen > client->reqbegin) { + memmove(client->reqbuf, client->reqbuf + client->reqbegin, + client->reqlen - client->reqbegin); + } + client->reqlen -= client->reqbegin; + client->reqbegin = 0; + + do_stream(IO_READ | IO_NOTNOW, client->fd, + client->reqlen, client->reqbuf, CHUNK_SIZE, + httpSpecialClientSideHandler, client); + return 1; +} + +int +httpSpecialClientSideHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + int push; + + if((request->object->flags & OBJECT_ABORTED) || + !(request->object->flags & OBJECT_INPROGRESS)) { + httpClientDiscardBody(connection); + httpClientError(request, 503, internAtom("Post aborted")); + return 1; + } + + if(status < 0) { + do_log_error(L_ERROR, -status, "Reading from client"); + if(status == -EDOGRACEFUL) + httpClientFinish(connection, 1); + else + httpClientFinish(connection, 2); + return 1; + } + + push = MIN(srequest->offset - connection->reqlen, + connection->bodylen - connection->reqoffset); + if(push > 0) { + connection->reqlen += push; + httpSpecialDoSide(request); + } + + do_log(L_ERROR, "Incomplete client request.\n"); + connection->flags &= ~CONN_READER; + httpClientRawError(connection, 502, + internAtom("Incomplete client request"), 1); + return 1; +} + +int +httpSpecialDoSideFinish(AtomPtr data, HTTPRequestPtr requestor) +{ + ObjectPtr object = requestor->object; + + if(matchUrl("/polipo/config", object)) { + AtomListPtr list = NULL; + int i, rc; + + if(disableConfiguration) { + abortObject(object, 403, internAtom("Action not allowed")); + goto out; + } + + list = urlDecode(data->string, data->length); + if(list == NULL) { + abortObject(object, 400, + internAtom("Couldn't parse variable to set")); + goto out; + } + for(i = 0; i < list->length; i++) { + rc = parseConfigLine(list->list[i]->string, NULL, 0, 1); + if(rc < 0) { + abortObject(object, 400, + rc == -1 ? + internAtom("Couldn't parse variable to set") : + internAtom("Variable is not settable")); + destroyAtomList(list); + goto out; + } + } + destroyAtomList(list); + object->date = current_time.tv_sec; + object->age = current_time.tv_sec; + object->headers = internAtom("\r\nLocation: /polipo/config?"); + object->code = 303; + object->message = internAtom("Done"); + object->flags &= ~OBJECT_INITIAL; + object->length = 0; + } else if(matchUrl("/polipo/status", object)) { + AtomListPtr list = NULL; + int i; + + if(disableConfiguration) { + abortObject(object, 403, internAtom("Action not allowed")); + goto out; + } + + list = urlDecode(data->string, data->length); + if(list == NULL) { + abortObject(object, 400, + internAtom("Couldn't parse action")); + goto out; + } + for(i = 0; i < list->length; i++) { + char *equals = + memchr(list->list[i]->string, '=', list->list[i]->length); + AtomPtr name = + equals ? + internAtomN(list->list[i]->string, + equals - list->list[i]->string) : + retainAtom(list->list[i]); + if(name == atomInitForbidden) + initForbidden(); + else if(name == atomReopenLog) + reopenLog(); + else if(name == atomDiscardObjects) + discardObjects(1, 0); + else if(name == atomWriteoutObjects) + writeoutObjects(1); + else if(name == atomFreeChunkArenas) + free_chunk_arenas(); + else { + abortObject(object, 400, internAtomF("Unknown action %s", + name->string)); + releaseAtom(name); + destroyAtomList(list); + goto out; + } + releaseAtom(name); + } + destroyAtomList(list); + object->date = current_time.tv_sec; + object->age = current_time.tv_sec; + object->headers = internAtom("\r\nLocation: /polipo/status?"); + object->code = 303; + object->message = internAtom("Done"); + object->flags &= ~OBJECT_INITIAL; + object->length = 0; + } else { + abortObject(object, 405, internAtom("Method not allowed")); + } + + out: + releaseAtom(data); + notifyObject(object); + requestor->connection->flags &= ~CONN_READER; + return 1; +} + +#ifdef HAVE_FORK +static void +fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure) +{ + int rc; + int filedes[2]; + pid_t pid; + sigset_t ss, old_mask; + + if(object->flags & OBJECT_INPROGRESS) + return; + + rc = pipe(filedes); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't create pipe"); + abortObject(object, 503, + internAtomError(errno, "Couldn't create pipe")); + return; + } + + fflush(stdout); + fflush(stderr); + flushLog(); + + /* Block signals that we handle specially until the child can + disable the handlers. */ + interestingSignals(&ss); + /* I'm a little confused. POSIX doesn't allow EINTR here, but I + think that both Linux and SVR4 do. */ + do { + rc = sigprocmask(SIG_BLOCK, &ss, &old_mask); + } while (rc < 0 && errno == EINTR); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Sigprocmask failed"); + abortObject(object, 503, internAtomError(errno, "Sigprocmask failed")); + close(filedes[0]); + close(filedes[1]); + return; + } + + pid = fork(); + if(pid < 0) { + do_log_error(L_ERROR, errno, "Couldn't fork"); + abortObject(object, 503, internAtomError(errno, "Couldn't fork")); + close(filedes[0]); + close(filedes[1]); + do { + rc = sigprocmask(SIG_SETMASK, &old_mask, NULL); + } while (rc < 0 && errno == EINTR); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't restore signal mask"); + polipoExit(); + } + return; + } + + if(pid > 0) { + SpecialRequestPtr request; + close(filedes[1]); + do { + rc = sigprocmask(SIG_SETMASK, &old_mask, NULL); + } while (rc < 0 && errno == EINTR); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't restore signal mask"); + polipoExit(); + return; + } + + request = malloc(sizeof(SpecialRequestRec)); + if(request == NULL) { + kill(pid, SIGTERM); + close(filedes[0]); + abortObject(object, 503, + internAtom("Couldn't allocate request\n")); + notifyObject(object); + /* specialRequestHandler will take care of the rest. */ + } else { + request->buf = get_chunk(); + if(request->buf == NULL) { + kill(pid, SIGTERM); + close(filedes[0]); + free(request); + abortObject(object, 503, + internAtom("Couldn't allocate request\n")); + notifyObject(object); + } + } + object->flags |= OBJECT_INPROGRESS; + retainObject(object); + request->object = object; + request->fd = filedes[0]; + request->pid = pid; + request->offset = 0; + /* Under any sensible scheduler, the child will run before the + parent. So no need for IO_NOTNOW. */ + do_stream(IO_READ, filedes[0], 0, request->buf, CHUNK_SIZE, + specialRequestHandler, request); + } else { + /* child */ + close(filedes[0]); + uninitEvents(); + do { + rc = sigprocmask(SIG_SETMASK, &old_mask, NULL); + } while (rc < 0 && errno == EINTR); + if(rc < 0) + exit(1); + + if(filedes[1] != 1) + dup2(filedes[1], 1); + + (*fn)(stdout, closure); + exit(0); + } +} + +int +specialRequestHandler(int status, + FdEventHandlerPtr event, StreamRequestPtr srequest) +{ + SpecialRequestPtr request = srequest->data; + int rc; + int killed = 0; + + if(status < 0) { + kill(request->pid, SIGTERM); + killed = 1; + request->object->flags &= ~OBJECT_INPROGRESS; + abortObject(request->object, 502, + internAtomError(-status, "Couldn't read from client")); + goto done; + } + + if(srequest->offset > 0) { + rc = objectAddData(request->object, request->buf, + request->offset, srequest->offset); + if(rc < 0) { + kill(request->pid, SIGTERM); + killed = 1; + request->object->flags &= ~OBJECT_INPROGRESS; + abortObject(request->object, 503, + internAtom("Couldn't add data to connection")); + goto done; + } + request->offset += srequest->offset; + } + if(status) { + request->object->flags &= ~OBJECT_INPROGRESS; + request->object->length = request->object->size; + goto done; + } + + /* If we're the only person interested in this object, let's abort + it now. */ + if(request->object->refcount <= 1) { + kill(request->pid, SIGTERM); + killed = 1; + request->object->flags &= ~OBJECT_INPROGRESS; + abortObject(request->object, 500, internAtom("Aborted")); + goto done; + } + notifyObject(request->object); + do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, CHUNK_SIZE, + specialRequestHandler, request); + return 1; + + done: + close(request->fd); + dispose_chunk(request->buf); + releaseNotifyObject(request->object); + /* That's a blocking wait. It shouldn't block for long, as we've + either already killed the child, or else we got EOF from it. */ + do { + rc = waitpid(request->pid, &status, 0); + } while(rc < 0 && errno == EINTR); + if(rc < 0) { + do_log(L_ERROR, "Wait for %d: %d\n", (int)request->pid, errno); + } else { + int normal = + (WIFEXITED(status) && WEXITSTATUS(status) == 0) || + (killed && WIFSIGNALED(status) && WTERMSIG(status) == SIGTERM); + char *reason = + WIFEXITED(status) ? "with status" : + WIFSIGNALED(status) ? "on signal" : + "with unknown status"; + int value = + WIFEXITED(status) ? WEXITSTATUS(status) : + WIFSIGNALED(status) ? WTERMSIG(status) : + status; + do_log(normal ? D_CHILD : L_ERROR, + "Child %d exited %s %d.\n", + (int)request->pid, reason, value); + } + free(request); + return 1; +} +#else +static void +fillSpecialObject(ObjectPtr object, void (*fn)(FILE*, char*), void* closure) +{ + FILE *tmp = NULL; + char *buf = NULL; + int rc, len, offset; + + if(object->flags & OBJECT_INPROGRESS) + return; + + buf = get_chunk(); + if(buf == NULL) { + abortObject(object, 503, internAtom("Couldn't allocate chunk")); + goto done; + } + + tmp = tmpfile(); + if(tmp == NULL) { + abortObject(object, 503, internAtom(pstrerror(errno))); + goto done; + } + + (*fn)(tmp, closure); + fflush(tmp); + + rewind(tmp); + offset = 0; + while(1) { + len = fread(buf, 1, CHUNK_SIZE, tmp); + if(len <= 0 && ferror(tmp)) { + abortObject(object, 503, internAtom(pstrerror(errno))); + goto done; + } + if(len <= 0) + break; + + rc = objectAddData(object, buf, offset, len); + if(rc < 0) { + abortObject(object, 503, internAtom("Couldn't add data to object")); + goto done; + } + + offset += len; + } + + object->length = offset; + + done: + if(buf) + dispose_chunk(buf); + if(tmp) + fclose(tmp); + notifyObject(object); +} +#endif diff --git a/jni/polipo/local.h b/jni/polipo/local.h new file mode 100644 index 00000000..24dde5ac --- /dev/null +++ b/jni/polipo/local.h @@ -0,0 +1,48 @@ +/* +Copyright (c) 2003, 2004 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +typedef struct _SpecialRequest { + ObjectPtr object; + int fd; + void *buf; + int offset; + pid_t pid; +} SpecialRequestRec, *SpecialRequestPtr; + +extern int disableConfiguration; +extern int disableIndexing; + +void preinitLocal(void); +void alternatingHttpStyle(FILE *out, char *id); +int httpLocalRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr, void *); +int httpSpecialRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr, void*); +int httpSpecialSideRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr requestor, void *closure); +int specialRequestHandler(int status, + FdEventHandlerPtr event, StreamRequestPtr request); +int httpSpecialDoSide(HTTPRequestPtr requestor); +int httpSpecialClientSideHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest); +int httpSpecialDoSideFinish(AtomPtr data, HTTPRequestPtr requestor); diff --git a/jni/polipo/log.c b/jni/polipo/log.c new file mode 100644 index 00000000..ffa7032b --- /dev/null +++ b/jni/polipo/log.c @@ -0,0 +1,492 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifdef HAVE_SYSLOG +#include +#endif + +static int logLevel = LOGGING_DEFAULT; +static int logSyslog = 0; +static AtomPtr logFile = NULL; +static FILE *logF; +static int logFilePermissions = 0640; +int scrubLogs = 0; + +#ifdef HAVE_SYSLOG +static AtomPtr logFacility = NULL; +static int facility; +#endif + +#define STR(x) XSTR(x) +#define XSTR(x) #x + +static void initSyslog(void); + +#ifdef HAVE_SYSLOG +static char *syslogBuf; +static int syslogBufSize; +static int syslogBufLength; + +static int translateFacility(AtomPtr facility); +static int translatePriority(int type); +static void accumulateSyslogV(int type, const char *f, va_list args); +static void accumulateSyslogN(int type, const char *s, int len); +#endif + +void +preinitLog() +{ + CONFIG_VARIABLE_SETTABLE(logLevel, CONFIG_HEX, configIntSetter, + "Logging level (max = " STR(LOGGING_MAX) ")."); + CONFIG_VARIABLE(logFile, CONFIG_ATOM, "Log file (stderr if empty and logSyslog is unset, /var/log/polipo if empty and daemonise is true)."); + CONFIG_VARIABLE(logFilePermissions, CONFIG_OCTAL, + "Access rights of the logFile."); + CONFIG_VARIABLE_SETTABLE(scrubLogs, CONFIG_BOOLEAN, configIntSetter, + "If true, don't include URLs in logs."); + +#ifdef HAVE_SYSLOG + CONFIG_VARIABLE(logSyslog, CONFIG_BOOLEAN, "Log to syslog."); + CONFIG_VARIABLE(logFacility, CONFIG_ATOM, "Syslog facility to use."); + logFacility = internAtom("user"); +#endif + + logF = stderr; +} + +int +loggingToStderr(void) { + return(logF == stderr); +} + +static FILE * +openLogFile(void) +{ + int fd; + FILE *f; + + fd = open(logFile->string, O_WRONLY | O_CREAT | O_APPEND, + logFilePermissions); + if(fd < 0) + return NULL; + + f = fdopen(fd, "a"); + if(f == NULL) { + int saved_errno = errno; + close(fd); + errno = saved_errno; + return NULL; + } + + setvbuf(f, NULL, _IOLBF, 0); + return f; +} + +void +initLog(void) +{ + if(daemonise && logFile == NULL && !logSyslog) + logFile = internAtom("/var/log/polipo"); + + if(logFile != NULL && logFile->length > 0) { + FILE *f; + f = openLogFile(); + if(f == NULL) { + do_log_error(L_ERROR, errno, "Couldn't open log file %s", + logFile->string); + exit(1); + } + logF = f; + } + + if(logSyslog) { + initSyslog(); + + if(logFile == NULL) { + logF = NULL; + } + } +} + +#ifdef HAVE_SYSLOG +static void +initSyslog() +{ + if(logSyslog) { + facility = translateFacility(logFacility); + closelog(); + openlog("polipo", LOG_PID, facility); + + if(!syslogBuf) { + syslogBuf = strdup(""); + syslogBufSize = 1; + } + } +} + +/* Map a user-provided name to a syslog facility. + + This is rendered quite ugly because POSIX hardly defines any, but we + should allow any the local system knows about. */ + +static int +translateFacility(AtomPtr facility) +{ + typedef struct + { + const char *name; + int facility; + } FacilitiesRec; + + /* List of all known valid syslog facilities. + + This list is terminated by a NULL facility name. */ + + FacilitiesRec facilities[] = { + /* These are all the facilities found in glibc 2.5. */ +#ifdef LOG_AUTH + { "auth", LOG_AUTH }, +#endif +#ifdef LOG_AUTHPRIV + { "authpriv", LOG_AUTHPRIV }, +#endif +#ifdef LOG_CRON + { "cron", LOG_CRON }, +#endif +#ifdef LOG_DAEMON + { "daemon", LOG_DAEMON }, +#endif +#ifdef LOG_FTP + { "ftp", LOG_FTP }, +#endif +#ifdef LOG_KERN + { "kern", LOG_KERN }, +#endif +#ifdef LOG_LPR + { "lpr", LOG_LPR }, +#endif +#ifdef LOG_MAIL + { "mail", LOG_MAIL }, +#endif +#ifdef LOG_NEWS + { "news", LOG_NEWS }, +#endif +#ifdef LOG_SYSLOG + { "syslog", LOG_SYSLOG }, +#endif +#ifdef LOG_uucp + { "uucp", LOG_UUCP }, +#endif + /* These are required by POSIX. */ + { "user", LOG_USER }, + { "local0", LOG_LOCAL0 }, + { "local1", LOG_LOCAL1 }, + { "local2", LOG_LOCAL2 }, + { "local3", LOG_LOCAL3 }, + { "local4", LOG_LOCAL4 }, + { "local5", LOG_LOCAL5 }, + { "local6", LOG_LOCAL6 }, + { "local7", LOG_LOCAL7 }, + { NULL, 0 }}; + + FacilitiesRec *current; + + /* It would be more fitting to return LOG_DAEMON, but POSIX does not + guarantee the existence of that facility. */ + + if(!facility) { + return LOG_USER; + } + + current = facilities; + while(current->name) { + if(!strcmp(current->name, atomString(facility))) { + return current->facility; + } + current++; + } + + /* This will go to stderr because syslog is not yet initialized. */ + do_log(L_ERROR, "Specified logFacility %s nonexistent on this system.", + atomString(facility)); + + return LOG_USER; +} + +/* Translate a Polipo error type into a syslog priority. */ + +static int +translatePriority(int type) +{ + typedef struct + { + int type; + int priority; + } PrioritiesRec; + + /* The list is terminated with a type of zero. */ + + PrioritiesRec priorities[] = {{ L_ERROR, LOG_ERR }, + { L_WARN, LOG_WARNING }, + { L_INFO, LOG_NOTICE }, + { L_FORBIDDEN, LOG_DEBUG }, + { L_UNCACHEABLE, LOG_DEBUG }, + { L_SUPERSEDED, LOG_DEBUG }, + { L_VARY, LOG_DEBUG }, + { L_TUNNEL, LOG_NOTICE }, + { 0, 0 }}; + PrioritiesRec *current; + + current = priorities; + while(current->type) { + if(current->type == type) { + return current->priority; + } + current++; + } + + return LOG_DEBUG; +} + +static int +expandSyslog(int len) +{ + int newsize; + char *newbuf; + + if(len < 0) + newsize = syslogBufSize * 2; + else + newsize = syslogBufLength + len + 1; + + newbuf = realloc(syslogBuf, newsize); + if(!newbuf) + return -1; + + syslogBuf = newbuf; + syslogBufSize = newsize; + return 1; +} + +static void +maybeFlushSyslog(int type) +{ + char *linefeed; + while(1) { + linefeed = memchr(syslogBuf, '\n', syslogBufLength); + if(linefeed == NULL) + break; + *linefeed = '\0'; + syslog(translatePriority(type), "%s", syslogBuf); + linefeed++; + syslogBufLength -= (linefeed - syslogBuf); + if(syslogBufLength > 0) + memmove(syslogBuf, linefeed, syslogBufLength); + } +} + +#ifndef va_copy +#define va_copy(a, b) do { a = b; } while(0) +#endif + +static void +accumulateSyslogV(int type, const char *f, va_list args) +{ + int rc; + va_list args_copy; + + again: + va_copy(args_copy, args); + rc = vsnprintf(syslogBuf + syslogBufLength, + syslogBufSize - syslogBufLength, + f, args_copy); + va_end(args_copy); + + if(rc < 0 || rc >= syslogBufSize - syslogBufLength) { + rc = expandSyslog(rc); + if(rc < 0) + return; + goto again; + } + + syslogBufLength += rc; + + maybeFlushSyslog(type); +} + +static void +accumulateSyslogN(int type, const char *s, int len) +{ + while(syslogBufSize - syslogBufLength <= len) + expandSyslog(len); + + memcpy(syslogBuf + syslogBufLength, s, len); + syslogBufLength += len; + syslogBuf[syslogBufLength] = '\0'; + + maybeFlushSyslog(type); +} + +#else +static void +initSyslog() +{ + return; +} +#endif + +/* Flush any messages waiting to be logged. */ +void flushLog() +{ + if(logF) + fflush(logF); + +#ifdef HAVE_SYSLOG + /* There shouldn't really be anything here, but let's be paranoid. + We can't pick a good value for `type', so just invent one. */ + if(logSyslog && syslogBuf[0] != '\0') { + accumulateSyslogN(L_INFO, "\n", 1); + } + + assert(syslogBufLength == 0); +#endif +} + +void +reopenLog() +{ + if(logFile && logFile->length > 0) { + FILE *f; + f = openLogFile(); + if(f == NULL) { + do_log_error(L_ERROR, errno, "Couldn't reopen log file %s", + logFile->string); + exit(1); + } + fclose(logF); + logF = f; + } + + if(logSyslog) + initSyslog(); +} + +void +really_do_log(int type, const char *f, ...) +{ + va_list args; + + va_start(args, f); + if(type & LOGGING_MAX & logLevel) + really_do_log_v(type, f, args); + va_end(args); +} + +void +really_do_log_v(int type, const char *f, va_list args) +{ + if(type & LOGGING_MAX & logLevel) { + if(logF) + { + va_list args_copy; + va_copy(args_copy, args); + vfprintf(logF, f, args_copy); + va_end(args_copy); + } +#ifdef HAVE_SYSLOG + if(logSyslog) + accumulateSyslogV(type, f, args); +#endif + } +} + +void +really_do_log_error(int type, int e, const char *f, ...) +{ + va_list args; + va_start(args, f); + if(type & LOGGING_MAX & logLevel) + really_do_log_error_v(type, e, f, args); + va_end(args); +} + +void +really_do_log_error_v(int type, int e, const char *f, va_list args) +{ + if((type & LOGGING_MAX & logLevel) != 0) { + char *es = pstrerror(e); + if(es == NULL) + es = "Unknown error"; + + if(logF) { + va_list args_copy; + va_copy(args_copy, args); + vfprintf(logF, f, args_copy); + fprintf(logF, ": %s\n", es); + va_end(args_copy); + } +#ifdef HAVE_SYSLOG + if(logSyslog) { + char msg[256]; + int n = 0; + + n = snnvprintf(msg, n, 256, f, args); + n = snnprintf(msg, n, 256, ": "); + n = snnprint_n(msg, n, 256, es, strlen (es)); + n = snnprintf(msg, n, 256, "\n"); + /* Overflow? Vanishingly unlikely; truncate at 255. */ + if(n < 0 || n > 256) { + n = 256; + msg[255] = '\0'; + } + else + msg[n] = '\0'; + + accumulateSyslogN(type, msg, n); + } +#endif + } +} + +void +really_do_log_n(int type, const char *s, int n) +{ + if((type & LOGGING_MAX & logLevel) != 0) { + if(logF) { + fwrite(s, n, 1, logF); + } +#ifdef HAVE_SYSLOG + if(logSyslog) + accumulateSyslogN(type, s, n); +#endif + } +} + +const char * +scrub(const char *message) +{ + if(scrubLogs) + return "(scrubbed)"; + else + return message; +} diff --git a/jni/polipo/log.h b/jni/polipo/log.h new file mode 100644 index 00000000..40d15fdd --- /dev/null +++ b/jni/polipo/log.h @@ -0,0 +1,140 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#define L_ERROR 0x1 +#define L_WARN 0x2 +#define L_INFO 0x4 +#define L_FORBIDDEN 0x8 +#define L_UNCACHEABLE 0x10 +#define L_SUPERSEDED 0x20 +#define L_VARY 0x40 +#define L_TUNNEL 0x80 + +#define D_SERVER_CONN 0x100 +#define D_SERVER_REQ 0x200 +#define D_CLIENT_CONN 0x400 +#define D_CLIENT_REQ 0x800 +#define D_ATOM_REFCOUNT 0x1000 +#define D_REFCOUNT 0x2000 +#define D_LOCK 0x4000 +#define D_OBJECT 0x8000 +#define D_OBJECT_DATA 0x10000 +#define D_SERVER_OFFSET 0x20000 +#define D_CLIENT_DATA 0x40000 +#define D_DNS 0x80000 +#define D_CHILD 0x100000 +#define D_IO 0x200000 + +#define LOGGING_DEFAULT (L_ERROR | L_WARN | L_INFO) +#define LOGGING_MAX 0xFF + +extern int scrubLogs; + +void preinitLog(void); +void initLog(void); +void reopenLog(void); +void flushLog(void); +int loggingToStderr(void); + +void really_do_log(int type, const char *f, ...) + ATTRIBUTE ((format (printf, 2, 3))); +void really_do_log_v(int type, const char *f, va_list args) + ATTRIBUTE ((format (printf, 2, 0))); +void really_do_log_n(int type, const char *s, int n); +void really_do_log_error(int type, int e, const char *f, ...) + ATTRIBUTE ((format (printf, 3, 4))); +void really_do_log_error_v(int type, int e, const char *f, va_list args) + ATTRIBUTE ((format (printf, 3, 0))); +const char *scrub(const char *message); + +#ifdef __GNUC__ +#define DO_BACKTRACE() \ + do { \ + int n; \ + void *buffer[10]; \ + n = backtrace(buffer, 5); \ + fflush(stderr); \ + backtrace_symbols_fd(buffer, n, 2); \ + } while(0) +#else +#define DO_BACKTRACE() /* */ +#endif + +/* These are macros because it's important that they should be + optimised away. */ + +#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L + +#define do_log(_type, ...) \ + do { \ + if((_type) & (LOGGING_MAX)) really_do_log((_type), __VA_ARGS__); \ + } while(0) +#define do_log_error(_type, _e, ...) \ + do { \ + if((_type) & (LOGGING_MAX)) \ + really_do_log_error((_type), (_e), __VA_ARGS__); \ + } while(0) + +#elif defined(__GNUC__) + +#define do_log(_type, _args...) \ + do { \ + if((_type) & (LOGGING_MAX)) really_do_log((_type), _args); \ + } while(0) +#define do_log_error(_type, _e, _args...) \ + do { \ + if((_type) & (LOGGING_MAX)) \ + really_do_log_error((_type), (_e), _args); \ + } while(0) + +#else + +/* No variadic macros -- let's hope inline works. */ + +static inline void +do_log(int type, const char *f, ...) +{ + va_list args; + + va_start(args, f); + if((type & (LOGGING_MAX)) != 0) + really_do_log_v(type, f, args); + va_end(args); +} + +static inline void +do_log_error(int type, int e, const char *f, ...) +{ + va_list args; + + va_start(args, f); + if((type & (LOGGING_MAX)) != 0) + really_do_log_error_v(type, e, f, args); + va_end(args); +} + +#endif + +#define do_log_n(_type, _s, _n) \ + do { \ + if((_type) & (LOGGING_MAX)) really_do_log_n((_type), (_s), (_n)); \ + } while(0) diff --git a/jni/polipo/main.c b/jni/polipo/main.c new file mode 100644 index 00000000..39c83b22 --- /dev/null +++ b/jni/polipo/main.c @@ -0,0 +1,169 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +AtomPtr configFile = NULL; +AtomPtr pidFile = NULL; +int daemonise = 0; + +static void +usage(char *argv0) +{ + fprintf(stderr, + "%s [ -h ] [ -v ] [ -x ] [ -c filename ] [ -- ] [ var=val... ]\n", + argv0); + fprintf(stderr, " -h: display this message.\n"); + fprintf(stderr, " -v: display the list of configuration variables.\n"); + fprintf(stderr, " -x: perform expiry on the disk cache.\n"); + fprintf(stderr, " -c: specify the configuration file to use.\n"); +} + +int +main(int argc, char **argv) +{ + FdEventHandlerPtr listener; + int i; + int rc; + int expire = 0, printConfig = 0; + + initAtoms(); + CONFIG_VARIABLE(daemonise, CONFIG_BOOLEAN, "Run as a daemon"); + CONFIG_VARIABLE(pidFile, CONFIG_ATOM, "File with pid of running daemon."); + + preinitChunks(); + preinitLog(); + preinitObject(); + preinitIo(); + preinitDns(); + preinitServer(); + preinitHttp(); + preinitDiskcache(); + preinitLocal(); + preinitForbidden(); + preinitSocks(); + + i = 1; + while(i < argc) { + if(argv[i][0] != '-') + break; + if(strcmp(argv[i], "--") == 0) { + i++; + break; + } else if(strcmp(argv[i], "-h") == 0) { + usage(argv[0]); + exit(0); + } else if(strcmp(argv[i], "-v") == 0) { + printConfig = 1; + i++; + } else if(strcmp(argv[i], "-x") == 0) { + expire = 1; + i++; + } else if(strcmp(argv[i], "-c") == 0) { + i++; + if(i >= argc) { + usage(argv[0]); + exit(1); + } + if(configFile) + releaseAtom(configFile); + configFile = internAtom(argv[i]); + i++; + } else { + usage(argv[0]); + exit(1); + } + } + + if(configFile) + configFile = expandTilde(configFile); + + if(configFile == NULL) { + configFile = expandTilde(internAtom("~/.polipo")); + if(configFile) + if(access(configFile->string, F_OK) < 0) { + releaseAtom(configFile); + configFile = NULL; + } + } + + if(configFile == NULL) { + if(access("/etc/polipo/config", F_OK) >= 0) + configFile = internAtom("/etc/polipo/config"); + if(configFile && access(configFile->string, F_OK) < 0) { + releaseAtom(configFile); + configFile = NULL; + } + } + + rc = parseConfigFile(configFile); + if(rc < 0) + exit(1); + + while(i < argc) { + rc = parseConfigLine(argv[i], "command line", 0, 0); + if(rc < 0) + exit(1); + i++; + } + + initChunks(); + initLog(); + initObject(); + if(!expire && !printConfig) + initEvents(); + initIo(); + initDns(); + initHttp(); + initServer(); + initDiskcache(); + initForbidden(); + initSocks(); + + if(printConfig) { + printConfigVariables(stdout, 0); + exit(0); + } + + if(expire) { + expireDiskObjects(); + exit(0); + } + + if(daemonise) + do_daemonise(loggingToStderr()); + + if(pidFile) + writePid(pidFile->string); + + listener = create_listener(proxyAddress->string, + proxyPort, httpAccept, NULL); + if(!listener) { + if(pidFile) unlink(pidFile->string); + exit(1); + } + + eventLoop(); + + if(pidFile) unlink(pidFile->string); + return 0; +} diff --git a/jni/polipo/md5.c b/jni/polipo/md5.c new file mode 100644 index 00000000..f997a6ec --- /dev/null +++ b/jni/polipo/md5.c @@ -0,0 +1,322 @@ +/* + *********************************************************************** + ** md5.c -- the source code for MD5 routines ** + ** RSA Data Security, Inc. MD5 Message-Digest Algorithm ** + ** Created: 2/17/90 RLR ** + ** Revised: 1/91 SRD,AJ,BSK,JT Reference C Version ** + ** Revised (for MD5): RLR 4/27/91 ** + ** -- G modified to have y&~z instead of y&z ** + ** -- FF, GG, HH modified to add in last register done ** + ** -- Access pattern: round 2 works mod 5, round 3 works mod 3 ** + ** -- distinct additive constant for each step ** + ** -- round 4 added, working mod 7 ** + *********************************************************************** + */ + +/* + *********************************************************************** + ** Copyright (C) 1990, RSA Data Security, Inc. All rights reserved. ** + ** ** + ** License to copy and use this software is granted provided that ** + ** it is identified as the "RSA Data Security, Inc. MD5 Message- ** + ** Digest Algorithm" in all material mentioning or referencing this ** + ** software or this function. ** + ** ** + ** License is also granted to make and use derivative works ** + ** provided that such works are identified as "derived from the RSA ** + ** Data Security, Inc. MD5 Message-Digest Algorithm" in all ** + ** material mentioning or referencing the derived work. ** + ** ** + ** RSA Data Security, Inc. makes no representations concerning ** + ** either the merchantability of this software or the suitability ** + ** of this software for any particular purpose. It is provided "as ** + ** is" without express or implied warranty of any kind. ** + ** ** + ** These notices must be retained in any copies of any part of this ** + ** documentation and/or software. ** + *********************************************************************** + */ + +#include "md5.h" + +/* + *********************************************************************** + ** Message-digest routines: ** + ** To form the message digest for a message M ** + ** (1) Initialize a context buffer mdContext using MD5Init ** + ** (2) Call MD5Update on mdContext and M ** + ** (3) Call MD5Final on mdContext ** + ** The message digest is now in mdContext->digest[0...15] ** + *********************************************************************** + */ + +/* forward declaration */ +static void Transform (UINT4 *, UINT4 *); + +#ifdef __STDC__ +static const +#else +static +#endif +unsigned char PADDING[64] = { + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +/* F, G, H and I are basic MD5 functions */ +#define F(x, y, z) (((x) & (y)) | ((~x) & (z))) +#define G(x, y, z) (((x) & (z)) | ((y) & (~z))) +#define H(x, y, z) ((x) ^ (y) ^ (z)) +#define I(x, y, z) ((y) ^ ((x) | (~z))) + +/* ROTATE_LEFT rotates x left n bits */ +#if defined(FAST_MD5) && defined(__GNUC__) && defined(mc68000) +/* + * If we're on a 68000 based CPU and using a GNU C compiler with + * inline assembly code, we can speed this up a bit. + */ +inline UINT4 ROTATE_LEFT(UINT4 x, int n) +{ + asm("roll %2,%0" : "=d" (x) : "0" (x), "Ir" (n)); + return x; +} +#else +#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n)))) +#endif + + +/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4 */ +/* Rotation is separate from addition to prevent recomputation */ +#define FF(a, b, c, d, x, s, ac) \ + {(a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } +#define GG(a, b, c, d, x, s, ac) \ + {(a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } +#define HH(a, b, c, d, x, s, ac) \ + {(a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } +#define II(a, b, c, d, x, s, ac) \ + {(a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \ + (a) = ROTATE_LEFT ((a), (s)); \ + (a) += (b); \ + } + +/* The routine MD5Init initializes the message-digest context + mdContext. All fields are set to zero. + */ +void MD5Init (mdContext) +MD5_CTX *mdContext; +{ + mdContext->i[0] = mdContext->i[1] = (UINT4)0; + + /* Load magic initialization constants. + */ + mdContext->buf[0] = (UINT4)0x67452301; + mdContext->buf[1] = (UINT4)0xefcdab89; + mdContext->buf[2] = (UINT4)0x98badcfe; + mdContext->buf[3] = (UINT4)0x10325476; +} + +/* The routine MD5Update updates the message-digest context to + account for the presence of each of the characters inBuf[0..inLen-1] + in the message whose digest is being computed. + */ +void MD5Update (mdContext, inBuf, inLen) +MD5_CTX *mdContext; +unsigned const char *inBuf; +unsigned int inLen; +{ + UINT4 in[16]; + int mdi; + unsigned int i, ii; + + /* compute number of bytes mod 64 */ + mdi = (int)((mdContext->i[0] >> 3) & 0x3F); + + /* update number of bits */ + if ((mdContext->i[0] + ((UINT4)inLen << 3)) < mdContext->i[0]) + mdContext->i[1]++; + mdContext->i[0] += ((UINT4)inLen << 3); + mdContext->i[1] += ((UINT4)inLen >> 29); + + while (inLen--) { + /* add new character to buffer, increment mdi */ + mdContext->in[mdi++] = *inBuf++; + + /* transform if necessary */ + if (mdi == 0x40) { + for (i = 0, ii = 0; i < 16; i++, ii += 4) + in[i] = (((UINT4)mdContext->in[ii+3]) << 24) | + (((UINT4)mdContext->in[ii+2]) << 16) | + (((UINT4)mdContext->in[ii+1]) << 8) | + ((UINT4)mdContext->in[ii]); + Transform (mdContext->buf, in); + mdi = 0; + } + } +} + +/* The routine MD5Final terminates the message-digest computation and + ends with the desired message digest in mdContext->digest[0...15]. + */ + +void MD5Final (mdContext) +MD5_CTX *mdContext; +{ + UINT4 in[16]; + int mdi; + unsigned int i, ii; + unsigned int padLen; + + /* save number of bits */ + in[14] = mdContext->i[0]; + in[15] = mdContext->i[1]; + + /* compute number of bytes mod 64 */ + mdi = (int)((mdContext->i[0] >> 3) & 0x3F); + + /* pad out to 56 mod 64 */ + padLen = (mdi < 56) ? (56 - mdi) : (120 - mdi); + MD5Update (mdContext, PADDING, padLen); + + /* append length in bits and transform */ + for (i = 0, ii = 0; i < 14; i++, ii += 4) + in[i] = (((UINT4)mdContext->in[ii+3]) << 24) | + (((UINT4)mdContext->in[ii+2]) << 16) | + (((UINT4)mdContext->in[ii+1]) << 8) | + ((UINT4)mdContext->in[ii]); + Transform (mdContext->buf, in); + + /* store buffer in digest */ + for (i = 0, ii = 0; i < 4; i++, ii += 4) { + mdContext->digest[ii] = (unsigned char)(mdContext->buf[i] & 0xFF); + mdContext->digest[ii+1] = + (unsigned char)((mdContext->buf[i] >> 8) & 0xFF); + mdContext->digest[ii+2] = + (unsigned char)((mdContext->buf[i] >> 16) & 0xFF); + mdContext->digest[ii+3] = + (unsigned char)((mdContext->buf[i] >> 24) & 0xFF); + } +} + +/* Basic MD5 step. Transforms buf based on in. + */ +static void Transform (buf, in) +UINT4 *buf; +UINT4 *in; +{ + UINT4 a = buf[0], b = buf[1], c = buf[2], d = buf[3]; + + /* Round 1 */ +#define S11 7 +#define S12 12 +#define S13 17 +#define S14 22 + + FF ( a, b, c, d, in[ 0], S11, 0xd76aa478); /* 1 */ + FF ( d, a, b, c, in[ 1], S12, 0xe8c7b756); /* 2 */ + FF ( c, d, a, b, in[ 2], S13, 0x242070db); /* 3 */ + FF ( b, c, d, a, in[ 3], S14, 0xc1bdceee); /* 4 */ + FF ( a, b, c, d, in[ 4], S11, 0xf57c0faf); /* 5 */ + FF ( d, a, b, c, in[ 5], S12, 0x4787c62a); /* 6 */ + FF ( c, d, a, b, in[ 6], S13, 0xa8304613); /* 7 */ + FF ( b, c, d, a, in[ 7], S14, 0xfd469501); /* 8 */ + FF ( a, b, c, d, in[ 8], S11, 0x698098d8); /* 9 */ + FF ( d, a, b, c, in[ 9], S12, 0x8b44f7af); /* 10 */ + FF ( c, d, a, b, in[10], S13, 0xffff5bb1); /* 11 */ + FF ( b, c, d, a, in[11], S14, 0x895cd7be); /* 12 */ + FF ( a, b, c, d, in[12], S11, 0x6b901122); /* 13 */ + FF ( d, a, b, c, in[13], S12, 0xfd987193); /* 14 */ + FF ( c, d, a, b, in[14], S13, 0xa679438e); /* 15 */ + FF ( b, c, d, a, in[15], S14, 0x49b40821); /* 16 */ + + /* Round 2 */ +#define S21 5 +#define S22 9 +#define S23 14 +#define S24 20 + GG ( a, b, c, d, in[ 1], S21, 0xf61e2562); /* 17 */ + GG ( d, a, b, c, in[ 6], S22, 0xc040b340); /* 18 */ + GG ( c, d, a, b, in[11], S23, 0x265e5a51); /* 19 */ + GG ( b, c, d, a, in[ 0], S24, 0xe9b6c7aa); /* 20 */ + GG ( a, b, c, d, in[ 5], S21, 0xd62f105d); /* 21 */ + GG ( d, a, b, c, in[10], S22, 0x2441453); /* 22 */ + GG ( c, d, a, b, in[15], S23, 0xd8a1e681); /* 23 */ + GG ( b, c, d, a, in[ 4], S24, 0xe7d3fbc8); /* 24 */ + GG ( a, b, c, d, in[ 9], S21, 0x21e1cde6); /* 25 */ + GG ( d, a, b, c, in[14], S22, 0xc33707d6); /* 26 */ + GG ( c, d, a, b, in[ 3], S23, 0xf4d50d87); /* 27 */ + GG ( b, c, d, a, in[ 8], S24, 0x455a14ed); /* 28 */ + GG ( a, b, c, d, in[13], S21, 0xa9e3e905); /* 29 */ + GG ( d, a, b, c, in[ 2], S22, 0xfcefa3f8); /* 30 */ + GG ( c, d, a, b, in[ 7], S23, 0x676f02d9); /* 31 */ + GG ( b, c, d, a, in[12], S24, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ +#define S31 4 +#define S32 11 +#define S33 16 +#define S34 23 + HH ( a, b, c, d, in[ 5], S31, 0xfffa3942); /* 33 */ + HH ( d, a, b, c, in[ 8], S32, 0x8771f681); /* 34 */ + HH ( c, d, a, b, in[11], S33, 0x6d9d6122); /* 35 */ + HH ( b, c, d, a, in[14], S34, 0xfde5380c); /* 36 */ + HH ( a, b, c, d, in[ 1], S31, 0xa4beea44); /* 37 */ + HH ( d, a, b, c, in[ 4], S32, 0x4bdecfa9); /* 38 */ + HH ( c, d, a, b, in[ 7], S33, 0xf6bb4b60); /* 39 */ + HH ( b, c, d, a, in[10], S34, 0xbebfbc70); /* 40 */ + HH ( a, b, c, d, in[13], S31, 0x289b7ec6); /* 41 */ + HH ( d, a, b, c, in[ 0], S32, 0xeaa127fa); /* 42 */ + HH ( c, d, a, b, in[ 3], S33, 0xd4ef3085); /* 43 */ + HH ( b, c, d, a, in[ 6], S34, 0x4881d05); /* 44 */ + HH ( a, b, c, d, in[ 9], S31, 0xd9d4d039); /* 45 */ + HH ( d, a, b, c, in[12], S32, 0xe6db99e5); /* 46 */ + HH ( c, d, a, b, in[15], S33, 0x1fa27cf8); /* 47 */ + HH ( b, c, d, a, in[ 2], S34, 0xc4ac5665); /* 48 */ + + /* Round 4 */ +#define S41 6 +#define S42 10 +#define S43 15 +#define S44 21 + II ( a, b, c, d, in[ 0], S41, 0xf4292244); /* 49 */ + II ( d, a, b, c, in[ 7], S42, 0x432aff97); /* 50 */ + II ( c, d, a, b, in[14], S43, 0xab9423a7); /* 51 */ + II ( b, c, d, a, in[ 5], S44, 0xfc93a039); /* 52 */ + II ( a, b, c, d, in[12], S41, 0x655b59c3); /* 53 */ + II ( d, a, b, c, in[ 3], S42, 0x8f0ccc92); /* 54 */ + II ( c, d, a, b, in[10], S43, 0xffeff47d); /* 55 */ + II ( b, c, d, a, in[ 1], S44, 0x85845dd1); /* 56 */ + II ( a, b, c, d, in[ 8], S41, 0x6fa87e4f); /* 57 */ + II ( d, a, b, c, in[15], S42, 0xfe2ce6e0); /* 58 */ + II ( c, d, a, b, in[ 6], S43, 0xa3014314); /* 59 */ + II ( b, c, d, a, in[13], S44, 0x4e0811a1); /* 60 */ + II ( a, b, c, d, in[ 4], S41, 0xf7537e82); /* 61 */ + II ( d, a, b, c, in[11], S42, 0xbd3af235); /* 62 */ + II ( c, d, a, b, in[ 2], S43, 0x2ad7d2bb); /* 63 */ + II ( b, c, d, a, in[ 9], S44, 0xeb86d391); /* 64 */ + + buf[0] += a; + buf[1] += b; + buf[2] += c; + buf[3] += d; +} + +/* + *********************************************************************** + ** End of md5.c ** + ******************************** (cut) ******************************** + */ diff --git a/jni/polipo/md5.h b/jni/polipo/md5.h new file mode 100644 index 00000000..aaf9548f --- /dev/null +++ b/jni/polipo/md5.h @@ -0,0 +1,60 @@ +/* + *********************************************************************** + ** md5.h -- header file for implementation of MD5 ** + ** RSA Data Security, Inc. MD5 Message-Digest Algorithm ** + ** Created: 2/17/90 RLR ** + ** Revised: 12/27/90 SRD,AJ,BSK,JT Reference C version ** + ** Revised (for MD5): RLR 4/27/91 ** + *********************************************************************** + */ + +/* + *********************************************************************** + ** Copyright (C) 1990, RSA Data Security, Inc. All rights reserved. ** + ** ** + ** License to copy and use this software is granted provided that ** + ** it is identified as the "RSA Data Security, Inc. MD5 Message- ** + ** Digest Algorithm" in all material mentioning or referencing this ** + ** software or this function. ** + ** ** + ** License is also granted to make and use derivative works ** + ** provided that such works are identified as "derived from the RSA ** + ** Data Security, Inc. MD5 Message-Digest Algorithm" in all ** + ** material mentioning or referencing the derived work. ** + ** ** + ** RSA Data Security, Inc. makes no representations concerning ** + ** either the merchantability of this software or the suitability ** + ** of this software for any particular purpose. It is provided "as ** + ** is" without express or implied warranty of any kind. ** + ** ** + ** These notices must be retained in any copies of any part of this ** + ** documentation and/or software. ** + *********************************************************************** + */ + +#ifdef HAS_STDINT_H +#include +#elif defined(HAS_INTTYPES_H) +#include +#endif + +/* typedef a 32-bit type */ +typedef uint32_t UINT4; + +/* Data structure for MD5 (Message-Digest) computation */ +typedef struct { + UINT4 i[2]; /* number of _bits_ handled mod 2^64 */ + UINT4 buf[4]; /* scratch buffer */ + unsigned char in[64]; /* input buffer */ + unsigned char digest[16]; /* actual digest after MD5Final call */ +} MD5_CTX; + +void MD5Init (MD5_CTX *mdContext); +void MD5Update (MD5_CTX *, unsigned const char *, unsigned int); +void MD5Final (MD5_CTX *); + +/* + *********************************************************************** + ** End of md5.h ** + ******************************** (cut) ******************************** + */ diff --git a/jni/polipo/md5import.c b/jni/polipo/md5import.c new file mode 100644 index 00000000..2e74c7bd --- /dev/null +++ b/jni/polipo/md5import.c @@ -0,0 +1,15 @@ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include + +#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L +#define HAS_STDINT_H +#else +typedef unsigned int my_uint32_t; +#undef uint32_t +#define uint32_t my_uint32_t +#endif +#include "md5.c" +#undef uint32_t diff --git a/jni/polipo/md5import.h b/jni/polipo/md5import.h new file mode 100644 index 00000000..c97c632f --- /dev/null +++ b/jni/polipo/md5import.h @@ -0,0 +1,9 @@ +#if defined __STDC_VERSION__ && __STDC_VERSION__ >= 199901L +#define HAS_STDINT_H +#else +typedef unsigned int my_uint32_t; +#undef uint32_t +#define uint32_t my_uint32_t +#endif +#include "md5.h" +#undef uint32_t diff --git a/jni/polipo/mingw.c b/jni/polipo/mingw.c new file mode 100644 index 00000000..c4d5032e --- /dev/null +++ b/jni/polipo/mingw.c @@ -0,0 +1,505 @@ +/* +Copyright (c) 2006 by Dan Kennedy. +Copyright (c) 2006 by Juliusz Chroboczek. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifndef WIN32 /*MINGW*/ + +static int dummy ATTRIBUTE((unused)); + +#else + +#undef poll +#undef socket +#undef connect +#undef accept +#undef shutdown +#undef getpeername +#undef sleep +#undef inet_aton +#undef gettimeofday +#undef stat + +/* Windows needs this header file for the implementation of inet_aton() */ +#include + +/* _snprintf and friends have broken NULL termination semantics */ +int win32_snprintf(char* dest, size_t count, const char* format, ...) +{ + int r; + va_list args; + va_start(args, format); + r = _vsnprintf(dest, count, format, args); + va_end(args); + if (count > 0) { + dest[count-1] = '\0'; + } + return r; +} + +/* + * Check whether "cp" is a valid ascii representation of an Internet address + * and convert to a binary address. Returns 1 if the address is valid, 0 if + * not. This replaces inet_addr, the return value from which cannot + * distinguish between failure and a local broadcast address. + * + * This implementation of the standard inet_aton() function was copied + * (with trivial modifications) from the OpenBSD project. + */ +int +win32_inet_aton(const char *cp, struct in_addr *addr) +{ + register unsigned int val; + register int base, n; + register char c; + unsigned int parts[4]; + register unsigned int *pp = parts; + + assert(sizeof(val) == 4); + + c = *cp; + while(1) { + /* + * Collect number up to ``.''. + * Values are specified as for C: + * 0x=hex, 0=octal, isdigit=decimal. + */ + if(!isdigit(c)) + return (0); + val = 0; base = 10; + if(c == '0') { + c = *++cp; + if(c == 'x' || c == 'X') + base = 16, c = *++cp; + else + base = 8; + } + while(1) { + if(isascii(c) && isdigit(c)) { + val = (val * base) + (c - '0'); + c = *++cp; + } else if(base == 16 && isascii(c) && isxdigit(c)) { + val = (val << 4) | + (c + 10 - (islower(c) ? 'a' : 'A')); + c = *++cp; + } else + break; + } + if(c == '.') { + /* + * Internet format: + * a.b.c.d + * a.b.c (with c treated as 16 bits) + * a.b (with b treated as 24 bits) + */ + if(pp >= parts + 3) + return (0); + *pp++ = val; + c = *++cp; + } else + break; + } + /* + * Check for trailing characters. + */ + if(c != '\0' && (!isascii(c) || !isspace(c))) + return (0); + /* + * Concoct the address according to + * the number of parts specified. + */ + n = pp - parts + 1; + switch(n) { + + case 0: + return (0); /* initial nondigit */ + + case 1: /* a -- 32 bits */ + break; + + case 2: /* a.b -- 8.24 bits */ + if((val > 0xffffff) || (parts[0] > 0xff)) + return (0); + val |= parts[0] << 24; + break; + + case 3: /* a.b.c -- 8.8.16 bits */ + if((val > 0xffff) || (parts[0] > 0xff) || (parts[1] > 0xff)) + return (0); + val |= (parts[0] << 24) | (parts[1] << 16); + break; + + case 4: /* a.b.c.d -- 8.8.8.8 bits */ + if((val > 0xff) || (parts[0] > 0xff) || + (parts[1] > 0xff) || (parts[2] > 0xff)) + return (0); + val |= (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8); + break; + } + if(addr) + addr->s_addr = htonl(val); + return (1); +} + +unsigned int +win32_sleep(unsigned int seconds) +{ + Sleep(seconds * 1000); + return 0; +} + +int +win32_gettimeofday(struct timeval *tv, char *tz) +{ + const long long EPOCHFILETIME = (116444736000000000LL); + FILETIME ft; + LARGE_INTEGER li; + long long t; + + /* This implementation doesn't support the timezone parameter. That's Ok, + * as at present polipo always passed NULL as the second arg. We + * also need to make sure that we have at least 8 bytes of space to + * do the math in - otherwise there will be overflow errors. + */ + assert(tz == NULL); + assert(sizeof(t) == 8); + + if(tv) { + GetSystemTimeAsFileTime(&ft); + li.LowPart = ft.dwLowDateTime; + li.HighPart = ft.dwHighDateTime; + t = li.QuadPart; /* In 100-nanosecond intervals */ + t -= EPOCHFILETIME; /* Offset to the Epoch time */ + t /= 10; /* In microseconds */ + tv->tv_sec = (long)(t / 1000000); + tv->tv_usec = (long)(t % 1000000); + } + return 0; +} + +int win32_poll(struct pollfd *fds, unsigned int nfds, int timo) +{ + struct timeval timeout, *toptr; + fd_set ifds, ofds, efds, *ip, *op; + int i, rc; + + /* Set up the file-descriptor sets in ifds, ofds and efds. */ + FD_ZERO(&ifds); + FD_ZERO(&ofds); + FD_ZERO(&efds); + for (i = 0, op = ip = 0; i < nfds; ++i) { + fds[i].revents = 0; + if(fds[i].events & (POLLIN|POLLPRI)) { + ip = &ifds; + FD_SET(fds[i].fd, ip); + } + if(fds[i].events & POLLOUT) { + op = &ofds; + FD_SET(fds[i].fd, op); + } + FD_SET(fds[i].fd, &efds); + } + + /* Set up the timeval structure for the timeout parameter */ + if(timo < 0) { + toptr = 0; + } else { + toptr = &timeout; + timeout.tv_sec = timo / 1000; + timeout.tv_usec = (timo - timeout.tv_sec * 1000) * 1000; + } + +#ifdef DEBUG_POLL + printf("Entering select() sec=%ld usec=%ld ip=%lx op=%lx\n", + (long)timeout.tv_sec, (long)timeout.tv_usec, (long)ip, (long)op); +#endif + rc = select(0, ip, op, &efds, toptr); +#ifdef DEBUG_POLL + printf("Exiting select rc=%d\n", rc); +#endif + + if(rc <= 0) + return rc; + + if(rc > 0) { + for (i = 0; i < nfds; ++i) { + int fd = fds[i].fd; + if(fds[i].events & (POLLIN|POLLPRI) && FD_ISSET(fd, &ifds)) + fds[i].revents |= POLLIN; + if(fds[i].events & POLLOUT && FD_ISSET(fd, &ofds)) + fds[i].revents |= POLLOUT; + if(FD_ISSET(fd, &efds)) + /* Some error was detected ... should be some way to know. */ + fds[i].revents |= POLLHUP; +#ifdef DEBUG_POLL + printf("%d %d %d revent = %x\n", + FD_ISSET(fd, &ifds), FD_ISSET(fd, &ofds), FD_ISSET(fd, &efds), + fds[i].revents + ); +#endif + } + } + return rc; +} + +int win32_close_socket(SOCKET fd) { + int rc; + + rc = closesocket(fd); + return 0; +} + +static void +set_errno(int winsock_err) +{ + switch(winsock_err) { + case WSAEWOULDBLOCK: + errno = EAGAIN; + break; + default: + errno = winsock_err; + break; + } +} + +int win32_write_socket(SOCKET fd, void *buf, int n) +{ + int rc = send(fd, buf, n, 0); + if(rc == SOCKET_ERROR) { + set_errno(WSAGetLastError()); + } + return rc; +} + +int win32_read_socket(SOCKET fd, void *buf, int n) +{ + int rc = recv(fd, buf, n, 0); + if(rc == SOCKET_ERROR) { + set_errno(WSAGetLastError()); + } + return rc; +} + + +/* + * Set the "non-blocking" flag on socket fd to the value specified by + * the second argument (i.e. if the nonblocking argument is non-zero, the + * socket is set to non-blocking mode). Zero is returned if the operation + * is successful, other -1. + */ +int +win32_setnonblocking(SOCKET fd, int nonblocking) +{ + int rc; + + unsigned long mode = 1; + rc = ioctlsocket(fd, FIONBIO, &mode); + if(rc != 0) { + set_errno(WSAGetLastError()); + } + return (rc == 0 ? 0 : -1); +} + +/* + * A wrapper around the socket() function. The purpose of this wrapper + * is to ensure that the global errno symbol is set if an error occurs, + * even if we are using winsock. + */ +SOCKET +win32_socket(int domain, int type, int protocol) +{ + SOCKET fd = socket(domain, type, protocol); + if(fd == INVALID_SOCKET) { + set_errno(WSAGetLastError()); + } + return fd; +} + +static void +set_connect_errno(int winsock_err) +{ + switch(winsock_err) { + case WSAEINVAL: + case WSAEALREADY: + case WSAEWOULDBLOCK: + errno = EINPROGRESS; + break; + default: + errno = winsock_err; + break; + } +} + +/* + * A wrapper around the connect() function. The purpose of this wrapper + * is to ensure that the global errno symbol is set if an error occurs, + * even if we are using winsock. + */ +int +win32_connect(SOCKET fd, struct sockaddr *addr, socklen_t addr_len) +{ + int rc = connect(fd, addr, addr_len); + assert(rc == 0 || rc == SOCKET_ERROR); + if(rc == SOCKET_ERROR) { + set_connect_errno(WSAGetLastError()); + } + return rc; +} + +/* + * A wrapper around the accept() function. The purpose of this wrapper + * is to ensure that the global errno symbol is set if an error occurs, + * even if we are using winsock. + */ +SOCKET +win32_accept(SOCKET fd, struct sockaddr *addr, socklen_t *addr_len) +{ + SOCKET newfd = accept(fd, addr, addr_len); + if(newfd == INVALID_SOCKET) { + set_errno(WSAGetLastError()); + newfd = -1; + } + return newfd; +} + +/* + * A wrapper around the shutdown() function. The purpose of this wrapper + * is to ensure that the global errno symbol is set if an error occurs, + * even if we are using winsock. + */ +int +win32_shutdown(SOCKET fd, int mode) +{ + int rc = shutdown(fd, mode); + assert(rc == 0 || rc == SOCKET_ERROR); + if(rc == SOCKET_ERROR) { + set_errno(WSAGetLastError()); + } + return rc; +} + +/* + * A wrapper around the getpeername() function. The purpose of this wrapper + * is to ensure that the global errno symbol is set if an error occurs, + * even if we are using winsock. + */ +int +win32_getpeername(SOCKET fd, struct sockaddr *name, socklen_t *namelen) +{ + int rc = getpeername(fd, name, namelen); + assert(rc == 0 || rc == SOCKET_ERROR); + if(rc == SOCKET_ERROR) { + set_errno(WSAGetLastError()); + } + return rc; +} + +/* Stat doesn't work on directories if the name ends in a slash. */ + +int +win32_stat(const char *filename, struct stat *ss) +{ + int len, rc, saved_errno; + char *noslash; + + len = strlen(filename); + if(len <= 1 || filename[len - 1] != '/') + return stat(filename, ss); + + noslash = malloc(len); + if(noslash == NULL) + return -1; + + memcpy(noslash, filename, len - 1); + noslash[len - 1] = '\0'; + + rc = stat(noslash, ss); + saved_errno = errno; + free(noslash); + errno = saved_errno; + return rc; +} +#endif /* #ifdef WIN32 MINGW */ + +#ifndef HAVE_READV_WRITEV + +int +polipo_writev(int fd, const struct iovec *vector, int count) +{ + int rc; /* Return Code */ + if(count == 1) { + rc = WRITE(fd, vector->iov_base, vector->iov_len); + } else { + int n = 0; /* Total bytes to write */ + char *buf = 0; /* Buffer to copy to before writing */ + int i; /* Counter var for looping over vector[] */ + int offset = 0; /* Offset for copying to buf */ + + /* Figure out the required buffer size */ + for(i = 0; i < count; i++) { + n += vector[i].iov_len; + } + + /* Allocate the buffer. If the allocation fails, bail out */ + buf = malloc(n); + if(!buf) { + errno = ENOMEM; + return -1; + } + + /* Copy the contents of the vector array to the buffer */ + for(i = 0; i < count; i++) { + memcpy(&buf[offset], vector[i].iov_base, vector[i].iov_len); + offset += vector[i].iov_len; + } + assert(offset == n); + + /* Write the entire buffer to the socket and free the allocation */ + rc = WRITE(fd, buf, n); + free(buf); + } + return rc; +} + +int +polipo_readv(int fd, const struct iovec *vector, int count) +{ + int ret = 0; /* Return value */ + int i; + for(i = 0; i < count; i++) { + int n = vector[i].iov_len; + int rc = READ(fd, vector[i].iov_base, n); + if(rc == n) { + ret += rc; + } else { + if(rc < 0) { + ret = (ret == 0 ? rc : ret); + } else { + ret += rc; + } + break; + } + } + return ret; +} +#endif diff --git a/jni/polipo/mingw.h b/jni/polipo/mingw.h new file mode 100644 index 00000000..f2845834 --- /dev/null +++ b/jni/polipo/mingw.h @@ -0,0 +1,197 @@ +/* +Copyright (c) 2006 by Dan Kennedy. +Copyright (c) 2006 by Juliusz Chroboczek. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* + * Polipo was originally designed to run on Unix-like systems. This + * header file (and it's accompanying implementation file mingw.c) contain + * code that allows polipo to run on Microsoft Windows too. + * + * The target MS windows compiler is Mingw (MINimal Gnu for Windows). The + * code in this file probably get's us pretty close to MSVC also, but + * this has not been tested. To build polipo for Mingw, define the MINGW + * symbol. For Unix or Unix-like systems, leave it undefined. + */ + +#ifdef WIN32 /*MINGW*/ + +/* Unfortunately, there's no hiding it. */ +#define HAVE_WINSOCK 1 + +#ifndef WINVER +#define WINVER 0x0501 +#endif + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0501 +#endif + +#ifndef _WIN32_WINDOWS +#define _WIN32_WINDOWS 0x0410 +#endif + +#ifndef _WIN32_IE +#define _WIN32_IE 0x0700 +#endif + +/* At time of writing, a fair bit of stuff doesn't work under Mingw. + * Hopefully they will be fixed later (especially the disk-cache). + */ +#define NO_IPv6 1 + +#define S_IROTH S_IREAD + +/* Pull in winsock2.h for (almost) berkeley sockets. */ +#include + +// here we smash the errno defines so we can smash the socket functions later to smash errno. yay! +#ifdef ENOTCONN + #undef ENOTCONN +#endif +#ifdef EWOULDBLOCK + #undef EWOULDBLOCK +#endif +#ifdef ENOBUFS + #undef ENOBUFS +#endif +#ifdef ECONNRESET + #undef ECONNRESET +#endif +#ifdef EAFNOSUPPORT + #undef EAFNOSUPPORT +#endif +#ifdef EPROTONOSUPPORT + #undef EPROTONOSUPPORT +#endif +#ifdef EINPROGRESS + #undef EINPROGRESS +#endif +#ifdef EISCONN + #undef EISCONN +#endif +#define ENOTCONN WSAENOTCONN +#define EWOULDBLOCK WSAEWOULDBLOCK +#define ENOBUFS WSAENOBUFS +#define ECONNRESET WSAECONNRESET +#define ESHUTDOWN WSAESHUTDOWN +#define EAFNOSUPPORT WSAEAFNOSUPPORT +#define EPROTONOSUPPORT WSAEPROTONOSUPPORT +#define EINPROGRESS WSAEINPROGRESS +#define EISCONN WSAEISCONN + + +#include +#include +#include + +/* winsock doesn't feature poll(), so there is a version implemented + * in terms of select() in mingw.c. The following definitions + * are copied from linux man pages. A poll() macro is defined to + * call the version in mingw.c. + */ +#define POLLIN 0x0001 /* There is data to read */ +#define POLLPRI 0x0002 /* There is urgent data to read */ +#define POLLOUT 0x0004 /* Writing now will not block */ +#define POLLERR 0x0008 /* Error condition */ +#define POLLHUP 0x0010 /* Hung up */ +#define POLLNVAL 0x0020 /* Invalid request: fd not open */ +struct pollfd { + SOCKET fd; /* file descriptor */ + short events; /* requested events */ + short revents; /* returned events */ +}; +#define poll(x, y, z) win32_poll(x, y, z) + +/* These wrappers do nothing special except set the global errno variable if + * an error occurs (winsock doesn't do this by default). They set errno + * to unix-like values (i.e. WSAEWOULDBLOCK is mapped to EAGAIN), so code + * outside of this file "shouldn't" have to worry about winsock specific error + * handling. + */ +#define socket(x, y, z) win32_socket(x, y, z) +#define connect(x, y, z) win32_connect(x, y, z) +#define accept(x, y, z) win32_accept(x, y, z) +#define shutdown(x, y) win32_shutdown(x, y) +#define getpeername(x, y, z) win32_getpeername(x, y, z) + +/* Wrapper macros to call misc. functions mingw is missing */ +#define sleep(x) win32_sleep(x) +#define inet_aton(x, y) win32_inet_aton(x, y) +#define gettimeofday(x, y) win32_gettimeofday(x, y) +#define stat(x, y) win32_stat(x, y) +#define snprintf win32_snprintf + +#define mkdir(x, y) mkdir(x) +#define getcwd _getcwd +#define getpid _getpid +#ifndef S_ISDIR +#define S_ISDIR(mode) (((mode) & S_IFMT) == S_IFDIR) +#endif +#ifndef S_ISREG +#define S_ISREG(mode) (((mode) & S_IFMT) == S_IFREG) +#endif + +/* Winsock uses int instead of the usual socklen_t */ +typedef int socklen_t; + +typedef int pid_t; + +/* Function prototypes for functions in mingw.c */ +unsigned int win32_sleep(unsigned int); +int win32_inet_aton(const char *, struct in_addr *); +int win32_gettimeofday(struct timeval *, char *); +int win32_poll(struct pollfd *, unsigned int, int); +SOCKET win32_socket(int, int, int); +int win32_connect(SOCKET, struct sockaddr*, socklen_t); +SOCKET win32_accept(SOCKET, struct sockaddr*, socklen_t *); +int win32_shutdown(SOCKET, int); +int win32_getpeername(SOCKET, struct sockaddr*, socklen_t *); +int win32_snprintf(char* dest, size_t count, const char* format, ...); + +/* Three socket specific macros */ +#define READ(x, y, z) win32_read_socket(x, y, z) +#define WRITE(x, y, z) win32_write_socket(x, y, z) +#define CLOSE(x) win32_close_socket(x) + +int win32_read_socket(SOCKET, void *, int); +int win32_write_socket(SOCKET, void *, int); +int win32_close_socket(SOCKET); + +int win32_setnonblocking(SOCKET, int); +int win32_stat(const char*, struct stat*); +#endif + +#ifndef HAVE_READV_WRITEV +/* + * The HAVE_READV_WRITEV symbol should be defined if the system features + * the vector IO functions readv() and writev() and those functions may + * be legally used with sockets. + */ +struct iovec { + void *iov_base; /* Starting address */ + size_t iov_len; /* Number of bytes */ +}; +#define WRITEV(x, y, z) polipo_writev(x, y, z) +#define READV(x, y, z) polipo_readv(x, y, z) +int polipo_readv(int fd, const struct iovec *vector, int count); +int polipo_writev(int fd, const struct iovec *vector, int count); +#endif diff --git a/jni/polipo/object.c b/jni/polipo/object.c new file mode 100644 index 00000000..ee5350fa --- /dev/null +++ b/jni/polipo/object.c @@ -0,0 +1,1040 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +int mindlesslyCacheVary = 0; +int objectHashTableSize = 0; +int log2ObjectHashTableSize; + +static ObjectPtr object_list = NULL; +static ObjectPtr object_list_end = NULL; + +int objectExpiryScheduled; + +int publicObjectCount; +int privateObjectCount; +int cacheIsShared = 1; +int publicObjectLowMark = 0, objectHighMark = 2048; + +static ObjectPtr *objectHashTable; +int maxExpiresAge = (30 * 24 + 1) * 3600; +int maxAge = (14 * 24 + 1) * 3600; +float maxAgeFraction = 0.1; +int maxNoModifiedAge = 23 * 60; +int maxWriteoutWhenIdle = 64 * 1024; +int maxObjectsWhenIdle = 32; +int idleTime = 20; +int dontCacheCookies = 0; + +void +preinitObject() +{ + CONFIG_VARIABLE_SETTABLE(idleTime, CONFIG_TIME, configIntSetter, + "Time to remain idle before writing out."); + CONFIG_VARIABLE_SETTABLE(maxWriteoutWhenIdle, CONFIG_INT, configIntSetter, + "Amount of data to write at a time when idle."); + CONFIG_VARIABLE_SETTABLE(maxObjectsWhenIdle, CONFIG_INT, configIntSetter, + "Number of objects to write at a time " + "when idle."); + CONFIG_VARIABLE_SETTABLE(cacheIsShared, CONFIG_BOOLEAN, configIntSetter, + "If false, ignore s-maxage and private."); + CONFIG_VARIABLE_SETTABLE(mindlesslyCacheVary, CONFIG_BOOLEAN, + configIntSetter, + "If true, mindlessly cache negotiated objects."); + CONFIG_VARIABLE(objectHashTableSize, CONFIG_INT, + "Size of the object hash table (0 = auto)."); + CONFIG_VARIABLE(objectHighMark, CONFIG_INT, + "High object count mark."); + CONFIG_VARIABLE(publicObjectLowMark, CONFIG_INT, + "Low object count mark (0 = auto)."); + CONFIG_VARIABLE_SETTABLE(maxExpiresAge, CONFIG_TIME, configIntSetter, + "Max age for objects with Expires header."); + CONFIG_VARIABLE_SETTABLE(maxAge, CONFIG_TIME, configIntSetter, + "Max age for objects without Expires header."); + CONFIG_VARIABLE_SETTABLE(maxAgeFraction, CONFIG_FLOAT, configFloatSetter, + "Fresh fraction of modification time."); + CONFIG_VARIABLE_SETTABLE(maxNoModifiedAge, CONFIG_TIME, configIntSetter, + "Max age for objects without Last-modified."); + CONFIG_VARIABLE_SETTABLE(dontCacheCookies, CONFIG_BOOLEAN, configIntSetter, + "Work around cachable cookies."); +} + +void +initObject() +{ + int q; + if(objectHighMark < 16) { + objectHighMark = 16; + do_log(L_WARN, "Impossibly low objectHighMark -- setting to %d.\n", + objectHighMark); + } + + q = 0; + if(publicObjectLowMark == 0) q = 1; + if(publicObjectLowMark < 8 || publicObjectLowMark >= objectHighMark - 4) { + publicObjectLowMark = objectHighMark / 2; + if(!q) + do_log(L_WARN, "Impossible publicObjectLowMark value -- " + "setting to %d.\n", publicObjectLowMark); + } + + q = 1; + if(objectHashTableSize <= objectHighMark / 2 || + objectHashTableSize > objectHighMark * 1024) { + if(objectHashTableSize != 0) q = 0; + objectHashTableSize = objectHighMark * 16; + } + log2ObjectHashTableSize = log2_ceil(objectHashTableSize); + objectHashTableSize = 1 << log2ObjectHashTableSize; + if(!q) + do_log(L_WARN, "Suspicious objectHashTableSize value -- " + "setting to %d.\n", objectHashTableSize); + + object_list = NULL; + object_list_end = NULL; + publicObjectCount = 0; + privateObjectCount = 0; + objectHashTable = calloc(1 << log2ObjectHashTableSize, + sizeof(ObjectPtr)); + if(!objectHashTable) { + do_log(L_ERROR, "Couldn't allocate object hash table.\n"); + exit(1); + } +} + +ObjectPtr +findObject(int type, const void *key, int key_size) +{ + int h; + ObjectPtr object; + + if(key_size >= 50000) + return NULL; + + h = hash(type, key, key_size, log2ObjectHashTableSize); + object = objectHashTable[h]; + if(!object) + return NULL; + if(object->type != type || object->key_size != key_size || + memcmp(object->key, key, key_size) != 0) { + return NULL; + } + if(object->next) + object->next->previous = object->previous; + if(object->previous) + object->previous->next = object->next; + if(object_list == object) + object_list = object->next; + if(object_list_end == object) + object_list_end = object->previous; + object->previous = NULL; + object->next = object_list; + if(object_list) + object_list->previous = object; + object_list = object; + if(!object_list_end) + object_list_end = object; + return retainObject(object); +} + +ObjectPtr +makeObject(int type, const void *key, int key_size, int public, int fromdisk, + RequestFunction request, void* request_closure) +{ + ObjectPtr object; + int h; + + object = findObject(type, key, key_size); + if(object != NULL) { + if(public) + return object; + else + privatiseObject(object, 0); + } + + if(publicObjectCount + privateObjectCount >= objectHighMark) { + if(!objectExpiryScheduled) + discardObjects(0, 0); + if(publicObjectCount + privateObjectCount >= objectHighMark) { + return NULL; + } + } + + if(publicObjectCount >= publicObjectLowMark && + !objectExpiryScheduled) { + TimeEventHandlerPtr event; + event = scheduleTimeEvent(-1, discardObjectsHandler, 0, NULL); + if(event) + objectExpiryScheduled = 1; + else + do_log(L_ERROR, "Couldn't schedule object expiry.\n"); + } + + object = malloc(sizeof(ObjectRec)); + if(object == NULL) + return NULL; + + object->type = type; + object->request = request; + object->request_closure = request_closure; + object->key = malloc(key_size + 1); + if(object->key == NULL) { + free(object); + return NULL; + } + memcpy(object->key, key, key_size); + /* In order to make it more convenient to use keys as strings, + they are NUL-terminated. */ + object->key[key_size] = '\0'; + object->key_size = key_size; + object->flags = (public?OBJECT_PUBLIC:0) | OBJECT_INITIAL; + if(public) { + h = hash(object->type, object->key, object->key_size, + log2ObjectHashTableSize); + if(objectHashTable[h]) { + writeoutToDisk(objectHashTable[h], objectHashTable[h]->size, -1); + privatiseObject(objectHashTable[h], 0); + assert(!objectHashTable[h]); + } + objectHashTable[h] = object; + object->next = object_list; + object->previous = NULL; + if(object_list) + object_list->previous = object; + object_list = object; + if(!object_list_end) + object_list_end = object; + } else { + object->next = NULL; + object->previous = NULL; + } + object->abort_data = NULL; + object->code = 0; + object->message = NULL; + initCondition(&object->condition); + object->headers = NULL; + object->via = NULL; + object->numchunks = 0; + object->chunks = NULL; + object->length = -1; + object->date = -1; + object->age = -1; + object->expires = -1; + object->last_modified = -1; + object->atime = -1; + object->etag = NULL; + object->cache_control = 0; + object->max_age = -1; + object->s_maxage = -1; + object->size = 0; + object->requestor = NULL; + object->disk_entry = NULL; + if(object->flags & OBJECT_PUBLIC) + publicObjectCount++; + else + privateObjectCount++; + object->refcount = 1; + + if(public && fromdisk) + objectGetFromDisk(object); + return object; +} + +void +objectMetadataChanged(ObjectPtr object, int revalidate) +{ + if(revalidate) { + revalidateDiskEntry(object); + } else { + object->flags &= ~OBJECT_DISK_ENTRY_COMPLETE; + dirtyDiskEntry(object); + } + return; +} + +ObjectPtr +retainObject(ObjectPtr object) +{ + do_log(D_REFCOUNT, "O 0x%lx %d++\n", + (unsigned long)object, object->refcount); + object->refcount++; + return object; +} + +void +releaseObject(ObjectPtr object) +{ + do_log(D_REFCOUNT, "O 0x%lx %d--\n", + (unsigned long)object, object->refcount); + object->refcount--; + if(object->refcount == 0) { + assert(!object->condition.handlers && + !(object->flags & OBJECT_INPROGRESS)); + if(!(object->flags & OBJECT_PUBLIC)) + destroyObject(object); + } +} + +void +releaseNotifyObject(ObjectPtr object) +{ + do_log(D_REFCOUNT, "O 0x%lx %d--\n", + (unsigned long)object, object->refcount); + object->refcount--; + if(object->refcount > 0) { + notifyObject(object); + } else { + assert(!object->condition.handlers && + !(object->flags & OBJECT_INPROGRESS)); + if(!(object->flags & OBJECT_PUBLIC)) + destroyObject(object); + } +} + +void +lockChunk(ObjectPtr object, int i) +{ + do_log(D_LOCK, "Lock 0x%lx[%d]: ", (unsigned long)object, i); + assert(i >= 0); + if(i >= object->numchunks) + objectSetChunks(object, i + 1); + object->chunks[i].locked++; + do_log(D_LOCK, "%d\n", object->chunks[i].locked); +} + +void +unlockChunk(ObjectPtr object, int i) +{ + do_log(D_LOCK, "Unlock 0x%lx[%d]: ", (unsigned long)object, i); + assert(i >= 0 && i < object->numchunks); + assert(object->chunks[i].locked > 0); + object->chunks[i].locked--; + do_log(D_LOCK, "%d\n", object->chunks[i].locked); +} + +int +objectSetChunks(ObjectPtr object, int numchunks) +{ + int n; + + if(numchunks <= object->numchunks) + return 0; + + if(object->length >= 0) + n = MAX(numchunks, (object->length + (CHUNK_SIZE - 1)) / CHUNK_SIZE); + else + n = MAX(numchunks, + MAX(object->numchunks + 2, object->numchunks * 5 / 4)); + + if(n == 0) { + assert(object->chunks == NULL); + } else if(object->numchunks == 0) { + object->chunks = calloc(n, sizeof(ChunkRec)); + if(object->chunks == NULL) { + return -1; + } + object->numchunks = n; + } else { + ChunkPtr newchunks; + newchunks = realloc(object->chunks, n * sizeof(ChunkRec)); + if(newchunks == NULL) + return -1; + memset(newchunks + object->numchunks, 0, + (n - object->numchunks) * sizeof(ChunkRec)); + object->chunks = newchunks; + object->numchunks = n; + } + return 0; +} + +ObjectPtr +objectPartial(ObjectPtr object, int length, struct _Atom *headers) +{ + object->headers = headers; + + if(length >= 0) { + if(object->size > length) { + abortObject(object, 502, + internAtom("Inconsistent Content-Length")); + notifyObject(object); + return object; + } + } + + if(length >= 0) + object->length = length; + + object->flags &= ~OBJECT_INITIAL; + revalidateDiskEntry(object); + notifyObject(object); + return object; +} + +static int +objectAddChunk(ObjectPtr object, const char *data, int offset, int plen) +{ + int i = offset / CHUNK_SIZE; + int rc; + + assert(offset % CHUNK_SIZE == 0); + assert(plen <= CHUNK_SIZE); + + if(object->numchunks <= i) { + rc = objectSetChunks(object, i + 1); + if(rc < 0) + return -1; + } + + lockChunk(object, i); + + if(object->chunks[i].data == NULL) { + object->chunks[i].data = get_chunk(); + if(object->chunks[i].data == NULL) + goto fail; + } + + if(object->chunks[i].size >= plen) { + unlockChunk(object, i); + return 0; + } + + if(object->size < offset + plen) + object->size = offset + plen; + object->chunks[i].size = plen; + memcpy(object->chunks[i].data, data, plen); + unlockChunk(object, i); + return 0; + + fail: + unlockChunk(object, i); + return -1; +} + +static int +objectAddChunkEnd(ObjectPtr object, const char *data, int offset, int plen) +{ + int i = offset / CHUNK_SIZE; + int rc; + + assert(offset % CHUNK_SIZE != 0 && + offset % CHUNK_SIZE + plen <= CHUNK_SIZE); + + if(object->numchunks <= i) { + rc = objectSetChunks(object, i + 1); + if(rc < 0) + return -1; + } + + lockChunk(object, i); + + if(object->chunks[i].data == NULL) + object->chunks[i].data = get_chunk(); + if(object->chunks[i].data == NULL) + goto fail; + + if(offset > object->size) { + goto fail; + } + + if(object->chunks[i].size < offset % CHUNK_SIZE) { + goto fail; + } + + if(object->size < offset + plen) + object->size = offset + plen; + object->chunks[i].size = offset % CHUNK_SIZE + plen; + memcpy(object->chunks[i].data + (offset % CHUNK_SIZE), + data, plen); + + unlockChunk(object, i); + return 0; + + fail: + unlockChunk(object, i); + return -1; +} + +int +objectAddData(ObjectPtr object, const char *data, int offset, int len) +{ + int rc; + + do_log(D_OBJECT_DATA, "Adding data to 0x%lx (%d) at %d: %d bytes\n", + (unsigned long)object, object->length, offset, len); + + if(len == 0) + return 1; + + if(object->length >= 0) { + if(offset + len > object->length) { + do_log(L_ERROR, + "Inconsistent object length (%d, should be at least %d).\n", + object->length, offset + len); + object->length = offset + len; + } + } + + object->flags &= ~OBJECT_FAILED; + + if(offset + len >= object->numchunks * CHUNK_SIZE) { + rc = objectSetChunks(object, (offset + len - 1) / CHUNK_SIZE + 1); + if(rc < 0) { + return -1; + } + } + + if(offset % CHUNK_SIZE != 0) { + int plen = CHUNK_SIZE - offset % CHUNK_SIZE; + if(plen >= len) + plen = len; + rc = objectAddChunkEnd(object, data, offset, plen); + if(rc < 0) { + return -1; + } + offset += plen; + data += plen; + len -= plen; + } + + while(len > 0) { + int plen = (len >= CHUNK_SIZE) ? CHUNK_SIZE : len; + rc = objectAddChunk(object, data, offset, plen); + if(rc < 0) { + return -1; + } + offset += plen; + data += plen; + len -= plen; + } + + return 1; +} + +void +objectPrintf(ObjectPtr object, int offset, const char *format, ...) +{ + char *buf; + int rc; + + va_list args; + va_start(args, format); + buf = vsprintf_a(format, args); + va_end(args); + + if(buf == NULL) { + abortObject(object, 500, internAtom("Couldn't allocate string")); + return; + } + + rc = objectAddData(object, buf, offset, strlen(buf)); + free(buf); + if(rc < 0) + abortObject(object, 500, internAtom("Couldn't add data to object")); +} + +int +objectHoleSize(ObjectPtr object, int offset) +{ + int size = 0, i; + + if(offset < 0 || offset / CHUNK_SIZE >= object->numchunks) + return -1; + + if(offset % CHUNK_SIZE != 0) { + if(object->chunks[offset / CHUNK_SIZE].size > offset % CHUNK_SIZE) + return 0; + else { + size += CHUNK_SIZE - offset % CHUNK_SIZE; + offset += CHUNK_SIZE - offset % CHUNK_SIZE; + if(offset < 0) { + /* Overflow */ + return -1; + } + } + } + + for(i = offset / CHUNK_SIZE; i < object->numchunks; i++) { + if(object->chunks[i].size == 0) + size += CHUNK_SIZE; + else + break; + } + if(i >= object->numchunks) + return -1; + return size; +} + + +/* Returns 2 if the data is wholly in memory, 1 if it's available on disk. + If the client request was a Range request, from & to specify the requested + range; otherwise 'from' is 0 and 'to' is -1. */ +int +objectHasData(ObjectPtr object, int from, int to) +{ + int first, last, i, upto; + + if(to < 0) { + if(object->length >= 0) + to = object->length; + else + return 0; + } + + first = from / CHUNK_SIZE; + last = to / CHUNK_SIZE; + + if(from >= to) + return 2; + + if(to > object->size) { + upto = to; + goto disk; + } + + if(last > object->numchunks || + object->chunks[last].size > to % CHUNK_SIZE) { + upto = to; + goto disk; + } + + for(i = last - 1; i >= first; i--) { + if(object->chunks[i].size < CHUNK_SIZE) { + upto = (i + 1) * CHUNK_SIZE; + goto disk; + } + } + + return 2; + + disk: + if(object->flags & OBJECT_DISK_ENTRY_COMPLETE) + return 1; + + if(diskEntrySize(object) >= upto) + return 1; + + return 0; +} + +void +destroyObject(ObjectPtr object) +{ + int i; + + assert(object->refcount == 0 && !object->requestor); + assert(!object->condition.handlers && + (object->flags & OBJECT_INPROGRESS) == 0); + + if(object->disk_entry) + destroyDiskEntry(object, 0); + + if(object->flags & OBJECT_PUBLIC) { + privatiseObject(object, 0); + } else { + object->type = -1; + if(object->message) releaseAtom(object->message); + if(object->key) free(object->key); + if(object->headers) releaseAtom(object->headers); + if(object->etag) free(object->etag); + if(object->via) releaseAtom(object->via); + for(i = 0; i < object->numchunks; i++) { + assert(!object->chunks[i].locked); + if(object->chunks[i].data) + dispose_chunk(object->chunks[i].data); + object->chunks[i].data = NULL; + object->chunks[i].size = 0; + } + if(object->chunks) free(object->chunks); + privateObjectCount--; + free(object); + } +} + +void +privatiseObject(ObjectPtr object, int linear) +{ + int i, h; + if(!(object->flags & OBJECT_PUBLIC)) { + if(linear) + object->flags |= OBJECT_LINEAR; + return; + } + + if(object->disk_entry) + destroyDiskEntry(object, 0); + object->flags &= ~OBJECT_PUBLIC; + + for(i = 0; i < object->numchunks; i++) { + if(object->chunks[i].locked) + break; + if(object->chunks[i].data) { + object->chunks[i].size = 0; + dispose_chunk(object->chunks[i].data); + object->chunks[i].data = NULL; + } + } + + h = hash(object->type, object->key, object->key_size, + log2ObjectHashTableSize); + assert(objectHashTable[h] == object); + objectHashTable[h] = NULL; + + if(object->previous) + object->previous->next = object->next; + if(object_list == object) + object_list = object->next; + if(object->next) + object->next->previous = object->previous; + if(object_list_end == object) + object_list_end = object->previous; + object->previous = NULL; + object->next = NULL; + + publicObjectCount--; + privateObjectCount++; + + if(object->refcount == 0) + destroyObject(object); + else { + if(linear) + object->flags |= OBJECT_LINEAR; + } +} + +void +abortObject(ObjectPtr object, int code, AtomPtr message) +{ + int i; + + assert(code != 0); + + object->flags &= ~(OBJECT_INITIAL | OBJECT_VALIDATING); + object->flags |= OBJECT_ABORTED; + object->code = code; + if(object->message) releaseAtom(object->message); + object->message = message; + object->length = 0; + object->date = object->age; + object->expires = object->age; + object->last_modified = -1; + if(object->etag) free(object->etag); + object->etag = NULL; + if(object->headers) releaseAtom(object->headers); + object->headers = NULL; + object->size = 0; + for(i = 0; i < object->numchunks; i++) { + if(object->chunks[i].data) { + if(!object->chunks[i].locked) { + dispose_chunk(object->chunks[i].data); + object->chunks[i].data = NULL; + object->chunks[i].size = 0; + } + } + } + privatiseObject(object, 0); +} + +void +supersedeObject(ObjectPtr object) +{ + object->flags |= OBJECT_SUPERSEDED; + destroyDiskEntry(object, 1); + privatiseObject(object, 0); + notifyObject(object); +} + +void +notifyObject(ObjectPtr object) +{ + retainObject(object); + signalCondition(&object->condition); + releaseObject(object); +} + +int +discardObjectsHandler(TimeEventHandlerPtr event) +{ + return discardObjects(0, 0); +} + +void +writeoutObjects(int all) +{ + ObjectPtr object = object_list; + int bytes; + int objects; + int n; + + if(diskIsClean) return; + + objects = 0; + bytes = 0; + while(object) { + do { + if(!all) { + if(objects >= maxObjectsWhenIdle || + bytes >= maxWriteoutWhenIdle) { + if(workToDo()) return; + objects = 0; + bytes = 0; + } + } + n = writeoutToDisk(object, -1, all ? -1 : maxWriteoutWhenIdle); + bytes += n; + } while(!all && n == maxWriteoutWhenIdle); + objects++; + object = object->next; + } + diskIsClean = 1; +} + +int +discardObjects(int all, int force) +{ + ObjectPtr object; + int i; + static int in_discardObjects = 0; + TimeEventHandlerPtr event; + + if(in_discardObjects) + return 0; + + in_discardObjects = 1; + + if(all || force || used_chunks >= CHUNKS(chunkHighMark) || + publicObjectCount >= publicObjectLowMark || + publicObjectCount + privateObjectCount >= objectHighMark) { + object = object_list_end; + while(object && + (all || force || used_chunks >= CHUNKS(chunkLowMark))) { + if(force || ((object->flags & OBJECT_PUBLIC) && + object->numchunks > CHUNKS(chunkLowMark) / 4)) { + int j; + for(j = 0; j < object->numchunks; j++) { + if(object->chunks[j].locked) { + break; + } + if(object->chunks[j].size < CHUNK_SIZE) { + continue; + } + writeoutToDisk(object, (j + 1) * CHUNK_SIZE, -1); + dispose_chunk(object->chunks[j].data); + object->chunks[j].data = NULL; + object->chunks[j].size = 0; + } + } + object = object->previous; + } + + i = 0; + object = object_list_end; + while(object && + (all || force || + used_chunks - i > CHUNKS(chunkLowMark) || + used_chunks > CHUNKS(chunkCriticalMark) || + publicObjectCount > publicObjectLowMark)) { + ObjectPtr next_object = object->previous; + if(object->refcount == 0) { + i += object->numchunks; + writeoutToDisk(object, object->size, -1); + privatiseObject(object, 0); + } else if(all || force) { + writeoutToDisk(object, object->size, -1); + destroyDiskEntry(object, 0); + } + object = next_object; + } + + object = object_list_end; + if(force || used_chunks > CHUNKS(chunkCriticalMark)) { + if(used_chunks > CHUNKS(chunkCriticalMark)) { + do_log(L_WARN, + "Short on chunk memory -- " + "attempting to punch holes " + "in the middle of objects.\n"); + } + while(object && + (force || used_chunks > CHUNKS(chunkCriticalMark))) { + if(force || (object->flags & OBJECT_PUBLIC)) { + int j; + for(j = object->numchunks - 1; j >= 0; j--) { + if(object->chunks[j].locked) + continue; + if(object->chunks[j].size < CHUNK_SIZE) + continue; + writeoutToDisk(object, (j + 1) * CHUNK_SIZE, -1); + dispose_chunk(object->chunks[j].data); + object->chunks[j].data = NULL; + object->chunks[j].size = 0; + } + } + object = object->previous; + } + } + event = scheduleTimeEvent(2, discardObjectsHandler, 0, NULL); + if(event) { + objectExpiryScheduled = 1; + } else { + objectExpiryScheduled = 0; + do_log(L_ERROR, "Couldn't schedule object expiry.\n"); + } + } else { + objectExpiryScheduled = 0; + } + + if(all) { + if(privateObjectCount + publicObjectCount != 0) { + do_log(L_WARN, + "Discarded all objects, " + "%d + %d objects left (%d chunks and %d atoms used).\n", + publicObjectCount, privateObjectCount, + used_chunks, used_atoms); + } else if(used_chunks != 0) { + do_log(L_WARN, + "Discarded all objects, " + "%d chunks and %d atoms left.\n", + used_chunks, used_atoms); + } + diskIsClean = 1; + } + + in_discardObjects = 0; + return 1; +} + +CacheControlRec no_cache_control = {0, -1, -1, -1, -1}; + +int +objectIsStale(ObjectPtr object, CacheControlPtr cache_control) +{ + int stale = 0x7FFFFFFF; + int flags; + int max_age, s_maxage; + time_t date; + + if(object->flags & OBJECT_INITIAL) + return 0; + + if(object->date >= 0) + date = object->date; + else if(object->age >= 0) + date = object->age; + else + date = current_time.tv_sec; + + if(cache_control == NULL) + cache_control = &no_cache_control; + flags = object->cache_control | cache_control->flags; + + if(cache_control->max_age >= 0) { + if(object->max_age >= 0) + max_age = MIN(cache_control->max_age, object->max_age); + else + max_age = cache_control->max_age; + } else + max_age = object->max_age; + + if(cache_control->s_maxage >= 0) { + if(object->s_maxage >= 0) + s_maxage = MIN(cache_control->s_maxage, object->s_maxage); + else + s_maxage = cache_control->s_maxage; + } else + s_maxage = object->s_maxage; + + if(max_age >= 0) + stale = MIN(stale, object->age + max_age); + + if(cacheIsShared && s_maxage >= 0) + stale = MIN(stale, object->age + s_maxage); + + if(object->expires >= 0 || object->max_age >= 0) + stale = MIN(stale, object->age + maxExpiresAge); + else + stale = MIN(stale, object->age + maxAge); + + /* RFC 2616 14.9.3: server-side max-age overrides expires */ + + if(object->expires >= 0 && object->max_age < 0) { + /* This protects against clock skew */ + stale = MIN(stale, object->age + object->expires - date); + } + + if(object->expires < 0 && object->max_age < 0) { + /* No server-side information -- heuristic expiration */ + if(object->last_modified >= 0) + /* Again, take care of clock skew */ + stale = MIN(stale, + object->age + + (date - object->last_modified) * maxAgeFraction); + else + stale = MIN(stale, object->age + maxNoModifiedAge); + } + + if(!(flags & CACHE_MUST_REVALIDATE) && + !(cacheIsShared && (flags & CACHE_PROXY_REVALIDATE))) { + /* Client side can relax transparency */ + if(cache_control->min_fresh >= 0) { + if(cache_control->max_stale >= 0) + stale = MIN(stale - cache_control->min_fresh, + stale + cache_control->max_stale); + else + stale = stale - cache_control->min_fresh; + } else if(cache_control->max_stale >= 0) { + stale = stale + cache_control->max_stale; + } + } + + return current_time.tv_sec > stale; +} + +int +objectMustRevalidate(ObjectPtr object, CacheControlPtr cache_control) +{ + int flags; + + if(cache_control == NULL) + cache_control = &no_cache_control; + if(object) + flags = object->cache_control | cache_control->flags; + else + flags = cache_control->flags; + + if(flags & (CACHE_NO | CACHE_NO_HIDDEN | CACHE_NO_STORE)) + return 1; + + if(cacheIsShared && (flags & CACHE_PRIVATE)) + return 1; + + if(!mindlesslyCacheVary && (flags & CACHE_VARY)) + return 1; + + if(dontCacheCookies && (flags & CACHE_COOKIE)) + return 1; + + if(object) + return objectIsStale(object, cache_control); + + return 0; +} + diff --git a/jni/polipo/object.h b/jni/polipo/object.h new file mode 100644 index 00000000..b0277852 --- /dev/null +++ b/jni/polipo/object.h @@ -0,0 +1,197 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#undef MAX +#undef MIN + +#define MAX(x,y) ((x)<=(y)?(y):(x)) +#define MIN(x,y) ((x)<=(y)?(x):(y)) + +struct _HTTPRequest; + +#if defined(USHRT_MAX) && CHUNK_SIZE <= USHRT_MAX +typedef unsigned short chunk_size_t; +#else +typedef unsigned int chunk_size_t; +#endif + +typedef struct _Chunk { + short int locked; + chunk_size_t size; + char *data; +} ChunkRec, *ChunkPtr; + +struct _Object; + +typedef int (*RequestFunction)(struct _Object *, int, int, int, + struct _HTTPRequest*, void*); + +typedef struct _Object { + short refcount; + unsigned char type; + RequestFunction request; + void *request_closure; + char *key; + unsigned short key_size; + unsigned short flags; + unsigned short code; + void *abort_data; + struct _Atom *message; + int length; + time_t date; + time_t age; + time_t expires; + time_t last_modified; + time_t atime; + char *etag; + unsigned short cache_control; + int max_age; + int s_maxage; + struct _Atom *headers; + struct _Atom *via; + int size; + int numchunks; + ChunkPtr chunks; + void *requestor; + struct _Condition condition; + struct _DiskCacheEntry *disk_entry; + struct _Object *next, *previous; +} ObjectRec, *ObjectPtr; + +typedef struct _CacheControl { + int flags; + int max_age; + int s_maxage; + int min_fresh; + int max_stale; +} CacheControlRec, *CacheControlPtr; + +extern int cacheIsShared; +extern int mindlesslyCacheVary; + +extern CacheControlRec no_cache_control; +extern int objectExpiryScheduled; +extern int publicObjectCount; +extern int privateObjectCount; +extern int idleTime; + +extern const time_t time_t_max; + +extern int publicObjectLowMark, objectHighMark; + +extern int log2ObjectHashTableSize; + +/* object->type */ +#define OBJECT_HTTP 1 +#define OBJECT_DNS 2 + +/* object->flags */ +/* object is public */ +#define OBJECT_PUBLIC 1 +/* object hasn't got any headers yet */ +#define OBJECT_INITIAL 2 +/* a server connection is already taking care of the object */ +#define OBJECT_INPROGRESS 4 +/* the object has been superseded -- don't try to fetch it */ +#define OBJECT_SUPERSEDED 8 +/* the object is private and aditionally can only be used by its requestor */ +#define OBJECT_LINEAR 16 +/* the object is currently being validated */ +#define OBJECT_VALIDATING 32 +/* object has been aborted */ +#define OBJECT_ABORTED 64 +/* last object request was a failure */ +#define OBJECT_FAILED 128 +/* Object is a local file */ +#define OBJECT_LOCAL 256 +/* The object's data has been entirely written out to disk */ +#define OBJECT_DISK_ENTRY_COMPLETE 512 +/* The object is suspected to be dynamic -- don't PMM */ +#define OBJECT_DYNAMIC 1024 +/* Used for synchronisation between client and server. */ +#define OBJECT_MUTATING 2048 + +/* object->cache_control and connection->cache_control */ +/* RFC 2616 14.9 */ +/* Non-standard: like no-cache, but kept internally */ +#define CACHE_NO_HIDDEN 1 +/* no-cache */ +#define CACHE_NO 2 +/* public */ +#define CACHE_PUBLIC 4 +/* private */ +#define CACHE_PRIVATE 8 +/* no-store */ +#define CACHE_NO_STORE 16 +/* no-transform */ +#define CACHE_NO_TRANSFORM 32 +/* must-revalidate */ +#define CACHE_MUST_REVALIDATE 64 +/* proxy-revalidate */ +#define CACHE_PROXY_REVALIDATE 128 +/* only-if-cached */ +#define CACHE_ONLY_IF_CACHED 256 +/* set if Vary header; treated as no-cache */ +#define CACHE_VARY 512 +/* set if Authorization header; treated specially */ +#define CACHE_AUTHORIZATION 1024 +/* set if cookie */ +#define CACHE_COOKIE 2048 +/* set if this object should never be combined with another resource */ +#define CACHE_MISMATCH 4096 + +struct _HTTPRequest; + +void preinitObject(void); +void initObject(void); +ObjectPtr findObject(int type, const void *key, int key_size); +ObjectPtr makeObject(int type, const void *key, int key_size, + int public, int fromdisk, + int (*request)(ObjectPtr, int, int, int, + struct _HTTPRequest*, void*), void*); +void objectMetadataChanged(ObjectPtr object, int dirty); +ObjectPtr retainObject(ObjectPtr); +void releaseObject(ObjectPtr); +int objectSetChunks(ObjectPtr object, int numchunks); +void lockChunk(ObjectPtr, int); +void unlockChunk(ObjectPtr, int); +void destroyObject(ObjectPtr object); +void privatiseObject(ObjectPtr object, int linear); +void abortObject(ObjectPtr object, int code, struct _Atom *message); +void supersedeObject(ObjectPtr); +void notifyObject(ObjectPtr); +void releaseNotifyObject(ObjectPtr); +ObjectPtr objectPartial(ObjectPtr object, int length, struct _Atom *headers); +int objectHoleSize(ObjectPtr object, int offset) + ATTRIBUTE ((pure)); +int objectHasData(ObjectPtr object, int from, int to) + ATTRIBUTE ((pure)); +int objectAddData(ObjectPtr object, const char *data, int offset, int len); +void objectPrintf(ObjectPtr object, int offset, const char *format, ...) + ATTRIBUTE ((format (printf, 3, 4))); +int discardObjectsHandler(TimeEventHandlerPtr); +void writeoutObjects(int); +int discardObjects(int all, int force); +int objectIsStale(ObjectPtr object, CacheControlPtr cache_control) + ATTRIBUTE ((pure)); +int objectMustRevalidate(ObjectPtr object, CacheControlPtr cache_control) + ATTRIBUTE ((pure)); diff --git a/jni/polipo/parse_time.c b/jni/polipo/parse_time.c new file mode 100644 index 00000000..71109b2e --- /dev/null +++ b/jni/polipo/parse_time.c @@ -0,0 +1,215 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +const time_t time_t_max = ((time_t)~(1U << 31)); + +static inline int +d2i(char c) +{ + if(c >= '0' && c <= '9') + return c - '0'; + else + return -1; +} + +static int +parse_int(const char *buf, int i, int len, int *val_return) +{ + int val, d; + + if(i >= len) + return -1; + + val = d2i(buf[i]); + if(val < 0) + return -1; + else + i++; + + while(i < len) { + d = d2i(buf[i]); + if(d < 0) + break; + val = val * 10 + d; + i++; + } + *val_return = val; + return i; +} + +static const char month_names[12][3] = { + "jan", "feb", "mar", "apr", "may", "jun", + "jul", "aug", "sep", "oct", "nov", "dec", +}; + +static int +skip_word(const char *buf, int i, int len) +{ + if(i >= len) + return -1; + + if(!letter(buf[i])) + return -1; + + while(i < len) { + if(!letter(buf[i])) + break; + i++; + } + + return i; +} + +static int +parse_month(const char *buf, int i, int len, int *val_return) +{ + int j, k, l; + j = skip_word(buf, i, len); + if(j != i + 3) + return -1; + for(k = 0; k < 12; k++) { + for(l = 0; l < 3; l++) { + if(lwr(buf[i + l]) != month_names[k][l]) + break; + } + if(l == 3) + break; + } + if(k >= 12) + return -1; + *val_return = k; + return j; +} + +static int +issep(char c) +{ + return c == ' ' || c == '\t' || c == ',' || c == ':' || c == '-'; +} + +int +skip_separator(const char *buf, int i, int len) +{ + if(i >= len) + return -1; + + if(issep(buf[i])) + i++; + else + return -1; + + while(i < len) { + if(issep(buf[i])) + i++; + else + break; + } + return i; +} + +int +parse_time(const char *buf, int offset, int len, time_t *time_return) +{ + struct tm tm; + time_t t; + int i = offset; + + i = skip_word(buf, i, len); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + + if(i >= len) + return -1; + + if(d2i(buf[i]) >= 0) { + i = parse_int(buf, i, len, &tm.tm_mday); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_month(buf, i, len, &tm.tm_mon); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_year); if(i < 0) return -1; + if(tm.tm_year < 100) + tm.tm_year += 1900; + if(tm.tm_year < 1937) + tm.tm_year += 100; + if(tm.tm_year < 1937) + return -1; + + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_hour); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_min); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_sec); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = skip_word(buf, i, len); if(i < 0) return -1; + } else { /* funny American format */ + i = parse_month(buf, i, len, &tm.tm_mon); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_mday); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_hour); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_min); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_sec); if(i < 0) return -1; + i = skip_separator(buf, i, len); if(i < 0) return -1; + i = parse_int(buf, i, len, &tm.tm_year); if(i < 0) return -1; + if(tm.tm_year < 100) + tm.tm_year += 1900; + if(tm.tm_year < 1937) + tm.tm_year += 100; + if(tm.tm_year < 1937 || tm.tm_year > 2040) + return -1; + } + + if(tm.tm_year < 2038) { + tm.tm_year -= 1900; + tm.tm_isdst = -1; + t = mktime_gmt(&tm); + if(t == -1) + return -1; + } else { + t = time_t_max; + } + + *time_return = t; + return i; +} + +int +format_time(char *buf, int i, int len, time_t t) +{ + struct tm *tm; + int rc; + + if(i < 0 || i > len) + return -1; + + tm = gmtime(&t); + if(tm == NULL) + return -1; + rc = strftime(buf + i, len - i, "%a, %d %b %Y %H:%M:%S GMT", tm); + if(rc <= 0) /* yes, that's <= */ + return -1; + return i + rc; +} diff --git a/jni/polipo/parse_time.h b/jni/polipo/parse_time.h new file mode 100644 index 00000000..dffdeb14 --- /dev/null +++ b/jni/polipo/parse_time.h @@ -0,0 +1,27 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* XXX */ +extern const time_t time_t_max; + +int parse_time(const char *buf, int i, int len, time_t *time_return); +int format_time(char *buf, int i, int len, time_t t); diff --git a/jni/polipo/polipo.h b/jni/polipo/polipo.h new file mode 100644 index 00000000..25d0c6c0 --- /dev/null +++ b/jni/polipo/polipo.h @@ -0,0 +1,241 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#ifndef WIN32 +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#ifndef _WIN32 +#include +#include +#include +#else +#include "dirent_compat.h" +#endif +#include +#include +#include +#ifndef WIN32 /*MINGW*/ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifdef __MINGW32_VERSION +#define MINGW +#endif + +#ifndef MAP_ANONYMOUS +#define MAP_ANONYMOUS MAP_ANON +#endif + +#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +/* nothing */ +#elif defined(__GNUC__) +#define inline __inline +#if (__GNUC__ >= 3) +#define restrict __restrict +#else +#define restrict /**/ +#endif +#else +#define inline /**/ +#define restrict /**/ +#endif + +#if defined(__GNUC__) && (__GNUC__ >= 3) +#define ATTRIBUTE(x) __attribute__(x) +#else +#define ATTRIBUTE(x) /**/ +#endif + +#if defined __GLIBC__ +#define HAVE_TM_GMTOFF +#ifndef __UCLIBC__ +#define HAVE_TIMEGM +#define HAVE_FTS +#define HAVE_FFSL +#define HAVE_FFSLL +#endif +#define HAVE_SETENV +#define HAVE_ASPRINTF +#if (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2) +#define HAVE_MEMRCHR +#endif +#endif + +#if defined(__linux__) && (__GNU_LIBRARY__ == 1) +/* Linux libc 5 */ +#define HAVE_TIMEGM +#define HAVE_SETENV +#endif + +#ifdef BSD +#define HAVE_TM_GMTOFF +#define HAVE_FTS +#define HAVE_SETENV +#endif + +#ifdef __CYGWIN__ +#define HAVE_SETENV +#define HAVE_ASPRINTF +#endif + +#ifndef O_BINARY +#define O_BINARY 0 +#endif + +#define HAVE_TZSET + +#if _POSIX_VERSION >= 200112L +#define HAVE_SETENV +#endif + +#ifndef NO_IPv6 + +#ifdef __GLIBC__ +#if (__GLIBC__ > 2) || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 2) +#define HAVE_IPv6 +#endif +#endif + +#ifdef __FreeBSD__ +#define HAVE_ASPRINTF +#if __FreeBSD_version >= 400000 +#define HAVE_IPv6 +#define HAVE_TIMEGM +#endif +#if __FreeBSD_version >= 503001 +#define HAVE_FFSL +#endif +#endif + +#ifdef __NetBSD__ +#if __NetBSD_Version__ >= 105000000 +#define HAVE_IPv6 +#endif +#if __NetBSD_Version__ >= 200000000 +#define HAVE_TIMEGM +#define HAVE_ASPRINTF +#endif +#endif + +#ifdef __OpenBSD__ +#if OpenBSD >= 200311 +#define HAVE_IPv6 +#endif +#endif + +#ifdef __APPLE__ +#define HAVE_ASPRINTF +#define HAVE_IPv6 +#define HAVE_TIMEGM +#define HAVE_FFSL +#endif + +#endif + +#if defined(i386) || defined(__mc68020__) || defined(__x86_64__) +#define UNALIGNED_ACCESS +#endif + +#ifndef WIN32 /*MINGW*/ +#define HAVE_FORK +#ifndef NO_SYSLOG +#define HAVE_SYSLOG +#endif +#define HAVE_READV_WRITEV +#define HAVE_FFS +#define READ(x, y, z) read(x, y, z) +#define WRITE(x, y, z) write(x, y, z) +#define CLOSE(x) close(x) +#else +#ifndef HAVE_REGEX +#define NO_FORBIDDEN +#endif +#ifndef MINGW +#define HAVE_MKGMTIME +#endif +#endif + +#ifdef HAVE_READV_WRITEV +#define WRITEV(x, y, z) writev(x, y, z) +#define READV(x, y, z) readv(x, y, z) +#endif + +#ifndef HAVE_FORK +#define NO_REDIRECTOR +#endif + +/* This is not going to work if va_list is interesting. But then, if you + have a non-trivial implementation of va_list, you should have va_copy. */ +#ifndef va_copy +#define va_copy(a, b) do { a = b; } while(0) +#endif + +#include "mingw.h" + +#include "ftsimport.h" +#include "atom.h" +#include "util.h" +#include "config.h" +#include "event.h" +#include "io.h" +#include "socks.h" +#include "chunk.h" +#include "object.h" +#include "dns.h" +#include "http.h" +#include "client.h" +#include "local.h" +#include "diskcache.h" +#include "server.h" +#include "http_parse.h" +#include "parse_time.h" +#include "forbidden.h" +#include "log.h" +#include "auth.h" +#include "tunnel.h" + +extern AtomPtr configFile; +extern int daemonise; +extern AtomPtr pidFile; diff --git a/jni/polipo/server.c b/jni/polipo/server.c new file mode 100644 index 00000000..408b9106 --- /dev/null +++ b/jni/polipo/server.c @@ -0,0 +1,2899 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +int serverExpireTime = 24 * 60 * 60; +int smallRequestTime = 10; +int replyUnpipelineTime = 20; +int replyUnpipelineSize = 1024 * 1024; +int pipelineAdditionalRequests = 1; +int maxPipelineTrain = 10; +AtomPtr parentProxy = NULL; +AtomPtr parentHost = NULL; +int parentPort = -1; +int pmmFirstSize = 0, pmmSize = 0; +int serverSlots = 2; +int serverSlots1 = 4; +int serverMaxSlots = 8; +int dontCacheRedirects = 0; +int maxSideBuffering = 1500; +int maxConnectionAge = 1260; +int maxConnectionRequests = 400; +int alwaysAddNoTransform = 0; + +static HTTPServerPtr servers = 0; + +static int httpServerContinueConditionHandler(int, ConditionHandlerPtr); +static int initParentProxy(void); +static int parentProxySetter(ConfigVariablePtr var, void *value); +static void httpServerDelayedFinish(HTTPConnectionPtr); +static int allowUnalignedRangeRequests = 0; + +void +preinitServer(void) +{ + CONFIG_VARIABLE_SETTABLE(parentProxy, CONFIG_ATOM_LOWER, parentProxySetter, + "Parent proxy (host:port)."); + CONFIG_VARIABLE(serverExpireTime, CONFIG_TIME, + "Time during which server data is valid."); + CONFIG_VARIABLE_SETTABLE(smallRequestTime, CONFIG_TIME, configIntSetter, + "Estimated time for a small request."); + CONFIG_VARIABLE_SETTABLE(replyUnpipelineTime, CONFIG_TIME, configIntSetter, + "Estimated time for a pipeline break."); + CONFIG_VARIABLE_SETTABLE(replyUnpipelineSize, CONFIG_INT, configIntSetter, + "Size for a pipeline break."); + CONFIG_VARIABLE_SETTABLE(pipelineAdditionalRequests, CONFIG_TRISTATE, + configIntSetter, + "Pipeline requests on an active connection."); + CONFIG_VARIABLE_SETTABLE(maxPipelineTrain, CONFIG_INT, + configIntSetter, + "Maximum number of requests " + "pipelined at a time."); + CONFIG_VARIABLE(pmmFirstSize, CONFIG_INT, + "The size of the first PMM chunk."); + CONFIG_VARIABLE(pmmSize, CONFIG_INT, + "The size of a PMM chunk."); + CONFIG_VARIABLE(serverSlots, CONFIG_INT, + "Maximum number of connections per server."); + CONFIG_VARIABLE(serverSlots1, CONFIG_INT, + "Maximum number of connections per HTTP/1.0 server."); + CONFIG_VARIABLE(serverMaxSlots, CONFIG_INT, + "Maximum number of connections per broken server."); + CONFIG_VARIABLE(dontCacheRedirects, CONFIG_BOOLEAN, + "If true, don't cache redirects."); + CONFIG_VARIABLE_SETTABLE(allowUnalignedRangeRequests, + CONFIG_BOOLEAN, configIntSetter, + "Allow unaligned range requests (unreliable)."); + CONFIG_VARIABLE_SETTABLE(maxSideBuffering, + CONFIG_INT, configIntSetter, + "Maximum buffering for PUT and POST requests."); + CONFIG_VARIABLE_SETTABLE(maxConnectionAge, + CONFIG_TIME, configIntSetter, + "Maximum age of a server-side connection."); + CONFIG_VARIABLE_SETTABLE(maxConnectionRequests, + CONFIG_INT, configIntSetter, + "Maximum number of requests on a server-side connection."); + CONFIG_VARIABLE(alwaysAddNoTransform, CONFIG_BOOLEAN, + "If true, add a no-transform directive to all requests."); +} + +static int +parentProxySetter(ConfigVariablePtr var, void *value) +{ + configAtomSetter(var, value); + initParentProxy(); + return 1; +} + +static void +discardServer(HTTPServerPtr server) +{ + HTTPServerPtr previous; + assert(!server->request); + + if(server == servers) + servers = server->next; + else { + previous = servers; + while(previous->next != server) + previous = previous->next; + previous->next = server->next; + } + + if(server->connection) + free(server->connection); + if(server->idleHandler) + free(server->idleHandler); + if(server->name) + free(server->name); + + free(server); +} + +static int +httpServerIdle(HTTPServerPtr server) +{ + int i; + if(server->request) + return 0; + for(i = 0; i < server->maxslots; i++) + if(server->connection[i]) + return 0; + return 1; +} + +static int +expireServersHandler(TimeEventHandlerPtr event) +{ + HTTPServerPtr server, next; + TimeEventHandlerPtr e; + server = servers; + while(server) { + next = server->next; + if(httpServerIdle(server) && + server->time + serverExpireTime < current_time.tv_sec) + discardServer(server); + server = next; + } + e = scheduleTimeEvent(serverExpireTime / 60 + 60, + expireServersHandler, 0, NULL); + if(!e) { + do_log(L_ERROR, "Couldn't schedule server expiry.\n"); + polipoExit(); + } + return 1; +} + +static int +roundSize(int size) +{ + return (size + CHUNK_SIZE - 1) / CHUNK_SIZE * CHUNK_SIZE; +} + +static int +initParentProxy() +{ + AtomPtr host, port_atom; + int rc, port; + + if(parentHost) { + releaseAtom(parentHost); + parentHost = NULL; + } + if(parentPort >= 0) + parentPort = -1; + + if(parentProxy != NULL && parentProxy->length == 0) { + releaseAtom(parentProxy); + parentProxy = NULL; + } + + if(parentProxy == NULL) + return 1; + + rc = atomSplit(parentProxy, ':', &host, &port_atom); + if(rc <= 0) { + do_log(L_ERROR, "Couldn't parse parentProxy."); + releaseAtom(parentProxy); + parentProxy = NULL; + return -1; + } + + port = atoi(port_atom->string); + if(port <= 0 || port >= 0x10000) { + releaseAtom(host); + releaseAtom(port_atom); + do_log(L_ERROR, "Couldn't parse parentProxy."); + releaseAtom(parentProxy); + parentProxy = NULL; + return -1; + } + + parentHost = host; + parentPort = port; + return 1; +} + +void +initServer(void) +{ + TimeEventHandlerPtr event; + servers = NULL; + + if(pmmFirstSize || pmmSize) { + if(pmmSize == 0) pmmSize = pmmFirstSize; + if(pmmFirstSize == 0) pmmFirstSize = pmmSize; + pmmSize = roundSize(pmmSize); + pmmFirstSize = roundSize(pmmFirstSize); + } + + if(serverMaxSlots < 1) + serverMaxSlots = 1; + if(serverSlots < 1) + serverSlots = 1; + if(serverSlots > serverMaxSlots) + serverSlots = serverMaxSlots; + if(serverSlots1 < serverSlots) + serverSlots1 = serverSlots; + if(serverSlots1 > serverMaxSlots) + serverSlots1 = serverMaxSlots; + + initParentProxy(); + + event = scheduleTimeEvent(serverExpireTime / 60 + 60, expireServersHandler, + 0, NULL); + if(event == NULL) { + do_log(L_ERROR, "Couldn't schedule server expiry.\n"); + exit(1); + } +} + +static HTTPServerPtr +getServer(char *name, int port, int proxy) +{ + HTTPServerPtr server; + int i; + + server = servers; + while(server) { + if(strcmp(server->name, name) == 0 && server->port == port && + server->isProxy == proxy) { + if(httpServerIdle(server) && + server->time + serverExpireTime < current_time.tv_sec) { + discardServer(server); + server = NULL; + break; + } else { + server->time = current_time.tv_sec; + return server; + } + } + server = server->next; + } + + server = malloc(sizeof(HTTPServerRec)); + if(server == NULL) { + do_log(L_ERROR, "Couldn't allocate server.\n"); + return NULL; + } + + server->connection = malloc(serverMaxSlots * sizeof(HTTPConnectionPtr)); + if(server->connection == NULL) { + do_log(L_ERROR, "Couldn't allocate server.\n"); + free(server); + return NULL; + } + + server->idleHandler = malloc(serverMaxSlots * sizeof(FdEventHandlerPtr)); + if(server->connection == NULL) { + do_log(L_ERROR, "Couldn't allocate server.\n"); + free(server->connection); + free(server); + return NULL; + } + + server->maxslots = serverMaxSlots; + + server->name = strdup(name); + if(server->name == NULL) { + do_log(L_ERROR, "Couldn't allocate server name.\n"); + free(server); + return NULL; + } + + server->port = port; + server->addrindex = 0; + server->isProxy = proxy; + server->version = HTTP_UNKNOWN; + server->persistent = 0; + server->pipeline = 0; + server->time = current_time.tv_sec; + server->rtt = -1; + server->rate = -1; + server->numslots = MIN(serverSlots, server->maxslots); + for(i = 0; i < server->maxslots; i++) { + server->connection[i] = NULL; + server->idleHandler[i] = NULL; + } + server->request = NULL; + server->request_last = NULL; + server->lies = 0; + + server->next = servers; + servers = server; + return server; +} + +int +httpServerQueueRequest(HTTPServerPtr server, HTTPRequestPtr request) +{ + assert(request->request && request->request->request == request); + assert(request->connection == NULL); + if(server->request) { + server->request_last->next = request; + server->request_last = request; + } else { + server->request_last = request; + server->request = request; + } + return 1; +} + +void +httpServerAbort(HTTPConnectionPtr connection, int fail, + int code, AtomPtr message) +{ + HTTPRequestPtr request = connection->request; + if(request) { + if(request->request) { + httpClientError(request->request, code, retainAtom(message)); + } + if(fail) { + request->object->flags |= OBJECT_FAILED; + if(request->object->flags & OBJECT_INITIAL) + abortObject(request->object, code, retainAtom(message)); + notifyObject(request->object); + } + } + releaseAtom(message); + if(!connection->connecting) + httpServerFinish(connection, 1, 0); +} + +void +httpServerAbortRequest(HTTPRequestPtr request, int fail, + int code, AtomPtr message) +{ + if(request->connection && request == request->connection->request) { + httpServerAbort(request->connection, fail, code, message); + } else { + HTTPRequestPtr requestor = request->request; + if(requestor) { + requestor->request = NULL; + request->request = NULL; + httpClientError(requestor, code, retainAtom(message)); + } + if(fail) { + request->object->flags |= OBJECT_FAILED; + if(request->object->flags & OBJECT_INITIAL) + abortObject(request->object, code, retainAtom(message)); + notifyObject(request->object); + } + releaseAtom(message); + } +} + +void +httpServerClientReset(HTTPRequestPtr request) +{ + if(request->connection && + request->connection->fd >= 0 && + !request->connection->connecting && + request->connection->request == request) + pokeFdEvent(request->connection->fd, -ECLIENTRESET, POLLIN | POLLOUT); +} + + +int +httpMakeServerRequest(char *name, int port, ObjectPtr object, + int method, int from, int to, HTTPRequestPtr requestor) +{ + HTTPServerPtr server; + HTTPRequestPtr request; + int rc; + + assert(!(object->flags & OBJECT_INPROGRESS)); + + if(parentHost) { + server = getServer(parentHost->string, parentPort, 1); + } else { + server = getServer(name, port, 0); + } + if(server == NULL) return -1; + + object->flags |= OBJECT_INPROGRESS; + object->requestor = requestor; + + request = httpMakeRequest(); + if(!request) { + do_log(L_ERROR, "Couldn't allocate request.\n"); + return -1; + } + + /* Because we allocate objects in chunks, we cannot have data that + doesn't start at a chunk boundary. */ + if(from % CHUNK_SIZE != 0) { + if(allowUnalignedRangeRequests) { + objectFillFromDisk(object, from / CHUNK_SIZE * CHUNK_SIZE, 1); + if(objectHoleSize(object, from - 1) != 0) + from = from / CHUNK_SIZE * CHUNK_SIZE; + } else { + from = from / CHUNK_SIZE * CHUNK_SIZE; + } + } + + request->object = retainObject(object); + request->method = method; + if(method == METHOD_CONDITIONAL_GET) { + if(server->lies > 0) + request->method = METHOD_HEAD; + } + request->flags = + REQUEST_PERSISTENT | + (expectContinue ? (requestor->flags & REQUEST_WAIT_CONTINUE) : 0); + request->from = from; + request->to = to; + request->request = requestor; + requestor->request = request; + request->cache_control = requestor->cache_control; + request->time0 = null_time; + request->time1 = null_time; + + rc = httpServerQueueRequest(server, request); + if(rc < 0) { + do_log(L_ERROR, "Couldn't queue request.\n"); + request->request = NULL; + requestor->request = NULL; + object->flags &= ~(OBJECT_INPROGRESS | OBJECT_VALIDATING); + releaseNotifyObject(object); + httpDestroyRequest(request); + return 1; + } + + if(request->flags & REQUEST_WAIT_CONTINUE) { + if(server->version == HTTP_10) { + httpServerAbortRequest(request, 1, + 417, internAtom("Expectation failed")); + return 1; + } + } else if(expectContinue >= 2 && server->version == HTTP_11) { + if(request->method == METHOD_POST || request->method == METHOD_PUT) + request->flags |= REQUEST_WAIT_CONTINUE; + } + + again: + rc = httpServerTrigger(server); + if(rc < 0) { + /* We must be very short on memory. If there are any requests + queued, we abort one and try again. If there aren't, we + give up. */ + do_log(L_ERROR, "Couldn't trigger server -- out of memory?\n"); + if(server->request) { + httpServerAbortRequest(server->request, 1, 503, + internAtom("Couldn't trigger server")); + goto again; + } + } + return 1; +} + +int +httpServerConnection(HTTPServerPtr server) +{ + HTTPConnectionPtr connection; + int i; + + connection = httpMakeConnection(); + if(connection == NULL) { + do_log(L_ERROR, "Couldn't allocate server connection.\n"); + return -1; + } + connection->server = server; + + for(i = 0; i < server->numslots; i++) { + if(!server->connection[i]) { + server->connection[i] = connection; + break; + } + } + assert(i < server->numslots); + + connection->request = NULL; + connection->request_last = NULL; + + do_log(D_SERVER_CONN, "C... %s:%d.\n", + scrub(connection->server->name), connection->server->port); + httpSetTimeout(connection, serverTimeout); + if(socksParentProxy) { + connection->connecting = CONNECTING_SOCKS; + do_socks_connect(server->name, connection->server->port, + httpServerSocksHandler, connection); + } else { + connection->connecting = CONNECTING_DNS; + do_gethostbyname(server->name, 0, + httpServerConnectionDnsHandler, + connection); + } + return 1; +} + +int +httpServerConnectionDnsHandler(int status, GethostbynameRequestPtr request) +{ + HTTPConnectionPtr connection = request->data; + + httpSetTimeout(connection, -1); + + if(status <= 0) { + AtomPtr message; + message = internAtomF("Host %s lookup failed: %s", + request->name ? + request->name->string : "(unknown)", + request->error_message ? + request->error_message->string : + pstrerror(-status)); + do_log(L_ERROR, "Host %s lookup failed: %s (%d).\n", + request->name ? + scrub(request->name->string) : "(unknown)", + request->error_message ? + request->error_message->string : + pstrerror(-status), -status); + connection->connecting = 0; + if(connection->server->request) + httpServerAbortRequest(connection->server->request, 1, 504, + retainAtom(message)); + httpServerAbort(connection, 1, 502, message); + return 1; + } + + if(request->addr->string[0] == DNS_CNAME) { + if(request->count > 10) { + AtomPtr message = internAtom("DNS CNAME loop"); + do_log(L_ERROR, "DNS CNAME loop.\n"); + connection->connecting = 0; + if(connection->server->request) + httpServerAbortRequest(connection->server->request, 1, 504, + retainAtom(message)); + httpServerAbort(connection, 1, 504, message); + return 1; + } + + httpSetTimeout(connection, serverTimeout); + do_gethostbyname(request->addr->string + 1, request->count + 1, + httpServerConnectionDnsHandler, + connection); + return 1; + } + + connection->connecting = CONNECTING_CONNECT; + httpSetTimeout(connection, serverTimeout); + do_connect(retainAtom(request->addr), connection->server->addrindex, + connection->server->port, + httpServerConnectionHandler, connection); + return 1; +} + +int +httpServerConnectionHandler(int status, + FdEventHandlerPtr event, + ConnectRequestPtr request) +{ + HTTPConnectionPtr connection = request->data; + + assert(connection->fd < 0); + if(request->fd >= 0) { + int rc; + connection->fd = request->fd; + connection->server->addrindex = request->index; + rc = setNodelay(connection->fd, 1); + if(rc < 0) + do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm"); + } + + return httpServerConnectionHandlerCommon(status, connection); +} + +int +httpServerSocksHandler(int status, SocksRequestPtr request) +{ + HTTPConnectionPtr connection = request->data; + + assert(connection->fd < 0); + if(request->fd >= 0) { + connection->fd = request->fd; + connection->server->addrindex = 0; + } + return httpServerConnectionHandlerCommon(status, connection); +} + +int +httpServerConnectionHandlerCommon(int status, HTTPConnectionPtr connection) +{ + httpSetTimeout(connection, -1); + + if(status < 0) { + AtomPtr message = + internAtomError(-status, "Connect to %s:%d failed", + connection->server->name, + connection->server->port); + if(status != -ECLIENTRESET) + do_log_error(L_ERROR, -status, "Connect to %s:%d failed", + scrub(connection->server->name), + connection->server->port); + connection->connecting = 0; + if(connection->server->request) + httpServerAbortRequest(connection->server->request, + status != -ECLIENTRESET, 504, + retainAtom(message)); + httpServerAbort(connection, status != -ECLIENTRESET, 504, message); + return 1; + } + + do_log(D_SERVER_CONN, "C %s:%d.\n", + scrub(connection->server->name), connection->server->port); + + connection->connecting = 0; + /* serverTrigger will take care of inserting any timeouts */ + httpServerTrigger(connection->server); + return 1; +} + +int +httpServerIdleHandler(int a, FdEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + HTTPServerPtr server = connection->server; + int i; + + assert(!connection->request); + + do_log(D_SERVER_CONN, "Idle connection to %s:%d died.\n", + scrub(connection->server->name), connection->server->port); + + for(i = 0; i < server->maxslots; i++) { + if(connection == server->connection[i]) { + server->idleHandler[i] = NULL; + break; + } + } + assert(i < server->maxslots); + + httpServerAbort(connection, 1, 504, internAtom("Timeout")); + return 1; +} + +/* Discard aborted requests at the head of the queue. */ +static void +httpServerDiscardRequests(HTTPServerPtr server) +{ + HTTPRequestPtr request; + while(server->request && !server->request->request) { + request = server->request; + server->request = request->next; + request->next = NULL; + if(server->request == NULL) + server->request_last = NULL; + request->object->flags &= ~(OBJECT_INPROGRESS | OBJECT_VALIDATING); + releaseNotifyObject(request->object); + request->object = NULL; + httpDestroyRequest(request); + } +} + +static int +pipelineIsSmall(HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + + if(pipelineAdditionalRequests <= 0) + return 0; + else if(pipelineAdditionalRequests >= 2) + return 1; + + if(!request) + return 1; + if(request->next || !(request->flags & REQUEST_PERSISTENT)) + return 0; + if(request->method == METHOD_HEAD || + request->method == METHOD_CONDITIONAL_GET) + return 1; + if(request->to >= 0 && connection->server->rate > 0 && + request->to - request->from < connection->server->rate * + smallRequestTime) + return 1; + return 0; +} + +static int +numRequests(HTTPServerPtr server) +{ + int n = 0; + HTTPRequestPtr request = server->request; + while(request) { + n++; + request = request->next; + } + return n; +} + +HTTPConnectionPtr +httpServerGetConnection(HTTPServerPtr server, int *idle_return) +{ + int i, j; + int connecting = 0, empty = 0, idle = 0; + + j = -1; + /* Try to find an idle connection */ + for(i = 0; i < server->numslots; i++) { + if(server->connection[i]) { + if(!server->connection[i]->connecting) { + if(!server->connection[i]->request) { + if(server->idleHandler[i]) + unregisterFdEvent(server->idleHandler[i]); + server->idleHandler[i] = NULL; + if(j < 0) j = i; + idle++; + } + } else + connecting++; + } else + empty++; + } + + if(j >= 0) { + *idle_return = idle; + return server->connection[j]; + } + + /* If there's an empty slot, schedule connection creation */ + if(empty) { + /* Don't open a connection if there are already enough in + progress, except if the server doesn't do persistent + connections and there's only one in progress. */ + if((connecting == 0 || (server->persistent <= 0 && connecting <= 1)) || + connecting < numRequests(server)) { + httpServerConnection(server); + } + } + + /* Find a connection that can accept additional requests */ + if(server->version == HTTP_11 && server->pipeline >= 4) { + for(i = 0; i < serverSlots; i++) { + if(server->connection[i] && !server->connection[i]->connecting && + pipelineIsSmall(server->connection[i])) { + if(server->idleHandler[i]) + unregisterFdEvent(server->idleHandler[i]); + server->idleHandler[i] = NULL; + *idle_return = 0; + return server->connection[i]; + } + } + } + *idle_return = idle; + return NULL; +} + +int +httpServerTrigger(HTTPServerPtr server) +{ + HTTPConnectionPtr connection; + HTTPRequestPtr request; + int idle, n, i, rc, numidle; + + while(server->request) { + httpServerDiscardRequests(server); + + if(!server->request) + break; + + if(REQUEST_SIDE(server->request)) { + rc = httpServerSideRequest(server); + /* If rc is 0, httpServerSideRequest didn't dequeue this + request. Go through the scheduling loop again, come + back later. */ + if(rc <= 0) break; + continue; + } + connection = httpServerGetConnection(server, &numidle); + if(!connection) break; + + /* If server->pipeline <= 0, we don't do pipelining. If + server->pipeline is 1, then we are ready to start probing + for pipelining on the server; we then send exactly two + requests in what is hopefully a single packet to check + whether the server has the nasty habit of discarding its + input buffers after each request. + If server->pipeline is 2 or 3, the pipelining probe is in + progress on this server, and we don't pipeline anything + until it succeeds. When server->pipeline >= 4, pipelining + is believed to work on this server. */ + if(server->version != HTTP_11 || server->pipeline <= 0 || + server->pipeline == 2 || server->pipeline == 3) { + if(connection->pipelined == 0) + n = 1; + else + n = 0; + } else if(server->pipeline == 1) { + if(connection->pipelined == 0) + n = MIN(2, maxPipelineTrain); + else + n = 0; + } else { + n = maxPipelineTrain; + } + + /* Don't pipeline if there are more idle connections */ + if(numidle >= 2) + n = MIN(n, 1); + + idle = !connection->pipelined; + i = 0; + while(server->request && connection->pipelined < n) { + httpServerDiscardRequests(server); + if(!server->request) break; + request = server->request; + assert(request->request->request == request); + rc = httpWriteRequest(connection, request, -1); + if(rc < 0) { + if(i == 0) + httpServerAbortRequest(request, rc != -ECLIENTRESET, 502, + internAtom("Couldn't " + "write request")); + break; + } + do_log(D_SERVER_CONN, "W: "); + do_log_n(D_SERVER_CONN, + request->object->key, request->object->key_size); + do_log(D_SERVER_CONN, " (%d)\n", request->method); + if(connection->pipelined > 0) + request->flags |= REQUEST_PIPELINED; + request->time0 = current_time; + i++; + server->request = request->next; + request->next = NULL; + if(server->request == NULL) + server->request_last = NULL; + httpQueueRequest(connection, request); + connection->pipelined++; + } + if(server->persistent > 0 && server->pipeline == 1 && i >= 2) + server->pipeline = 2; + + if(i > 0) httpServerSendRequest(connection); + + if(idle && connection->pipelined > 0) + httpServerReply(connection, 0); + + if(i == 0) break; + } + + for(i = 0; i < server->maxslots; i++) { + if(server->connection[i] && + !server->connection[i]->connecting && + !server->connection[i]->request) { + /* Artificially age any fresh connections that aren't used + straight away; this is necessary for the logic for POST and + the logic that determines whether a given request should be + restarted. */ + if(server->connection[i]->serviced == 0) + server->connection[i]->serviced = 1; + if(!server->idleHandler[i]) + server->idleHandler[i] = + registerFdEvent(server->connection[i]->fd, POLLIN, + httpServerIdleHandler, + sizeof(HTTPConnectionPtr), + &server->connection[i]); + if(!server->idleHandler[i]) { + do_log(L_ERROR, "Couldn't register idle handler.\n"); + httpServerFinish(server->connection[i], 1, 0); + } + httpSetTimeout(server->connection[i], serverIdleTimeout); + } + } + + return 1; +} + +int +httpServerSideRequest(HTTPServerPtr server) +{ + HTTPRequestPtr request = server->request; + HTTPConnectionPtr connection; + HTTPRequestPtr requestor = request->request; + HTTPConnectionPtr client = requestor->connection; + int rc, i, freeslots, idle, connecting; + + assert(REQUEST_SIDE(request)); + + connection = NULL; + freeslots = 0; + idle = -1; + connecting = 0; + + /* Find a fresh connection */ + for(i = 0; i < server->numslots; i++) { + if(!server->connection[i]) + freeslots++; + else if(!server->connection[i]->connecting) { + if(!server->connection[i]->request) { + if(server->connection[i]->serviced == 0) { + if(server->idleHandler[i]) + unregisterFdEvent(server->idleHandler[i]); + server->idleHandler[i] = NULL; + connection = server->connection[i]; + break; + } else { + idle = i; + } + } + } else { + connecting++; + } + } + + if(!connection) { + /* Make sure that a fresh connection will be established at some + point, then wait until httpServerTrigger calls us again. */ + if(freeslots) { + httpServerConnection(server); + } else { + if(idle >= 0) { + /* Shutdown a random idle connection */ + pokeFdEvent(server->connection[idle]->fd, + -EDOSHUTDOWN, POLLIN | POLLOUT); + } + } + return 0; + } + + rc = httpWriteRequest(connection, request, client->bodylen); + if(rc < 0) { + do_log(L_ERROR, "Couldn't write POST or PUT request.\n"); + httpServerAbortRequest(request, rc != -ECLIENTRESET, 502, + internAtom("Couldn't write request")); + return 0; + } + server->request = request->next; + request->next = NULL; + if(server->request == NULL) + server->request_last = NULL; + httpQueueRequest(connection, request); + connection->pipelined = 1; + request->time0 = current_time; + connection->reqoffset = 0; + connection->bodylen = client->bodylen; + httpServerDoSide(connection); + return 1; +} + +int +httpServerDoSide(HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + HTTPRequestPtr requestor = request->request; + HTTPConnectionPtr client = requestor->connection; + int len = MIN(client->reqlen - client->reqbegin, + connection->bodylen - connection->reqoffset); + int doflush = + len > 0 && + (len >= maxSideBuffering || + client->reqbegin > 0 || + (connection->reqoffset + client->reqlen - client->reqbegin) >= + connection->bodylen); + int done = connection->reqoffset >= connection->bodylen; + + assert(connection->bodylen >= 0); + + httpSetTimeout(connection, 60); + + if(connection->reqlen > 0) { + /* Send the headers, but don't send any part of the body if + we're in wait_continue. */ + do_stream_2(IO_WRITE, + connection->fd, 0, + connection->reqbuf, connection->reqlen, + client->reqbuf + client->reqbegin, + (request->flags & REQUEST_WAIT_CONTINUE) ? 0 : len, + httpServerSideHandler2, connection); + httpServerReply(connection, 0); + } else if(request->object->flags & OBJECT_ABORTED) { + if(connection->reqbuf) + dispose_chunk(connection->reqbuf); + connection->reqbuf = NULL; + connection->reqlen = 0; + pokeFdEvent(connection->fd, -ESHUTDOWN, POLLIN); + if(client->flags & CONN_READER) { + client->flags |= CONN_SIDE_READER; + do_stream(IO_READ | IO_IMMEDIATE | IO_NOTNOW, + client->fd, 0, NULL, 0, + httpClientSideHandler, client); + } + } else if(!(request->flags & REQUEST_WAIT_CONTINUE) && doflush) { + /* Make sure there's a reqbuf, as httpServerFinish uses + it to determine if there's a writer. */ + if(connection->reqbuf == NULL) + connection->reqbuf = get_chunk(); + assert(connection->reqbuf != NULL); + do_stream(IO_WRITE, + connection->fd, 0, + client->reqbuf + client->reqbegin, len, + httpServerSideHandler, connection); + } else { + if(connection->reqbuf) { + httpConnectionDestroyReqbuf(connection); + connection->reqlen = 0; + } + if(request->flags & REQUEST_WAIT_CONTINUE) { + ConditionHandlerPtr chandler; + do_log(D_SERVER_CONN, "W... %s:%d.\n", + scrub(connection->server->name), connection->server->port); + chandler = + conditionWait(&request->object->condition, + httpServerContinueConditionHandler, + sizeof(connection), &connection); + if(chandler) + return 1; + else + do_log(L_ERROR, "Couldn't register condition handler.\n"); + /* Fall through -- the client side will clean up. */ + } + client->flags |= CONN_SIDE_READER; + do_stream(IO_READ | (done ? IO_IMMEDIATE : 0 ) | IO_NOTNOW, + client->fd, client->reqlen, + client->reqbuf, CHUNK_SIZE, + httpClientSideHandler, client); + } + return 1; +} + +static int +httpClientDelayedDoSideHandler(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + httpServerDoSide(connection); + return 1; +} + +static int +httpServerDelayedDoSide(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr handler; + handler = scheduleTimeEvent(0, httpClientDelayedDoSideHandler, + sizeof(connection), &connection); + if(!handler) { + do_log(L_ERROR, "Couldn't schedule DoSide -- freeing memory.\n"); + free_chunk_arenas(); + handler = scheduleTimeEvent(0, httpClientDelayedDoSideHandler, + sizeof(connection), &connection); + do_log(L_ERROR, "Couldn't schedule DoSide.\n"); + /* Somebody will hopefully end up timing out. */ + return 1; + } + return 1; +} + +static int +httpServerSideHandlerCommon(int kind, int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + HTTPRequestPtr requestor = request->request; + HTTPConnectionPtr client = requestor->connection; + int bodylen; + + assert(request->object->flags & OBJECT_INPROGRESS); + + if(status) { + do_log_error(L_ERROR, -status, "Couldn't write to server"); + httpConnectionDestroyReqbuf(connection); + if(status != -ECLIENTRESET) + shutdown(connection->fd, 2); + abortObject(request->object, 502, + internAtom("Couldn't write to server")); + /* Let the read side handle the error */ + httpServerDoSide(connection); + return 1; + } + + assert(srequest->offset > 0); + + if(kind == 2) { + if(srequest->offset < connection->reqlen) + return 0; + bodylen = srequest->offset - connection->reqlen; + connection->reqlen = 0; + httpConnectionDestroyReqbuf(connection); + } else { + bodylen = srequest->offset; + } + + + assert(client->reqbegin + bodylen <= client->reqlen); + + if(client->reqlen > client->reqbegin + bodylen) + memmove(client->reqbuf, client->reqbuf + client->reqbegin + bodylen, + client->reqlen - client->reqbegin - bodylen); + client->reqlen -= bodylen + client->reqbegin; + client->reqbegin = 0; + connection->reqoffset += bodylen; + httpServerDoSide(connection); + return 1; +} + +int +httpServerSideHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + return httpServerSideHandlerCommon(1, status, event, srequest); +} + +int +httpServerSideHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + return httpServerSideHandlerCommon(2, status, event, srequest); +} + +static int +httpServerContinueConditionHandler(int status, ConditionHandlerPtr chandler) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)chandler->data; + + if(connection->request->flags & REQUEST_WAIT_CONTINUE) + return 0; + httpServerDelayedDoSide(connection); + return 1; +} + +/* s is 0 to keep the connection alive, 1 to shutdown the connection */ +void +httpServerFinish(HTTPConnectionPtr connection, int s, int offset) +{ + HTTPServerPtr server = connection->server; + HTTPRequestPtr request = connection->request; + int i; + + if(request) { + assert(connection->pipelined >= 1); + assert((connection->pipelined > 1) == (request->next != NULL)); + } else { + assert(connection->pipelined == 0); + } + + if(!s && (!connection->request || + !(connection->request->flags & REQUEST_PERSISTENT))) + s = 1; + + if(connection->serviced >= maxConnectionRequests || + connection->time < current_time.tv_sec - maxConnectionAge) + s = 1; + + if(connection->reqbuf) { + /* As most normal requests go out in a single packet, this is + extremely unlikely to happen. As for POST/PUT requests, + they are not pipelined, so this can only happen if the + server sent an error reply early. */ + assert(connection->fd >= 0); + shutdown(connection->fd, 1); + pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLOUT); + httpServerDelayedFinish(connection); + goto done; + } + + if(request) { + /* Update statistics about the server */ + int size = -1, d = -1, rtt = -1, rate = -1; + if(connection->offset > 0 && request->from >= 0) + size = connection->offset - request->from; + if(request->time1.tv_sec != null_time.tv_sec) { + d = timeval_minus_usec(¤t_time, &request->time1); + if(!(request->flags & REQUEST_PIPELINED) && + request->time0.tv_sec != null_time.tv_sec) + rtt = timeval_minus_usec(&request->time1, &request->time0); + if(size >= 8192 && d > 50000) + rate = ((double)size / (double)d) * 1000000.0 + 0.5; + } + request->time0 = null_time; + request->time1 = null_time; + + if(rtt >= 0) { + if(server->rtt >= 0) + server->rtt = (3 * server->rtt + rtt + 2) / 4; + else + server->rtt = rtt; + } + if(rate >= 0) { + if(server->rate >= 0) + server->rate = (3 * server->rate + rate + 2) / 4; + else + server->rate = rate; + } + + httpDequeueRequest(connection); + connection->pipelined--; + request->object->flags &= ~(OBJECT_INPROGRESS | OBJECT_VALIDATING); + if(request->request) { + request->request->request = NULL; + request->request = NULL; + } + releaseNotifyObject(request->object); + request->object = NULL; + httpDestroyRequest(request); + } + + do_log(D_SERVER_CONN, "Done with server %s:%d connection (%d)\n", + scrub(connection->server->name), connection->server->port, s); + + assert(offset <= connection->len); + + if(!s) { + if(offset < connection->len) { + assert(connection->buf != NULL); + if(!connection->pipelined) { + do_log(L_WARN, + "Closing connection to %s:%d: " + "%d stray bytes of data.\n", + scrub(server->name), server->port, + connection->len - offset); + s = 1; + } else { + memmove(connection->buf, connection->buf + offset, + connection->len - offset); + connection->len = connection->len - offset; + if((connection->flags & CONN_BIGBUF) && + connection->len <= CHUNK_SIZE) + httpConnectionUnbigify(connection); + } + } else { + connection->len = 0; + } + } + + connection->server->time = current_time.tv_sec; + connection->serviced++; + + if(s) { + if(connection->timeout) + cancelTimeEvent(connection->timeout); + connection->timeout = NULL; + httpConnectionDestroyBuf(connection); + if(connection->fd >= 0) + CLOSE(connection->fd); + connection->fd = -1; + server->persistent -= 1; + if(server->persistent < -5) + server->numslots = MIN(server->maxslots, serverMaxSlots); + if(connection->request) { + HTTPRequestPtr req; + do_log(D_SERVER_CONN, "Restarting pipeline to %s:%d.\n", + scrub(server->name), server->port); + if(server->pipeline == 2) + server->pipeline -= 20; + else + server->pipeline -= 5; + req = connection->request; + while(req) { + req->connection = NULL; + req = req->next; + } + if(server->request) + connection->request_last->next = server->request; + else + server->request_last = connection->request_last; + server->request = connection->request; + connection->request = NULL; + connection->request_last = NULL; + } + /* Make sure we don't get confused into thinking a probe + is in progress. */ + if(server->pipeline == 2 || server->pipeline == 3) + server->pipeline = 1; + for(i = 0; i < server->maxslots; i++) + if(connection == server->connection[i]) + break; + assert(i < server->maxslots); + if(server->idleHandler[i]) + unregisterFdEvent(server->idleHandler[i]); + server->idleHandler[i] = NULL; + server->connection[i] = NULL; + free(connection); + } else { + server->persistent += 1; + if(server->persistent > 0) + server->numslots = MIN(server->maxslots, + server->version == HTTP_10 ? + serverSlots1 : serverSlots); + httpSetTimeout(connection, serverTimeout); + /* See httpServerTrigger */ + if(connection->pipelined || + (server->version == HTTP_11 && server->pipeline <= 0) || + (server->pipeline == 3)) { + server->pipeline++; + } + if(connection->pipelined) { + httpServerReply(connection, 1); + } else { + httpConnectionDestroyBuf(connection); + } + } + + done: + httpServerTrigger(server); +} + +static int +httpServerDelayedFinishHandler(TimeEventHandlerPtr event) +{ + HTTPConnectionPtr connection = *(HTTPConnectionPtr*)event->data; + httpServerFinish(connection, 1, 0); + return 1; +} + +static void +httpServerDelayedFinish(HTTPConnectionPtr connection) +{ + TimeEventHandlerPtr handler; + + handler = scheduleTimeEvent(1, httpServerDelayedFinishHandler, + sizeof(connection), &connection); + if(!handler) { + do_log(L_ERROR, + "Couldn't schedule delayed finish -- freeing memory."); + free_chunk_arenas(); + handler = scheduleTimeEvent(1, httpServerDelayedFinishHandler, + sizeof(connection), &connection); + if(!handler) { + do_log(L_ERROR, + "Couldn't schedule delayed finish -- aborting.\n"); + polipoExit(); + } + } +} + +void +httpServerReply(HTTPConnectionPtr connection, int immediate) +{ + assert(connection->pipelined > 0); + + if(connection->request->request == NULL) { + do_log(L_WARN, "Aborting pipeline on %s:%d.\n", + scrub(connection->server->name), connection->server->port); + httpServerFinish(connection, 1, 0); + return; + } + + do_log(D_SERVER_CONN, "R: %s (%d)\n", + scrub(connection->request->object->key), + connection->request->method); + + if(connection->len == 0) + httpConnectionDestroyBuf(connection); + + httpSetTimeout(connection, serverTimeout); + do_stream_buf(IO_READ | (immediate ? IO_IMMEDIATE : 0) | IO_NOTNOW, + connection->fd, connection->len, + &connection->buf, CHUNK_SIZE, + httpServerReplyHandler, connection); +} + +int +httpConnectionPipelined(HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + int i = 0; + while(request) { + i++; + request = request->next; + } + return i; +} + +void +httpServerUnpipeline(HTTPRequestPtr request) +{ + HTTPConnectionPtr connection = request->connection; + HTTPServerPtr server = connection->server; + + request->flags &= ~REQUEST_PERSISTENT; + if(request->next) { + HTTPRequestPtr req; + do_log(L_WARN, + "Restarting pipeline to %s:%d.\n", + scrub(connection->server->name), connection->server->port); + req = request->next; + while(req) { + req->connection = NULL; + req = req->next; + } + if(server->request) + connection->request_last->next = server->request; + else + server->request_last = connection->request_last; + server->request = request->next; + request->next = NULL; + connection->request_last = request; + } + connection->pipelined = httpConnectionPipelined(connection); +} + +void +httpServerRestart(HTTPConnectionPtr connection) +{ + HTTPServerPtr server = connection->server; + HTTPRequestPtr request = connection->request; + + if(request) { + HTTPRequestPtr req; + if(request->next) + do_log(L_WARN, + "Restarting pipeline to %s:%d.\n", + scrub(connection->server->name), connection->server->port); + req = request; + while(req) { + req->connection = NULL; + req = req->next; + } + if(server->request) + connection->request_last->next = server->request; + else + server->request_last = connection->request_last; + server->request = request; + connection->request = NULL; + connection->request_last = NULL; + } + connection->pipelined = 0; + httpServerFinish(connection, 1, 0); +} + +int +httpServerRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr requestor, void *closure) +{ + int rc; + char name[132]; + int port; + int x, y, z; + + assert(from >= 0 && (to < 0 || to > from)); + assert(closure == NULL); + assert(!(object->flags & OBJECT_LOCAL)); + assert(object->type == OBJECT_HTTP); + + if(object->flags & OBJECT_INPROGRESS) + return 1; + + if(requestor->flags & REQUEST_REQUESTED) + return 0; + + assert(requestor->request == NULL); + + if(proxyOffline) + return -1; + + rc = parseUrl(object->key, object->key_size, &x, &y, &port, &z); + + if(rc < 0 || x < 0 || y < 0 || y - x > 131) { + do_log(L_ERROR, "Couldn't parse URL %s\n", scrub(object->key)); + abortObject(object, 400, internAtom("Couldn't parse URL")); + notifyObject(object); + return 1; + } + + if(!intListMember(port, allowedPorts)) { + do_log(L_ERROR, "Attempted connection to port %d.\n", port); + abortObject(object, 403, internAtom("Forbidden port")); + notifyObject(object); + return 1; + } + + memcpy(name, ((char*)object->key) + x, y - x); + name[y - x] = '\0'; + + requestor->flags |= REQUEST_REQUESTED; + rc = httpMakeServerRequest(name, port, object, method, from, to, + requestor); + + if(rc < 0) { + abortObject(object, + 503, internAtom("Couldn't schedule server request")); + notifyObject(object); + return 1; + } + + return 1; +} + +int +httpWriteRequest(HTTPConnectionPtr connection, HTTPRequestPtr request, + int bodylen) +{ + ObjectPtr object = request->object; + int from = request->from, to = request->to, method = request->method; + char *url = object->key, *m; + int url_size = object->key_size; + int x, y, port, z, location_size; + char *location; + int l, n, rc, bufsize; + + assert(method != METHOD_NONE); + + if(request->method == METHOD_GET || + request->method == METHOD_CONDITIONAL_GET) { + if(to >= 0) { + assert(to >= from); + if(to == from) { + do_log(L_ERROR, "Requesting empty segment?\n"); + return -1; + } + } + + if(object->flags & OBJECT_DYNAMIC) { + from = 0; + to = -1; + } else { + objectFillFromDisk(object, from / CHUNK_SIZE * CHUNK_SIZE, 1); + l = objectHoleSize(request->object, from); + if(l > 0) { + if(to <= 0 || to > from + l) + to = from + l; + } + + if(pmmSize && connection->server->pipeline >= 4) { + if(from == 0) + to = to < 0 ? pmmFirstSize : MIN(to, pmmFirstSize); + else + to = to < 0 ? from + pmmSize : MIN(to, from + pmmSize); + } + + if(from % CHUNK_SIZE != 0) + if(objectHoleSize(object, from - 1) != 0) + from = from / CHUNK_SIZE * CHUNK_SIZE; + } + } + + rc = parseUrl(url, url_size, &x, &y, &port, &z); + + if(rc < 0 || x < 0 || y < 0) { + return -1; + } + + if(connection->reqbuf == NULL) { + connection->reqbuf = get_chunk(); + if(connection->reqbuf == NULL) + return -1; + connection->reqlen = 0; + } + + if(method == METHOD_CONDITIONAL_GET && + object->last_modified < 0 && object->etag == NULL) + method = request->method = METHOD_GET; + + again: + bufsize = + (connection->flags & CONN_BIGREQBUF) ? bigBufferSize : CHUNK_SIZE; + n = connection->reqlen; + switch(method) { + case METHOD_GET: + case METHOD_CONDITIONAL_GET: m = "GET"; break; + case METHOD_HEAD: m = "HEAD"; break; + case METHOD_POST: m = "POST"; break; + case METHOD_PUT: m = "PUT"; break; + default: abort(); + } + n = snnprintf(connection->reqbuf, n, bufsize, "%s ", m); + + if(connection->server->isProxy) { + n = snnprint_n(connection->reqbuf, n, bufsize, + url, url_size); + } else { + if(url_size - z == 0) { + location = "/"; + location_size = 1; + } else { + location = url + z; + location_size = url_size - z; + } + + n = snnprint_n(connection->reqbuf, n, bufsize, + location, location_size); + } + + do_log(D_SERVER_REQ, "Server request: "); + do_log_n(D_SERVER_REQ, url + x, y - x); + do_log(D_SERVER_REQ, ": "); + do_log_n(D_SERVER_REQ, connection->reqbuf, n); + do_log(D_SERVER_REQ, " (method %d from %d to %d, 0x%lx for 0x%lx)\n", + method, from, to, + (unsigned long)connection, (unsigned long)object); + + n = snnprintf(connection->reqbuf, n, bufsize, " HTTP/1.1"); + + n = snnprintf(connection->reqbuf, n, bufsize, "\r\nHost: "); + n = snnprint_n(connection->reqbuf, n, bufsize, url + x, y - x); + if(port != 80) + n = snnprintf(connection->reqbuf, n, bufsize, ":%d", port); + + if(connection->server->isProxy && parentAuthCredentials) { + n = buildServerAuthHeaders(connection->reqbuf, n, bufsize, + parentAuthCredentials); + } + + if(bodylen >= 0) + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nContent-Length: %d", bodylen); + + if(request->flags & REQUEST_WAIT_CONTINUE) + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nExpect: 100-continue"); + + if(method != METHOD_HEAD && (from > 0 || to >= 0)) { + if(to >= 0) { + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nRange: bytes=%d-%d", from, to - 1); + } else { + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nRange: bytes=%d-", from); + } + } + + if(method == METHOD_GET && object->etag && (from > 0 || to >= 0)) { + if(request->request && request->request->request == request && + request->request->from == 0 && request->request->to == -1 && + pmmSize == 0 && pmmFirstSize == 0) + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nIf-Range: \"%s\"", object->etag); + } + + if(method == METHOD_CONDITIONAL_GET) { + if(object->last_modified >= 0) { + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nIf-Modified-Since: "); + n = format_time(connection->reqbuf, n, bufsize, + object->last_modified); + } + if(object->etag) { + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nIf-None-Match: \"%s\"", object->etag); + } + } + + n = httpPrintCacheControl(connection->reqbuf, n, bufsize, + alwaysAddNoTransform ? CACHE_NO_TRANSFORM : 0, + &request->cache_control); + if(n < 0) + goto fail; + + if(request->request && request->request->headers) { + n = snnprint_n(connection->reqbuf, n, bufsize, + request->request->headers->string, + request->request->headers->length); + } + if(!disableVia) { + if(request->request && request->request->via) { + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nVia: %s, 1.1 %s", + request->request->via->string, proxyName->string); + } else { + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nVia: 1.1 %s", + proxyName->string); + } + } + + n = snnprintf(connection->reqbuf, n, bufsize, + "\r\nConnection: %s\r\n\r\n", + (request->flags & REQUEST_PERSISTENT) ? + "keep-alive" : "close"); + if(n < 0 || n >= bufsize - 1) + goto fail; + connection->reqlen = n; + return n; + + fail: + rc = 0; + if(!(connection->flags & CONN_BIGREQBUF)) + rc = httpConnectionBigifyReqbuf(connection); + if(rc == 1) + goto again; + return -1; +} + +int +httpServerHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + AtomPtr message; + + assert(connection->request->object->flags & OBJECT_INPROGRESS); + + if(connection->reqlen == 0) { + do_log(D_SERVER_REQ, "Writing aborted on 0x%lx\n", + (unsigned long)connection); + goto fail; + } + + if(status == 0 && !streamRequestDone(srequest)) { + httpSetTimeout(connection, serverTimeout); + return 0; + } + + httpConnectionDestroyReqbuf(connection); + + if(status) { + if(connection->serviced >= 1) { + httpServerRestart(connection); + return 1; + } + if(status >= 0 || status == ECONNRESET) { + message = internAtom("Couldn't send request to server: " + "short write"); + } else { + if(status != -EPIPE) + do_log_error(L_ERROR, -status, + "Couldn't send request to server"); + message = + internAtomError(-status, "Couldn't send request to server"); + } + goto fail; + } + + return 1; + + fail: + httpConnectionDestroyReqbuf(connection); + shutdown(connection->fd, 2); + pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN); + httpSetTimeout(connection, 60); + return 1; +} + +int +httpServerSendRequest(HTTPConnectionPtr connection) +{ + assert(connection->server); + + if(connection->reqlen == 0) { + do_log(D_SERVER_REQ, + "Writing aborted on 0x%lx\n", (unsigned long)connection); + httpConnectionDestroyReqbuf(connection); + shutdown(connection->fd, 2); + pokeFdEvent(connection->fd, -EDOSHUTDOWN, POLLIN | POLLOUT); + return -1; + } + + httpSetTimeout(connection, serverTimeout); + do_stream(IO_WRITE, connection->fd, 0, + connection->reqbuf, connection->reqlen, + httpServerHandler, connection); + return 1; +} + +int +httpServerReplyHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + int i, body; + int bufsize = + (connection->flags & CONN_BIGBUF) ? bigBufferSize : CHUNK_SIZE; + + assert(request->object->flags & OBJECT_INPROGRESS); + if(status < 0) { + if(connection->serviced >= 1) { + httpServerRestart(connection); + return 1; + } + if(status != -ECLIENTRESET) + do_log_error(L_ERROR, -status, "Read from server failed"); + httpServerAbort(connection, status != -ECLIENTRESET, 502, + internAtomError(-status, "Read from server failed")); + return 1; + } + + i = findEndOfHeaders(connection->buf, 0, srequest->offset, &body); + connection->len = srequest->offset; + + if(i >= 0) { + request->time1 = current_time; + return httpServerHandlerHeaders(status, event, srequest, connection); + } + + if(status) { + if(connection->serviced >= 1) { + httpServerRestart(connection); + return 1; + } + if(status < 0) { + do_log(L_ERROR, + "Error reading server headers: %d\n", -status); + httpServerAbort(connection, status != -ECLIENTRESET, 502, + internAtomError(-status, + "Error reading server headers")); + } else + httpServerAbort(connection, 1, 502, + internAtom("Server dropped connection")); + return 1; + } + + if(connection->len >= bufsize) { + int rc = 0; + if(!(connection->flags & CONN_BIGBUF)) + rc = httpConnectionBigify(connection); + if(rc == 0) { + do_log(L_ERROR, "Couldn't find end of server's headers.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Couldn't find end " + "of server's headers")); + return 1; + } else if(rc < 0) { + do_log(L_ERROR, "Couldn't allocate big buffer.\n"); + httpServerAbort(connection, 1, 500, + internAtom("Couldn't allocate big buffer")); + return 1; + } + /* Can't just return 0 -- buf has moved. */ + do_stream(IO_READ, + connection->fd, connection->len, + connection->buf, bigBufferSize, + httpServerReplyHandler, connection); + return 1; + } + + return 0; +} + +int +httpServerHandlerHeaders(int eof, + FdEventHandlerPtr event, + StreamRequestPtr srequest, + HTTPConnectionPtr connection) +{ + HTTPRequestPtr request = connection->request; + ObjectPtr object = request->object; + int rc; + int code, version; + int full_len; + AtomPtr headers; + int len; + int te; + CacheControlRec cache_control; + int age = -1; + time_t date, last_modified, expires; + struct timeval *init_time; + char *etag; + AtomPtr via, new_via; + int expect_body; + HTTPRangeRec content_range; + ObjectPtr new_object = NULL, old_object = NULL; + int supersede = 0; + AtomPtr message = NULL; + int suspectDynamic; + AtomPtr url = NULL; + + assert(request->object->flags & OBJECT_INPROGRESS); + assert(eof >= 0); + + httpSetTimeout(connection, -1); + + if(request->flags & REQUEST_WAIT_CONTINUE) { + do_log(D_SERVER_CONN, "W %s:%d.\n", + connection->server->name, connection->server->port); + request->flags &= ~REQUEST_WAIT_CONTINUE; + } + + rc = httpParseServerFirstLine(connection->buf, &code, &version, &message); + if(rc <= 0) { + do_log(L_ERROR, "Couldn't parse server status line.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Couldn't parse server status line")); + return 1; + } + + do_log(D_SERVER_REQ, "Server status: "); + do_log_n(D_SERVER_REQ, connection->buf, + connection->buf[rc - 1] == '\r' ? rc - 2 : rc - 2); + do_log(D_SERVER_REQ, " (0x%lx for 0x%lx)\n", + (unsigned long)connection, (unsigned long)object); + + if(version != HTTP_10 && version != HTTP_11) { + do_log(L_ERROR, "Unknown server HTTP version\n"); + httpServerAbort(connection, 1, 502, + internAtom("Unknown server HTTP version")); + releaseAtom(message); + return 1; + } + + connection->version = version; + connection->server->version = version; + request->flags |= REQUEST_PERSISTENT; + + url = internAtomN(object->key, object->key_size); + rc = httpParseHeaders(0, url, connection->buf, rc, request, + &headers, &len, &cache_control, NULL, &te, + &date, &last_modified, &expires, NULL, NULL, NULL, + &age, &etag, NULL, NULL, &content_range, + NULL, &via, NULL); + if(rc < 0) { + do_log(L_ERROR, "Couldn't parse server headers\n"); + releaseAtom(url); + releaseAtom(message); + httpServerAbort(connection, 1, 502, + internAtom("Couldn't parse server headers")); + return 1; + } + + if(date < 0) + date = current_time.tv_sec; + + if(code == 100) { + releaseAtom(url); + releaseAtom(message); + /* We've already reset wait_continue above, but we must still + ensure that the writer notices. */ + notifyObject(request->object); + connection->len -= rc; + if(connection->len > 0) + memmove(connection->buf, connection->buf + rc, connection->len); + httpServerReply(connection, 1); + return 1; + } + + if(code == 101) { + httpServerAbort(connection, 1, 501, + internAtom("Upgrade not implemented")); + goto fail; + } + + if(via && !checkVia(proxyName, via)) { + httpServerAbort(connection, 1, 504, internAtom("Proxy loop detected")); + goto fail; + } + full_len = content_range.full_length; + + if(code == 206) { + if(content_range.from == -1 || content_range.to == -1) { + do_log(L_ERROR, "Partial content without range.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Partial content without range")); + goto fail; + } + if(len >= 0 && len != content_range.to - content_range.from) { + do_log(L_ERROR, "Inconsistent partial content.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Inconsistent partial content")); + goto fail; + } + } else if(code < 400 && + (content_range.from >= 0 || content_range.to >= 0 || + content_range.full_length >= 0)) { + do_log(L_WARN, "Range without partial content.\n"); + /* Damn anakata. */ + content_range.from = -1; + content_range.to = -1; + content_range.full_length = -1; + } else if(code != 304 && code != 412) { + full_len = len; + } + + if(te != TE_IDENTITY && te != TE_CHUNKED) { + do_log(L_ERROR, "Unsupported transfer-encoding\n"); + httpServerAbort(connection, 1, 502, + internAtom("Unsupported transfer-encoding")); + goto fail; + } + + if(code == 304) { + if(request->method != METHOD_CONDITIONAL_GET) { + do_log(L_ERROR, "Unexpected \"not changed\" reply from server\n"); + httpServerAbort(connection, 1, 502, + internAtom("Unexpected \"not changed\" " + "reply from server")); + goto fail; + } + if(object->etag && !etag) { + /* RFC 2616 10.3.5. Violated by some front-end proxies. */ + do_log(L_WARN, "\"Not changed\" reply with no ETag.\n"); + } + } + + if(code == 412) { + if(request->method != METHOD_CONDITIONAL_GET || + (!object->etag && !object->last_modified)) { + do_log(L_ERROR, + "Unexpected \"precondition failed\" reply from server.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Unexpected \"precondition failed\" " + "reply from server")); + goto fail; + } + } + + releaseAtom(url); + + /* Okay, we're going to accept this reply. */ + + if((code == 200 || code == 206 || code == 304 || code == 412) && + (cache_control.flags & (CACHE_NO | CACHE_NO_STORE) || + cache_control.max_age == 0 || + (cacheIsShared && cache_control.s_maxage == 0) || + (expires >= 0 && expires <= object->age))) { + do_log(L_UNCACHEABLE, "Uncacheable object %s (%d)\n", + scrub(object->key), cache_control.flags); + } + + if(request->time0.tv_sec != null_time.tv_sec) + init_time = &request->time0; + else + init_time = ¤t_time; + age = MIN(init_time->tv_sec - age, init_time->tv_sec); + + if(request->method == METHOD_HEAD || + code < 200 || code == 204 || code == 304) + expect_body = 0; + else if(te == TE_IDENTITY) + expect_body = (len != 0); + else + expect_body = 1; + + connection->chunk_remaining = -1; + connection->te = te; + + old_object = object; + + connection->server->lies--; + + if(object->cache_control & CACHE_MISMATCH) + supersede = 1; + + if(code == 304 || code == 412) { + if((object->etag && etag && strcmp(object->etag, etag) != 0) || + (object->last_modified >= 0 && last_modified >= 0 && + object->last_modified != last_modified)) { + do_log(L_ERROR, "Inconsistent \"%s\" reply for %s\n", + code == 304 ? "not changed":"precondition failed", + scrub(object->key)); + object->flags |= OBJECT_DYNAMIC; + supersede = 1; + } + } else if(!(object->flags & OBJECT_INITIAL)) { + if((object->last_modified < 0 || last_modified < 0) && + (!object->etag || !etag)) + supersede = 1; + else if(object->last_modified != last_modified) + supersede = 1; + else if(object->etag || etag) { + /* We need to be permissive here so as to deal with some + front-end proxies that discard ETags on partial + replies but not on full replies. */ + if(etag && object->etag && strcmp(object->etag, etag) != 0) + supersede = 1; + else if(!object->etag) + supersede = 1; + } + + if(!supersede && (object->cache_control & CACHE_VARY) && + dontTrustVaryETag >= 1) { + /* Check content-type to work around mod_gzip bugs */ + if(!httpHeaderMatch(atomContentType, object->headers, headers) || + !httpHeaderMatch(atomContentEncoding, object->headers, headers)) + supersede = 1; + } + + if(full_len < 0 && te == TE_IDENTITY) { + /* It's an HTTP/1.0 CGI. Be afraid. */ + if(expect_body && content_range.from < 0 && content_range.to < 0) + supersede = 1; + } + + if(!supersede && object->length >= 0 && full_len >= 0 && + object->length != full_len) { + do_log(L_WARN, "Inconsistent length.\n"); + supersede = 1; + } + + if(!supersede && + ((object->last_modified >= 0 && last_modified >= 0) || + (object->etag && etag))) { + if(request->method == METHOD_CONDITIONAL_GET) { + do_log(L_WARN, "Server ignored conditional request.\n"); + connection->server->lies += 10; + /* Drop the connection? */ + } + } + } else if(code == 416) { + do_log(L_ERROR, "Unexpected \"range not satisfiable\" reply\n"); + httpServerAbort(connection, 1, 502, + internAtom("Unexpected \"range not satisfiable\" " + "reply")); + /* The object may be superseded. Make sure the next request + won't be partial. */ + abortObject(object, 502, + internAtom("Unexpected \"range not satisfiable\" reply")); + return 1; + } + + if(object->flags & OBJECT_INITIAL) + supersede = 0; + + if(supersede) { + do_log(L_SUPERSEDED, + "Superseding object %s (%d %d %d %s -> %d %d %d %s)\n", + scrub(old_object->key), + object->code, object->length, (int)object->last_modified, + object->etag ? object->etag : "(none)", + code, full_len, (int)last_modified, + etag ? etag : "(none)"); + privatiseObject(old_object, 0); + new_object = makeObject(object->type, object->key, + object->key_size, 1, 0, + object->request, NULL); + if(new_object == NULL) { + do_log(L_ERROR, "Couldn't allocate object\n"); + httpServerAbort(connection, 1, 500, + internAtom("Couldn't allocate object")); + return 1; + } + if(urlIsLocal(new_object->key, new_object->key_size)) + new_object->flags |= OBJECT_LOCAL; + } else { + new_object = object; + } + + suspectDynamic = + (!etag && last_modified < 0) || + (cache_control.flags & + (CACHE_NO_HIDDEN | CACHE_NO | CACHE_NO_STORE | + (cacheIsShared ? CACHE_PRIVATE : 0))) || + (cache_control.max_age >= 0 && cache_control.max_age <= 2) || + (cacheIsShared && + cache_control.s_maxage >= 0 && cache_control.s_maxage <= 5) || + (old_object->last_modified >= 0 && old_object->expires >= 0 && + (old_object->expires - old_object->last_modified <= 1)) || + (supersede && (old_object->date - date <= 5)); + + if(suspectDynamic) + new_object->flags |= OBJECT_DYNAMIC; + else if(!supersede) + new_object->flags &= ~OBJECT_DYNAMIC; + else if(old_object->flags & OBJECT_DYNAMIC) + new_object->flags |= OBJECT_DYNAMIC; + + new_object->age = age; + new_object->cache_control |= cache_control.flags; + new_object->max_age = cache_control.max_age; + new_object->s_maxage = cache_control.s_maxage; + new_object->flags &= ~OBJECT_FAILED; + + if(date >= 0) + new_object->date = date; + if(last_modified >= 0) + new_object->last_modified = last_modified; + if(expires >= 0) + new_object->expires = expires; + if(new_object->etag == NULL) + new_object->etag = etag; + else + free(etag); + + switch(code) { + case 200: + case 300: case 301: case 302: case 303: case 307: + case 403: case 404: case 405: case 401: + if(new_object->message) releaseAtom(new_object->message); + new_object->code = code; + new_object->message = message; + break; + case 206: case 304: case 412: + if(new_object->code != 200 || !new_object->message) { + if(new_object->message) releaseAtom(new_object->message); + new_object->code = 200; + new_object->message = internAtom("OK"); + } + releaseAtom(message); + break; + default: + if(new_object->message) releaseAtom(new_object->message); + new_object->code = code; + new_object->message = retainAtom(message); + break; + } + + httpTweakCachability(new_object); + + if(!via) + new_via = internAtomF("%s %s", + version == HTTP_11 ? "1.1" : "1.0", + proxyName->string); + else + new_via = internAtomF("%s, %s %s", via->string, + version == HTTP_11 ? "1.1" : "1.0", + proxyName->string); + if(new_via == NULL) { + do_log(L_ERROR, "Couldn't allocate Via.\n"); + } else { + if(new_object->via) releaseAtom(new_object->via); + new_object->via = new_via; + } + + if(new_object->flags & OBJECT_INITIAL) { + objectPartial(new_object, full_len, headers); + } else { + if(new_object->length < 0) + new_object->length = full_len; + /* XXX -- RFC 2616 13.5.3 */ + releaseAtom(headers); + } + + if(supersede) { + assert(new_object != old_object); + supersedeObject(old_object); + } + + if(new_object != old_object) { + if(new_object->flags & OBJECT_INPROGRESS) { + /* Make sure we don't fetch this object two times at the + same time. Just drop the connection. */ + releaseObject(new_object); + httpServerFinish(connection, 1, 0); + return 1; + } + old_object->flags &= ~OBJECT_VALIDATING; + new_object->flags |= OBJECT_INPROGRESS; + /* Signal the client side to switch to the new object -- see + httpClientGetHandler. If it doesn't, we'll give up on this + request below. */ + new_object->flags |= OBJECT_MUTATING; + request->can_mutate = new_object; + notifyObject(old_object); + request->can_mutate = NULL; + new_object->flags &= ~OBJECT_MUTATING; + old_object->flags &= ~OBJECT_INPROGRESS; + if(request->object == old_object) { + if(request->request) + request->request->request = NULL; + request->request = NULL; + request->object = new_object; + } else { + assert(request->object == new_object); + } + releaseNotifyObject(old_object); + old_object = NULL; + object = new_object; + } else { + objectMetadataChanged(new_object, 0); + } + + if(object->flags & OBJECT_VALIDATING) { + object->flags &= ~OBJECT_VALIDATING; + notifyObject(object); + } + + if(!expect_body) { + httpServerFinish(connection, 0, rc); + return 1; + } + + if(request->request == NULL) { + httpServerFinish(connection, 1, 0); + return 1; + } + + if(code == 412) { + /* 412 replies contain a useless body. For now, we + drop the connection. */ + httpServerFinish(connection, 1, 0); + return 1; + } + + + if(request->flags & REQUEST_PERSISTENT) { + if(request->method != METHOD_HEAD && + connection->te == TE_IDENTITY && len < 0) { + do_log(L_ERROR, "Persistent reply with no Content-Length\n"); + /* That's potentially dangerous, as we could start reading + arbitrary data into the object. Unfortunately, some + servers do that. */ + request->flags &= ~REQUEST_PERSISTENT; + } + } + + /* we're getting a body */ + if(content_range.from > 0) + connection->offset = content_range.from; + else + connection->offset = 0; + + if(content_range.to >= 0) + request->to = content_range.to; + + do_log(D_SERVER_OFFSET, "0x%lx(0x%lx): offset = %d\n", + (unsigned long)connection, (unsigned long)object, + connection->offset); + + if(connection->len > rc) { + rc = connectionAddData(connection, rc); + if(rc) { + if(rc < 0) { + if(rc == -2) { + do_log(L_ERROR, "Couldn't parse chunk size.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Couldn't parse chunk size")); + } else { + do_log(L_ERROR, "Couldn't add data to connection.\n"); + httpServerAbort(connection, 1, 500, + internAtom("Couldn't add data " + "to connection")); + } + return 1; + } else { + if(code != 206) { + if(object->length < 0) { + object->length = object->size; + objectMetadataChanged(object, 0); + } else if(object->length != object->size) { + httpServerAbort(connection, 1, 500, + internAtom("Inconsistent " + "object size")); + object->length = -1; + return 1; + } + } + httpServerFinish(connection, 0, 0); + return 1; + } + } + } else { + connection->len = 0; + } + + if(eof) { + if(connection->te == TE_CHUNKED || + (object->length >= 0 && + connection->offset < object->length)) { + do_log(L_ERROR, "Server closed connection.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Server closed connection")); + return 1; + } else { + if(code != 206 && eof > 0 && object->length < 0) { + object->length = object->size; + objectMetadataChanged(object, 0); + } + httpServerFinish(connection, 1, 0); + return 1; + } + } else { + return httpServerReadData(connection, 1); + } + return 0; + + fail: + releaseAtom(url); + releaseAtom(message); + if(headers) + releaseAtom(headers); + if(etag) + free(etag); + if(via) + releaseAtom(via); + return 1; +} + +int +httpServerIndirectHandlerCommon(HTTPConnectionPtr connection, int eof) +{ + HTTPRequestPtr request = connection->request; + + assert(eof >= 0); + assert(request->object->flags & OBJECT_INPROGRESS); + + if(connection->len > 0) { + int rc; + rc = connectionAddData(connection, 0); + if(rc) { + if(rc < 0) { + if(rc == -2) { + do_log(L_ERROR, "Couldn't parse chunk size.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Couldn't parse chunk size")); + } else { + do_log(L_ERROR, "Couldn't add data to connection.\n"); + httpServerAbort(connection, 1, 500, + internAtom("Couldn't add data " + "to connection")); + } + return 1; + } else { + if(request->to < 0) { + if(request->object->length < 0) { + request->object->length = request->object->size; + objectMetadataChanged(request->object, 0); + } else if(request->object->length != + request->object->size) { + request->object->length = -1; + httpServerAbort(connection, 1, 502, + internAtom("Inconsistent " + "object size")); + return 1; + } + } + httpServerFinish(connection, 0, 0); + } + return 1; + } + } + + if(eof && connection->len == 0) { + if(connection->te == TE_CHUNKED || + (request->to >= 0 && connection->offset < request->to)) { + do_log(L_ERROR, "Server dropped connection.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Server dropped connection")); + return 1; + } else { + if(request->object->length < 0 && eof > 0 && + (request->to < 0 || request->to > request->object->size)) { + request->object->length = request->object->size; + objectMetadataChanged(request->object, 0); + } + httpServerFinish(connection, 1, 0); + return 1; + } + } else { + return httpServerReadData(connection, 0); + } +} + +int +httpServerIndirectHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + assert(connection->request->object->flags & OBJECT_INPROGRESS); + + httpSetTimeout(connection, -1); + if(status < 0) { + if(status != -ECLIENTRESET) + do_log_error(L_ERROR, -status, "Read from server failed"); + httpServerAbort(connection, status != -ECLIENTRESET, 502, + internAtomError(-status, "Read from server failed")); + return 1; + } + + connection->len = srequest->offset; + + return httpServerIndirectHandlerCommon(connection, status); +} + +int +httpServerReadData(HTTPConnectionPtr connection, int immediate) +{ + HTTPRequestPtr request = connection->request; + ObjectPtr object = request->object; + int to = -1; + + assert(object->flags & OBJECT_INPROGRESS); + + if(request->request == NULL) { + httpServerFinish(connection, 1, 0); + return 1; + } + + if(request->to >= 0) + to = request->to; + else + to = object->length; + + if(to >= 0 && to == connection->offset) { + httpServerFinish(connection, 0, 0); + return 1; + } + + if(connection->len == 0 && + ((connection->te == TE_IDENTITY && to > connection->offset) || + (connection->te == TE_CHUNKED && connection->chunk_remaining > 0))) { + /* Read directly into the object */ + int i = connection->offset / CHUNK_SIZE; + int j = connection->offset % CHUNK_SIZE; + int end, len, more; + /* See httpServerDirectHandlerCommon if you change this */ + if(connection->te == TE_CHUNKED) { + len = connection->chunk_remaining; + /* The logic here is that we want more to just fit the + chunk header if we're doing a large read, but do a + large read if we would otherwise do a small one. The + magic constant 2000 comes from the assumption that the + server uses chunks that have a size that are a power of + two (possibly including the chunk header), and that we + want a full ethernet packet to fit into our read. */ + more = (len >= 2000 ? 20 : MIN(2048 - len, CHUNK_SIZE)); + } else { + len = to - connection->offset; + /* We read more data only when there is a reasonable + chance of there being another reply coming. */ + more = (connection->pipelined > 1) ? CHUNK_SIZE : 0; + } + end = len + connection->offset; + + httpConnectionDestroyBuf(connection); + + /* The order of allocation is important in case we run out of + memory. */ + lockChunk(object, i); + if(object->chunks[i].data == NULL) + object->chunks[i].data = get_chunk(); + if(object->chunks[i].data && object->chunks[i].size >= j) { + if(len + j > CHUNK_SIZE) { + lockChunk(object, i + 1); + if(object->chunks[i + 1].data == NULL) + object->chunks[i + 1].data = get_chunk(); + /* Unless we're grabbing all len of data, we do not + want to do an indirect read immediately afterwards. */ + if(more && len + j <= 2 * CHUNK_SIZE) { + if(!connection->buf) + connection->buf = get_chunk(); /* checked below */ + } + if(object->chunks[i + 1].data) { + do_stream_3(IO_READ | IO_NOTNOW, connection->fd, j, + object->chunks[i].data, CHUNK_SIZE, + object->chunks[i + 1].data, + MIN(CHUNK_SIZE, + end - (i + 1) * CHUNK_SIZE), + connection->buf, connection->buf ? more : 0, + httpServerDirectHandler2, connection); + return 1; + } + unlockChunk(object, i + 1); + } + if(more && len + j <= CHUNK_SIZE) { + if(!connection->buf) + connection->buf = get_chunk(); + } + do_stream_2(IO_READ | IO_NOTNOW, connection->fd, j, + object->chunks[i].data, + MIN(CHUNK_SIZE, end - i * CHUNK_SIZE), + connection->buf, connection->buf ? more : 0, + httpServerDirectHandler, connection); + return 1; + } else { + unlockChunk(object, i); + } + } + + if(connection->len == 0) + httpConnectionDestroyBuf(connection); + + httpSetTimeout(connection, serverTimeout); + do_stream_buf(IO_READ | IO_NOTNOW | + ((immediate && connection->len) ? IO_IMMEDIATE : 0), + connection->fd, connection->len, + &connection->buf, + (connection->te == TE_CHUNKED ? + MIN(2048, CHUNK_SIZE) : CHUNK_SIZE), + httpServerIndirectHandler, connection); + return 1; +} + +int +httpServerDirectHandlerCommon(int kind, int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + HTTPConnectionPtr connection = srequest->data; + HTTPRequestPtr request = connection->request; + ObjectPtr object = request->object; + int i = connection->offset / CHUNK_SIZE; + int to, end, end1; + + assert(request->object->flags & OBJECT_INPROGRESS); + + httpSetTimeout(connection, -1); + + if(status < 0) { + unlockChunk(object, i); + if(kind == 2) unlockChunk(object, i + 1); + if(status != -ECLIENTRESET) + do_log_error(L_ERROR, -status, "Read from server failed"); + httpServerAbort(connection, status != -ECLIENTRESET, 502, + internAtomError(-status, "Read from server failed")); + return 1; + } + + /* We have incestuous knowledge of the decisions made in + httpServerReadData */ + if(request->to >= 0) + to = request->to; + else + to = object->length; + if(connection->te == TE_CHUNKED) + end = connection->offset + connection->chunk_remaining; + else + end = to; + /* The amount of data actually read into the object */ + end1 = MIN(end, i * CHUNK_SIZE + MIN(kind * CHUNK_SIZE, srequest->offset)); + + assert(end >= 0); + assert(end1 >= i * CHUNK_SIZE); + assert(end1 - 2 * CHUNK_SIZE <= i * CHUNK_SIZE); + + object->chunks[i].size = + MAX(object->chunks[i].size, MIN(end1 - i * CHUNK_SIZE, CHUNK_SIZE)); + if(kind == 2 && end1 > (i + 1) * CHUNK_SIZE) { + object->chunks[i + 1].size = + MAX(object->chunks[i + 1].size, end1 - (i + 1) * CHUNK_SIZE); + } + if(connection->te == TE_CHUNKED) { + connection->chunk_remaining -= (end1 - connection->offset); + assert(connection->chunk_remaining >= 0); + } + connection->offset = end1; + object->size = MAX(object->size, end1); + unlockChunk(object, i); + if(kind == 2) unlockChunk(object, i + 1); + + if(i * CHUNK_SIZE + srequest->offset > end1) { + connection->len = i * CHUNK_SIZE + srequest->offset - end1; + return httpServerIndirectHandlerCommon(connection, status); + } else { + notifyObject(object); + if(status) { + if(connection->te == TE_CHUNKED || + (end >= 0 && connection->offset < end)) { + do_log(L_ERROR, "Server dropped connection.\n"); + httpServerAbort(connection, 1, 502, + internAtom("Server dropped connection")); + } else + httpServerFinish(connection, 1, 0); + return 1; + } else { + return httpServerReadData(connection, 0); + } + } +} + +int +httpServerDirectHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + return httpServerDirectHandlerCommon(1, status, event, srequest); +} + +int +httpServerDirectHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + return httpServerDirectHandlerCommon(2, status, event, srequest); +} + +/* Add the data accumulated in connection->buf into the object in + connection->request. Returns 0 in the normal case, 1 if the TE is + self-terminating and we're done, -1 if there was a problem with + objectAddData, -2 if there was a problem with the data. */ +int +connectionAddData(HTTPConnectionPtr connection, int skip) +{ + HTTPRequestPtr request = connection->request; + ObjectPtr object = request->object; + int rc; + + if(connection->te == TE_IDENTITY) { + int len; + + len = connection->len - skip; + if(object->length >= 0) { + len = MIN(object->length - connection->offset, len); + } + if(request->to >= 0) + len = MIN(request->to - connection->offset, len); + if(len > 0) { + rc = objectAddData(object, connection->buf + skip, + connection->offset, len); + if(rc < 0) + return -1; + connection->offset += len; + connection->len -= (len + skip); + do_log(D_SERVER_OFFSET, "0x%lx(0x%lx): offset = %d\n", + (unsigned long)connection, (unsigned long)object, + connection->offset); + } + + if(connection->len > 0 && skip + len > 0) { + memmove(connection->buf, + connection->buf + skip + len, connection->len); + } + + if((object->length >= 0 && object->length <= connection->offset) || + (request->to >= 0 && request->to <= connection->offset)) { + notifyObject(object); + return 1; + } else { + if(len > 0) + notifyObject(object); + return 0; + } + } else if(connection->te == TE_CHUNKED) { + int i = skip, j, size; + /* connection->chunk_remaining is 0 at the end of a chunk, -1 + after the CR/LF pair ending a chunk, and -2 after we've + seen a chunk of length 0. */ + if(connection->chunk_remaining > -2) { + while(1) { + if(connection->chunk_remaining <= 0) { + if(connection->chunk_remaining == 0) { + if(connection->len < i + 2) + break; + if(connection->buf[i] != '\r' || + connection->buf[i + 1] != '\n') + return -1; + i += 2; + connection->chunk_remaining = -1; + } + if(connection->len < i + 2) + break; + j = parseChunkSize(connection->buf, i, + connection->len, &size); + if(j < 0) + return -2; + if(j == 0) + break; + else + i = j; + if(size == 0) { + connection->chunk_remaining = -2; + break; + } else { + connection->chunk_remaining = size; + } + } else { + /* connection->chunk_remaining > 0 */ + size = MIN(connection->chunk_remaining, + connection->len - i); + if(size <= 0) + break; + rc = objectAddData(object, connection->buf + i, + connection->offset, size); + connection->offset += size; + if(rc < 0) + return -1; + i += size; + connection->chunk_remaining -= size; + do_log(D_SERVER_OFFSET, "0x%lx(0x%lx): offset = %d\n", + (unsigned long)connection, + (unsigned long)object, + connection->offset); + } + } + } + connection->len -= i; + if(connection->len > 0) + memmove(connection->buf, connection->buf + i, connection->len); + if(i > 0 || connection->chunk_remaining == -2) + notifyObject(object); + if(connection->chunk_remaining == -2) + return 1; + else + return 0; + } else { + abort(); + } +} + +void +listServers(FILE *out) +{ + HTTPServerPtr server; + int i, n, m, entry; + + fprintf(out, "\n" + "\n" + "\r\nKnown servers\n" + "\n" + "

Known servers

\n"); + + alternatingHttpStyle(out, "servers"); + fprintf(out, "\n"); + fprintf(out, "" + "" + "" + "" + "" + "" + "" + "" + "\n"); + fprintf(out, "\n"); + server = servers; + entry = 0; + while(server) { + fprintf(out, "", entry % 2 == 0 ? "even" : "odd"); + if(server->port == 80) + fprintf(out, "", server->name); + else + fprintf(out, "", server->name, server->port); + + if(server->version == HTTP_11) + fprintf(out, ""); + else if(server->version == HTTP_10) + fprintf(out, ""); + else + fprintf(out, ""); + + if(server->persistent < 0) + fprintf(out, ""); + else if(server->persistent > 0) + fprintf(out, ""); + else + fprintf(out, ""); + + if(server->version != HTTP_11 || server->persistent <= 0) + fprintf(out, ""); + else if(server->pipeline < 0) + fprintf(out, ""); + else if(server->pipeline >= 0 && server->pipeline <= 1) + fprintf(out, ""); + else if(server->pipeline == 2 || server->pipeline == 3) + fprintf(out, ""); + else + fprintf(out, ""); + + n = 0; m = 0; + for(i = 0; i < server->maxslots; i++) + if(server->connection[i] && !server->connection[i]->connecting) { + if(i < server->numslots) + n++; + else + m++; + } + + fprintf(out, "", m); + else + fprintf(out, ""); + + if(server->lies > 0) + fprintf(out, "", (server->lies + 9) / 10); + else + fprintf(out, ""); + + if(server->rtt > 0) + fprintf(out, "", (double)server->rtt / 1000000.0); + else + fprintf(out, ""); + if(server->rate > 0) + fprintf(out, "", server->rate); + else + fprintf(out, ""); + + fprintf(out, "\n"); + server = server->next; + entry++; + } + fprintf(out, "\n"); + fprintf(out, "
ServerVersionPersistentPipelineConnectionsrttrate
%s%s:%d1.11.0unknownnoyesunknownnounknownprobingyes%d/%d", n, server->numslots); + if(m) + fprintf(out, " + %d(%d lies)%.3f%d
\n"); + fprintf(out, "

back

"); + fprintf(out, "\n"); +} diff --git a/jni/polipo/server.h b/jni/polipo/server.h new file mode 100644 index 00000000..c2cbb409 --- /dev/null +++ b/jni/polipo/server.h @@ -0,0 +1,113 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +extern int serverExpireTime, dontCacheRedirects; + +typedef struct _HTTPServer { + char *name; + int port; + int addrindex; + int isProxy; + int version; + int persistent; + int pipeline; + int lies; + int rtt; + int rate; + time_t time; + int numslots; + int maxslots; + HTTPConnectionPtr *connection; + FdEventHandlerPtr *idleHandler; + HTTPRequestPtr request, request_last; + struct _HTTPServer *next; +} HTTPServerRec, *HTTPServerPtr; + +extern AtomPtr parentHost; +extern int parentPort; + +void preinitServer(void); +void initServer(void); + +void httpServerAbortHandler(ObjectPtr object); +int httpMakeServerRequest(char *name, int port, ObjectPtr object, + int method, int from, int to, + HTTPRequestPtr requestor); +int httpServerQueueRequest(HTTPServerPtr server, HTTPRequestPtr request); +int httpServerTrigger(HTTPServerPtr server); +int httpServerSideRequest(HTTPServerPtr server); +int httpServerDoSide(HTTPConnectionPtr connection); +int httpServerSideHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest); +int httpServerSideHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest); +int httpServerConnectionDnsHandler(int status, + GethostbynameRequestPtr request); +int httpServerConnectionHandler(int status, + FdEventHandlerPtr event, + ConnectRequestPtr request); +int httpServerSocksHandler(int status, SocksRequestPtr request); +int httpServerConnectionHandlerCommon(int status, + HTTPConnectionPtr connection); +void httpServerFinish(HTTPConnectionPtr connection, int s, int offset); + +void httpServerReply(HTTPConnectionPtr connection, int immediate); +void httpServerAbort(HTTPConnectionPtr connection, int, int, struct _Atom *); +void httpServerAbortRequest(HTTPRequestPtr request, int, int, struct _Atom *); +void httpServerClientReset(HTTPRequestPtr request); +void httpServerUnpipeline(HTTPRequestPtr request); +int +httpServerSendRequest(HTTPConnectionPtr connection); +int +httpServerHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int +httpServerReplyHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int +httpServerIndirectHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int +httpServerDirectHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int +httpServerDirectHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr request); +int httpServerRequest(ObjectPtr object, int method, int from, int to, + HTTPRequestPtr, void*); +int httpServerHandlerHeaders(int eof, + FdEventHandlerPtr event, + StreamRequestPtr request, + HTTPConnectionPtr connection); +int httpServerReadData(HTTPConnectionPtr, int); +int connectionAddData(HTTPConnectionPtr connection, int skip); +int +httpWriteRequest(HTTPConnectionPtr connection, HTTPRequestPtr request, int); + +void listServers(FILE*); diff --git a/jni/polipo/socks.c b/jni/polipo/socks.c new file mode 100644 index 00000000..ce8f600e --- /dev/null +++ b/jni/polipo/socks.c @@ -0,0 +1,519 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifdef NO_SOCKS + +AtomPtr socksParentProxy = NULL; + +void +preinitSocks() +{ + return; +} + +void +initSocks() +{ + return; +} + +int +do_socks_connect(char *name, int port, + int (*handler)(int, SocksRequestPtr), + void *data) +{ + SocksRequestRec request; + request.name = internAtomLowerN(name, strlen(name)); + request.port = port; + request.handler = handler; + request.buf = NULL; + request.data = data; + + handler(-ENOSYS, &request); + releaseAtom(request.name); + return 1; +} + +#else + +AtomPtr socksParentProxy = NULL; +AtomPtr socksProxyHost = NULL; +int socksProxyPort = -1; +AtomPtr socksProxyAddress = NULL; +int socksProxyAddressIndex = -1; +AtomPtr socksUserName = NULL; +AtomPtr socksProxyType = NULL; +AtomPtr aSocks4a, aSocks5; + +static int socksParentProxySetter(ConfigVariablePtr, void*); +static int socksProxyTypeSetter(ConfigVariablePtr, void*); +static int do_socks_connect_common(SocksRequestPtr); +static int socksDnsHandler(int, GethostbynameRequestPtr); +static int socksConnectHandler(int, FdEventHandlerPtr, ConnectRequestPtr); +static int socksWriteHandler(int, FdEventHandlerPtr, StreamRequestPtr); +static int socksReadHandler(int, FdEventHandlerPtr, StreamRequestPtr); +static int socks5ReadHandler(int, FdEventHandlerPtr, StreamRequestPtr); +static int socks5WriteHandler(int, FdEventHandlerPtr, StreamRequestPtr); +static int socks5ReadHandler2(int, FdEventHandlerPtr, StreamRequestPtr); + +void +preinitSocks() +{ + aSocks4a = internAtom("socks4a"); + aSocks5 = internAtom("socks5"); + socksProxyType = retainAtom(aSocks5); + socksUserName = internAtom(""); + CONFIG_VARIABLE_SETTABLE(socksParentProxy, CONFIG_ATOM_LOWER, + socksParentProxySetter, + "SOCKS parent proxy (host:port)"); + CONFIG_VARIABLE_SETTABLE(socksUserName, CONFIG_ATOM, + configAtomSetter, + "SOCKS4a user name"); + CONFIG_VARIABLE_SETTABLE(socksProxyType, CONFIG_ATOM_LOWER, + socksProxyTypeSetter, + "One of socks4a or socks5"); +} + +static int +socksParentProxySetter(ConfigVariablePtr var, void *value) +{ + configAtomSetter(var, value); + initSocks(); + return 1; +} + +static int +socksProxyTypeSetter(ConfigVariablePtr var, void *value) +{ + if(*var->value.a != aSocks4a && *var->value.a != aSocks5) { + do_log(L_ERROR, "Unknown socksProxyType %s\n", (*var->value.a)->string); + return -1; + } + + return configAtomSetter(var, value); +} + +void +initSocks() +{ + int port = -1; + AtomPtr host = NULL, port_atom; + int rc; + + if(socksParentProxy != NULL && socksParentProxy->length == 0) { + releaseAtom(socksParentProxy); + socksParentProxy = NULL; + } + + if(socksParentProxy) { + rc = atomSplit(socksParentProxy, ':', &host, &port_atom); + if(rc <= 0) { + do_log(L_ERROR, "Couldn't parse socksParentProxy"); + exit(1); + } + port = atoi(port_atom->string); + releaseAtom(port_atom); + } + + if(socksProxyHost) + releaseAtom(socksProxyHost); + socksProxyHost = host; + socksProxyPort = port; + if(socksProxyAddress) + releaseAtom(socksProxyAddress); + socksProxyAddress = NULL; + socksProxyAddressIndex = -1; + + if(socksProxyType != aSocks4a && socksProxyType != aSocks5) { + do_log(L_ERROR, "Unknown socksProxyType %s\n", socksProxyType->string); + exit(1); + } +} + +static void +destroySocksRequest(SocksRequestPtr request) +{ + releaseAtom(request->name); + if(request->buf) + free(request->buf); + free(request); +} + +int +do_socks_connect(char *name, int port, + int (*handler)(int, SocksRequestPtr), + void *data) +{ + SocksRequestPtr request = malloc(sizeof(SocksRequestRec)); + SocksRequestRec request_nomem; + if(request == NULL) + goto nomem; + + request->name = internAtomLowerN(name, strlen(name)); + if(request->name == NULL) { + free(request); + goto nomem; + } + + request->port = port; + request->fd = -1; + request->handler = handler; + request->buf = NULL; + request->data = data; + + if(socksProxyAddress == NULL) { + do_gethostbyname(socksProxyHost->string, 0, + socksDnsHandler, + request); + return 1; + } + + return do_socks_connect_common(request); + + nomem: + request_nomem.name = internAtomLowerN(name, strlen(name)); + request_nomem.port = port; + request_nomem.handler = handler; + request_nomem.buf = NULL; + request_nomem.data = data; + + handler(-ENOMEM, &request_nomem); + releaseAtom(request_nomem.name); + return 1; +} + +static int +do_socks_connect_common(SocksRequestPtr request) +{ + assert(socksProxyAddressIndex >= 0); + + do_connect(retainAtom(socksProxyAddress), + socksProxyAddressIndex, + socksProxyPort, + socksConnectHandler, request); + return 1; +} + +static int +socksDnsHandler(int status, GethostbynameRequestPtr grequest) +{ + SocksRequestPtr request = grequest->data; + if(status <= 0) { + request->handler(status, request); + destroySocksRequest(request); + return 1; + } + + if(grequest->addr->string[0] == DNS_CNAME) { + if(grequest->count > 10) { + do_log(L_ERROR, "DNS CNAME loop.\n"); + request->handler(-EDNS_CNAME_LOOP, request); + destroySocksRequest(request); + return 1; + } + do_gethostbyname(grequest->addr->string + 1, grequest->count + 1, + httpServerConnectionDnsHandler, request); + return 1; + } + + + socksProxyAddress = retainAtom(grequest->addr); + socksProxyAddressIndex = 0; + + do_socks_connect_common(request); + return 1; +} + +static int +socksConnectHandler(int status, + FdEventHandlerPtr event, + ConnectRequestPtr crequest) +{ + SocksRequestPtr request = crequest->data; + int rc; + + if(status < 0) { + request->handler(status, request); + destroySocksRequest(request); + return 1; + } + + assert(request->fd < 0); + request->fd = crequest->fd; + socksProxyAddressIndex = crequest->index; + + rc = setNodelay(request->fd, 1); + if(rc < 0) + do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm"); + + if(socksProxyType == aSocks4a) { + request->buf = malloc(8 + + socksUserName->length + 1 + + request->name->length + 1); + if(request->buf == NULL) { + CLOSE(request->fd); + request->fd = -1; + request->handler(-ENOMEM, request); + destroySocksRequest(request); + return 1; + } + + request->buf[0] = 4; /* VN */ + request->buf[1] = 1; /* CD = REQUEST */ + request->buf[2] = (request->port >> 8) & 0xFF; + request->buf[3] = request->port & 0xFF; + request->buf[4] = request->buf[5] = request->buf[6] = 0; + request->buf[7] = 3; + + memcpy(request->buf + 8, socksUserName->string, socksUserName->length); + request->buf[8 + socksUserName->length] = '\0'; + + memcpy(request->buf + 8 + socksUserName->length + 1, + request->name->string, request->name->length); + request->buf[8 + socksUserName->length + 1 + request->name->length] = + '\0'; + + do_stream(IO_WRITE, request->fd, 0, request->buf, + 8 + socksUserName->length + 1 + request->name->length + 1, + socksWriteHandler, request); + } else if(socksProxyType == aSocks5) { + request->buf = malloc(8); /* 8 needed for the subsequent read */ + if(request->buf == NULL) { + CLOSE(request->fd); + request->fd = -1; + request->handler(-ENOMEM, request); + destroySocksRequest(request); + return 1; + } + + request->buf[0] = 5; /* ver */ + request->buf[1] = 1; /* nmethods */ + request->buf[2] = 0; /* no authentication required */ + do_stream(IO_WRITE, request->fd, 0, request->buf, 3, + socksWriteHandler, request); + } else { + request->handler(-EUNKNOWN, request); + } + return 1; +} + +static int +socksWriteHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + SocksRequestPtr request = srequest->data; + + if(status < 0) + goto error; + + if(!streamRequestDone(srequest)) { + if(status) { + status = -ESOCKS_PROTOCOL; + goto error; + } + return 0; + } + + do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, 8, + socksProxyType == aSocks5 ? + socks5ReadHandler : socksReadHandler, + request); + return 1; + + error: + CLOSE(request->fd); + request->fd = -1; + request->handler(status, request); + destroySocksRequest(request); + return 1; +} + +static int +socksReadHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + SocksRequestPtr request = srequest->data; + + if(status < 0) + goto error; + + if(srequest->offset < 8) { + if(status) { + status = -ESOCKS_PROTOCOL; + goto error; + } + return 0; + } + + if(request->buf[0] != 0 || request->buf[1] != 90) { + if(request->buf[1] >= 91 && request->buf[1] <= 93) + status = -(ESOCKS_PROTOCOL + request->buf[1] - 90); + else + status = -ESOCKS_PROTOCOL; + goto error; + } + + request->handler(1, request); + destroySocksRequest(request); + return 1; + + error: + CLOSE(request->fd); + request->fd = -1; + request->handler(status, request); + destroySocksRequest(request); + return 1; +} + +static int +socks5ReadHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + SocksRequestPtr request = srequest->data; + + if(status < 0) + goto error; + + if(srequest->offset < 2) { + if(status) { + status = -ESOCKS_PROTOCOL; + goto error; + } + return 0; + } + + if(request->buf[0] != 5 || request->buf[1] != 0) { + status = -ESOCKS_PROTOCOL; + goto error; + } + + free(request->buf); + request->buf = malloc(5 + request->name->length + 2); + if(request->buf == NULL) { + status = -ENOMEM; + goto error; + } + + request->buf[0] = 5; /* ver */ + request->buf[1] = 1; /* cmd */ + request->buf[2] = 0; /* rsv */ + request->buf[3] = 3; /* atyp */ + request->buf[4] = request->name->length; + memcpy(request->buf + 5, request->name->string, request->name->length); + request->buf[5 + request->name->length] = (request->port >> 8) & 0xFF; + request->buf[5 + request->name->length + 1] = request->port & 0xFF; + + do_stream(IO_WRITE, request->fd, 0, + request->buf, 5 + request->name->length + 2, + socks5WriteHandler, request); + return 1; + + error: + CLOSE(request->fd); + request->fd = -1; + request->handler(status, request); + destroySocksRequest(request); + return 1; +} + +static int +socks5WriteHandler(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + SocksRequestPtr request = srequest->data; + + if(status < 0) + goto error; + + if(!streamRequestDone(srequest)) { + if(status) { + status = -ESOCKS_PROTOCOL; + goto error; + } + return 0; + } + + do_stream(IO_READ | IO_NOTNOW, request->fd, 0, request->buf, 10, + socks5ReadHandler2, request); + return 1; + + error: + request->handler(status, request); + destroySocksRequest(request); + return 1; +} + +static int +socks5ReadHandler2(int status, + FdEventHandlerPtr event, + StreamRequestPtr srequest) +{ + SocksRequestPtr request = srequest->data; + + if(status < 0) + goto error; + + if(srequest->offset < 4) { + if(status) { + status = -ESOCKS_PROTOCOL; + goto error; + } + return 0; + } + + if(request->buf[0] != 5) { + status = -ESOCKS_PROTOCOL; + goto error; + } + + if(request->buf[1] != 0) { + status = -(ESOCKS5_BASE + request->buf[1]); + goto error; + } + + if(request->buf[3] != 1) { + status = -ESOCKS_PROTOCOL; + goto error; + } + + if(srequest->offset < 10) + return 0; + + request->handler(1, request); + destroySocksRequest(request); + return 1; + + error: + CLOSE(request->fd); + request->fd = -1; + request->handler(status, request); + destroySocksRequest(request); + return 1; +} + +#endif diff --git a/jni/polipo/socks.h b/jni/polipo/socks.h new file mode 100644 index 00000000..56a39cad --- /dev/null +++ b/jni/polipo/socks.h @@ -0,0 +1,36 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +extern AtomPtr socksParentProxy; + +typedef struct _SocksRequest { + AtomPtr name; + int port; + int fd; + int (*handler)(int, struct _SocksRequest*); + char *buf; + void *data; +} SocksRequestRec, *SocksRequestPtr; + +void preinitSocks(void); +void initSocks(void); +int do_socks_connect(char*, int, int (*)(int, SocksRequestPtr), void*); diff --git a/jni/polipo/tunnel.c b/jni/polipo/tunnel.c new file mode 100644 index 00000000..ddb9edff --- /dev/null +++ b/jni/polipo/tunnel.c @@ -0,0 +1,572 @@ +/* +Copyright (c) 2004-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +#ifdef NO_TUNNEL + +void +do_tunnel(int fd, char *buf, int offset, int len, AtomPtr url) +{ + int n; + assert(buf); + + n = httpWriteErrorHeaders(buf, CHUNK_SIZE, 0, 1, + 501, internAtom("CONNECT not available " + "in this version."), + 1, NULL, url->string, url->length, NULL); + releaseAtom(url); + if(n >= 0) { + /* This is completely wrong. The write is non-blocking, and we + don't reschedule it if it fails. But then, if the write + blocks, we'll simply drop the connection with no error message. */ + write(fd, buf, n); + } + dispose_chunk(buf); + lingeringClose(fd); + return; +} + +#else + +static void tunnelDispatch(TunnelPtr); +static int tunnelRead1Handler(int, FdEventHandlerPtr, StreamRequestPtr); +static int tunnelRead2Handler(int, FdEventHandlerPtr, StreamRequestPtr); +static int tunnelWrite1Handler(int, FdEventHandlerPtr, StreamRequestPtr); +static int tunnelWrite2Handler(int, FdEventHandlerPtr, StreamRequestPtr); +static int tunnelDnsHandler(int, GethostbynameRequestPtr); +static int tunnelConnectionHandler(int, FdEventHandlerPtr, ConnectRequestPtr); +static int tunnelSocksHandler(int, SocksRequestPtr); +static int tunnelHandlerCommon(int, TunnelPtr); +static int tunnelError(TunnelPtr, int, AtomPtr); + +static int +circularBufferFull(CircularBufferPtr buf) +{ + if(buf->head == buf->tail - 1) + return 1; + if(buf->head == CHUNK_SIZE - 1 && buf->tail == 0) + return 1; + return 0; +} + +static int +circularBufferEmpty(CircularBufferPtr buf) +{ + return buf->head == buf->tail; +} + +static void +logTunnel(TunnelPtr tunnel, int blocked) +{ + do_log(L_TUNNEL,"tunnel %s:%d %s\n", tunnel->hostname->string, tunnel->port, + blocked ? "blocked" : "allowed"); +} + +static TunnelPtr +makeTunnel(int fd, char *buf, int offset, int len) +{ + TunnelPtr tunnel; + assert(offset < CHUNK_SIZE); + + tunnel = malloc(sizeof(TunnelRec)); + if(tunnel == NULL) + return NULL; + + tunnel->hostname = NULL; + tunnel->port = -1; + tunnel->flags = 0; + tunnel->fd1 = fd; + tunnel->fd2 = -1; + tunnel->buf1.buf = buf; + if(offset == len) { + tunnel->buf1.tail = 0; + tunnel->buf1.head = 0; + } else { + tunnel->buf1.tail = offset; + tunnel->buf1.head = len; + } + tunnel->buf2.buf = NULL; + tunnel->buf2.tail = 0; + tunnel->buf2.head = 0; + return tunnel; +} + +static void +destroyTunnel(TunnelPtr tunnel) +{ + assert(tunnel->fd1 < 0 && tunnel->fd2 < 0); + releaseAtom(tunnel->hostname); + if(tunnel->buf1.buf) + dispose_chunk(tunnel->buf1.buf); + if(tunnel->buf2.buf) + dispose_chunk(tunnel->buf2.buf); + free(tunnel); +} + +void +do_tunnel(int fd, char *buf, int offset, int len, AtomPtr url) +{ + TunnelPtr tunnel; + int port; + char *p, *q; + + tunnel = makeTunnel(fd, buf, offset, len); + if(tunnel == NULL) { + do_log(L_ERROR, "Couldn't allocate tunnel.\n"); + releaseAtom(url); + dispose_chunk(buf); + CLOSE(fd); + return; + } + + if(proxyOffline) { + do_log(L_INFO, "Attemted CONNECT when disconnected.\n"); + releaseAtom(url); + tunnelError(tunnel, 502, + internAtom("Cannot CONNECT when disconnected.")); + return; + } + + p = memrchr(url->string, ':', url->length); + q = NULL; + if(p) + port = strtol(p + 1, &q, 10); + if(!p || q != url->string + url->length) { + do_log(L_ERROR, "Couldn't parse CONNECT.\n"); + releaseAtom(url); + tunnelError(tunnel, 400, internAtom("Couldn't parse CONNECT")); + return; + } + tunnel->hostname = internAtomLowerN(url->string, p - url->string); + if(tunnel->hostname == NULL) { + releaseAtom(url); + tunnelError(tunnel, 501, internAtom("Couldn't allocate hostname")); + return; + } + + if(!intListMember(port, tunnelAllowedPorts)) { + releaseAtom(url); + tunnelError(tunnel, 403, internAtom("Forbidden port")); + return; + } + tunnel->port = port; + + if (tunnelIsMatched(url->string, url->length, + tunnel->hostname->string, tunnel->hostname->length)) { + releaseAtom(url); + tunnelError(tunnel, 404, internAtom("Forbidden tunnel")); + logTunnel(tunnel,1); + return; + } + + logTunnel(tunnel,0); + + releaseAtom(url); + + if(socksParentProxy) + do_socks_connect(parentHost ? + parentHost->string : tunnel->hostname->string, + parentHost ? parentPort : tunnel->port, + tunnelSocksHandler, tunnel); + else + do_gethostbyname(parentHost ? + parentHost->string : tunnel->hostname->string, 0, + tunnelDnsHandler, tunnel); +} + +static int +tunnelDnsHandler(int status, GethostbynameRequestPtr request) +{ + TunnelPtr tunnel = request->data; + + if(status <= 0) { + tunnelError(tunnel, 504, + internAtomError(-status, + "Host %s lookup failed", + atomString(tunnel->hostname))); + return 1; + } + + if(request->addr->string[0] == DNS_CNAME) { + if(request->count > 10) + tunnelError(tunnel, 504, internAtom("CNAME loop")); + do_gethostbyname(request->addr->string + 1, request->count + 1, + tunnelDnsHandler, tunnel); + return 1; + } + + do_connect(retainAtom(request->addr), 0, + parentHost ? parentPort : tunnel->port, + tunnelConnectionHandler, tunnel); + return 1; +} + +static int +tunnelConnectionHandler(int status, + FdEventHandlerPtr event, + ConnectRequestPtr request) +{ + TunnelPtr tunnel = request->data; + int rc; + + if(status < 0) { + tunnelError(tunnel, 504, internAtomError(-status, "Couldn't connect")); + return 1; + } + + rc = setNodelay(request->fd, 1); + if(rc < 0) + do_log_error(L_WARN, errno, "Couldn't disable Nagle's algorithm"); + + return tunnelHandlerCommon(request->fd, tunnel); +} + +static int +tunnelSocksHandler(int status, SocksRequestPtr request) +{ + TunnelPtr tunnel = request->data; + + if(status < 0) { + tunnelError(tunnel, 504, internAtomError(-status, "Couldn't connect")); + return 1; + } + + return tunnelHandlerCommon(request->fd, tunnel); +} + +static int +tunnelHandlerParent(int fd, TunnelPtr tunnel) +{ + char *message; + int n; + + if(tunnel->buf1.buf == NULL) + tunnel->buf1.buf = get_chunk(); + if(tunnel->buf1.buf == NULL) { + message = "Couldn't allocate buffer"; + goto fail; + } + if(tunnel->buf1.tail != tunnel->buf1.head) { + message = "Pipelined connect to parent proxy not implemented"; + goto fail; + } + + n = snnprintf(tunnel->buf1.buf, tunnel->buf1.tail, CHUNK_SIZE, + "CONNECT %s:%d HTTP/1.1", + tunnel->hostname->string, tunnel->port); + if (parentAuthCredentials) + n = buildServerAuthHeaders(tunnel->buf1.buf, n, CHUNK_SIZE, + parentAuthCredentials); + n = snnprintf(tunnel->buf1.buf, n, CHUNK_SIZE, "\r\n\r\n"); + + if(n < 0) { + message = "Buffer overflow"; + goto fail; + } + tunnel->buf1.head = n; + tunnelDispatch(tunnel); + return 1; + + fail: + CLOSE(fd); + tunnel->fd2 = -1; + tunnelError(tunnel, 501, internAtom(message)); + return 1; +} + +static int +tunnelHandlerCommon(int fd, TunnelPtr tunnel) +{ + const char *message = "HTTP/1.1 200 Tunnel established\r\n\r\n"; + + tunnel->fd2 = fd; + + if(parentHost) + return tunnelHandlerParent(fd, tunnel); + + if(tunnel->buf2.buf == NULL) + tunnel->buf2.buf = get_chunk(); + if(tunnel->buf2.buf == NULL) { + CLOSE(fd); + tunnelError(tunnel, 501, internAtom("Couldn't allocate buffer")); + return 1; + } + + memcpy(tunnel->buf2.buf, message, MIN(CHUNK_SIZE - 1, strlen(message))); + tunnel->buf2.head = MIN(CHUNK_SIZE - 1, strlen(message)); + + tunnelDispatch(tunnel); + return 1; +} + +static void +bufRead(int fd, CircularBufferPtr buf, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + int tail; + + if(buf->tail == 0) + tail = CHUNK_SIZE - 1; + else + tail = buf->tail - 1; + + if(buf->head == 0) + do_stream_buf(IO_READ | IO_NOTNOW, + fd, 0, + &buf->buf, tail, + handler, data); + else if(buf->tail > buf->head) + do_stream(IO_READ | IO_NOTNOW, + fd, buf->head, + buf->buf, tail, + handler, data); + else + do_stream_2(IO_READ | IO_NOTNOW, + fd, buf->head, + buf->buf, CHUNK_SIZE, + buf->buf, tail, + handler, data); +} + +static void +bufWrite(int fd, CircularBufferPtr buf, + int (*handler)(int, FdEventHandlerPtr, StreamRequestPtr), + void *data) +{ + if(buf->head > buf->tail) + do_stream(IO_WRITE, + fd, buf->tail, + buf->buf, buf->head, + handler, data); + else + do_stream_2(IO_WRITE, + fd, buf->tail, + buf->buf, CHUNK_SIZE, + buf->buf, buf->head, + handler, data); +} + +static void +tunnelDispatch(TunnelPtr tunnel) +{ + if(circularBufferEmpty(&tunnel->buf1)) { + if(tunnel->buf1.buf && + !(tunnel->flags & (TUNNEL_READER1 | TUNNEL_WRITER2))) { + dispose_chunk(tunnel->buf1.buf); + tunnel->buf1.buf = NULL; + tunnel->buf1.head = tunnel->buf1.tail = 0; + } + } + + if(circularBufferEmpty(&tunnel->buf2)) { + if(tunnel->buf2.buf && + !(tunnel->flags & (TUNNEL_READER2 | TUNNEL_WRITER1))) { + dispose_chunk(tunnel->buf2.buf); + tunnel->buf2.buf = NULL; + tunnel->buf2.head = tunnel->buf2.tail = 0; + } + } + + if(tunnel->fd1 >= 0) { + if(!(tunnel->flags & (TUNNEL_READER1 | TUNNEL_EOF1)) && + !circularBufferFull(&tunnel->buf1)) { + tunnel->flags |= TUNNEL_READER1; + bufRead(tunnel->fd1, &tunnel->buf1, tunnelRead1Handler, tunnel); + } + if(!(tunnel->flags & (TUNNEL_WRITER1 | TUNNEL_EPIPE1)) && + !circularBufferEmpty(&tunnel->buf2)) { + tunnel->flags |= TUNNEL_WRITER1; + /* There's no IO_NOTNOW in bufWrite, so it might close the + file descriptor straight away. Wait until we're + rescheduled. */ + bufWrite(tunnel->fd1, &tunnel->buf2, tunnelWrite1Handler, tunnel); + return; + } + if(tunnel->fd2 < 0 || (tunnel->flags & TUNNEL_EOF2)) { + if(!(tunnel->flags & TUNNEL_EPIPE1)) + shutdown(tunnel->fd1, 1); + tunnel->flags |= TUNNEL_EPIPE1; + } else if(tunnel->fd1 < 0 || (tunnel->flags & TUNNEL_EPIPE2)) { + if(!(tunnel->flags & TUNNEL_EOF1)) + shutdown(tunnel->fd1, 0); + tunnel->flags |= TUNNEL_EOF1; + } + if((tunnel->flags & TUNNEL_EOF1) && (tunnel->flags & TUNNEL_EPIPE1)) { + if(!(tunnel->flags & (TUNNEL_READER1 | TUNNEL_WRITER1))) { + CLOSE(tunnel->fd1); + tunnel->fd1 = -1; + } + } + } + + if(tunnel->fd2 >= 0) { + if(!(tunnel->flags & (TUNNEL_READER2 | TUNNEL_EOF2)) && + !circularBufferFull(&tunnel->buf2)) { + tunnel->flags |= TUNNEL_READER2; + bufRead(tunnel->fd2, &tunnel->buf2, tunnelRead2Handler, tunnel); + } + if(!(tunnel->flags & (TUNNEL_WRITER2 | TUNNEL_EPIPE2)) && + !circularBufferEmpty(&tunnel->buf1)) { + tunnel->flags |= TUNNEL_WRITER2; + bufWrite(tunnel->fd2, &tunnel->buf1, tunnelWrite2Handler, tunnel); + return; + } + if(tunnel->fd1 < 0 || (tunnel->flags & TUNNEL_EOF1)) { + if(!(tunnel->flags & TUNNEL_EPIPE2)) + shutdown(tunnel->fd2, 1); + tunnel->flags |= TUNNEL_EPIPE2; + } else if(tunnel->fd1 < 0 || (tunnel->flags & TUNNEL_EPIPE1)) { + if(!(tunnel->flags & TUNNEL_EOF2)) + shutdown(tunnel->fd2, 0); + tunnel->flags |= TUNNEL_EOF2; + } + if((tunnel->flags & TUNNEL_EOF2) && (tunnel->flags & TUNNEL_EPIPE2)) { + if(!(tunnel->flags & (TUNNEL_READER2 | TUNNEL_WRITER2))) { + CLOSE(tunnel->fd2); + tunnel->fd2 = -1; + } + } + } + + if(tunnel->fd1 < 0 && tunnel->fd2 < 0) + destroyTunnel(tunnel); + else + assert(tunnel->flags & (TUNNEL_READER1 | TUNNEL_WRITER1 | + TUNNEL_READER2 | TUNNEL_WRITER2)); +} + +static int +tunnelRead1Handler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + TunnelPtr tunnel = request->data; + if(status) { + if(status < 0) + do_log_error(L_ERROR, -status, "Couldn't read from client"); + tunnel->flags |= TUNNEL_EOF1; + goto done; + } + tunnel->buf1.head = request->offset % CHUNK_SIZE; + done: + /* Keep buffer empty to avoid a deadlock */ + if((tunnel->flags & TUNNEL_EPIPE2)) + tunnel->buf1.tail = tunnel->buf1.head; + tunnel->flags &= ~TUNNEL_READER1; + tunnelDispatch(tunnel); + return 1; +} + +static int +tunnelRead2Handler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + TunnelPtr tunnel = request->data; + if(status) { + if(status < 0) + do_log_error(L_ERROR, -status, "Couldn't read from server"); + tunnel->flags |= TUNNEL_EOF2; + goto done; + } + tunnel->buf2.head = request->offset % CHUNK_SIZE; +done: + /* Keep buffer empty to avoid a deadlock */ + if((tunnel->flags & TUNNEL_EPIPE1)) + tunnel->buf2.tail = tunnel->buf2.head; + tunnel->flags &= ~TUNNEL_READER2; + tunnelDispatch(tunnel); + return 1; +} + +static int +tunnelWrite1Handler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + TunnelPtr tunnel = request->data; + if(status || (tunnel->flags & TUNNEL_EPIPE1)) { + tunnel->flags |= TUNNEL_EPIPE1; + if(status < 0 && status != -EPIPE) + do_log_error(L_ERROR, -status, "Couldn't write to client"); + /* Empty the buffer to avoid a deadlock */ + tunnel->buf2.tail = tunnel->buf2.head; + goto done; + } + tunnel->buf2.tail = request->offset % CHUNK_SIZE; + done: + tunnel->flags &= ~TUNNEL_WRITER1; + tunnelDispatch(tunnel); + return 1; +} + +static int +tunnelWrite2Handler(int status, + FdEventHandlerPtr event, StreamRequestPtr request) +{ + TunnelPtr tunnel = request->data; + if(status || (tunnel->flags & TUNNEL_EPIPE2)) { + tunnel->flags |= TUNNEL_EPIPE2; + if(status < 0 && status != -EPIPE) + do_log_error(L_ERROR, -status, "Couldn't write to server"); + /* Empty the buffer to avoid a deadlock */ + tunnel->buf1.tail = tunnel->buf1.head; + goto done; + } + tunnel->buf1.tail = request->offset % CHUNK_SIZE; + done: + tunnel->flags &= ~TUNNEL_WRITER2; + tunnelDispatch(tunnel); + return 1; +} + +static int +tunnelError(TunnelPtr tunnel, int code, AtomPtr message) +{ + int n; + if(tunnel->fd2 > 0) { + CLOSE(tunnel->fd2); + tunnel->fd2 = -1; + } + + if(tunnel->buf2.buf == NULL) + tunnel->buf2.buf = get_chunk(); + if(tunnel->buf2.buf == NULL) + goto fail; + + n = httpWriteErrorHeaders(tunnel->buf2.buf, CHUNK_SIZE - 1, 0, + 1, code, message, 1, NULL, + NULL, 0, NULL); + + if(n <= 0) goto fail; + + tunnel->buf2.head = n; + + tunnelDispatch(tunnel); + return 1; + + fail: + CLOSE(tunnel->fd1); + tunnel->fd1 = -1; + tunnelDispatch(tunnel); + return 1; +} +#endif diff --git a/jni/polipo/tunnel.h b/jni/polipo/tunnel.h new file mode 100644 index 00000000..a277b139 --- /dev/null +++ b/jni/polipo/tunnel.h @@ -0,0 +1,50 @@ +/* +Copyright (c) 2004-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +typedef struct _CircularBuffer { + int head; + int tail; + char *buf; +} CircularBufferRec, *CircularBufferPtr; + +#define TUNNEL_READER1 1 +#define TUNNEL_WRITER1 2 +#define TUNNEL_EOF1 4 +#define TUNNEL_EPIPE1 8 +#define TUNNEL_READER2 16 +#define TUNNEL_WRITER2 32 +#define TUNNEL_EOF2 64 +#define TUNNEL_EPIPE2 128 + +typedef struct _Tunnel { + AtomPtr hostname; + int port; + int flags; + int fd1; + CircularBufferRec buf1; + int fd2; + CircularBufferRec buf2; +} TunnelRec, *TunnelPtr; + +void do_tunnel(int fd, char *buf, int offset, int len, AtomPtr url); + +void listTunnels(FILE *out); diff --git a/jni/polipo/util.c b/jni/polipo/util.c new file mode 100644 index 00000000..2132a54d --- /dev/null +++ b/jni/polipo/util.c @@ -0,0 +1,839 @@ +/* +Copyright (c) 2003-2007 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +#include "polipo.h" + +/* Note that this is different from GNU's strndup(3). */ +char * +strdup_n(const char *restrict buf, int n) +{ + char *s; + s = malloc(n + 1); + if(s == NULL) + return NULL; + memcpy(s, buf, n); + s[n] = '\0'; + return s; +} + +int +snnprintf(char *restrict buf, int n, int len, const char *format, ...) +{ + va_list args; + int rc; + va_start(args, format); + rc = snnvprintf(buf, n, len, format, args); + va_end(args); + return rc; +} + +int +snnvprintf(char *restrict buf, int n, int len, const char *format, va_list args) +{ + int rc = -1; + if(n < 0) return -2; + if(n < len) + rc = vsnprintf(buf + n, len - n, format, args); + if(rc >= 0 && n + rc <= len) + return n + rc; + else + return -1; +} + +int +snnprint_n(char *restrict buf, int n, int len, const char *s, int slen) +{ + int i = 0; + if(n < 0) return -2; + while(i < slen && n < len) + buf[n++] = s[i++]; + if(n < len) + return n; + else + return -1; +} + +int +strcmp_n(const char *string, const char *buf, int n) +{ + int i; + i = 0; + while(string[i] != '\0' && i < n) { + if(string[i] < buf[i]) + return -1; + else if(string[i] > buf[i]) + return 1; + i++; + } + if(string[i] == '\0' || i == n) + return 0; + else if(i == n) + return 1; + else + return -1; +} + +int +letter(char c) +{ + if(c >= 'A' && c <= 'Z') return 1; + if(c >= 'a' && c <= 'z') return 1; + return 0; +} + +int +digit(char c) +{ + if(c >= '0' && c <= '9') + return 1; + return 0; +} + +char +lwr(char a) +{ + if(a >= 'A' && a <= 'Z') + return a | 0x20; + else + return a; +} + +char * +lwrcpy(char *restrict dst, const char *restrict src, int n) +{ + int i; + for(i = 0; i < n; i++) + dst[i] = lwr(src[i]); + return dst; +} + +int +lwrcmp(const char *as, const char *bs, int n) +{ + int i; + for(i = 0; i < n; i++) { + char a = lwr(as[i]), b = lwr(bs[i]); + if(a < b) + return -1; + else if(a > b) + return 1; + } + return 0; +} + +int +strcasecmp_n(const char *string, const char *buf, int n) +{ + int i; + i = 0; + while(string[i] != '\0' && i < n) { + char a = lwr(string[i]), b = lwr(buf[i]); + if(a < b) + return -1; + else if(a > b) + return 1; + i++; + } + if(string[i] == '\0' && i == n) + return 0; + else if(i == n) + return 1; + else + return -1; +} + +int +atoi_n(const char *restrict string, int n, int len, int *value_return) +{ + int i = n; + int val = 0; + + if(i >= len || !digit(string[i])) + return -1; + + while(i < len && digit(string[i])) { + val = val * 10 + (string[i] - '0'); + i++; + } + *value_return = val; + return i; +} + +int +isWhitespace(const char *string) +{ + while(*string != '\0') { + if(*string == ' ' || *string == '\t') + string++; + else + return 0; + } + return 1; +} + +#ifndef HAVE_MEMRCHR +void * +memrchr(const void *s, int c, size_t n) +{ + const unsigned char *ss = s; + unsigned char cc = c; + size_t i; + for(i = n - 1; i >= 0; i--) + if(ss[i] == cc) + return (void*)(ss + i); + return NULL; +} +#endif + +int +h2i(char h) +{ + if(h >= '0' && h <= '9') + return h - '0'; + else if(h >= 'a' && h <= 'f') + return h - 'a' + 10; + else if(h >= 'A' && h <= 'F') + return h - 'A' + 10; + else + return -1; +} + +char +i2h(int i) +{ + if(i < 0 || i >= 16) + return '?'; + if(i < 10) + return i + '0'; + else + return i - 10 + 'A'; +} + +/* floor(log2(x)) */ +int +log2_floor(int x) +{ + int i, j; + + assert(x > 0); + + i = 0; + j = 1; + while(2 * j <= x) { + i++; + j *= 2; + } + return i; +} + +/* ceil(log2(x)) */ +int +log2_ceil(int x) +{ + int i, j; + + assert(x > 0); + + i = 0; + j = 1; + while(j < x) { + i++; + j *= 2; + } + return i; +} + +#ifdef HAVE_ASPRINTF +char * +vsprintf_a(const char *f, va_list args) +{ + char *r; + int rc; + + rc = vasprintf(&r, f, args); + if(rc < 0) + return NULL; + return r; + +} + +#else + +char* +vsprintf_a(const char *f, va_list args) +{ + int n, size; + char buf[64]; + char *string; + va_list args_copy; + + va_copy(args_copy, args); + n = vsnprintf(buf, 64, f, args_copy); + if(n >= 0 && n < 64) { + return strdup_n(buf, n); + } + if(n >= 64) + size = n + 1; + else + size = 96; + + while(1) { + string = malloc(size); + if(!string) + return NULL; + va_copy(args_copy, args); + n = vsnprintf(string, size, f, args_copy); + if(n >= 0 && n < size) + return string; + else if(n >= size) + size = n + 1; + else + size = size * 3 / 2; + free(string); + if(size > 16 * 1024) + return NULL; + } + /* NOTREACHED */ +} +#endif + +char* +sprintf_a(const char *f, ...) +{ + char *s; + va_list args; + va_start(args, f); + s = vsprintf_a(f, args); + va_end(args); + return s; +} + +unsigned int +hash(unsigned int seed, const void *restrict key, int key_size, + unsigned int hash_size) +{ + int i; + unsigned int h; + + h = seed; + for(i = 0; i < key_size; i++) + h = (h << 5) + (h >> (hash_size - 5)) + + ((unsigned char*)key)[i]; + return h & ((1 << hash_size) - 1); +} + +char * +pstrerror(int e) +{ + char *s; + static char buf[20]; + + switch(e) { + case EDOSHUTDOWN: s = "Immediate shutdown requested"; break; + case EDOGRACEFUL: s = "Graceful shutdown requested"; break; + case EDOTIMEOUT: s = "Timeout"; break; + case ECLIENTRESET: s = "Connection reset by client"; break; + case ESYNTAX: s = "Incorrect syntax"; break; + case EREDIRECTOR: s = "Redirector error"; break; + case EDNS_HOST_NOT_FOUND: s = "Host not found"; break; + case EDNS_NO_ADDRESS: s = "No address"; break; + case EDNS_NO_RECOVERY: s = "Permanent name server failure"; break; + case EDNS_TRY_AGAIN: s = "Temporary name server failure"; break; + case EDNS_INVALID: s = "Invalid reply from name server"; break; + case EDNS_UNSUPPORTED: s = "Unsupported DNS reply"; break; + case EDNS_FORMAT: s = "Invalid DNS query"; break; + case EDNS_REFUSED: s = "DNS query refused by server"; break; + case EDNS_CNAME_LOOP: s = "DNS CNAME loop"; break; +#ifndef NO_SOCKS + case ESOCKS_PROTOCOL: s = "SOCKS protocol error"; break; + case ESOCKS_REJECT_FAIL: s = "SOCKS request rejected or failed"; break; + case ESOCKS_REJECT_IDENTD: s = "SOCKS request rejected: " + "server couldn't connect to identd"; + break; + case ESOCKS_REJECT_UID_MISMATCH: s = "SOCKS request rejected: " + "uid mismatch"; + break; + case ESOCKS5_BASE: s = "SOCKS success"; break; + case ESOCKS5_BASE + 1: s = "General SOCKS server failure"; break; + case ESOCKS5_BASE + 2: s = "SOCKS connection not allowed"; break; + case ESOCKS5_BASE + 3: s = "SOCKS error: network unreachable"; break; + case ESOCKS5_BASE + 4: s = "SOCKS error: host unreachable"; break; + case ESOCKS5_BASE + 5: s = "SOCKS error: connection refused"; break; + case ESOCKS5_BASE + 6: s = "SOCKS error: TTL expired"; break; + case ESOCKS5_BASE + 7: s = "SOCKS command not supported"; break; + case ESOCKS5_BASE + 8: s = "SOCKS error: address type not supported"; + break; +#endif + case EUNKNOWN: s = "Unknown error"; break; + default: s = NULL; break; + } + if(!s) s = strerror(e); +#ifdef WIN32 /*MINGW*/ + if(!s) { + if(e >= WSABASEERR && e <= WSABASEERR + 2000) { + /* This should be okay, as long as the caller discards the + pointer before another error occurs. */ + snprintf(buf, 20, "Winsock error %d", e); + s = buf; + } + } +#endif + if(!s) { + snprintf(buf, 20, "Unknown error %d", e); + s = buf; + } + return s; +} + +/* Like mktime(3), but UTC rather than local time */ +#if defined(HAVE_TIMEGM) +time_t +mktime_gmt(struct tm *tm) +{ + return timegm(tm); +} +#elif defined(HAVE_MKGMTIME) +time_t +mktime_gmt(struct tm *tm) +{ + return _mkgmtime(tm); +} +#elif defined(HAVE_TM_GMTOFF) +time_t +mktime_gmt(struct tm *tm) +{ + time_t t; + struct tm *ltm; + + t = mktime(tm); + if(t < 0) + return -1; + ltm = localtime(&t); + if(ltm == NULL) + return -1; + return t + ltm->tm_gmtoff; +} +#elif defined(HAVE_TZSET) +#ifdef HAVE_SETENV +/* Taken from the Linux timegm(3) man page. */ +time_t +mktime_gmt(struct tm *tm) +{ + time_t t; + char *tz; + + tz = getenv("TZ"); + setenv("TZ", "GMT", 1); + tzset(); + t = mktime(tm); + if(tz) + setenv("TZ", tz, 1); + else + unsetenv("TZ"); + tzset(); + return t; +} +#else +time_t +mktime_gmt(struct tm *tm) +{ + time_t t; + char *tz; + static char *old_tz = NULL; + + tz = getenv("TZ"); + putenv("TZ=GMT"); + tzset(); + t = mktime(tm); + if(old_tz) + free(old_tz); + if(tz) + old_tz = sprintf_a("TZ=%s", tz); + else + old_tz = strdup("TZ"); /* XXX - non-portable? */ + if(old_tz) + putenv(old_tz); + tzset(); + return t; +} +#endif +#else +#error no mktime_gmt implementation on this platform +#endif + + +AtomPtr +expandTilde(AtomPtr filename) +{ + char *buf; + char *home; + int len; + AtomPtr ret; + + if(filename == NULL || filename->length < 1 || + filename->string[0] != '~' || filename->string[1] != '/') + return filename; + + home = getenv("HOME"); + if(home == NULL) { + return NULL; + } + len = strlen(home); + buf = malloc(len + 1 + 1 + filename->length - 2); + if(buf == NULL) { + do_log(L_ERROR, "Could not allocate buffer.\n"); + return NULL; + } + + memcpy(buf, home, len); + if(buf[len - 1] != '/') + buf[len++] = '/'; + memcpy(buf + len, filename->string + 2, filename->length - 2); + len += filename->length - 2; + ret = internAtomN(buf, len); + free(buf); + if(ret != NULL) + releaseAtom(filename); + return ret; +} + +#ifdef HAVE_FORK +void +do_daemonise(int noclose) +{ + int rc; + + fflush(stdout); + fflush(stderr); + + rc = fork(); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't fork"); + exit(1); + } + + if(rc > 0) + exit(0); + + if(!noclose) { + int fd; + close(0); + close(1); + close(2); + /* Leaving the default file descriptors free is not a good + idea, as it will cause library functions such as abort to + thrash the on-disk cache. */ + fd = open("/dev/null", O_RDONLY); + if(fd > 0) { + dup2(fd, 0); + close(fd); + } + fd = open("/dev/null", O_WRONLY); + if(fd >= 0) { + if(fd != 1) + dup2(fd, 1); + if(fd != 2) + dup2(fd, 2); + if(fd != 1 && fd != 2) + close(fd); + } + } + rc = setsid(); + if(rc < 0) { + do_log_error(L_ERROR, errno, "Couldn't create new session"); + exit(1); + } +} + +#else + +void +do_daemonise(int noclose) +{ + do_log(L_ERROR, "Cannot daemonise on this platform"); + exit(1); +} +#endif + + +void +writePid(char *pidfile) +{ + int fd, n, rc; + char buf[16]; + + fd = open(pidfile, O_WRONLY | O_CREAT | O_EXCL, 0666); + if(fd < 0) { + do_log_error(L_ERROR, errno, + "Couldn't create pid file %s", pidfile); + exit(1); + } + n = snprintf(buf, 16, "%ld\n", (long)getpid()); + if(n < 0 || n >= 16) { + close(fd); + unlink(pidfile); + do_log(L_ERROR, "Couldn't format pid.\n"); + exit(1); + } + rc = write(fd, buf, n); + if(rc != n) { + close(fd); + unlink(pidfile); + do_log_error(L_ERROR, errno, "Couldn't write pid"); + exit(1); + } + + close(fd); + return; +} + +static const char b64[64] = +"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +/* "/" replaced with "-" */ +static const char b64fss[64] = +"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"; + +int +b64cpy(char *restrict dst, const char *restrict src, int n, int fss) +{ + const char *b = fss ? b64fss: b64; + int i, j; + + j = 0; + for(i = 0; i < n; i += 3) { + unsigned char a0, a1, a2; + a0 = src[i]; + a1 = i < n - 1 ? src[i + 1] : 0; + a2 = i < n - 2 ? src[i + 2] : 0; + dst[j++] = b[(a0 >> 2) & 0x3F]; + dst[j++] = b[((a0 << 4) & 0x30) | ((a1 >> 4) & 0x0F)]; + if(i < n - 1) + dst[j++] = b[((a1 << 2) & 0x3C) | ((a2 >> 6) & 0x03)]; + else + dst[j++] = '='; + if(i < n - 2) + dst[j++] = b[a2 & 0x3F]; + else + dst[j++] = '='; + } + return j; +} + +int +b64cmp(const char *restrict a, int an, const char *restrict b, int bn) +{ + char *buf; + int r; + + if(an % 4 != 0) + return -1; + if((bn + 2) / 3 != an / 4) + return -1; + buf = malloc(an); + if(buf == NULL) + return -1; + b64cpy(buf, b, bn, 0); + r = memcmp(buf, a, an); + free(buf); + return r; +} + +IntListPtr +makeIntList(int size) +{ + IntListPtr list; + if(size <= 0) + size = 4; + + list = malloc(sizeof(IntListRec)); + if(list == NULL) + return NULL; + + list->ranges = malloc(size * sizeof(IntRangeRec)); + if(list->ranges == NULL) { + free(list); + return NULL; + } + + list->length = 0; + list->size = size; + return list; +} + +void +destroyIntList(IntListPtr list) +{ + free(list->ranges); + free(list); +} + +int +intListMember(int n, IntListPtr list) +{ + int lo = 0, hi = list->length - 1; + int mid; + while(hi >= lo) { + mid = (hi + lo) / 2; + if(list->ranges[mid].from > n) + hi = mid - 1; + else if(list->ranges[mid].to < n) + lo = mid + 1; + else + return 1; + } + return 0; +} + +static int +deleteRange(IntListPtr list, int i) +{ + assert(list->length > i); + if(list->length > i + 1) + memmove(list->ranges + i, list->ranges + i + 1, + (list->length - i - 1) * sizeof(IntRangeRec)); + list->length--; + return 1; +} + +static int +insertRange(int from, int to, IntListPtr list, int i) +{ + assert(i >= 0 && i <= list->length); + assert(i == 0 || list->ranges[i - 1].to < from - 1); + assert(i == list->length || list->ranges[i].from > to + 1); + + if(list->length >= list->size) { + int newsize = list->size * 2 + 1; + IntRangePtr newranges = + realloc(list->ranges, newsize * sizeof(IntRangeRec)); + if(newranges == NULL) + return -1; + list->size = newsize; + list->ranges = newranges; + } + + if(i < list->length) + memmove(list->ranges + i + 1, list->ranges + i, + list->length - i); + list->length++; + list->ranges[i].from = from; + list->ranges[i].to = to; + return 1; +} + +static int +maybeMergeRanges(IntListPtr list, int i) +{ + int rc; + + while(i > 0 && list->ranges[i].from <= list->ranges[i - 1].to + 1) { + list->ranges[i - 1].from = + MIN(list->ranges[i - 1].from, list->ranges[i].from); + list->ranges[i - 1].to = + MAX(list->ranges[i - 1].to, list->ranges[i].to); + rc = deleteRange(list, i); + if(rc < 0) return -1; + i--; + } + + while(i < list->length - 1 && + list->ranges[i].to >= list->ranges[i + 1].from - 1) { + list->ranges[i + 1].from = + MIN(list->ranges[i + 1].from, list->ranges[i].from); + list->ranges[i - 1].to = + MAX(list->ranges[i + 1].to, list->ranges[i].to); + rc = deleteRange(list, i); + if(rc < 0) return -1; + } + return 1; +} + +int +intListCons(int from, int to, IntListPtr list) +{ + int i; + + /* Don't bother with the dichotomy. */ + for(i = 0; i < list->length; i++) { + if(list->ranges[i].to >= from - 1) + break; + } + + if(i < list->length && + (from >= list->ranges[i].from - 1 || to <= list->ranges[i].to + 1)) { + if(from <= list->ranges[i].from) + list->ranges[i].from = from; + if(to >= list->ranges[i].to) + list->ranges[i].to = to; + return maybeMergeRanges(list, i); + } + return insertRange(from, to, list, i); +} + +/* Return the amount of physical memory on the box, -1 if unknown or + over two gigs. */ +#if defined(__linux__) + +#include +int +physicalMemory() +{ + int rc; + struct sysinfo info; + + rc = sysinfo(&info); + if(rc < 0) + return -1; + + if(info.totalram <= 0x7fffffff / info.mem_unit) + return (int)(info.totalram * info.mem_unit); + + return -1; +} + +#elif defined(__FreeBSD__) + +#include +int +physicalMemory() +{ + unsigned long membytes; + size_t len; + int res; + + len = sizeof(membytes); + res = sysctlbyname("hw.physmem", &membytes, &len, NULL, 0); + if (res || membytes > INT_MAX) + return -1; + + return (int)membytes; +} + +#else + +int +physicalMemory() +{ + return -1; +} +#endif diff --git a/jni/polipo/util.h b/jni/polipo/util.h new file mode 100644 index 00000000..128e1991 --- /dev/null +++ b/jni/polipo/util.h @@ -0,0 +1,108 @@ +/* +Copyright (c) 2003-2006 by Juliusz Chroboczek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +/* These are Polipo's error codes. They need to be positive integers, + and must not collide with possible errno values. + Starting at 2^16 should be safe enough. */ + +#define E0 (1 << 16) +#define E1 (2 << 16) +#define E2 (3 << 16) +#define E3 (4 << 16) +#define EUNKNOWN (E0) +#define EDOSHUTDOWN (E0 + 1) +#define EDOGRACEFUL (E0 + 2) +#define EDOTIMEOUT (E0 + 3) +#define ECLIENTRESET (E0 + 4) +#define ESYNTAX (E0 + 5) +#define EREDIRECTOR (E0 + 6) +#define EDNS_HOST_NOT_FOUND (E1) +#define EDNS_NO_ADDRESS (E1 + 1) +#define EDNS_NO_RECOVERY (E1 + 2) +#define EDNS_TRY_AGAIN (E1 + 3) +#define EDNS_INVALID (E1 + 4) +#define EDNS_UNSUPPORTED (E1 + 5) +#define EDNS_FORMAT (E1 + 6) +#define EDNS_REFUSED (E1 + 7) +#define EDNS_CNAME_LOOP (E1 + 8) +#define ESOCKS_PROTOCOL (E2) +/* These correspond to SOCKS status codes 91 through 93 */ +#define ESOCKS_REJECT_FAIL (E2 + 1) +#define ESOCKS_REJECT_IDENTD (E2 + 2) +#define ESOCKS_REJECT_UID_MISMATCH (E2 + 3) +/* (ESOCKS5_BASE + n) corresponds to SOCKS5 status code n (0 to 8) */ +#define ESOCKS5_BASE (E3) + +typedef struct _IntRange { + int from; + int to; +} IntRangeRec, *IntRangePtr; + +typedef struct _IntList { + int length; + int size; + IntRangePtr ranges; +} IntListRec, *IntListPtr; + +char *strdup_n(const char *restrict buf, int n) ATTRIBUTE ((malloc)); +int snnprintf(char *restrict buf, int n, int len, const char *format, ...) + ATTRIBUTE ((format (printf, 4, 5))); +int snnvprintf(char *restrict buf, int n, int len, const char *format, va_list args) + ATTRIBUTE ((format (printf, 4, 0))); +int snnprint_n(char *restrict buf, int n, int len, const char *s, int slen); +int strcmp_n(const char *string, const char *buf, int n) ATTRIBUTE ((pure)); +int digit(char) ATTRIBUTE ((const)); +int letter(char) ATTRIBUTE ((const)); +char lwr(char) ATTRIBUTE ((const)); +char* lwrcpy(char *restrict dst, const char *restrict src, int n); +int lwrcmp(const char *as, const char *bs, int n) ATTRIBUTE ((pure)); +int strcasecmp_n(const char *string, const char *buf, int n) + ATTRIBUTE ((pure)); +int atoi_n(const char *restrict string, int n, int len, int *value_return); +int isWhitespace(const char *string) ATTRIBUTE((pure)); +#ifndef HAVE_MEMRCHR +void *memrchr(const void *s, int c, size_t n) ATTRIBUTE ((pure)); +#endif +int h2i(char h) ATTRIBUTE ((const)); +char i2h(int i) ATTRIBUTE ((const)); +int log2_floor(int x) ATTRIBUTE ((const)); +int log2_ceil(int x) ATTRIBUTE ((const)); +char* vsprintf_a(const char *f, va_list args) + ATTRIBUTE ((malloc, format (printf, 1, 0))); +char* sprintf_a(const char *f, ...) + ATTRIBUTE ((malloc, format (printf, 1, 2))); +unsigned int hash(unsigned seed, const void *restrict key, int key_size, + unsigned int hash_size) + ATTRIBUTE ((pure)); +char *pstrerror(int e); +time_t mktime_gmt(struct tm *tm) ATTRIBUTE ((pure)); +AtomPtr expandTilde(AtomPtr filename); +void do_daemonise(int noclose); +void writePid(char *pidfile); +int b64cpy(char *restrict dst, const char *restrict src, int n, int fss); +int b64cmp(const char *restrict a, int an, const char *restrict b, int bn) + ATTRIBUTE ((pure)); +IntListPtr makeIntList(int size) ATTRIBUTE ((malloc)); +void destroyIntList(IntListPtr list); +int intListMember(int n, IntListPtr list) ATTRIBUTE ((pure)); +int intListCons(int from, int to, IntListPtr list); +int physicalMemory(void); diff --git a/libs/armeabi/pdnsd b/libs/armeabi/pdnsd deleted file mode 100755 index d82c8adae0913df7b4a35f4335b22bc0ca30ea88..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 153256 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

HUBW&xVW@;6LAsch|s;&m)ZD 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 E1NF|oy8r+H diff --git a/libs/armeabi/shadowsocks b/libs/armeabi/shadowsocks deleted file mode 100755 index 4a06e40da0a8b60f557263442738c6b0c367822b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128488 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�b66Yf*}71fVK6Yf|2ZPiaf6CP0g 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 TEjrX?3oX+ampil)3?2O+R>{D{ diff --git a/pom.xml b/pom.xml index 1261dff0..e1306e2e 100755 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.github.shadowsocks shadowsocks - 1.2.0 + 1.3 apk Shadowsocks diff --git a/res/values/strings.xml b/res/values/strings.xml index afead46d..43d63fb4 100755 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -72,6 +72,9 @@ rc4 + HTTP Proxy + Local HTTP Proxy + 209.0.0.0/8 72.0.0.0/8 diff --git a/res/xml/shadowsocks_preference.xml b/res/xml/shadowsocks_preference.xml index 3b026e80..07c8a3f7 100755 --- a/res/xml/shadowsocks_preference.xml +++ b/res/xml/shadowsocks_preference.xml @@ -49,6 +49,13 @@ + + " + BASE + "redsocks.conf\n" @@ -342,6 +361,9 @@ public class ShadowsocksService extends Service { */ public boolean handleConnection() { + if (isHTTPProxy) { + startPolipoDaemon(); + } startShadowsocksDaemon(); startDnsDaemon(); startRedsocksDaemon(); @@ -451,26 +473,11 @@ public class ShadowsocksService extends Service { getString(R.string.service_stopped), Notification.FLAG_AUTO_CANCEL); - try { - if (httpOS != null) { - httpOS.close(); - httpOS = null; - } - if (httpProcess != null) { - httpProcess.destroy(); - httpProcess = null; - } - } catch (Exception e) { - Log.e(TAG, "HTTP Server close unexpected"); - } - new Thread() { @Override public void run() { - // Make sure the connection is closed, important here onDisconnect(); - } }.start(); @@ -500,6 +507,11 @@ public class ShadowsocksService extends Service { if (!waitForProcess("shadowsocks")) { sb.append("kill -9 `cat /data/data/com.github.shadowsocks/shadowsocks.pid`").append("\n"); } + if (isHTTPProxy) { + if (!waitForProcess("polipo")) { + sb.append("kill -9 `cat /data/data/com.github.shadowsocks/polipo.pid`").append("\n"); + } + } Utils.runRootCommand(sb.toString()); }