123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278openColombe.SigsopenColombe.StateopenColombemoduleSend_mail_tls_p=structtypehelo=Domain.ttypestarttls=Rfc1869.ttypequit=unittypepp_220=stringlisttypepp_250=stringlisttype'xt=|Helo:helot|Starttls:starttlst|Quit:quitt|PP_220:pp_220t|PP_250:pp_250tletpp:typex.xtFmt.t=funppf->function|Helo->Fmt.stringppf"EHLO"|Starttls->Fmt.stringppf"STARTTLS"|Quit->Fmt.stringppf"QUIT"|PP_220->Fmt.stringppf"PP-220"|PP_250->Fmt.stringppf"PP-250"letis_request(typea)(w:at):bool=matchwwith|Helo->true|Starttls->true|Quit->true|_->falsetypeerror=|DecoderofDecoder.error|EncoderofEncoder.error|Unexpected_request:'xt*Request.t->error|Unexpected_reply:'xt*Reply.t->error|Invalid_stateletpp_errorppf=function|Decodererr->Decoder.pp_errorppferr|Encodererr->Encoder.pp_errorppferr|Unexpected_request(w,v)->Fmt.pfppf"(Unexpected_request expect:%a,@ received:@[<hov>%a]@)"ppwRequest.ppv|Unexpected_reply(w,v)->Fmt.pfppf"(Unexpected_reply expect:%a,@ received:@[<hov>%a@])"ppwReply.ppv|Invalid_state->Fmt.pfppf"Invalid_state"letuncast:typeo.ot->o->(Request.t,Reply.t)either=funwv->matchw,vwith|Helo,domain->L(`Hellodomain)|Starttls,starttls->L(`Extensionstarttls)|Quit,()->L`Quit|PP_220,txts->R(`PP_220txts)|PP_250,txts->R(`PP_250txts)letencode:typeo.ot*o->(ctx->('s,error)process)->ctx->('s,error)process=fun(w,v)kctx->letrecgo=function|Encoder.Ok->kctx|Encoder.Write{buffer;off;len;continue;}->letcontinuen=go(continuen)inWrite{buffer;off;len;k=continue;}|Encoder.Errorerr->Error(Encodererr)inletres=matchuncastwvwith|Lrequest->Request.Encoder.requestrequestctx.encoder|Rreply->Reply.Encoder.responsereplyctx.encoderingoresletcast:typex.(ctx->x->('s,error)process)->ctx->xt->[Request.t|Reply.t]->('s,error)process=funkctxwv->matchw,vwith|Helo,`Hellodomain->kctxdomain|PP_220,`PP_220txts->kctxtxts|PP_250,`PP_250txts->kctxtxts|_,(#Request.tasv)->Error(Unexpected_request(w,v))|_,(#Reply.tasv)->Error(Unexpected_reply(w,v))letencode_raw:(string*int*int)->(ctx->int->('s,error)process)->ctx->('s,error)process=fun(buf,off,len)kctx->letrecgo=function|Encoder.Write{buffer;off;len;continue;}->letkn=go(continuen)inWrite{buffer;off;len;k;}|Encoder.Ok->Encoder.blit~buf~off~lenctx.encoder;kctxlen|Encoder.Errorerr->Error(Encodererr)ingo(Encoder.flush(fun_->Ok)ctx.encoder)letrecdecode_raw:(bytes*int*int)->(ctx->int->('s,error)process)->ctx->('s,error)process=fun(buf,off,len)kctx->letoff',max'=ctx.decoder.Decoder.pos,ctx.decoder.Decoder.maxinifmax'-off'>0then(letres=(min:int->int->int)len(max'-off')inctx.decoder.pos<-ctx.decoder.pos+res;Bytes.blitctx.decoder.Decoder.bufferoff'bufoffres;kctxres)else(ctx.decoder.pos<-0;ctx.decoder.max<-0;Read{buffer=ctx.decoder.Decoder.buffer;off=ctx.decoder.Decoder.pos;len=Bytes.lengthctx.decoder.Decoder.buffer-ctx.decoder.Decoder.pos;k=(funlen->ctx.decoder.max<-len;decode_raw(buf,off,len)kctx)})letdecode:typei.it->(ctx->i->('s,error)process)->ctx->('s,error)process=funwkctx->letrecgo=function|Decoder.Okv->castkctxwv|Decoder.Read{buffer;off;len;continue;}->letcontinuen=go(continuen)inRead{buffer;off;len;k=continue;}|Decoder.Error{error;_}->Error(Decodererror)inmatchis_requestwwith|true->go(Request.Decoder.requestctx.decoder:>[Request.t|Reply.t]Decoder.state)|false->go(Reply.Decoder.responsectx.decoder:>[Request.t|Reply.t]Decoder.state)endmoduleSend_mail_tls_s=structtype'st={q:[`q0|`q1|`q2|`q3|`q4|`q5|`q6|`q7|`q8];tls:Rfc1869.t;domain:Domain.t;tls_buf:Bytes.t}endletsrc=Logs.Src.create"sendmail-tls"~doc:"logs sendmail-tls's events"moduleState=State.Make(Send_mail_tls_s)(Send_mail_tls_p)moduleLog=(valLogs.src_logsrc:Logs.LOG)letokxy=Ok(x,y)let($)fx=fxlettransition:'sSend_mail_tls_s.t->State.event->(State.action*'sSend_mail_tls_s.t,Send_mail_tls_p.error*'sSend_mail_tls_s.t)result=funqe->letopenStateinmatchq.q,ewith|`q0,Recv(PP_220,_txts)->ok$sendHeloq.domain${qwithq=`q1}|`q1,SendHelo->ok$recvPP_250${qwithq=`q2}|`q2,Recv(PP_250,_ehlo::exts)->letexts=List.map(funext->matchAstring.String.cut~sep:" "extwith|Some(ext,args)->(ext,args)|None->(ext,""))extsinletaction,q=matchList.assoc_optStarttls.description.elhoextswith|Someargs->letRfc1869.V(starttls,(moduleStarttls),inj)=Rfc1869.prjq.tlsin(matchStarttls.ehlostarttlsargswith|Okstarttls->sendStarttls(injstarttls),{qwithq=`q3;tls=injstarttls}|Errorerror->Log.err(funm->m"Retrieve an error while extension negociation: %a"Starttls.pp_errorerror);sendQuit(),{qwithq=`q8})|None->Log.err(funm->m"STARTTLS is not available");sendQuit(),{qwithq=`q7}inok$action$q|`q3,SendStarttls->Log.info(funm->m"TLS chunk sended .\n%!");letRfc1869.V(starttls,(moduleStarttls),inj)=Rfc1869.prjq.tlsinletaction,q'=matchStarttls.actionstarttlswith|Some(Rfc1869.Recv_code220)->recvPP_220,`q4|SomeRfc1869.Waiting_payload->read~buf:q.tls_buf~off:0~len:(Bytes.lengthq.tls_buf),`q4|SomeRfc1869.(Send_)->sendStarttls(injstarttls),`q5|Some(Rfc1869.Recv_code_)->assertfalse|None->Close,`q6inok$action${qwithtls=injstarttls;q=q'}|`q5,SendStarttls->letRfc1869.V(starttls,(moduleStarttls),inj)=Rfc1869.prjq.tlsinletstarttls=Starttls.handlestarttlsinlettls=injstarttlsinletaction,q'=matchStarttls.actionstarttlswith|Some(Rfc1869.Recv_code_)->assertfalse|SomeRfc1869.Waiting_payload->read~buf:q.tls_buf~off:0~len:(Bytes.lengthq.tls_buf),`q4|SomeRfc1869.(Send_)->Log.info(funm->m"Ask FSM to send TLS chunk.\n%!");sendStarttlstls,`q5(* XXX(dinosaure): ok dragoon here, if we move
to [q3], we send TLS chunk twice times. Why
I make this new state? Why we not use only
[q3] to send TLS chunk? Why [handle] is only
on [q5]? WHY? *)|None->Close,`q6inok$action${qwithtls;q=q'}|`q4,Recv(PP_220,txts)->letRfc1869.V(starttls,(moduleStarttls),inj)=Rfc1869.prjq.tlsin(matchStarttls.decode(Rfc1869.Response{code=220;txts;})starttlswith|Okstarttls->lettls=injstarttlsinok$sendStarttlstls${qwithtls;q=`q5}|Errorerror->Log.err(funm->m"Retrieve an error while decoding 220 response (@[<hov>%a@]): %a"Fmt.(Dump.liststring)txtsStarttls.pp_errorerror);ok$sendQuit()${qwithq=`q8})|`q4,Readlen->letRfc1869.V(starttls,(moduleStarttls),inj)=Rfc1869.prjq.tlsin(matchStarttls.decode(Rfc1869.Payload{buf=q.tls_buf;off=0;len;})starttlswith|Okstarttls->letaction,q'=matchStarttls.actionstarttlswith|SomeRfc1869.Waiting_payload->read~buf:q.tls_buf~off:0~len:(Bytes.lengthq.tls_buf),`q4|SomeRfc1869.(Send_)->Log.info(funm->m"Ask FSM to send TLS chunk.\n%!");sendStarttls(injstarttls),`q5|Some(Rfc1869.Recv_code_)->assertfalse(* XXX(dinosaure): should not occur at this stage! *)|None->Close,`q6inok$action${qwithtls=injstarttls;q=q'}|Errorerror->Log.err(funm->m"Retrieve an error while decoding payload: %a"Starttls.pp_errorerror);ok$sendQuit()${qwithq=`q8})|`q6,Close->ok$Close${qwithq=`q6}|`q7,SendQuit->ok$Close${qwithq=`q6}|`q8,SendQuit->ok$Close${qwithq=`q6}|_,_->Error(Send_mail_tls_p.Invalid_state,q)typeerror=Send_mail_tls_p.errorletpp_error=Send_mail_tls_p.pp_errortype'xstate='xSend_mail_tls_s.ttype'xt='xState.tletdomain_to_domain_namex=letx=matchxwith|Colombe.Domain.IPv4ipv4->Domain_name.of_string(Ipaddr.V4.to_stringipv4)(* TODO: fuzz! *)|Colombe.Domain.IPv6ipv6->Domain_name.of_string(Ipaddr.V6.to_stringipv6)(* TODO: fuzz! *)|Colombe.Domain.Extension(k,v)->Domain_name.of_string(Fmt.strf"%s:%s"kv)|Colombe.Domain.Domainlst->Domain_name.of_stringslstinRresult.R.bindxDomain_name.hostletmake_state?logger?encoding~domain~from~recipientsauthmailtls_config=letsendmail_state=Sendmail.make_state?logger?encoding~domain~from~recipientsauthmailinletsendmail_state=Sendmail.makesendmail_stateinletsendmail_ctx=Colombe.State.make_ctx()inletfiber=Sendmail.State.runsendmail_statesendmail_ctx(Sendmail.State.Recv(Sendmail.PP_220,[]))inletfiber=Starttls.fiberfiberinletopenRresult.Rindomain_to_domain_namedomain>>|funvalid_domain->{Send_mail_tls_s.q=`q0;domain;tls=Starttls.inj(Starttls.makefiber~domain:valid_domaintls_config);tls_buf=Bytes.create4096}letmakestate=State.make~init:statetransitionletrun:typesflow.simpl->(flow,s)rdwr->flow->'xt->ctx->(('xstate,error)result,s)io=funimplrdwrflowstatectx->let(>>=)=impl.bindinletreturn=impl.returninletrecgo=function|Read{buffer;off;len;k;}->rdwr.rdflowbufferofflen>>=funlen->go(klen)|Write{buffer;off;len;k;}->rdwr.wrflowbufferofflen>>=fun()->go(klen)|Returnv->return(Okv)|Errorerr->return(Rresult.R.errorerr)inletrecpp_220=function|Read{buffer;off;len;k;}->rdwr.rdflowbufferofflen>>=funlen->pp_220(klen)|Write{buffer;off;len;k;}->rdwr.wrflowbufferofflen>>=fun()->pp_220(klen)|Returntxts->go(State.runstatectx(State.Recv(PP_220,txts)))|Errorerr->return(Rresult.R.errorerr)inpp_220Send_mail_tls_p.(decodePP_220(fun_v->Returnv)ctx)