123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422(* Ocsigen
* http://www.ocsigen.org
* Module deflatemod.ml
* Copyright (C) 2007 Gabriel Kerneis
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, with linking exception;
* either version 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*)(* Compress output sent by the server *)openLwt.Infixletsection=Lwt_log.Section.make"ocsigen:ext:deflate"(* Content-type *)typefilter=[|`Typeofstringoption*stringoption|`Extensionofstring]letshould_compress(t,t')urlchoice_list=letcheck=function|`Type(None,None)->true|`Type(None,Somex')->x'=t'|`Type(Somex,None)->x=t|`Type(Somex,Somex')->x=t&&x'=t'|`Extensionsuff->Filename.check_suffixurlsuffinmatchchoice_listwith|`Onlyl->List.existscheckl|`All_butl->List.for_all(func->not(checkc))lletcompress_level=letpreprocessi=ifi>=0&&i<=9thenielse6inOcsigen_config.Custom.key~preprocess()letbuffer_size=letpreprocesss=ifs>0thenselse8192inOcsigen_config.Custom.key~preprocess()(* Minimal header, by X. Leroy *)letgzip_header_length=10letgzip_header=letgzip_header=Bytes.makegzip_header_length(Char.chr0)inBytes.setgzip_header0@@Char.chr0x1F;Bytes.setgzip_header1@@Char.chr0x8B;Bytes.setgzip_header2@@Char.chr8;Bytes.setgzip_header9@@Char.chr0xFF;Bytes.unsafe_to_stringgzip_header(* inspired by an auxiliary function from camlzip, by Xavier Leroy *)typeoutput_buffer={stream:Zlib.stream;buf:bytes;mutablepos:int;mutableavail:int;mutablesize:int32;mutablecrc:int32;mutableadd_trailer:bool}letwrite_int32ozn=fori=0to3doBytes.setoz.buf(oz.pos+i)(Char.chr(Int32.to_int(Int32.shift_right_logicaln(8*i))land0xff))done;oz.pos<-oz.pos+4;oz.avail<-oz.avail-4;assert(oz.avail>=0)(* puts in oz the content of buf, from pos to pos + len ;
* f is the continuation of the current stream *)letrecoutputozfbufposlen=assert(pos>=0&&len>=0&&pos+len<=String.lengthbuf);ifoz.avail=0thenbeginletcont()=outputozfbufposleninLwt_log.ign_info~section"Flushing because output buffer is full";flushozcontendelseiflen=0thennext_contozfelsebeginlet(_,used_in,used_out)=tryZlib.deflateoz.stream(Bytes.unsafe_of_stringbuf)poslenoz.bufoz.posoz.availZlib.Z_NO_FLUSHwithZlib.Error(s,s')->raise(Ocsigen_stream.Stream_error("Error during compression: "^s^" "^s'))inoz.pos<-oz.pos+used_out;oz.avail<-oz.avail-used_out;oz.size<-Int32.addoz.size(Int32.of_intused_in);oz.crc<-Zlib.update_crc_stringoz.crcbufposused_in;outputozfbuf(pos+used_in)(len-used_in)end(* Flush oz, ie. produces a new_stream with the content of oz, cleans it
* and returns the continuation of the stream *)andflushozcont=letlen=oz.posiniflen=0thencont()elsebeginletbuf_len=Bytes.lengthoz.bufinlets=iflen=buf_lenthenBytes.to_stringoz.bufelseBytes.sub_stringoz.buf0leninLwt_log.ign_info~section"Flushing!";oz.pos<-0;oz.avail<-buf_len;Ocsigen_stream.contscontendandnext_contozstream=Ocsigen_stream.next(stream:stringOcsigen_stream.stream)>>=fune->matchewith|Ocsigen_stream.FinishedNone->Lwt_log.ign_info~section"End of stream: big cleaning for zlib";(* loop until there is nothing left to compress and flush *)letrecfinish()=(* buffer full *)ifoz.avail=0thenflushozfinishelse((* no more input, deflates only what were left because output buffer
* was full *)let(finished,_,used_out)=Zlib.deflateoz.streamoz.buf00oz.bufoz.posoz.availZlib.Z_FINISHinoz.pos<-oz.pos+used_out;oz.avail<-oz.avail-used_out;ifnotfinishedthenfinish()elsewrite_trailer())andwrite_trailer()=ifoz.add_trailer&&oz.avail<8thenflushozwrite_trailerelsebeginifoz.add_trailerthenbeginwrite_int32ozoz.crc;write_int32ozoz.sizeend;Lwt_log.ign_info~section"Zlib.deflate finished, last flush";flushoz(fun()->Ocsigen_stream.emptyNone)endinfinish()|Ocsigen_stream.Finished(Somes)->next_contozs|Ocsigen_stream.Cont(s,f)->outputozfs0(String.lengths)(* deflate param : true = deflate ; false = gzip (no header in this case) *)letcompressdeflatestream:stringOcsigen_stream.t=letzstream=Zlib.deflate_init(Ocsigen_lib.Option.get'6(Ocsigen_config.Custom.findcompress_level))deflateinletfinalizestatus=Ocsigen_stream.finalizestreamstatus>>=fun_e->(tryZlib.deflate_endzstreamwith(* ignore errors, deflate_end cleans everything anyway *)Zlib.Error_->());Lwt.return(Lwt_log.ign_info~section"Zlib stream closed")inletoz=letbuffer_size=Ocsigen_lib.Option.get'8192(Ocsigen_config.Custom.findbuffer_size)in{stream=zstream;buf=Bytes.createbuffer_size;pos=0;avail=buffer_size;size=0l;crc=0l;add_trailer=notdeflate}inletnew_stream()=next_contoz(Ocsigen_stream.getstream)inLwt_log.ign_info~section"Zlib stream initialized";ifdeflatethenOcsigen_stream.make~finalizenew_streamelseOcsigen_stream.make~finalize(fun()->Ocsigen_stream.contgzip_headernew_stream)(* We implement Content-Encoding, not Transfer-Encoding *)typeencoding=Deflate|Gzip|Id|Star|Not_acceptableletqvalue=functionSomex->x|None->1.0letenc_compareee'=matche,e'with|(Star,_),(_,_)->-1(* star should be at the very end *)|(_,_),(Star,_)->1|(_,v),(_,v')whenv<v'->1(* then, sort by qvalue *)|(_,v),(_,v')whenv>v'->-1|(x,_),(x',_)whenx=x'->0|(Deflate,_),(_,_)->1(* and subsort by encoding *)|(_,_),(Deflate,_)->-1|(Gzip,_),(_,_)->1|(_,_),(Gzip,_)->-1|(Id,_),(_,_)->1|(_,_),(Id,_)->-1|_->assertfalseletrecfiltermapf=function|[]->[]|t::q->matchftwith|Somes->s::(filtermapfq)|None->filtermapfqletconvert=function|(Some"deflate",v)->Some(Deflate,qvaluev)|(Some"gzip",v)|(Some"x-gzip",v)->Some(Gzip,qvaluev)|(Some"identity",v)->Some(Id,qvaluev)|(None,v)->Some(Star,qvaluev)|_->None(* Follow http's RFC to select the transfer encoding *)letselect_encodingaccept_header=leth=List.sortenc_compare(filtermapconvertaccept_header)inlet(exclude,accept)=let(e,a)=List.partition(funx->sndx=0.)hin(List.mapfste,List.mapfsta)inletrecaux=function|[]->if((List.memStarexclude)||(List.memIdexclude))thenNot_acceptableelseId|t::q->if(List.memtexclude)thenauxqelsetinauxaccept(* deflate = true -> mode deflate
deflate = false -> mode gzip *)letstream_filtercontentencodingurldeflatechoiceres=Lwt.return(Ocsigen_extensions.Ext_found(fun()->try(matchOcsigen_response.headerresOcsigen_header.Name.content_typewith|None->Lwt.returnres|Somecontenttype->letcontenttype=tryString.subcontenttype0(String.indexcontenttype';')withNot_found->contenttypeinmatchOcsigen_header.Mime_type.parsecontenttypewith|None,_|_,None->Lwt.returnres|Somea,Somebwhenshould_compress(a,b)urlchoice->letresponse,body=Ocsigen_response.to_cohttpresinletresponse=letheaders=Cohttp.Response.headersresponseinletheaders=letname=Ocsigen_header.Name.(to_stringetag)inmatchCohttp.Header.getheadersnamewith|Somee->Cohttp.Header.replaceheadersname((ifdeflatethen"Ddeflatemod"else"Gdeflatemod")^e)|None->headersinletheaders=Cohttp.Header.replaceheadersOcsigen_header.Name.(to_stringcontent_encoding)contentencodingin{responsewithCohttp.Response.headers;Cohttp.Response.encoding=Cohttp.Transfer.Chunked}andbody=Cohttp_lwt.Body.to_streambody|>Ocsigen_stream.of_lwt_stream|>compressdeflate|>Ocsigen_stream.to_lwt_stream|>Cohttp_lwt.Body.of_streaminLwt.return(Ocsigen_response.updateres~body~response)|_->Lwt.returnres)withNot_found->Lwt.returnres))letfilterchoice_list=function|Ocsigen_extensions.Req_not_found(code,_)->Lwt.return(Ocsigen_extensions.Ext_nextcode)|Ocsigen_extensions.Req_found({Ocsigen_extensions.request_info=ri;_},res)->matchOcsigen_request.header_multiriOcsigen_header.Name.accept_encoding|>Ocsigen_header.Accept_encoding.parse|>select_encodingwith|Deflate->stream_filter"deflate"(Ocsigen_request.sub_path_stringri)truechoice_listres|Gzip->stream_filter"gzip"(Ocsigen_request.sub_path_stringri)falsechoice_listres|Id|Star->Lwt.return(Ocsigen_extensions.Ext_found(fun()->Lwt.returnres))|Not_acceptable->Lwt.return(Ocsigen_extensions.Ext_stop_all(Ocsigen_response.cookiesres,`Not_acceptable))letrecparse_global_config=function|[]->()|Xml.Element("compress",["level",i],[])::ll->leti=tryint_of_stringiwithFailure_->raise(Ocsigen_extensions.Error_in_config_file"Compress level should be an integer between 0 and 9")inOcsigen_config.Custom.setcompress_leveli;parse_global_configll|Xml.Element("buffer",["size",s],[])::ll->lets=tryint_of_stringswithFailure_->raise(Ocsigen_extensions.Error_in_config_file"Buffer size should be a positive integer")inOcsigen_config.Custom.setbuffer_sizes;parse_global_configll|_->raise(Ocsigen_extensions.Error_in_config_file"Unexpected content inside deflatemod config")letparse_configconfig_elem=letmode=ref`Onlyinletpages=ref[]inOcsigen_extensions.(Configuration.process_element~in_tag:"host"~other_elements:(funt__->raise(Bad_config_tag_for_extensiont))~elements:[Configuration.element~name:"deflate"~attributes:[Configuration.attribute~name:"compress"~obligatory:true(function|"only"->mode:=`Only|"allbut"->mode:=`All_but|_->badconfig"Attribute 'compress' should be 'allbut' or 'only'");]~elements:[Configuration.element~name:"type"~pcdata:(funs->let(a,b)=Ocsigen_header.Mime_type.parsesinpages:=`Type(a,b)::!pages)();Configuration.element~name:"extension"~pcdata:(funs->pages:=`Extensions::!pages)();]()]config_elem);match!pageswith|[]->Ocsigen_extensions.badconfig"Unexpected element inside contenttype \
(should be <type> or <extension>)"|l->filter(match!modewith`Only->`Onlyl|`All_but->`All_butl)let()=Ocsigen_extensions.register~name:"deflatemod"~fun_site:(fun______->parse_config)~init_fun:parse_global_config()letmode=Ocsigen_server.Site.Config.key()letextension=Ocsigen_server.Site.create_extension(fun{Ocsigen_server.Site.Config.accessor}->matchaccessormodewith|Somemode->filtermode|None->failwith"Deflatemod.mode not set")