123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340(* 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=Logs.Src.create"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=ref6letset_compress_leveli=compress_level:=ifi>=0&&i<=9thenielse6letbuffer_size=ref8192letset_buffer_sizes=buffer_size:=ifs>0thenselse8192(* Compression backend, provided by bytesrw. A codec is given as a bytesrw
writer filter (see [compress_body]). *)letstream_errore=Ocsigen_base.Ocsigen_stream.Stream_error("Error during compression: "^Bytesrw.Bytes.Stream.error_messagee)(* Compress [body] with the bytesrw writer filter [cfilter], pushing each
produced chunk to [flush].
bytesrw writers are synchronous, whereas [flush] is an Lwt operation. We let
the compressor write its output into an in-memory buffer, then flush that
buffer downstream after each input chunk. This bridges the synchronous
compressor with the Lwt output stream without ever blocking. *)letcompress_body(cfilter:Bytesrw.Bytes.Writer.filter)body=funflush->letout=Buffer.create!buffer_sizeinletcw=cfilter~eod:true(Bytesrw.Bytes.Writer.of_bufferout)inletflush_out()=ifBuffer.lengthout=0thenLwt.return_unitelselets=Buffer.contentsoutinBuffer.clearout;flushsinletwritef=tryf()withBytesrw.Bytes.Stream.Errore->raise(stream_errore)inbody(funinbuf->write(fun()->Bytesrw.Bytes.Writer.write_stringcwinbuf);flush_out())>>=fun()->write(fun()->Bytesrw.Bytes.Writer.write_eodcw);Logs.debug~src:section(funfmt->fmt"Close stream");flush_out()(* We implement Content-Encoding, not Transfer-Encoding *)typeencoding=Deflate|Gzip|Zstd|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|(Zstd,_),(_,_)->-1(* zstd is preferred at equal qvalue *)|(_,_),(Zstd,_)->1|(Deflate,_),(_,_)->1(* and subsort by encoding *)|(_,_),(Deflate,_)->-1|(Gzip,_),(_,_)->1|(_,_),(Gzip,_)->-1|(Id,_),(_,_)->1|(_,_),(Id,_)->-1|_->assertfalseletrecfiltermapf=function|[]->[]|t::q->(matchftwithSomes->s::filtermapfq|None->filtermapfq)letconvert=function|Some"deflate",v->Some(Deflate,qvaluev)|Some"gzip",v|Some"x-gzip",v->Some(Gzip,qvaluev)|Some"zstd",v->Some(Zstd,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)inletexclude,accept=lete,a=List.partition(funx->sndx=0.)hinList.mapfste,List.mapfstainletrecaux=function|[]->ifList.memStarexclude||List.memIdexcludethenNot_acceptableelseId|t::q->ifList.memtexcludethenauxqelsetinauxaccept(* Compressing changes the bytes of the representation, but not its meaning. A
strong entity tag must therefore be weakened, whereas a weak one may be kept:
RFC 9110 8.8.1 lets semantically equivalent representations share it, which is
what the compressed and uncompressed forms of a body are. Keeping it also
keeps conditional requests on compressed responses working. *)letweaken_etage=lete=String.trimeinifString.lengthe>=2&&String.equal(String.sube02)"W/"theneelse"W/"^e(* The body we send depends on the request's Accept-Encoding, so the response
must say so: without it a shared cache may serve a compressed response to a
client that did not ask for one (RFC 9111 4.1). An existing Vary is extended
rather than replaced, and "*" is left alone since it already covers
everything. *)letadd_vary_accept_encodingheaders=letname=Ocsigen_http.Header.Name.(to_stringvary)inletvalue=Ocsigen_http.Header.Name.(to_stringaccept_encoding)inmatchCohttp.Header.getheadersnamewith|None->Cohttp.Header.addheadersnamevalue|Someexisting->letmentions_encoding=String.split_on_char','existing|>List.exists(funfield->letfield=String.trimfieldinString.equalfield"*"||String.equal(String.lowercase_asciifield)(String.lowercase_asciivalue))inifmentions_encodingthenheaderselseCohttp.Header.replaceheadersname(existing^", "^value)(* [cfilter] is the bytesrw writer filter for the negotiated codec, and
[contentencoding] the matching Content-Encoding token. *)letstream_filtercfiltercontentencodingurlchoiceres=Lwt.return(Ocsigen.Extensions.Ext_found(fun()->trymatchOcsigen.Response.headerresOcsigen_http.Header.Name.content_typewith|None->Lwt.returnres|Somecontenttype->(letcontenttype=tryString.subcontenttype0(String.indexcontenttype';')withNot_found->contenttypeinmatchOcsigen_http.Header.Mime_type.parsecontenttypewith|None,_|_,None->Lwt.returnres|Somea,Somebwhenshould_compress(a,b)urlchoice->letresponse=let{Http.Response.headers;status;version}=Ocsigen.Response.responseresinletheaders=letname=Ocsigen_http.Header.Name.(to_stringetag)inmatchCohttp.Header.getheadersnamewith|Somee->Cohttp.Header.replaceheadersname(weaken_etage)|None->headersinletheaders=Http.Header.replaceheadersOcsigen_http.Header.Name.(to_stringcontent_encoding)contentencodinginletheaders=add_vary_accept_encodingheadersin(* The body about to be sent is the compressed one, of a
length we do not know before streaming it, so any
content-length the producer computed is now wrong. A
message may not carry both anyway (RFC 9112 6.1): left
in place, it truncates the response at the original
length. *)letheaders=Http.Header.removeheadersOcsigen_http.Header.Name.(to_stringcontent_length)inHttp.Response.make~headers~status~version()andbody=Ocsigen.Response.Body.makeCohttp.Transfer.Chunked(compress_bodycfilter(Ocsigen.Response.Body.write(Ocsigen.Response.bodyres)))inLwt.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_http.Header.Name.accept_encoding|>Ocsigen_http.Header.Accept_encoding.parse|>select_encodingwith|Deflate->(* HTTP "deflate" is the zlib format (RFC 1950), not raw deflate. *)stream_filter(Bytesrw_zlib.Zlib.compress_writes~level:!compress_level())"deflate"(Ocsigen.Request.sub_path_stringri)choice_listres|Gzip->stream_filter(Bytesrw_zlib.Gzip.compress_writes~level:!compress_level())"gzip"(Ocsigen.Request.sub_path_stringri)choice_listres|Zstd->stream_filter(Bytesrw_zstd.compress_writes())"zstd"(Ocsigen.Request.sub_path_stringri)choice_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")inset_compress_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")inset_buffer_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->leta,b=Ocsigen_http.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()letrun~mode()___=filtermode(* Content types compressed by default in the one-command serve mode. Covers
the usual text-based, highly compressible resources; binary formats (images,
video, archives) are already compressed and left untouched. *)letdefault_serve_mode=`Only[`Type(Some"text",None);`Type(Some"application",Some"javascript");`Type(Some"application",Some"x-javascript");`Type(Some"application",Some"json");`Type(Some"application",Some"xml");`Type(Some"application",Some"xhtml+xml");`Type(Some"application",Some"wasm");`Type(Some"image",Some"svg+xml")]let()=Ocsigen.Server.register_compression(run~mode:default_serve_mode())