12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344634563466347634863496350635163526353635463556356635763586359636063616362636363646365636663676368636963706371637263736374637563766377637863796380638163826383638463856386638763886389639063916392639363946395639663976398639964006401640264036404640564066407640864096410641164126413641464156416641764186419642064216422642364246425642664276428642964306431643264336434643564366437643864396440644164426443644464456446644764486449645064516452645364546455645664576458645964606461646264636464646564666467646864696470647164726473647464756476647764786479648064816482648364846485648664876488648964906491649264936494649564966497649864996500650165026503650465056506650765086509651065116512651365146515651665176518651965206521652265236524652565266527652865296530653165326533653465356536653765386539654065416542654365446545654665476548654965506551655265536554655565566557655865596560656165626563656465656566656765686569657065716572657365746575657665776578657965806581658265836584658565866587658865896590659165926593659465956596659765986599660066016602660366046605660666076608660966106611661266136614661566166617661866196620662166226623662466256626662766286629663066316632 |
- package context
- import (
- "bytes"
- "context"
- "encoding/json"
- "encoding/xml"
- "errors"
- "fmt"
- "io"
- "mime"
- "mime/multipart"
- "net"
- "net/http"
- "net/url"
- "os"
- "path"
- "path/filepath"
- "reflect"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "sync/atomic"
- "time"
- "unsafe"
- "github.com/kataras/iris/v12/core/memstore"
- "github.com/kataras/iris/v12/core/netutil"
- "github.com/Shopify/goreferrer"
- "github.com/fatih/structs"
- "github.com/gomarkdown/markdown"
- "github.com/gomarkdown/markdown/html"
- "github.com/iris-contrib/schema"
- "github.com/mailru/easyjson"
- "github.com/mailru/easyjson/jwriter"
- "github.com/microcosm-cc/bluemonday"
- "github.com/vmihailenco/msgpack/v5"
- "golang.org/x/net/publicsuffix"
- "golang.org/x/time/rate"
- "google.golang.org/protobuf/encoding/protojson"
- "google.golang.org/protobuf/proto"
- "gopkg.in/yaml.v3"
- )
- var (
- // BuildRevision holds the vcs commit id information of the program's build.
- // Available at go version 1.18+
- BuildRevision string
- // BuildTime holds the vcs commit time information of the program's build.
- // Available at go version 1.18+
- BuildTime string
- )
- type (
- // BodyDecoder is an interface which any struct can implement in order to customize the decode action
- // from ReadJSON and ReadXML
- //
- // Trivial example of this could be:
- // type User struct { Username string }
- //
- // func (u *User) Decode(data []byte) error {
- // return json.Unmarshal(data, u)
- // }
- //
- // the 'Context.ReadJSON/ReadXML(&User{})' will call the User's
- // Decode option to decode the request body
- //
- // Note: This is totally optionally, the default decoders
- // for ReadJSON is the encoding/json and for ReadXML is the encoding/xml.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-per-type/main.go
- BodyDecoder interface {
- Decode(data []byte) error
- }
- // BodyDecoderWithContext same as BodyDecoder but it can accept a standard context,
- // which is binded to the HTTP request's context.
- BodyDecoderWithContext interface {
- DecodeContext(ctx context.Context, data []byte) error
- }
- // Unmarshaler is the interface implemented by types that can unmarshal any raw data.
- // TIP INFO: Any pointer to a value which implements the BodyDecoder can be override the unmarshaler.
- Unmarshaler interface {
- Unmarshal(data []byte, outPtr interface{}) error
- }
- // UnmarshalerFunc a shortcut for the Unmarshaler interface
- //
- // See 'Unmarshaler' and 'BodyDecoder' for more.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-via-unmarshaler/main.go
- UnmarshalerFunc func(data []byte, outPtr interface{}) error
- // DecodeFunc is a generic type of decoder function.
- // When the returned error is not nil the decode operation
- // is terminated and the error is received by the ReadJSONStream method,
- // otherwise it continues to read the next available object.
- // Look the `Context.ReadJSONStream` method.
- DecodeFunc func(outPtr interface{}) error
- )
- // Unmarshal parses the X-encoded data and stores the result in the value pointed to by v.
- // Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps,
- // slices, and pointers as necessary.
- func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error {
- return u(data, v)
- }
- // LimitRequestBodySize is a middleware which sets a request body size limit
- // for all next handlers in the chain.
- var LimitRequestBodySize = func(maxRequestBodySizeBytes int64) Handler {
- return func(ctx *Context) {
- ctx.SetMaxRequestBodySize(maxRequestBodySizeBytes)
- ctx.Next()
- }
- }
- // Map is just a type alias of the map[string]interface{} type.
- type Map = map[string]interface{}
- // Context is the midle-man server's "object" dealing with incoming requests.
- //
- // A New context is being acquired from a sync.Pool on each connection.
- // The Context is the most important thing on the iris's http flow.
- //
- // Developers send responses to the client's request through a Context.
- // Developers get request information from the client's request a Context.
- type Context struct {
- // the http.ResponseWriter wrapped by custom writer.
- writer ResponseWriter
- // the original http.Request
- request *http.Request
- // the current route registered to this request path.
- currentRoute RouteReadOnly
- // the local key-value storage
- params RequestParams // url named parameters.
- values memstore.Store // generic storage, middleware communication.
- query url.Values // GET url query temp cache, useful on many URLParamXXX calls.
- // the underline application app.
- app Application
- // the route's handlers
- handlers Handlers
- // the current position of the handler's chain
- currentHandlerIndex int
- // proceeded reports whether `Proceed` method
- // called before a `Next`. It is a flash field and it is set
- // to true on `Next` call when its called on the last handler in the chain.
- // Reports whether a `Next` is called,
- // even if the handler index remains the same (last handler).
- //
- // Also it's responsible to keep the old value of the last known handler index
- // before StopExecution. See ResumeExecution.
- proceeded int
- // if true, caller is responsible to release the context (put the context to the pool).
- manualRelease bool
- }
- // NewContext returns a new Context instance.
- func NewContext(app Application) *Context {
- return &Context{app: app}
- }
- /* Not required, unless requested.
- // SetApplication sets an Iris Application on-fly.
- // Do NOT use it after ServeHTTPC is fired.
- func (ctx *Context) SetApplication(app Application) {
- ctx.app = app
- }
- */
- // Clone returns a copy of the context that
- // can be safely used outside the request's scope.
- // Note that if the request-response lifecycle terminated
- // or request canceled by the client (can be checked by `ctx.IsCanceled()`)
- // then the response writer is totally useless.
- // The http.Request pointer value is shared.
- func (ctx *Context) Clone() *Context {
- valuesCopy := make(memstore.Store, len(ctx.values))
- copy(valuesCopy, ctx.values)
- paramsCopy := make(memstore.Store, len(ctx.params.Store))
- copy(paramsCopy, ctx.params.Store)
- queryCopy := make(url.Values, len(ctx.query))
- for k, v := range ctx.query {
- queryCopy[k] = v
- }
- req := ctx.request.Clone(ctx.request.Context())
- return &Context{
- app: ctx.app,
- values: valuesCopy,
- params: RequestParams{Store: paramsCopy},
- query: queryCopy,
- writer: ctx.writer.Clone(),
- request: req,
- currentHandlerIndex: stopExecutionIndex,
- proceeded: ctx.proceeded,
- manualRelease: ctx.manualRelease,
- currentRoute: ctx.currentRoute,
- }
- }
- // BeginRequest is executing once for each request
- // it should prepare the (new or acquired from pool) context's fields for the new request.
- // Do NOT call it manually. Framework calls it automatically.
- //
- // Resets
- // 1. handlers to nil.
- // 2. values to empty.
- // 3. the defer function.
- // 4. response writer to the http.ResponseWriter.
- // 5. request to the *http.Request.
- func (ctx *Context) BeginRequest(w http.ResponseWriter, r *http.Request) {
- ctx.currentRoute = nil
- ctx.handlers = nil // will be filled by router.Serve/HTTP
- ctx.values = ctx.values[0:0] // >> >> by context.Values().Set
- ctx.params.Store = ctx.params.Store[0:0]
- ctx.query = nil
- ctx.request = r
- ctx.currentHandlerIndex = 0
- ctx.proceeded = 0
- ctx.manualRelease = false
- ctx.writer = AcquireResponseWriter()
- ctx.writer.BeginResponse(w)
- }
- // EndRequest is executing once after a response to the request was sent and this context is useless or released.
- // Do NOT call it manually. Framework calls it automatically.
- //
- // 1. executes the OnClose function (if any).
- // 2. flushes the response writer's result or fire any error handler.
- // 3. releases the response writer.
- func (ctx *Context) EndRequest() {
- if !ctx.app.ConfigurationReadOnly().GetDisableAutoFireStatusCode() &&
- StatusCodeNotSuccessful(ctx.GetStatusCode()) {
- ctx.app.FireErrorCode(ctx)
- }
- ctx.writer.FlushResponse()
- ctx.writer.EndResponse()
- }
- // DisablePoolRelease disables the auto context pool Put call.
- // Do NOT use it, unless you know what you are doing.
- func (ctx *Context) DisablePoolRelease() {
- ctx.manualRelease = true
- }
- // IsCanceled reports whether the client canceled the request
- // or the underlying connection has gone.
- // Note that it will always return true
- // when called from a goroutine after the request-response lifecycle.
- func (ctx *Context) IsCanceled() bool {
- var err error
- if reqCtx := ctx.request.Context(); reqCtx != nil {
- err = reqCtx.Err()
- } else {
- err = ctx.GetErr()
- }
- return IsErrCanceled(err)
- }
- // IsErrCanceled reports whether the "err" is caused by a cancellation or timeout.
- func IsErrCanceled(err error) bool {
- if err == nil {
- return false
- }
- var netErr net.Error
- return (errors.As(err, &netErr) && netErr.Timeout()) ||
- errors.Is(err, context.Canceled) ||
- errors.Is(err, context.DeadlineExceeded) ||
- errors.Is(err, http.ErrHandlerTimeout) ||
- err.Error() == "closed pool"
- }
- // OnConnectionClose registers the "cb" Handler
- // which will be fired on its on goroutine on a cloned Context
- // when the underlying connection has gone away.
- //
- // The code inside the given callback is running on its own routine,
- // as explained above, therefore the callback should NOT
- // try to access to handler's Context response writer.
- //
- // This mechanism can be used to cancel long operations on the server
- // if the client has disconnected before the response is ready.
- //
- // It depends on the Request's Context.Done() channel.
- //
- // Finally, it reports whether the protocol supports pipelines (HTTP/1.1 with pipelines disabled is not supported).
- // The "cb" will not fire for sure if the output value is false.
- //
- // Note that you can register only one callback per route.
- //
- // See `OnClose` too.
- func (ctx *Context) OnConnectionClose(cb Handler) bool {
- if cb == nil {
- return false
- }
- reqCtx := ctx.Request().Context()
- if reqCtx == nil {
- return false
- }
- notifyClose := reqCtx.Done()
- if notifyClose == nil {
- return false
- }
- go func() {
- <-notifyClose
- // Note(@kataras): No need to clone if not canceled,
- // EndRequest will be called on the end of the handler chain,
- // no matter the cancelation.
- // therefore the context will still be there.
- cb(ctx.Clone())
- }()
- return true
- }
- // OnConnectionCloseErr same as `OnConnectionClose` but instead it
- // receives a function which returns an error.
- // If error is not nil, it will be logged as a debug message.
- func (ctx *Context) OnConnectionCloseErr(cb func() error) bool {
- if cb == nil {
- return false
- }
- reqCtx := ctx.Request().Context()
- if reqCtx == nil {
- return false
- }
- notifyClose := reqCtx.Done()
- if notifyClose == nil {
- return false
- }
- go func() {
- <-notifyClose
- if err := cb(); err != nil {
- // Can be ignored.
- ctx.app.Logger().Debugf("OnConnectionCloseErr: received error: %v", err)
- }
- }()
- return true
- }
- // OnClose registers a callback which
- // will be fired when the underlying connection has gone away(request canceled)
- // on its own goroutine or in the end of the request-response lifecylce
- // on the handler's routine itself (Context access).
- //
- // See `OnConnectionClose` too.
- func (ctx *Context) OnClose(cb Handler) {
- if cb == nil {
- return
- }
- // Note(@kataras):
- // - on normal request-response lifecycle
- // the `SetBeforeFlush` will be called first
- // and then `OnConnectionClose`,
- // - when request was canceled before handler finish its job
- // then the `OnConnectionClose` will be called first instead,
- // and when the handler function completed then `SetBeforeFlush` is fired.
- // These are synchronized, they cannot be executed the same exact time,
- // below we just make sure the "cb" is executed once
- // by simple boolean check or an atomic one.
- var executed uint32
- callback := func(ctx *Context) {
- if atomic.CompareAndSwapUint32(&executed, 0, 1) {
- cb(ctx)
- }
- }
- ctx.OnConnectionClose(callback)
- onFlush := func() {
- callback(ctx)
- }
- ctx.writer.SetBeforeFlush(onFlush)
- }
- // OnCloseErr same as `OnClose` but instead it
- // receives a function which returns an error.
- // If error is not nil, it will be logged as a debug message.
- func (ctx *Context) OnCloseErr(cb func() error) {
- if cb == nil {
- return
- }
- var executed uint32
- callback := func() error {
- if atomic.CompareAndSwapUint32(&executed, 0, 1) {
- return cb()
- }
- return nil
- }
- ctx.OnConnectionCloseErr(callback)
- onFlush := func() {
- if err := callback(); err != nil {
- // Can be ignored.
- ctx.app.Logger().Debugf("OnClose: SetBeforeFlush: received error: %v", err)
- }
- }
- ctx.writer.SetBeforeFlush(onFlush)
- }
- /* Note(@kataras): just leave end-developer decide.
- const goroutinesContextKey = "iris.goroutines"
- type goroutines struct {
- wg *sync.WaitGroup
- length int
- mu sync.RWMutex
- }
- var acquireGoroutines = func() interface{} {
- return &goroutines{wg: new(sync.WaitGroup)}
- }
- func (ctx *Context) Go(fn func(cancelCtx context.Context)) (running int) {
- g := ctx.values.GetOrSet(goroutinesContextKey, acquireGoroutines).(*goroutines)
- if fn != nil {
- g.wg.Add(1)
- g.mu.Lock()
- g.length++
- g.mu.Unlock()
- ctx.waitFunc = g.wg.Wait
- go func(reqCtx context.Context) {
- fn(reqCtx)
- g.wg.Done()
- g.mu.Lock()
- g.length--
- g.mu.Unlock()
- }(ctx.request.Context())
- }
- g.mu.RLock()
- running = g.length
- g.mu.RUnlock()
- return
- }
- */
- // ResponseWriter returns an http.ResponseWriter compatible response writer, as expected.
- func (ctx *Context) ResponseWriter() ResponseWriter {
- return ctx.writer
- }
- // ResetResponseWriter sets a new ResponseWriter implementation
- // to this Context to use as its writer.
- // Note, to change the underline http.ResponseWriter use
- // ctx.ResponseWriter().SetWriter(http.ResponseWriter) instead.
- func (ctx *Context) ResetResponseWriter(newResponseWriter ResponseWriter) {
- if rec, ok := ctx.IsRecording(); ok {
- releaseResponseRecorder(rec)
- }
- ctx.writer = newResponseWriter
- }
- // Request returns the original *http.Request, as expected.
- func (ctx *Context) Request() *http.Request {
- return ctx.request
- }
- // ResetRequest sets the Context's Request,
- // It is useful to store the new request created by a std *http.Request#WithContext() into Iris' Context.
- // Use `ResetRequest` when for some reason you want to make a full
- // override of the *http.Request.
- // Note that: when you just want to change one of each fields you can use the Request() which returns a pointer to Request,
- // so the changes will have affect without a full override.
- // Usage: you use a native http handler which uses the standard "context" package
- // to get values instead of the Iris' Context#Values():
- // r := ctx.Request()
- // stdCtx := context.WithValue(r.Context(), key, val)
- // ctx.ResetRequest(r.WithContext(stdCtx)).
- func (ctx *Context) ResetRequest(r *http.Request) {
- ctx.request = r
- }
- // SetCurrentRoute sets the route internally,
- // See `GetCurrentRoute()` method too.
- // It's being initialized by the Router.
- // See `Exec` or `SetHandlers/AddHandler` methods to simulate a request.
- func (ctx *Context) SetCurrentRoute(route RouteReadOnly) {
- ctx.currentRoute = route
- }
- // GetCurrentRoute returns the current "read-only" route that
- // was registered to this request's path.
- func (ctx *Context) GetCurrentRoute() RouteReadOnly {
- return ctx.currentRoute
- }
- // Do sets the "handlers" as the chain
- // and executes the first handler,
- // handlers should not be empty.
- //
- // It's used by the router, developers may use that
- // to replace and execute handlers immediately.
- func (ctx *Context) Do(handlers Handlers) {
- if len(handlers) == 0 {
- return
- }
- ctx.handlers = handlers
- handlers[0](ctx)
- }
- // AddHandler can add handler(s)
- // to the current request in serve-time,
- // these handlers are not persistenced to the router.
- //
- // Router is calling this function to add the route's handler.
- // If AddHandler called then the handlers will be inserted
- // to the end of the already-defined route's handler.
- func (ctx *Context) AddHandler(handlers ...Handler) {
- ctx.handlers = append(ctx.handlers, handlers...)
- }
- // SetHandlers replaces all handlers with the new.
- func (ctx *Context) SetHandlers(handlers Handlers) {
- ctx.handlers = handlers
- }
- // Handlers keeps tracking of the current handlers.
- func (ctx *Context) Handlers() Handlers {
- return ctx.handlers
- }
- // HandlerIndex sets the current index of the
- // current context's handlers chain.
- // If n < 0 or the current handlers length is 0 then it just returns the
- // current handler index without change the current index.
- //
- // Look Handlers(), Next() and StopExecution() too.
- func (ctx *Context) HandlerIndex(n int) (currentIndex int) {
- if n < 0 || n > len(ctx.handlers)-1 {
- return ctx.currentHandlerIndex
- }
- ctx.currentHandlerIndex = n
- return n
- }
- // Proceed is an alternative way to check if a particular handler
- // has been executed.
- // The given "h" Handler can report a failure with `StopXXX` methods
- // or ignore calling a `Next` (see `iris.ExecutionRules` too).
- //
- // This is useful only when you run a handler inside
- // another handler. It justs checks for before index and the after index.
- //
- // A usecase example is when you want to execute a middleware
- // inside controller's `BeginRequest` that calls the `ctx.Next` inside it.
- // The Controller looks the whole flow (BeginRequest, method handler, EndRequest)
- // as one handler, so `ctx.Next` will not be reflected to the method handler
- // if called from the `BeginRequest`.
- //
- // Although `BeginRequest` should NOT be used to call other handlers,
- // the `BeginRequest` has been introduced to be able to set
- // common data to all method handlers before their execution.
- // Controllers can accept middleware(s) from the MVC's Application's Router as normally.
- //
- // That said let's see an example of `ctx.Proceed`:
- //
- // var authMiddleware = basicauth.New(basicauth.Config{
- // Users: map[string]string{
- // "admin": "password",
- // },
- // })
- //
- // func (c *UsersController) BeginRequest(ctx iris.Context) {
- // if !ctx.Proceed(authMiddleware) {
- // ctx.StopExecution()
- // }
- // }
- //
- // This Get() will be executed in the same handler as `BeginRequest`,
- // internally controller checks for `ctx.StopExecution`.
- // So it will not be fired if BeginRequest called the `StopExecution`.
- //
- // func(c *UsersController) Get() []models.User {
- // return c.Service.GetAll()
- // }
- //
- // Alternative way is `!ctx.IsStopped()` if middleware make use of the `ctx.StopExecution()` on failure.
- func (ctx *Context) Proceed(h Handler) bool {
- _, ok := ctx.ProceedAndReportIfStopped(h)
- return ok
- }
- // ProceedAndReportIfStopped same as "Proceed" method
- // but the first output parameter reports whether the "h"
- // called "StopExecution" manually.
- func (ctx *Context) ProceedAndReportIfStopped(h Handler) (bool, bool) {
- ctx.proceeded = internalPauseExecutionIndex
- // Store the current index.
- beforeIdx := ctx.currentHandlerIndex
- h(ctx)
- // Retrieve the next one, if Next is called this is beforeIdx + 1 and so on.
- afterIdx := ctx.currentHandlerIndex
- // Restore prev index, no matter what.
- ctx.currentHandlerIndex = beforeIdx
- proceededByNext := ctx.proceeded == internalProceededHandlerIndex
- ctx.proceeded = beforeIdx
- // Stop called, return false but keep the handlers index.
- if afterIdx == stopExecutionIndex {
- return true, false
- }
- if proceededByNext {
- return false, true
- }
- // Next called or not.
- return false, afterIdx > beforeIdx
- }
- // HandlerName returns the current handler's name, helpful for debugging.
- func (ctx *Context) HandlerName() string {
- return HandlerName(ctx.handlers[ctx.currentHandlerIndex])
- }
- // HandlerFileLine returns the current running handler's function source file and line information.
- // Useful mostly when debugging.
- func (ctx *Context) HandlerFileLine() (file string, line int) {
- return HandlerFileLine(ctx.handlers[ctx.currentHandlerIndex])
- }
- // RouteName returns the route name that this handler is running on.
- // Note that it may return empty on not found handlers.
- func (ctx *Context) RouteName() string {
- if ctx.currentRoute == nil {
- return ""
- }
- return ctx.currentRoute.Name()
- }
- // Next calls the next handler from the handlers chain,
- // it should be used inside a middleware.
- func (ctx *Context) Next() {
- if ctx.IsStopped() {
- return
- }
- if ctx.proceeded <= internalPauseExecutionIndex /* pause and proceeded */ {
- ctx.proceeded = internalProceededHandlerIndex
- return
- }
- nextIndex, n := ctx.currentHandlerIndex+1, len(ctx.handlers)
- if nextIndex < n {
- ctx.currentHandlerIndex = nextIndex
- ctx.handlers[nextIndex](ctx)
- }
- }
- // NextOr checks if chain has a next handler, if so then it executes it
- // otherwise it sets a new chain assigned to this Context based on the given handler(s)
- // and executes its first handler.
- //
- // Returns true if next handler exists and executed, otherwise false.
- //
- // Note that if no next handler found and handlers are missing then
- // it sends a Status Not Found (404) to the client and it stops the execution.
- func (ctx *Context) NextOr(handlers ...Handler) bool {
- if next := ctx.NextHandler(); next != nil {
- ctx.Skip() // skip this handler from the chain.
- next(ctx)
- return true
- }
- if len(handlers) == 0 {
- ctx.NotFound()
- ctx.StopExecution()
- return false
- }
- ctx.Do(handlers)
- return false
- }
- // NextOrNotFound checks if chain has a next handler, if so then it executes it
- // otherwise it sends a Status Not Found (404) to the client and stops the execution.
- //
- // Returns true if next handler exists and executed, otherwise false.
- func (ctx *Context) NextOrNotFound() bool { return ctx.NextOr() }
- // NextHandler returns (it doesn't execute) the next handler from the handlers chain.
- //
- // Use .Skip() to skip this handler if needed to execute the next of this returning handler.
- func (ctx *Context) NextHandler() Handler {
- if ctx.IsStopped() {
- return nil
- }
- nextIndex := ctx.currentHandlerIndex + 1
- // check if it has a next middleware
- if nextIndex < len(ctx.handlers) {
- return ctx.handlers[nextIndex]
- }
- return nil
- }
- // Skip skips/ignores the next handler from the handlers chain,
- // it should be used inside a middleware.
- func (ctx *Context) Skip() {
- ctx.HandlerIndex(ctx.currentHandlerIndex + 1)
- }
- const (
- stopExecutionIndex = -1
- internalPauseExecutionIndex = -2
- internalProceededHandlerIndex = -3
- )
- // StopExecution stops the handlers chain of this request.
- // Meaning that any following `Next` calls are ignored,
- // as a result the next handlers in the chain will not be fire.
- //
- // See ResumeExecution too.
- func (ctx *Context) StopExecution() {
- if curIdx := ctx.currentHandlerIndex; curIdx != stopExecutionIndex {
- // Protect against multiple calls of StopExecution.
- // Resume should set the last proceeded handler index.
- // Store the current index.
- ctx.proceeded = curIdx
- // And stop.
- ctx.currentHandlerIndex = stopExecutionIndex
- }
- }
- // IsStopped reports whether the current position of the context's handlers is -1,
- // means that the StopExecution() was called at least once.
- func (ctx *Context) IsStopped() bool {
- return ctx.currentHandlerIndex == stopExecutionIndex
- }
- // ResumeExecution sets the current handler index to the last
- // index of the executed handler before StopExecution method was fired.
- //
- // Reports whether it's restored after a StopExecution call.
- func (ctx *Context) ResumeExecution() bool {
- if ctx.IsStopped() {
- ctx.currentHandlerIndex = ctx.proceeded
- return true
- }
- return false
- }
- // StopWithStatus stops the handlers chain and writes the "statusCode".
- //
- // If the status code is a failure one then
- // it will also fire the specified error code handler.
- func (ctx *Context) StopWithStatus(statusCode int) {
- ctx.StopExecution()
- ctx.StatusCode(statusCode)
- }
- // StopWithText stops the handlers chain and writes the "statusCode"
- // among with a fmt-style text of "format" and optional arguments.
- //
- // If the status code is a failure one then
- // it will also fire the specified error code handler.
- func (ctx *Context) StopWithText(statusCode int, format string, args ...interface{}) {
- ctx.StopWithStatus(statusCode)
- ctx.WriteString(fmt.Sprintf(format, args...))
- }
- // StopWithError stops the handlers chain and writes the "statusCode"
- // among with the error "err".
- // It Calls the `SetErr` method so error handlers can access the given error.
- //
- // If the status code is a failure one then
- // it will also fire the specified error code handler.
- //
- // If the given "err" is private then the
- // status code's text is rendered instead (unless a registered error handler overrides it).
- func (ctx *Context) StopWithError(statusCode int, err error) {
- if err == nil {
- return
- }
- ctx.SetErr(err)
- if _, ok := err.(ErrPrivate); ok {
- // error is private, we SHOULD not render it,
- // leave the error handler alone to
- // render the code's text instead.
- ctx.StopWithStatus(statusCode)
- return
- }
- ctx.StopWithText(statusCode, err.Error())
- }
- // StopWithPlainError like `StopWithError` but it does NOT
- // write anything to the response writer, it stores the error
- // so any error handler matching the given "statusCode" can handle it by its own.
- func (ctx *Context) StopWithPlainError(statusCode int, err error) {
- if err == nil {
- return
- }
- ctx.SetErr(err)
- ctx.StopWithStatus(statusCode)
- }
- // StopWithJSON stops the handlers chain, writes the status code
- // and sends a JSON response.
- //
- // If the status code is a failure one then
- // it will also fire the specified error code handler.
- func (ctx *Context) StopWithJSON(statusCode int, jsonObject interface{}) error {
- ctx.StopWithStatus(statusCode)
- return ctx.writeJSON(jsonObject, &DefaultJSONOptions) // do not modify - see errors.DefaultContextErrorHandler.
- }
- // StopWithProblem stops the handlers chain, writes the status code
- // and sends an application/problem+json response.
- // See `iris.NewProblem` to build a "problem" value correctly.
- //
- // If the status code is a failure one then
- // it will also fire the specified error code handler.
- func (ctx *Context) StopWithProblem(statusCode int, problem Problem) error {
- ctx.StopWithStatus(statusCode)
- problem.Status(statusCode)
- return ctx.Problem(problem)
- }
- // +------------------------------------------------------------+
- // | Current "user/request" storage |
- // | and share information between the handlers - Values(). |
- // | Save and get named path parameters - Params() |
- // +------------------------------------------------------------+
- // Params returns the current url's named parameters key-value storage.
- // Named path parameters are being saved here.
- // This storage, as the whole context, is per-request lifetime.
- func (ctx *Context) Params() *RequestParams {
- return &ctx.params
- }
- // Values returns the current "user" storage.
- // Named path parameters and any optional data can be saved here.
- // This storage, as the whole context, is per-request lifetime.
- //
- // You can use this function to Set and Get local values
- // that can be used to share information between handlers and middleware.
- func (ctx *Context) Values() *memstore.Store {
- return &ctx.values
- }
- // +------------------------------------------------------------+
- // | Path, Host, Subdomain, IP, Headers etc... |
- // +------------------------------------------------------------+
- // Method returns the request.Method, the client's http method to the server.
- func (ctx *Context) Method() string {
- return ctx.request.Method
- }
- // Path returns the full request path,
- // escaped if EnablePathEscape config field is true.
- func (ctx *Context) Path() string {
- return ctx.RequestPath(ctx.app.ConfigurationReadOnly().GetEnablePathEscape())
- }
- // DecodeQuery returns the uri parameter as url (string)
- // useful when you want to pass something to a database and be valid to retrieve it via context.Param
- // use it only for special cases, when the default behavior doesn't suits you.
- //
- // http://www.blooberry.com/indexdot/html/topics/urlencoding.htm
- // it uses just the url.QueryUnescape
- func DecodeQuery(path string) string {
- if path == "" {
- return ""
- }
- encodedPath, err := url.QueryUnescape(path)
- if err != nil {
- return path
- }
- return encodedPath
- }
- // DecodeURL returns the decoded uri
- // useful when you want to pass something to a database and be valid to retrieve it via context.Param
- // use it only for special cases, when the default behavior doesn't suits you.
- //
- // http://www.blooberry.com/indexdot/html/topics/urlencoding.htm
- // it uses just the url.Parse
- func DecodeURL(uri string) string {
- u, err := url.Parse(uri)
- if err != nil {
- return uri
- }
- return u.String()
- }
- // RequestPath returns the full request path,
- // based on the 'escape'.
- func (ctx *Context) RequestPath(escape bool) string {
- if escape {
- return ctx.request.URL.EscapedPath() // DecodeQuery(ctx.request.URL.EscapedPath())
- }
- return ctx.request.URL.Path // RawPath returns empty, requesturi can be used instead also.
- }
- const sufscheme = "://"
- // GetScheme returns the full scheme of the request URL (https://, http:// or ws:// and e.t.c.).
- func GetScheme(r *http.Request) string {
- scheme := r.URL.Scheme
- if scheme == "" {
- if r.TLS != nil {
- scheme = netutil.SchemeHTTPS
- } else {
- scheme = netutil.SchemeHTTP
- }
- }
- return scheme + sufscheme
- }
- // Scheme returns the full scheme of the request (including :// suffix).
- func (ctx *Context) Scheme() string {
- return GetScheme(ctx.Request())
- }
- // PathPrefixMap accepts a map of string and a handler.
- // The key of "m" is the key, which is the prefix, regular expressions are not valid.
- // The value of "m" is the handler that will be executed if HasPrefix(context.Path).
- // func (ctx *Context) PathPrefixMap(m map[string]context.Handler) bool {
- // path := ctx.Path()
- // for k, v := range m {
- // if strings.HasPrefix(path, k) {
- // v(ctx)
- // return true
- // }
- // }
- // return false
- // } no, it will not work because map is a random peek data structure.
- // GetHost returns the host part of the current URI.
- func GetHost(r *http.Request) string {
- // contains subdomain.
- if host := r.URL.Host; host != "" {
- return host
- }
- return r.Host
- }
- // Host returns the host:port part of the request URI, calls the `Request().Host`.
- // To get the subdomain part as well use the `Request().URL.Host` method instead.
- // To get the subdomain only use the `Subdomain` method instead.
- // This method makes use of the `Configuration.HostProxyHeaders` field too.
- func (ctx *Context) Host() string {
- for header, ok := range ctx.app.ConfigurationReadOnly().GetHostProxyHeaders() {
- if !ok {
- continue
- }
- if host := ctx.GetHeader(header); host != "" {
- return host
- }
- }
- return GetHost(ctx.request)
- }
- // GetDomain resolves and returns the server's domain.
- // To customize its behavior, developers can modify this package-level function at initialization.
- var GetDomain = func(hostport string) string {
- host := hostport
- if tmp, _, err := net.SplitHostPort(hostport); err == nil {
- host = tmp
- }
- switch host {
- // We could use the netutil.LoopbackRegex but leave it as it's for now, it's faster.
- case "localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1":
- // loopback.
- return "localhost"
- default:
- if net.ParseIP(host) != nil { // if it's an IP, see #1945.
- return host
- }
- if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
- host = domain
- }
- return host
- }
- }
- // Domain returns the root level domain.
- func (ctx *Context) Domain() string {
- return GetDomain(ctx.Host())
- }
- // GetSubdomainFull returns the full subdomain level, e.g.
- // [test.user.]mydomain.com.
- func GetSubdomainFull(r *http.Request) string {
- host := GetHost(r) // host:port
- rootDomain := GetDomain(host) // mydomain.com
- rootDomainIdx := strings.Index(host, rootDomain)
- if rootDomainIdx == -1 {
- return ""
- }
- return host[0:rootDomainIdx]
- }
- // SubdomainFull returns the full subdomain level, e.g.
- // [test.user.]mydomain.com.
- // Note that HostProxyHeaders are being respected here.
- func (ctx *Context) SubdomainFull() string {
- host := ctx.Host() // host:port
- rootDomain := GetDomain(host) // mydomain.com
- rootDomainIdx := strings.Index(host, rootDomain)
- if rootDomainIdx == -1 {
- return ""
- }
- return host[0:rootDomainIdx]
- }
- // Subdomain returns the first subdomain of this request,
- // e.g. [user.]mydomain.com.
- // See `SubdomainFull` too.
- func (ctx *Context) Subdomain() (subdomain string) {
- host := ctx.Host()
- if index := strings.IndexByte(host, '.'); index > 0 {
- subdomain = host[0:index]
- }
- // listening on mydomain.com:80
- // subdomain = mydomain, but it's wrong, it should return ""
- vhost := ctx.app.ConfigurationReadOnly().GetVHost()
- if strings.Contains(vhost, subdomain) { // then it's not subdomain
- return ""
- }
- return
- }
- // FindClosest returns a list of "n" paths close to
- // this request based on subdomain and request path.
- //
- // Order may change.
- // Example: https://github.com/kataras/iris/tree/main/_examples/routing/intelligence/manual
- func (ctx *Context) FindClosest(n int) []string {
- return ctx.app.FindClosestPaths(ctx.Subdomain(), ctx.Path(), n)
- }
- // IsWWW returns true if the current subdomain (if any) is www.
- func (ctx *Context) IsWWW() bool {
- host := ctx.Host()
- if index := strings.IndexByte(host, '.'); index > 0 {
- // if it has a subdomain and it's www then return true.
- if subdomain := host[0:index]; !strings.Contains(ctx.app.ConfigurationReadOnly().GetVHost(), subdomain) {
- return subdomain == "www"
- }
- }
- return false
- }
- // FullRequestURI returns the full URI,
- // including the scheme, the host and the relative requested path/resource.
- func (ctx *Context) FullRequestURI() string {
- return ctx.AbsoluteURI(ctx.Path())
- }
- // RemoteAddr tries to parse and return the real client's request IP.
- //
- // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders.
- //
- // If parse based on these headers fail then it will return the Request's `RemoteAddr` field
- // which is filled by the server before the HTTP handler,
- // unless the Configuration.RemoteAddrHeadersForce was set to true
- // which will force this method to return the first IP from RemoteAddrHeaders
- // even if it's part of a private network.
- //
- // Look `Configuration.RemoteAddrHeaders`,
- //
- // Configuration.RemoteAddrHeadersForce,
- // Configuration.WithRemoteAddrHeader(...),
- // Configuration.WithoutRemoteAddrHeader(...) and
- // Configuration.RemoteAddrPrivateSubnetsW for more.
- func (ctx *Context) RemoteAddr() string {
- if remoteHeaders := ctx.app.ConfigurationReadOnly().GetRemoteAddrHeaders(); len(remoteHeaders) > 0 {
- privateSubnets := ctx.app.ConfigurationReadOnly().GetRemoteAddrPrivateSubnets()
- for _, headerName := range remoteHeaders {
- ipAddresses := strings.Split(ctx.GetHeader(headerName), ",")
- if ip, ok := netutil.GetIPAddress(ipAddresses, privateSubnets); ok {
- return ip
- }
- }
- if ctx.app.ConfigurationReadOnly().GetRemoteAddrHeadersForce() {
- for _, headerName := range remoteHeaders {
- // return the first valid IP,
- // even if it's a part of a private network.
- ipAddresses := strings.Split(ctx.GetHeader(headerName), ",")
- for _, addr := range ipAddresses {
- if ip, _, err := net.SplitHostPort(addr); err == nil {
- return ip
- }
- }
- }
- }
- }
- addr := strings.TrimSpace(ctx.request.RemoteAddr)
- if addr != "" {
- // if addr has port use the net.SplitHostPort otherwise(error occurs) take as it is
- if ip, _, err := net.SplitHostPort(addr); err == nil {
- return ip
- }
- }
- return addr
- }
- // TrimHeaderValue returns the "v[0:first space or semicolon]".
- func TrimHeaderValue(v string) string {
- for i, char := range v {
- if char == ' ' || char == ';' {
- return v[:i]
- }
- }
- return v
- }
- // GetHeader returns the request header's value based on its name.
- func (ctx *Context) GetHeader(name string) string {
- return ctx.request.Header.Get(name)
- }
- // IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest)
- //
- // There is no a 100% way of knowing that a request was made via Ajax.
- // You should never trust data coming from the client, they can be easily overcome by spoofing.
- //
- // Note that "X-Requested-With" Header can be modified by any client(because of "X-"),
- // so don't rely on IsAjax for really serious stuff,
- // try to find another way of detecting the type(i.e, content type),
- // there are many blogs that describe these problems and provide different kind of solutions,
- // it's always depending on the application you're building,
- // this is the reason why this `IsAjax` is simple enough for general purpose use.
- //
- // Read more at: https://developer.mozilla.org/en-US/docs/AJAX
- // and https://xhr.spec.whatwg.org/
- func (ctx *Context) IsAjax() bool {
- return ctx.GetHeader("X-Requested-With") == "XMLHttpRequest"
- }
- var isMobileRegex = regexp.MustCompile("(?:hpw|i|web)os|alamofire|alcatel|amoi|android|avantgo|blackberry|blazer|cell|cfnetwork|darwin|dolfin|dolphin|fennec|htc|ip(?:hone|od|ad)|ipaq|j2me|kindle|midp|minimo|mobi|motorola|nec-|netfront|nokia|opera m(ob|in)i|palm|phone|pocket|portable|psp|silk-accelerated|skyfire|sony|ucbrowser|up.browser|up.link|windows ce|xda|zte|zune")
- // IsMobile checks if client is using a mobile device(phone or tablet) to communicate with this server.
- // If the return value is true that means that the http client using a mobile
- // device to communicate with the server, otherwise false.
- //
- // Keep note that this checks the "User-Agent" request header.
- func (ctx *Context) IsMobile() bool {
- s := strings.ToLower(ctx.GetHeader("User-Agent"))
- return isMobileRegex.MatchString(s)
- }
- var isScriptRegex = regexp.MustCompile("curl|wget|collectd|python|urllib|java|jakarta|httpclient|phpcrawl|libwww|perl|go-http|okhttp|lua-resty|winhttp|awesomium")
- // IsScript reports whether a client is a script.
- func (ctx *Context) IsScript() bool {
- s := strings.ToLower(ctx.GetHeader("User-Agent"))
- return isScriptRegex.MatchString(s)
- }
- // IsSSL reports whether the client is running under HTTPS SSL.
- //
- // See `IsHTTP2` too.
- func (ctx *Context) IsSSL() bool {
- ssl := strings.EqualFold(ctx.request.URL.Scheme, "https") || ctx.request.TLS != nil
- if !ssl {
- for k, v := range ctx.app.ConfigurationReadOnly().GetSSLProxyHeaders() {
- if ctx.GetHeader(k) == v {
- ssl = true
- break
- }
- }
- }
- return ssl
- }
- // IsHTTP2 reports whether the protocol version for incoming request was HTTP/2.
- // The client code always uses either HTTP/1.1 or HTTP/2.
- //
- // See `IsSSL` too.
- func (ctx *Context) IsHTTP2() bool {
- return ctx.request.ProtoMajor == 2
- }
- // IsGRPC reports whether the request came from a gRPC client.
- func (ctx *Context) IsGRPC() bool {
- return ctx.IsHTTP2() && strings.Contains(ctx.GetContentTypeRequested(), ContentGRPCHeaderValue)
- }
- type (
- // Referrer contains the extracted information from the `GetReferrer`
- //
- // The structure contains struct tags for JSON, form, XML, YAML and TOML.
- // Look the `GetReferrer() Referrer` and `goreferrer` external package.
- Referrer struct {
- // The raw refer(r)er URL.
- Raw string `json:"raw" form:"raw" xml:"Raw" yaml:"Raw" toml:"Raw"`
- Type ReferrerType `json:"type" form:"referrer_type" xml:"Type" yaml:"Type" toml:"Type"`
- Label string `json:"label" form:"referrer_form" xml:"Label" yaml:"Label" toml:"Label"`
- URL string `json:"url" form:"referrer_url" xml:"URL" yaml:"URL" toml:"URL"`
- Subdomain string `json:"subdomain" form:"referrer_subdomain" xml:"Subdomain" yaml:"Subdomain" toml:"Subdomain"`
- Domain string `json:"domain" form:"referrer_domain" xml:"Domain" yaml:"Domain" toml:"Domain"`
- Tld string `json:"tld" form:"referrer_tld" xml:"Tld" yaml:"Tld" toml:"Tld"`
- Path string `json:"path" form:"referrer_path" xml:"Path" yaml:"Path" toml:"Path"`
- Query string `json:"query" form:"referrer_query" xml:"Query" yaml:"Query" toml:"GoogleType"`
- GoogleType ReferrerGoogleSearchType `json:"googleType" form:"referrer_google_type" xml:"GoogleType" yaml:"GoogleType" toml:"GoogleType"`
- }
- // ReferrerType is the goreferrer enum for a referrer type (indirect, direct, email, search, social).
- ReferrerType = goreferrer.ReferrerType
- // ReferrerGoogleSearchType is the goreferrer enum for a google search type (organic, adwords).
- ReferrerGoogleSearchType = goreferrer.GoogleSearchType
- )
- // String returns the raw ref url.
- func (ref Referrer) String() string {
- return ref.Raw
- }
- // Contains the available values of the goreferrer enums.
- const (
- ReferrerInvalid ReferrerType = iota
- ReferrerIndirect
- ReferrerDirect
- ReferrerEmail
- ReferrerSearch
- ReferrerSocial
- ReferrerNotGoogleSearch ReferrerGoogleSearchType = iota
- ReferrerGoogleOrganicSearch
- ReferrerGoogleAdwords
- )
- // unnecessary but good to know the default values upfront.
- var emptyReferrer = Referrer{Type: ReferrerInvalid, GoogleType: ReferrerNotGoogleSearch}
- // GetReferrer extracts and returns the information from the "Referer" (or "Referrer") header
- // and url query parameter as specified in https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy.
- func (ctx *Context) GetReferrer() Referrer {
- // the underline net/http follows the https://tools.ietf.org/html/rfc7231#section-5.5.2,
- // so there is nothing special left to do.
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
- refURL := ctx.GetHeader("Referer")
- if refURL == "" {
- refURL = ctx.GetHeader("Referrer")
- if refURL == "" {
- refURL = ctx.URLParam("referer")
- if refURL == "" {
- refURL = ctx.URLParam("referrer")
- }
- }
- }
- if refURL == "" {
- return emptyReferrer
- }
- if ref := goreferrer.DefaultRules.Parse(refURL); ref.Type > goreferrer.Invalid {
- return Referrer{
- Raw: refURL,
- Type: ReferrerType(ref.Type),
- Label: ref.Label,
- URL: ref.URL,
- Subdomain: ref.Subdomain,
- Domain: ref.Domain,
- Tld: ref.Tld,
- Path: ref.Path,
- Query: ref.Query,
- GoogleType: ReferrerGoogleSearchType(ref.GoogleType),
- }
- }
- return emptyReferrer
- }
- // SetLanguage force-sets the language for i18n, can be used inside a middleare.
- // It has the highest priority over the rest and if it is empty then it is ignored,
- // if it set to a static string of "default" or to the default language's code
- // then the rest of the language extractors will not be called at all and
- // the default language will be set instead.
- //
- // See `i18n.ExtractFunc` for a more organised way of the same feature.
- func (ctx *Context) SetLanguage(langCode string) {
- ctx.values.Set(ctx.app.ConfigurationReadOnly().GetLanguageContextKey(), langCode)
- }
- // GetLocale returns the current request's `Locale` found by i18n middleware.
- // It always fallbacks to the default one.
- // See `Tr` too.
- func (ctx *Context) GetLocale() Locale {
- // Cache the Locale itself for multiple calls of `Tr` method.
- contextKey := ctx.app.ConfigurationReadOnly().GetLocaleContextKey()
- if v := ctx.values.Get(contextKey); v != nil {
- if locale, ok := v.(Locale); ok {
- return locale
- }
- }
- if locale := ctx.app.I18nReadOnly().GetLocale(ctx); locale != nil {
- ctx.values.Set(contextKey, locale)
- return locale
- }
- return nil
- }
- // Tr returns a i18n localized message based on format with optional arguments.
- // See `GetLocale` too.
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/i18n
- func (ctx *Context) Tr(key string, args ...interface{}) string {
- return ctx.app.I18nReadOnly().TrContext(ctx, key, args...)
- }
- // +------------------------------------------------------------+
- // | Response Headers helpers |
- // +------------------------------------------------------------+
- // Header adds a header to the response, if value is empty
- // it removes the header by its name.
- func (ctx *Context) Header(name string, value string) {
- if value == "" {
- ctx.writer.Header().Del(name)
- return
- }
- ctx.writer.Header().Add(name, value)
- }
- const contentTypeContextKey = "iris.content_type"
- func shouldAppendCharset(cType string) bool {
- if idx := strings.IndexRune(cType, '/'); idx > 1 && len(cType) > idx+1 {
- typ := cType[0:idx]
- if typ == "application" {
- switch cType[idx+1:] {
- case "json", "xml", "yaml", "problem+json", "problem+xml":
- return true
- default:
- return false
- }
- }
- }
- return true
- }
- func (ctx *Context) contentTypeOnce(cType string, charset string) {
- if charset == "" {
- charset = ctx.app.ConfigurationReadOnly().GetCharset()
- }
- if shouldAppendCharset(cType) {
- cType += "; charset=" + charset
- }
- ctx.values.Set(contentTypeContextKey, cType)
- ctx.writer.Header().Set(ContentTypeHeaderKey, cType)
- }
- // ContentType sets the response writer's
- // header "Content-Type" to the 'cType'.
- func (ctx *Context) ContentType(cType string) {
- if cType == "" {
- return
- }
- if _, wroteOnce := ctx.values.GetEntry(contentTypeContextKey); wroteOnce {
- return
- }
- // 1. if it's path or a filename or an extension,
- // then take the content type from that,
- // ^ No, it's not always a file,e .g. vnd.$type
- // if strings.Contains(cType, ".") {
- // ext := filepath.Ext(cType)
- // cType = mime.TypeByExtension(ext)
- // }
- // if doesn't contain a charset already then append it
- if shouldAppendCharset(cType) {
- if !strings.Contains(cType, "charset") {
- cType += "; charset=" + ctx.app.ConfigurationReadOnly().GetCharset()
- }
- }
- ctx.writer.Header().Set(ContentTypeHeaderKey, cType)
- }
- // GetContentType returns the response writer's
- // header value of "Content-Type".
- func (ctx *Context) GetContentType() string {
- return ctx.writer.Header().Get(ContentTypeHeaderKey)
- }
- // GetContentTypeRequested returns the request's
- // trim-ed(without the charset and priority values)
- // header value of "Content-Type".
- func (ctx *Context) GetContentTypeRequested() string {
- // could use mime.ParseMediaType too.
- return TrimHeaderValue(ctx.GetHeader(ContentTypeHeaderKey))
- }
- // GetContentLength returns the request's
- // header value of "Content-Length".
- func (ctx *Context) GetContentLength() int64 {
- if v := ctx.GetHeader(ContentLengthHeaderKey); v != "" {
- n, _ := strconv.ParseInt(v, 10, 64)
- return n
- }
- return 0
- }
- // StatusCode sets the status code header to the response.
- // Look .GetStatusCode & .FireStatusCode too.
- //
- // Note that you must set status code before write response body (except when recorder is used).
- func (ctx *Context) StatusCode(statusCode int) {
- ctx.writer.WriteHeader(statusCode)
- }
- // NotFound emits an error 404 to the client, using the specific custom error error handler.
- // Note that you may need to call ctx.StopExecution() if you don't want the next handlers
- // to be executed. Next handlers are being executed on iris because you can alt the
- // error code and change it to a more specific one, i.e
- // users := app.Party("/users")
- // users.Done(func(ctx iris.Context){ if ctx.GetStatusCode() == 400 { /* custom error code for /users */ }})
- func (ctx *Context) NotFound() {
- ctx.StatusCode(http.StatusNotFound)
- }
- // GetStatusCode returns the current status code of the response.
- // Look StatusCode too.
- func (ctx *Context) GetStatusCode() int {
- return ctx.writer.StatusCode()
- }
- // +------------------------------------------------------------+
- // | Various Request and Post Data |
- // +------------------------------------------------------------+
- func (ctx *Context) getQuery() url.Values {
- if ctx.query == nil {
- ctx.query = ctx.request.URL.Query()
- }
- return ctx.query
- }
- // URLParamExists returns true if the url parameter exists, otherwise false.
- func (ctx *Context) URLParamExists(name string) bool {
- _, exists := ctx.getQuery()[name]
- return exists
- }
- // URLParamDefault returns the get parameter from a request, if not found then "def" is returned.
- func (ctx *Context) URLParamDefault(name string, def string) string {
- if v := ctx.getQuery().Get(name); v != "" {
- return v
- }
- return def
- }
- // URLParam returns the get parameter from a request, if any.
- func (ctx *Context) URLParam(name string) string {
- return ctx.URLParamDefault(name, "")
- }
- // URLParamSlice a shortcut of ctx.Request().URL.Query()[name].
- // Like `URLParam` but it returns all values instead of a single string separated by commas.
- // Returns the values of a url query of the given "name" as string slice, e.g.
- // ?names=john&names=doe&names=kataras and ?names=john,doe,kataras will return [ john doe kataras].
- //
- // Note that, this method skips any empty entries.
- //
- // See `URLParamsSorted` for sorted values.
- func (ctx *Context) URLParamSlice(name string) []string {
- values := ctx.getQuery()[name]
- n := len(values)
- if n == 0 {
- return values
- }
- var sep string
- if sepPtr := ctx.app.ConfigurationReadOnly().GetURLParamSeparator(); sepPtr != nil {
- sep = *sepPtr
- }
- normalizedValues := make([]string, 0, n)
- for _, v := range values {
- if v == "" {
- continue
- }
- if sep != "" {
- values := strings.Split(v, sep)
- normalizedValues = append(normalizedValues, values...)
- continue
- }
- normalizedValues = append(normalizedValues, v)
- }
- return normalizedValues
- }
- // URLParamTrim returns the url query parameter with trailing white spaces removed from a request.
- func (ctx *Context) URLParamTrim(name string) string {
- return strings.TrimSpace(ctx.URLParam(name))
- }
- // URLParamEscape returns the escaped url query parameter from a request.
- func (ctx *Context) URLParamEscape(name string) string {
- return DecodeQuery(ctx.URLParam(name))
- }
- // ErrNotFound is the type error which API users can make use of
- // to check if a `Context` action of a `Handler` is type of Not Found,
- // e.g. URL Query Parameters.
- // Example:
- //
- // n, err := context.URLParamInt("url_query_param_name")
- //
- // if errors.Is(err, context.ErrNotFound) {
- // // [handle error...]
- // }
- //
- // Another usage would be `err == context.ErrNotFound`
- // HOWEVER prefer use the new `errors.Is` as API details may change in the future.
- var ErrNotFound = errors.New("not found")
- // URLParamInt returns the url query parameter as int value from a request,
- // returns -1 and an error if parse failed or not found.
- func (ctx *Context) URLParamInt(name string) (int, error) {
- if v := ctx.URLParam(name); v != "" {
- n, err := strconv.Atoi(v)
- if err != nil {
- return -1, err
- }
- return n, nil
- }
- return -1, ErrNotFound
- }
- // URLParamIntDefault returns the url query parameter as int value from a request,
- // if not found or parse failed then "def" is returned.
- func (ctx *Context) URLParamIntDefault(name string, def int) int {
- v, err := ctx.URLParamInt(name)
- if err != nil {
- return def
- }
- return v
- }
- // URLParamInt32Default returns the url query parameter as int32 value from a request,
- // if not found or parse failed then "def" is returned.
- func (ctx *Context) URLParamInt32Default(name string, def int32) int32 {
- if v := ctx.URLParam(name); v != "" {
- n, err := strconv.ParseInt(v, 10, 32)
- if err != nil {
- return def
- }
- return int32(n)
- }
- return def
- }
- // URLParamInt64 returns the url query parameter as int64 value from a request,
- // returns -1 and an error if parse failed or not found.
- func (ctx *Context) URLParamInt64(name string) (int64, error) {
- if v := ctx.URLParam(name); v != "" {
- n, err := strconv.ParseInt(v, 10, 64)
- if err != nil {
- return -1, err
- }
- return n, nil
- }
- return -1, ErrNotFound
- }
- // URLParamInt64Default returns the url query parameter as int64 value from a request,
- // if not found or parse failed then "def" is returned.
- func (ctx *Context) URLParamInt64Default(name string, def int64) int64 {
- v, err := ctx.URLParamInt64(name)
- if err != nil {
- return def
- }
- return v
- }
- // URLParamUint64 returns the url query parameter as uint64 value from a request.
- // Returns 0 on parse errors or when the URL parameter does not exist in the Query.
- func (ctx *Context) URLParamUint64(name string) uint64 {
- if v := ctx.URLParam(name); v != "" {
- n, err := strconv.ParseUint(v, 10, 64)
- if err != nil {
- return 0
- }
- return n
- }
- return 0
- }
- // URLParamFloat64 returns the url query parameter as float64 value from a request,
- // returns an error and -1 if parse failed.
- func (ctx *Context) URLParamFloat64(name string) (float64, error) {
- if v := ctx.URLParam(name); v != "" {
- n, err := strconv.ParseFloat(v, 64)
- if err != nil {
- return -1, err
- }
- return n, nil
- }
- return -1, ErrNotFound
- }
- // URLParamFloat64Default returns the url query parameter as float64 value from a request,
- // if not found or parse failed then "def" is returned.
- func (ctx *Context) URLParamFloat64Default(name string, def float64) float64 {
- v, err := ctx.URLParamFloat64(name)
- if err != nil {
- return def
- }
- return v
- }
- // URLParamBool returns the url query parameter as boolean value from a request,
- // returns an error if parse failed.
- func (ctx *Context) URLParamBool(name string) (bool, error) {
- return strconv.ParseBool(ctx.URLParam(name))
- }
- // URLParamBoolDefault returns the url query parameter as boolean value from a request,
- // if not found or parse failed then "def" is returned.
- func (ctx *Context) URLParamBoolDefault(name string, def bool) bool {
- v, err := ctx.URLParamBool(name)
- if err != nil {
- return def
- }
- return v
- }
- // URLParams returns a map of URL Query parameters.
- // If the value of a URL parameter is a slice,
- // then it is joined as one separated by comma.
- // It returns an empty map on empty URL query.
- //
- // See URLParamsSorted too.
- func (ctx *Context) URLParams() map[string]string {
- q := ctx.getQuery()
- values := make(map[string]string, len(q))
- for k, v := range q {
- values[k] = strings.Join(v, ",")
- }
- return values
- }
- // URLParamsSorted returns a sorted (by key) slice
- // of key-value entries of the URL Query parameters.
- func (ctx *Context) URLParamsSorted() []memstore.StringEntry {
- q := ctx.getQuery()
- n := len(q)
- if n == 0 {
- return nil
- }
- keys := make([]string, 0, n)
- for key := range q {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- entries := make([]memstore.StringEntry, 0, n)
- for _, key := range keys {
- value := q[key]
- entries = append(entries, memstore.StringEntry{
- Key: key,
- Value: strings.Join(value, ","),
- })
- }
- return entries
- }
- // ResetQuery clears the GET URL Query request, temporary, cache.
- // Any new URLParamXXX calls will receive the new parsed values.
- func (ctx *Context) ResetQuery() {
- ctx.query = nil
- }
- // No need anymore, net/http checks for the Form already.
- // func (ctx *Context) askParseForm() error {
- // if ctx.request.Form == nil {
- // if err := ctx.request.ParseForm(); err != nil {
- // return err
- // }
- // }
- // return nil
- // }
- // FormValueDefault returns a single parsed form value by its "name",
- // including both the URL field's query parameters and the POST or PUT form data.
- //
- // Returns the "def" if not found.
- func (ctx *Context) FormValueDefault(name string, def string) string {
- if form, has := ctx.form(); has {
- if v := form[name]; len(v) > 0 {
- return v[0]
- }
- }
- return def
- }
- // FormValueDefault retruns a single parsed form value.
- func FormValueDefault(r *http.Request, name string, def string, postMaxMemory int64, resetBody bool) string {
- if form, has := GetForm(r, postMaxMemory, resetBody); has {
- if v := form[name]; len(v) > 0 {
- return v[0]
- }
- }
- return def
- }
- // FormValue returns a single parsed form value by its "name",
- // including both the URL field's query parameters and the POST or PUT form data.
- func (ctx *Context) FormValue(name string) string {
- return ctx.FormValueDefault(name, "")
- }
- // FormValues returns the parsed form data, including both the URL
- // field's query parameters and the POST or PUT form data.
- //
- // The default form's memory maximum size is 32MB, it can be changed by the
- // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
- // NOTE: A check for nil is necessary.
- func (ctx *Context) FormValues() map[string][]string {
- form, _ := ctx.form()
- return form
- }
- // Form contains the parsed form data, including both the URL
- // field's query parameters and the POST or PUT form data.
- func (ctx *Context) form() (form map[string][]string, found bool) {
- return GetForm(ctx.request, ctx.app.ConfigurationReadOnly().GetPostMaxMemory(), ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal())
- }
- // GetForm returns the request form (url queries, post or multipart) values.
- func GetForm(r *http.Request, postMaxMemory int64, resetBody bool) (form map[string][]string, found bool) {
- /*
- net/http/request.go#1219
- for k, v := range f.Value {
- r.Form[k] = append(r.Form[k], v...)
- // r.PostForm should also be populated. See Issue 9305.
- r.PostForm[k] = append(r.PostForm[k], v...)
- }
- */
- if form := r.Form; len(form) > 0 {
- return form, true
- }
- if form := r.PostForm; len(form) > 0 {
- return form, true
- }
- if m := r.MultipartForm; m != nil {
- if len(m.Value) > 0 {
- return m.Value, true
- }
- }
- if resetBody {
- // on POST, PUT and PATCH it will read the form values from request body otherwise from URL queries.
- if m := r.Method; m == "POST" || m == "PUT" || m == "PATCH" {
- body, restoreBody, err := GetBody(r, resetBody)
- if err != nil {
- return nil, false
- }
- setBody(r, body) // so the ctx.request.Body works
- defer restoreBody() // so the next GetForm calls work.
- // r.Body = io.NopCloser(io.TeeReader(r.Body, buf))
- } else {
- resetBody = false
- }
- }
- // ParseMultipartForm calls `request.ParseForm` automatically
- // therefore we don't need to call it here, although it doesn't hurt.
- // After one call to ParseMultipartForm or ParseForm,
- // subsequent calls have no effect, are idempotent.
- err := r.ParseMultipartForm(postMaxMemory)
- // if resetBody {
- // r.Body = io.NopCloser(bytes.NewBuffer(bodyCopy))
- // }
- if err != nil && err != http.ErrNotMultipart {
- return nil, false
- }
- if form := r.Form; len(form) > 0 {
- return form, true
- }
- if form := r.PostForm; len(form) > 0 {
- return form, true
- }
- if m := r.MultipartForm; m != nil {
- if len(m.Value) > 0 {
- return m.Value, true
- }
- }
- return nil, false
- }
- // PostValues returns all the parsed form data from POST, PATCH,
- // or PUT body parameters based on a "name" as a string slice.
- //
- // The default form's memory maximum size is 32MB, it can be changed by the
- // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
- //
- // In addition, it reports whether the form was empty
- // or when the "name" does not exist
- // or whether the available values are empty.
- // It strips any empty key-values from the slice before return.
- //
- // Look ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- // See `PostValueMany` method too.
- func (ctx *Context) PostValues(name string) ([]string, error) {
- _, ok := ctx.form()
- if !ok {
- if !ctx.app.ConfigurationReadOnly().GetFireEmptyFormError() {
- return nil, nil
- }
- return nil, ErrEmptyForm // empty form.
- }
- values, ok := ctx.request.PostForm[name]
- if !ok {
- return nil, ErrNotFound // field does not exist
- }
- if len(values) == 0 ||
- // Fast check for its first empty value (see below).
- strings.TrimSpace(values[0]) == "" {
- return nil, fmt.Errorf("%w: %s", ErrEmptyFormField, name)
- }
- for _, value := range values {
- if value == "" { // if at least one empty value, then perform the strip from the beginning.
- result := make([]string, 0, len(values))
- for _, value := range values {
- if strings.TrimSpace(value) != "" {
- result = append(result, value) // we store the value as it is, not space-trimmed.
- }
- }
- if len(result) == 0 {
- return nil, fmt.Errorf("%w: %s", ErrEmptyFormField, name)
- }
- return result, nil
- }
- }
- return values, nil
- }
- // PostValueMany is like `PostValues` method, it returns the post data of a given key.
- // In addition to `PostValues` though, the returned value is a single string
- // separated by commas on multiple values.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueMany(name string) (string, error) {
- values, err := ctx.PostValues(name)
- if err != nil || len(values) == 0 {
- return "", err
- }
- return strings.Join(values, ","), nil
- }
- // PostValueDefault returns the last parsed form data from POST, PATCH,
- // or PUT body parameters based on a "name".
- //
- // If not found then "def" is returned instead.
- func (ctx *Context) PostValueDefault(name string, def string) string {
- values, err := ctx.PostValues(name)
- if err != nil || len(values) == 0 {
- return def // it returns "def" even if it's empty here.
- }
- return values[len(values)-1]
- }
- // PostValueString same as `PostValue` method but it reports
- // an error if the value with key equals to "name" does not exist.
- func (ctx *Context) PostValueString(name string) (string, error) {
- values, err := ctx.PostValues(name)
- if err != nil {
- return "", err
- }
- if len(values) == 0 { // just in case.
- return "", ErrEmptyForm
- }
- return values[len(values)-1], nil
- }
- // PostValue returns the last parsed form data from POST, PATCH,
- // or PUT body parameters based on a "name".
- //
- // See `PostValueMany` too.
- func (ctx *Context) PostValue(name string) string {
- return ctx.PostValueDefault(name, "")
- }
- // PostValueTrim returns the last parsed form data from POST, PATCH,
- // or PUT body parameters based on a "name", without trailing spaces.
- func (ctx *Context) PostValueTrim(name string) string {
- return strings.TrimSpace(ctx.PostValue(name))
- }
- // PostValueUint returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as unassigned number.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueUint(name string) (uint, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseUint(value)
- }
- // PostValueUint returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as unassigned number.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueUint8(name string) (uint8, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseUint8(value)
- }
- // PostValueUint returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as unassigned number.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueUint16(name string) (uint16, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseUint16(value)
- }
- // PostValueUint returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as unassigned number.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueUint32(name string) (uint32, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseUint32(value)
- }
- // PostValueUint returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as unassigned number.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueUint64(name string) (uint64, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseUint64(value)
- }
- // PostValueInt returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as signed number.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueInt(name string) (int, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseInt(value)
- }
- // PostValueIntDefault same as PostValueInt but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueIntDefault(name string, def int) int {
- value, err := ctx.PostValueInt(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueInt8 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as int8.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueInt8(name string) (int8, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseInt8(value)
- }
- // PostValueInt8Default same as PostValueInt8 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueInt8Default(name string, def int8) int8 {
- value, err := ctx.PostValueInt8(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueInt16 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as int16.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueInt16(name string) (int16, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseInt16(value)
- }
- // PostValueInt16Default same as PostValueInt16 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueInt16Default(name string, def int16) int16 {
- value, err := ctx.PostValueInt16(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueInt32 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as int32.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueInt32(name string) (int32, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseInt32(value)
- }
- // PostValueInt32Default same as PostValueInt32 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueInt32Default(name string, def int32) int32 {
- value, err := ctx.PostValueInt32(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueInt64 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as int64.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueInt64(name string) (int64, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseInt64(value)
- }
- // PostValueInt64Default same as PostValueInt64 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueInt64Default(name string, def int64) int64 {
- value, err := ctx.PostValueInt64(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueFloat32 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as float32.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueFloat32(name string) (float32, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseFloat32(value)
- }
- // PostValueFloat32Default same as PostValueFloat32 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueFloat32Default(name string, def float32) float32 {
- value, err := ctx.PostValueFloat32(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueFloat64 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as float64.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueFloat64(name string) (float64, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseFloat64(value)
- }
- // PostValueFloat64Default same as PostValueFloat64 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueFloat64Default(name string, def float64) float64 {
- value, err := ctx.PostValueFloat64(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueComplex64 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as complex64.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueComplex64(name string) (complex64, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseComplex64(value)
- }
- // PostValueComplex64Default same as PostValueComplex64 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueComplex64Default(name string, def complex64) complex64 {
- value, err := ctx.PostValueComplex64(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueComplex128 returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as complex128.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueComplex128(name string) (complex128, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseComplex128(value)
- }
- // PostValueComplex128Default same as PostValueComplex128 but if errored it returns
- // the given "def" default value.
- func (ctx *Context) PostValueComplex128Default(name string, def complex128) complex128 {
- value, err := ctx.PostValueComplex128(name)
- if err != nil {
- return def
- }
- return value
- }
- // PostValueBool returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as bool.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueBool(name string) (bool, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return false, err
- }
- return strParseBool(value)
- }
- // PostValueWeekday returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as time.Weekday.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueWeekday(name string) (time.Weekday, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return 0, err
- }
- return strParseWeekday(value)
- }
- // PostValueTime returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as time.Time with the given "layout".
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueTime(layout, name string) (time.Time, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return time.Time{}, err
- }
- return strParseTime(layout, value)
- }
- // PostValueSimpleDate returns the last parsed form data matches the given "name" key
- // from POST, PATCH, or PUT body request parameters as time.Time with "2006/01/02"
- // or "2006-01-02" time layout.
- //
- // See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
- func (ctx *Context) PostValueSimpleDate(name string) (time.Time, error) {
- value, err := ctx.PostValueString(name)
- if err != nil {
- return time.Time{}, err
- }
- return strParseSimpleDate(value)
- }
- // FormFile returns the first uploaded file that received from the client.
- //
- // The default form's memory maximum size is 32MB, it can be changed by the
- // `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/file-server/upload-file
- func (ctx *Context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
- // we don't have access to see if the request is body stream
- // and then the ParseMultipartForm can be useless
- // here but do it in order to apply the post limit,
- // the internal request.FormFile will not do it if that's filled
- // and it's not a stream body.
- if err := ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory()); err != nil {
- return nil, nil, err
- }
- return ctx.request.FormFile(key)
- }
- // FormFiles same as FormFile but may return multiple file inputs based on a key, e.g. "files[]".
- func (ctx *Context) FormFiles(key string, before ...func(*Context, *multipart.FileHeader) bool) (files []multipart.File, headers []*multipart.FileHeader, err error) {
- err = ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory())
- if err != nil {
- return
- }
- if ctx.request.MultipartForm != nil {
- fhs := ctx.request.MultipartForm.File
- if n := len(fhs); n > 0 {
- files = make([]multipart.File, 0, n)
- headers = make([]*multipart.FileHeader, 0, n)
- innerLoop:
- for _, header := range fhs[key] {
- header.Filename = filepath.Base(header.Filename)
- for _, b := range before {
- if !b(ctx, header) {
- continue innerLoop
- }
- }
- file, fErr := header.Open()
- if fErr != nil { // exit on first error but return the succeed.
- return files, headers, fErr
- }
- files = append(files, file)
- headers = append(headers, header)
- }
- }
- return
- }
- return nil, nil, http.ErrMissingFile
- }
- var (
- // ValidFileNameRegexp is used to validate the user input by using a regular expression.
- // See `Context.UploadFormFiles` method.
- ValidFilenameRegexp = regexp.MustCompile(`^[a-zA-Z0-9_\-\.]+$`)
- // ValidExtensionRegexp acts as an allowlist of valid extensions. It's optional. Defaults to nil (all file extensions are allowed to be uploaded).
- // See `Context.UploadFormFiles` method.
- ValidExtensionRegexp *regexp.Regexp
- )
- // UploadFormFiles uploads any received file(s) from the client
- // to the system physical location "destDirectory".
- //
- // The second optional argument "before" gives caller the chance to
- // modify or cancel the *miltipart.FileHeader before saving to the disk,
- // it can be used to change a file's name based on the current request,
- // all FileHeader's options can be changed. You can ignore it if
- // you don't need to use this capability before saving a file to the disk.
- //
- // Note that it doesn't check if request body streamed.
- //
- // Returns the copied length as int64 and
- // a not nil error if at least one new file
- // can't be created due to the operating system's permissions or
- // http.ErrMissingFile if no file received.
- //
- // If you want to receive & accept files and manage them manually you can use the `context#FormFile`
- // instead and create a copy function that suits your needs or use the `SaveFormFile` method,
- // the below is for generic usage.
- //
- // The default form's memory maximum size is 32MB, it can be changed by
- // the `WithPostMaxMemory` configurator or by `SetMaxRequestBodySize` or
- // by the `LimitRequestBodySize` middleware (depends the use case).
- //
- // See `FormFile` and `FormFiles` to a more controlled way to receive a file.
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/file-server/upload-files
- func (ctx *Context) UploadFormFiles(destDirectory string, before ...func(*Context, *multipart.FileHeader) bool) (uploaded []*multipart.FileHeader, n int64, err error) {
- err = ctx.request.ParseMultipartForm(ctx.app.ConfigurationReadOnly().GetPostMaxMemory())
- if err != nil {
- return nil, 0, err
- }
- if ctx.request.MultipartForm != nil {
- if fhs := ctx.request.MultipartForm.File; fhs != nil {
- for _, files := range fhs {
- innerLoop:
- for _, file := range files {
- for _, b := range before {
- if !b(ctx, file) {
- continue innerLoop
- }
- }
- // Security fix for go < 1.17.5:
- // Reported by Kirill Efimov (snyk.io) through security reports.
- filename := filepath.Base(filepath.ToSlash(file.Filename))
- // CWE-99.
- // Sanitize the user input by using a regular expression
- // and an allowlist of valid extensions
- isValidFilename := ValidFilenameRegexp.MatchString(filename)
- if !isValidFilename {
- // Reject the input as it is invalid or unsafe.
- continue innerLoop
- }
- if ValidExtensionRegexp != nil && !ValidExtensionRegexp.MatchString(filename) {
- // Reject the input as it is invalid or unsafe.
- continue innerLoop
- }
- // Join the sanitized input with the destination directory.
- destPath := filepath.Join(destDirectory, filename)
- // Get the canonical path of the destination
- // canonicalDestPath, err := filepath.EvalSymlinks(destPath)
- // if err != nil {
- // return nil, 0, fmt.Errorf("dest path: %s: eval symlinks: %w", destPath, err)
- // }
- // ^ No, it will try to find the file before uploaded.
- // Get the canonical path of the destination directory.
- canonicalDestDir, err := filepath.EvalSymlinks(destDirectory) // the destDirectory should exists.
- if err != nil {
- return nil, 0, fmt.Errorf("dest directory: %s: eval symlinks: %w", destDirectory, err)
- }
- // Check if the destination path is within the destination directory.
- if !strings.HasPrefix(destPath, canonicalDestDir) {
- // Reject the input as it is a path traversal attempt.
- continue innerLoop
- }
- file.Filename = filename
- n0, err0 := ctx.SaveFormFile(file, destPath)
- if err0 != nil {
- return nil, 0, err0
- }
- n += n0
- uploaded = append(uploaded, file)
- }
- }
- return uploaded, n, nil
- }
- }
- return nil, 0, http.ErrMissingFile
- }
- // SaveFormFile saves a result of `FormFile` to the "dest" disk full path (directory + filename).
- // See `FormFile` and `UploadFormFiles` too.
- func (ctx *Context) SaveFormFile(fh *multipart.FileHeader, dest string) (int64, error) {
- src, err := fh.Open()
- if err != nil {
- return 0, err
- }
- defer src.Close()
- out, err := os.Create(dest)
- if err != nil {
- return 0, err
- }
- defer out.Close()
- return io.Copy(out, src)
- }
- // AbsoluteURI parses the "s" and returns its absolute URI form.
- func (ctx *Context) AbsoluteURI(s string) string {
- if s == "" {
- return ""
- }
- userInfo := ""
- if s[0] == '@' {
- endUserInfoIdx := strings.IndexByte(s, '/')
- if endUserInfoIdx > 0 && len(s) > endUserInfoIdx {
- userInfo = s[1:endUserInfoIdx] + "@"
- s = s[endUserInfoIdx:]
- }
- }
- if s[0] == '/' {
- scheme := ctx.request.URL.Scheme
- if scheme == "" {
- if ctx.request.TLS != nil {
- scheme = "https:"
- } else {
- scheme = "http:"
- }
- }
- host := ctx.Host()
- return scheme + "//" + userInfo + host + path.Clean(s)
- }
- if u, err := url.Parse(s); err == nil {
- r := ctx.request
- if u.Scheme == "" && u.Host == "" {
- oldpath := r.URL.Path
- if oldpath == "" {
- oldpath = "/"
- }
- if s == "" || s[0] != '/' {
- olddir, _ := path.Split(oldpath)
- s = olddir + s
- }
- var query string
- if i := strings.Index(s, "?"); i != -1 {
- s, query = s[:i], s[i:]
- }
- // clean up but preserve trailing slash
- trailing := strings.HasSuffix(s, "/")
- s = path.Clean(s)
- if trailing && !strings.HasSuffix(s, "/") {
- s += "/"
- }
- s += query
- }
- }
- return s
- }
- // Redirect sends a redirect response to the client
- // of an absolute or relative target URL.
- // It accepts 2 input arguments, a string and an optional integer.
- // The first parameter is the target url to redirect.
- // The second one is the HTTP status code should be sent
- // among redirection response,
- // If the second parameter is missing, then it defaults to 302 (StatusFound).
- // It can be set to 301 (Permant redirect), StatusTemporaryRedirect(307)
- // or 303 (StatusSeeOther) if POST method.
- func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) {
- ctx.StopExecution()
- // get the previous status code given by the end-developer.
- status := ctx.GetStatusCode()
- if status < 300 { // the previous is not a RCF-valid redirect status.
- status = 0
- }
- if len(statusHeader) > 0 {
- // check if status code is passed via receivers.
- if s := statusHeader[0]; s > 0 {
- status = s
- }
- }
- if status == 0 {
- // if status remains zero then default it.
- // a 'temporary-redirect-like' which works better than for our purpose
- status = http.StatusFound
- }
- http.Redirect(ctx.writer, ctx.request, urlToRedirect, status)
- }
- // +------------------------------------------------------------+
- // | Body Readers |
- // +------------------------------------------------------------+
- // SetMaxRequestBodySize sets a limit to the request body size
- // should be called before reading the request body from the client.
- func (ctx *Context) SetMaxRequestBodySize(limitOverBytes int64) {
- ctx.request.Body = http.MaxBytesReader(ctx.writer, ctx.request.Body, limitOverBytes)
- }
- var emptyFunc = func() {}
- // GetBody reads and returns the request body.
- func GetBody(r *http.Request, resetBody bool) ([]byte, func(), error) {
- data, err := io.ReadAll(r.Body)
- if err != nil {
- return nil, nil, err
- }
- if resetBody {
- // * remember, Request.Body has no Bytes(), we have to consume them first
- // and after re-set them to the body, this is the only solution.
- return data, func() {
- setBody(r, data)
- }, nil
- }
- return data, emptyFunc, nil
- }
- func setBody(r *http.Request, data []byte) {
- r.Body = io.NopCloser(bytes.NewBuffer(data))
- }
- const disableRequestBodyConsumptionContextKey = "iris.request.body.record"
- // RecordRequestBody same as the Application's DisableBodyConsumptionOnUnmarshal
- // configuration field but acts only for the current request.
- // It makes the request body readable more than once.
- func (ctx *Context) RecordRequestBody(b bool) {
- ctx.values.Set(disableRequestBodyConsumptionContextKey, b)
- }
- // IsRecordingBody reports whether the request body can be readen multiple times.
- func (ctx *Context) IsRecordingBody() bool {
- if ctx.app.ConfigurationReadOnly().GetDisableBodyConsumptionOnUnmarshal() {
- return true
- }
- value, _ := ctx.values.GetBool(disableRequestBodyConsumptionContextKey)
- return value
- }
- // GetBody reads and returns the request body.
- // The default behavior for the http request reader is to consume the data readen
- // but you can change that behavior by passing the `WithoutBodyConsumptionOnUnmarshal` Iris option
- // or by calling the `RecordRequestBody` method.
- //
- // However, whenever you can use the `ctx.Request().Body` instead.
- func (ctx *Context) GetBody() ([]byte, error) {
- body, release, err := GetBody(ctx.request, ctx.IsRecordingBody())
- if err != nil {
- return nil, err
- }
- release()
- return body, nil
- }
- // Validator is the validator for request body on Context methods such as
- // ReadJSON, ReadMsgPack, ReadXML, ReadYAML, ReadForm, ReadQuery, ReadBody and e.t.c.
- type Validator interface {
- Struct(interface{}) error
- // If community asks for more than a struct validation on JSON, XML, MsgPack, Form, Query and e.t.c
- // then we should add more methods here, alternative approach would be to have a
- // `Validator:Validate(interface{}) error` and a map[reflect.Kind]Validator instead.
- }
- // UnmarshalBody reads the request's body and binds it to a value or pointer of any type
- // Examples of usage: context.ReadJSON, context.ReadXML.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-custom-via-unmarshaler/main.go
- func (ctx *Context) UnmarshalBody(outPtr interface{}, unmarshaler Unmarshaler) error {
- if ctx.request.Body == nil {
- return fmt.Errorf("unmarshal: empty body: %w", ErrNotFound)
- }
- rawData, err := ctx.GetBody()
- if err != nil {
- return err
- }
- if decoderWithCtx, ok := outPtr.(BodyDecoderWithContext); ok {
- return decoderWithCtx.DecodeContext(ctx.request.Context(), rawData)
- }
- // check if the v contains its own decode
- // in this case the v should be a pointer also,
- // but this is up to the user's custom Decode implementation*
- //
- // See 'BodyDecoder' for more.
- if decoder, isDecoder := outPtr.(BodyDecoder); isDecoder {
- return decoder.Decode(rawData)
- }
- // // check if v is already a pointer, if yes then pass as it's
- // if reflect.TypeOf(v).Kind() == reflect.Ptr {
- // return unmarshaler.Unmarshal(rawData, v)
- // } <- no need for that, ReadJSON is documented enough to receive a pointer,
- // we don't need to reduce the performance here by using the reflect.TypeOf method.
- // f the v doesn't contains a self-body decoder use the custom unmarshaler to bind the body.
- err = unmarshaler.Unmarshal(rawData, outPtr)
- if err != nil {
- return err
- }
- return ctx.app.Validate(outPtr)
- }
- // JSONReader holds the JSON decode options of the `Context.ReadJSON, ReadBody` methods.
- type JSONReader struct { // Note(@kataras): struct instead of optional funcs to keep consistently with the encoder options.
- // DisallowUnknownFields causes the json decoder to return an error when the destination
- // is a struct and the input contains object keys which do not match any
- // non-ignored, exported fields in the destination.
- DisallowUnknownFields bool
- // If set to true then a bit faster json decoder is used instead,
- // note that if this is true then it overrides
- // the Application's EnableOptimizations configuration field.
- Optimize bool
- // This field only applies to the ReadJSONStream.
- // The Optimize field has no effect when this is true.
- // If set to true the request body stream MUST start with a `[`
- // and end with `]` literals, example:
- // [
- // {"username":"john"},
- // {"username": "makis"},
- // {"username": "george"}
- // ]
- // Defaults to false: decodes a json object one by one, example:
- // {"username":"john"}
- // {"username": "makis"}
- // {"username": "george"}
- ArrayStream bool
- }
- var ReadJSON = func(ctx *Context, outPtr interface{}, opts ...JSONReader) error {
- var body io.Reader
- if ctx.IsRecordingBody() {
- data, err := io.ReadAll(ctx.request.Body)
- if err != nil {
- return err
- }
- setBody(ctx.request, data)
- body = bytes.NewReader(data)
- } else {
- body = ctx.request.Body
- }
- decoder := json.NewDecoder(body)
- // decoder := gojson.NewDecoder(ctx.Request().Body)
- if len(opts) > 0 {
- options := opts[0]
- if options.DisallowUnknownFields {
- decoder.DisallowUnknownFields()
- }
- }
- if err := decoder.Decode(&outPtr); err != nil {
- return err
- }
- return ctx.app.Validate(outPtr)
- }
- // ReadJSON reads JSON from request's body and binds it to a value of any json-valid type.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-json/main.go
- func (ctx *Context) ReadJSON(outPtr interface{}, opts ...JSONReader) error {
- return ReadJSON(ctx, outPtr, opts...)
- }
- // ReadJSONStream is an alternative of ReadJSON which can reduce the memory load
- // by reading only one json object every time.
- // It buffers just the content required for a single json object instead of the entire string,
- // and discards that once it reaches an end of value that can be decoded into the provided struct
- // inside the onDecode's DecodeFunc.
- //
- // It accepts a function which accepts the json Decode function and returns an error.
- // The second variadic argument is optional and can be used to customize the decoder even further.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-json-stream/main.go
- func (ctx *Context) ReadJSONStream(onDecode func(DecodeFunc) error, opts ...JSONReader) error {
- decoder := json.NewDecoder(ctx.request.Body)
- if len(opts) > 0 && opts[0].ArrayStream {
- _, err := decoder.Token() // read open bracket.
- if err != nil {
- return err
- }
- for decoder.More() { // hile the array contains values.
- if err = onDecode(decoder.Decode); err != nil {
- return err
- }
- }
- _, err = decoder.Token() // read closing bracket.
- return err
- }
- // while the array contains values
- for decoder.More() {
- if err := onDecode(decoder.Decode); err != nil {
- return err
- }
- }
- return nil
- }
- // ReadXML reads XML from request's body and binds it to a value of any xml-valid type.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-xml/main.go
- func (ctx *Context) ReadXML(outPtr interface{}) error {
- return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(xml.Unmarshal))
- }
- // ReadYAML reads YAML from request's body and binds it to the "outPtr" value.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-yaml/main.go
- func (ctx *Context) ReadYAML(outPtr interface{}) error {
- return ctx.UnmarshalBody(outPtr, UnmarshalerFunc(yaml.Unmarshal))
- }
- var (
- // IsErrEmptyJSON reports whether the given "err" is caused by a
- // Client.ReadJSON call when the request body was empty or
- // didn't start with { or [.
- IsErrEmptyJSON = func(err error) bool {
- if err == nil {
- return false
- }
- if errors.Is(err, io.EOF) {
- return true
- }
- if v, ok := err.(*json.SyntaxError); ok {
- // standard go json encoder error.
- return v.Offset == 0 && v.Error() == "unexpected end of JSON input"
- }
- errMsg := err.Error()
- // 3rd party pacakges:
- return strings.Contains(errMsg, "readObjectStart: expect {") || strings.Contains(errMsg, "readArrayStart: expect [")
- }
- // IsErrPath can be used at `context#ReadForm` and `context#ReadQuery`.
- // It reports whether the incoming error
- // can be ignored when server allows unknown post values to be sent by the client.
- //
- // A shortcut for the `schema#IsErrPath`.
- IsErrPath = schema.IsErrPath
- // IsErrPathCRSFToken reports whether the given "err" is caused
- // by unknown key error on "csrf.token". See `context#ReadForm` for more.
- IsErrPathCRSFToken = func(err error) bool {
- if err == nil || CSRFTokenFormKey == "" {
- return false
- }
- if m, ok := err.(schema.MultiError); ok {
- if csrfErr, hasCSRFToken := m[CSRFTokenFormKey]; hasCSRFToken {
- _, is := csrfErr.(schema.UnknownKeyError)
- return is
- }
- }
- return false
- }
- // ErrEmptyForm is returned by
- // - `context#ReadForm`
- // - `context#ReadQuery`
- // - `context#ReadBody`
- // when the request data (form, query and body respectfully) is empty.
- ErrEmptyForm = errors.New("empty form")
- // ErrEmptyFormField reports whether a specific field exists but it's empty.
- // Usage: errors.Is(err, ErrEmptyFormField)
- // See postValue method. It's only returned on parsed post value methods.
- ErrEmptyFormField = errors.New("empty form field")
- // ConnectionCloseErrorSubstr if at least one of the given
- // substrings are found in a net.OpError:os.SyscallError error type
- // on `IsErrConnectionReset` then the function will report true.
- ConnectionCloseErrorSubstr = []string{
- "broken pipe",
- "connection reset by peer",
- }
- // IsErrConnectionClosed reports whether the given "err"
- // is caused because of a broken connection.
- IsErrConnectionClosed = func(err error) bool {
- if err == nil {
- return false
- }
- if opErr, ok := err.(*net.OpError); ok {
- if syscallErr, ok := opErr.Err.(*os.SyscallError); ok {
- errStr := strings.ToLower(syscallErr.Error())
- for _, s := range ConnectionCloseErrorSubstr {
- if strings.Contains(errStr, s) {
- return true
- }
- }
- }
- }
- return false
- }
- )
- // CSRFTokenFormKey the CSRF token key of the form data.
- //
- // See ReadForm method for more.
- const CSRFTokenFormKey = "csrf.token"
- // ReadForm binds the request body of a form to the "formObject".
- // It supports any kind of type, including custom structs.
- // It will return nothing if request data are empty.
- // The struct field tag is "form".
- // Note that it will return nil error on empty form data if `Configuration.FireEmptyFormError`
- // is false (as defaulted) in this case the caller should check the pointer to
- // see if something was actually binded.
- //
- // If a client sent an unknown field, this method will return an error,
- // in order to ignore that error use the `err != nil && !iris.IsErrPath(err)`.
- //
- // As of 15 Aug 2022, ReadForm does not return an error over unknown CSRF token form key,
- // to change this behavior globally, set the `context.CSRFTokenFormKey` to an empty value.
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-form/main.go
- func (ctx *Context) ReadForm(formObject interface{}) error {
- values := ctx.FormValues()
- if len(values) == 0 {
- if ctx.app.ConfigurationReadOnly().GetFireEmptyFormError() {
- return ErrEmptyForm
- }
- return nil
- }
- err := schema.DecodeForm(values, formObject)
- if err != nil && !IsErrPathCRSFToken(err) {
- return err
- }
- return ctx.app.Validate(formObject)
- }
- type (
- // MultipartRelated is the result of the context.ReadMultipartRelated method.
- MultipartRelated struct {
- // ContentIDs keeps an ordered list of all the
- // content-ids of the multipart related request.
- ContentIDs []string
- // Contents keeps each part's information by Content-ID.
- // Contents holds each of the multipart/related part's data.
- Contents map[string]MultipartRelatedContent
- }
- // MultipartRelatedContent holds a multipart/related part's id, header and body.
- MultipartRelatedContent struct {
- // ID holds the Content-ID.
- ID string
- // Headers holds the part's request headers.
- Headers map[string][]string
- // Body holds the part's body.
- Body []byte
- }
- )
- // ReadMultipartRelated returns a structure which contain
- // information about each part (id, headers, body).
- //
- // Read more at: https://www.ietf.org/rfc/rfc2387.txt.
- //
- // Example request (2387/5.2 Text/X-Okie):
- // Content-Type: Multipart/Related; boundary=example-2;
- // start="<950118.AEBH@XIson.com>"
- // type="Text/x-Okie"
- //
- // --example-2
- // Content-Type: Text/x-Okie; charset=iso-8859-1;
- // declaration="<950118.AEB0@XIson.com>"
- // Content-ID: <950118.AEBH@XIson.com>
- // Content-Description: Document
- //
- // {doc}
- // This picture was taken by an automatic camera mounted ...
- // {image file=cid:950118.AECB@XIson.com}
- // {para}
- // Now this is an enlargement of the area ...
- // {image file=cid:950118:AFDH@XIson.com}
- // {/doc}
- // --example-2
- // Content-Type: image/jpeg
- // Content-ID: <950118.AFDH@XIson.com>
- // Content-Transfer-Encoding: BASE64
- // Content-Description: Picture A
- //
- // [encoded jpeg image]
- // --example-2
- // Content-Type: image/jpeg
- // Content-ID: <950118.AECB@XIson.com>
- // Content-Transfer-Encoding: BASE64
- // Content-Description: Picture B
- //
- // [encoded jpeg image]
- // --example-2--
- func (ctx *Context) ReadMultipartRelated() (MultipartRelated, error) {
- contentType, params, err := mime.ParseMediaType(ctx.GetHeader(ContentTypeHeaderKey))
- if err != nil {
- return MultipartRelated{}, err
- }
- if !strings.HasPrefix(contentType, ContentMultipartRelatedHeaderValue) {
- return MultipartRelated{}, ErrEmptyForm
- }
- var (
- contentIDs []string
- contents = make(map[string]MultipartRelatedContent)
- )
- if ctx.IsRecordingBody() {
- // * remember, Request.Body has no Bytes(), we have to consume them first
- // and after re-set them to the body, this is the only solution.
- body, restoreBody, err := GetBody(ctx.request, true)
- if err != nil {
- return MultipartRelated{}, fmt.Errorf("multipart related: body copy because of iris.Configuration.DisableBodyConsumptionOnUnmarshal: %w", err)
- }
- setBody(ctx.request, body) // so the ctx.request.Body works
- defer restoreBody() // so the next ctx.GetBody calls work.
- }
- multipartReader := multipart.NewReader(ctx.request.Body, params["boundary"])
- for {
- part, err := multipartReader.NextPart()
- if err != nil {
- if err == io.EOF {
- break
- }
- return MultipartRelated{}, fmt.Errorf("multipart related: next part: %w", err)
- }
- defer part.Close()
- b, err := io.ReadAll(part)
- if err != nil {
- return MultipartRelated{}, fmt.Errorf("multipart related: next part: read: %w", err)
- }
- contentID := part.Header.Get("Content-ID")
- contentIDs = append(contentIDs, contentID)
- contents[contentID] = MultipartRelatedContent{ // replace if same Content-ID appears, which it shouldn't.
- ID: contentID,
- Headers: http.Header(part.Header),
- Body: b,
- }
- }
- if len(contents) != len(contentIDs) {
- contentIDs = distinctStrings(contentIDs)
- }
- result := MultipartRelated{
- ContentIDs: contentIDs,
- Contents: contents,
- }
- return result, nil
- }
- func distinctStrings(values []string) []string {
- seen := make(map[string]struct{}, len(values))
- result := make([]string, 0, len(values))
- for _, val := range values {
- if _, ok := seen[val]; !ok {
- seen[val] = struct{}{}
- result = append(result, val)
- }
- }
- return result
- }
- // ReadQuery binds URL Query to "ptr". The struct field tag is "url".
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-query/main.go
- func (ctx *Context) ReadQuery(ptr interface{}) error {
- values := ctx.getQuery()
- if len(values) == 0 {
- if ctx.app.ConfigurationReadOnly().GetFireEmptyFormError() {
- return ErrEmptyForm
- }
- return nil
- }
- err := schema.DecodeQuery(values, ptr)
- if err != nil {
- return err
- }
- return ctx.app.Validate(ptr)
- }
- // ReadHeaders binds request headers to "ptr". The struct field tag is "header".
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-headers/main.go
- func (ctx *Context) ReadHeaders(ptr interface{}) error {
- err := schema.DecodeHeaders(ctx.request.Header, ptr)
- if err != nil {
- return err
- }
- return ctx.app.Validate(ptr)
- }
- // ReadParams binds URI Dynamic Path Parameters to "ptr". The struct field tag is "param".
- //
- // Example: https://github.com/kataras/iris/blob/main/_examples/request-body/read-params/main.go
- func (ctx *Context) ReadParams(ptr interface{}) error {
- n := ctx.params.Len()
- if n == 0 {
- return nil
- }
- values := make(map[string][]string, n)
- ctx.params.Visit(func(key string, value string) {
- // []string on path parameter, e.g.
- // /.../{tail:path}
- // Tail []string `param:"tail"`
- values[key] = strings.Split(value, "/")
- })
- err := schema.DecodeParams(values, ptr)
- if err != nil {
- return err
- }
- return ctx.app.Validate(ptr)
- }
- // ReadURL is a shortcut of ReadParams and ReadQuery.
- // It binds dynamic path parameters and URL query parameters
- // to the "ptr" pointer struct value.
- // The struct fields may contain "url" or "param" binding tags.
- // If a validator exists then it validates the result too.
- func (ctx *Context) ReadURL(ptr interface{}) error {
- values := make(map[string][]string, ctx.params.Len())
- ctx.params.Visit(func(key string, value string) {
- values[key] = strings.Split(value, "/")
- })
- for k, v := range ctx.getQuery() {
- values[k] = append(values[k], v...)
- }
- // Decode using all available binding tags (url, header, param).
- err := schema.Decode(values, ptr)
- if err != nil {
- return err
- }
- return ctx.app.Validate(ptr)
- }
- // ReadProtobuf binds the body to the "ptr" of a proto Message and returns any error.
- // Look `ReadJSONProtobuf` too.
- func (ctx *Context) ReadProtobuf(ptr proto.Message) error {
- rawData, err := ctx.GetBody()
- if err != nil {
- return err
- }
- return proto.Unmarshal(rawData, ptr)
- }
- // ProtoUnmarshalOptions is a type alias for protojson.UnmarshalOptions.
- type ProtoUnmarshalOptions = protojson.UnmarshalOptions
- var defaultProtobufUnmarshalOptions ProtoUnmarshalOptions
- // ReadJSONProtobuf reads a JSON body request into the given "ptr" proto.Message.
- // Look `ReadProtobuf` too.
- func (ctx *Context) ReadJSONProtobuf(ptr proto.Message, opts ...ProtoUnmarshalOptions) error {
- rawData, err := ctx.GetBody()
- if err != nil {
- return err
- }
- opt := defaultProtobufUnmarshalOptions
- if len(opts) > 0 {
- opt = opts[0]
- }
- return opt.Unmarshal(rawData, ptr)
- }
- // ReadMsgPack binds the request body of msgpack format to the "ptr" and returns any error.
- func (ctx *Context) ReadMsgPack(ptr interface{}) error {
- rawData, err := ctx.GetBody()
- if err != nil {
- return err
- }
- err = msgpack.Unmarshal(rawData, ptr)
- if err != nil {
- return err
- }
- return ctx.app.Validate(ptr)
- }
- // ReadBody binds the request body to the "ptr" depending on the HTTP Method and the Request's Content-Type.
- // If a GET method request then it reads from a form (or URL Query), otherwise
- // it tries to match (depending on the request content-type) the data format e.g.
- // JSON, Protobuf, MsgPack, XML, YAML, MultipartForm and binds the result to the "ptr".
- // As a special case if the "ptr" was a pointer to string or []byte
- // then it will bind it to the request body as it is.
- func (ctx *Context) ReadBody(ptr interface{}) error {
- // If the ptr is string or byte, read the body as it's.
- switch v := ptr.(type) {
- case *string:
- b, err := ctx.GetBody()
- if err != nil {
- return err
- }
- *v = string(b)
- case *[]byte:
- b, err := ctx.GetBody()
- if err != nil {
- return err
- }
- copy(*v, b)
- }
- if ctx.Method() == http.MethodGet {
- if ctx.Request().URL.RawQuery != "" {
- // try read from query.
- return ctx.ReadQuery(ptr)
- }
- // otherwise use the ReadForm,
- // it's actually the same except
- // ReadQuery will not fire errors on:
- // 1. unknown or empty url query parameters
- // 2. empty query or form (if FireEmptyFormError is enabled).
- return ctx.ReadForm(ptr)
- }
- switch ctx.GetContentTypeRequested() {
- case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
- return ctx.ReadXML(ptr)
- // "%v reflect.Indirect(reflect.ValueOf(ptr)).Interface())
- case ContentYAMLHeaderValue, ContentYAMLTextHeaderValue:
- return ctx.ReadYAML(ptr)
- case ContentFormHeaderValue, ContentFormMultipartHeaderValue:
- return ctx.ReadForm(ptr)
- case ContentMultipartRelatedHeaderValue:
- return fmt.Errorf("context: read body: cannot bind multipart/related: use ReadMultipartRelated instead")
- case ContentJSONHeaderValue:
- return ctx.ReadJSON(ptr)
- case ContentProtobufHeaderValue:
- msg, ok := ptr.(proto.Message)
- if !ok {
- return ErrContentNotSupported
- }
- return ctx.ReadProtobuf(msg)
- case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue:
- return ctx.ReadMsgPack(ptr)
- default:
- if ctx.Request().URL.RawQuery != "" {
- // try read from query.
- return ctx.ReadQuery(ptr)
- }
- // otherwise default to JSON.
- return ctx.ReadJSON(ptr)
- }
- }
- // +------------------------------------------------------------+
- // | Body (raw) Writers |
- // +------------------------------------------------------------+
- // Write writes the data to the connection as part of an HTTP reply.
- //
- // If WriteHeader has not yet been called, Write calls
- // WriteHeader(http.StatusOK) before writing the data. If the Header
- // does not contain a Content-Type line, Write adds a Content-Type set
- // to the result of passing the initial 512 bytes of written data to
- // DetectContentType.
- //
- // Depending on the HTTP protocol version and the client, calling
- // Write or WriteHeader may prevent future reads on the
- // Request.Body. For HTTP/1.x requests, handlers should read any
- // needed request body data before writing the response. Once the
- // headers have been flushed (due to either an explicit Flusher.Flush
- // call or writing enough data to trigger a flush), the request body
- // may be unavailable. For HTTP/2 requests, the Go HTTP server permits
- // handlers to continue to read the request body while concurrently
- // writing the response. However, such behavior may not be supported
- // by all HTTP/2 clients. Handlers should read before writing if
- // possible to maximize compatibility.
- //
- // It reports any write errors back to the caller, Application.SetContentErrorHandler does NOT apply here
- // as this is a lower-level method which must be remain as it is.
- func (ctx *Context) Write(rawBody []byte) (int, error) {
- return ctx.writer.Write(rawBody)
- }
- // Writef formats according to a format specifier and writes to the response.
- //
- // Returns the number of bytes written and any write error encountered.
- func (ctx *Context) Writef(format string, a ...interface{}) (n int, err error) {
- /* if len(a) == 0 {
- return ctx.WriteString(format)
- } ^ No, let it complain about arguments, because go test will do even if the app is running.
- Users should use WriteString instead of (format, args)
- when format may contain go-sprintf reserved chars (e.g. %).*/
- return fmt.Fprintf(ctx.writer, format, a...)
- }
- // WriteString writes a simple string to the response.
- //
- // Returns the number of bytes written and any write error encountered.
- func (ctx *Context) WriteString(body string) (n int, err error) {
- return io.WriteString(ctx.writer, body)
- }
- const (
- // ContentTypeHeaderKey is the header key of "Content-Type".
- ContentTypeHeaderKey = "Content-Type"
- // LastModifiedHeaderKey is the header key of "Last-Modified".
- LastModifiedHeaderKey = "Last-Modified"
- // IfModifiedSinceHeaderKey is the header key of "If-Modified-Since".
- IfModifiedSinceHeaderKey = "If-Modified-Since"
- // CacheControlHeaderKey is the header key of "Cache-Control".
- CacheControlHeaderKey = "Cache-Control"
- // ETagHeaderKey is the header key of "ETag".
- ETagHeaderKey = "ETag"
- // ContentDispositionHeaderKey is the header key of "Content-Disposition".
- ContentDispositionHeaderKey = "Content-Disposition"
- // ContentLengthHeaderKey is the header key of "Content-Length"
- ContentLengthHeaderKey = "Content-Length"
- // ContentEncodingHeaderKey is the header key of "Content-Encoding".
- ContentEncodingHeaderKey = "Content-Encoding"
- // GzipHeaderValue is the header value of "gzip".
- GzipHeaderValue = "gzip"
- // AcceptEncodingHeaderKey is the header key of "Accept-Encoding".
- AcceptEncodingHeaderKey = "Accept-Encoding"
- // VaryHeaderKey is the header key of "Vary".
- VaryHeaderKey = "Vary"
- )
- var unixEpochTime = time.Unix(0, 0)
- // IsZeroTime reports whether t is obviously unspecified (either zero or Unix()=0).
- func IsZeroTime(t time.Time) bool {
- return t.IsZero() || t.Equal(unixEpochTime)
- }
- // ParseTime parses a time header (such as the Date: header),
- // trying each forth formats (or three if Application's configuration's TimeFormat is defaulted)
- // that are allowed by HTTP/1.1:
- // Application's configuration's TimeFormat or/and http.TimeFormat,
- // time.RFC850, and time.ANSIC.
- //
- // Look `context#FormatTime` for the opossite operation (Time to string).
- var ParseTime = func(ctx *Context, text string) (t time.Time, err error) {
- t, err = time.Parse(ctx.Application().ConfigurationReadOnly().GetTimeFormat(), text)
- if err != nil {
- return http.ParseTime(text)
- }
- return
- }
- // FormatTime returns a textual representation of the time value formatted
- // according to the Application's configuration's TimeFormat field
- // which defines the format.
- //
- // Look `context#ParseTime` for the opossite operation (string to Time).
- var FormatTime = func(ctx *Context, t time.Time) string {
- return t.Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
- }
- // SetLastModified sets the "Last-Modified" based on the "modtime" input.
- // If "modtime" is zero then it does nothing.
- //
- // It's mostly internally on core/router and context packages.
- func (ctx *Context) SetLastModified(modtime time.Time) {
- if !IsZeroTime(modtime) {
- ctx.Header(LastModifiedHeaderKey, FormatTime(ctx, modtime.UTC())) // or modtime.UTC()?
- }
- }
- // ErrPreconditionFailed may be returned from `Context` methods
- // that has to perform one or more client side preconditions before the actual check, e.g. `CheckIfModifiedSince`.
- // Usage:
- // ok, err := context.CheckIfModifiedSince(modTime)
- //
- // if err != nil {
- // if errors.Is(err, context.ErrPreconditionFailed) {
- // [handle missing client conditions,such as not valid request method...]
- // }else {
- // [the error is probably a time parse error...]
- // }
- // }
- var ErrPreconditionFailed = errors.New("precondition failed")
- // CheckIfModifiedSince checks if the response is modified since the "modtime".
- // Note that it has nothing to do with server-side caching.
- // It does those checks by checking if the "If-Modified-Since" request header
- // sent by client or a previous server response header
- // (e.g with WriteWithExpiration or HandleDir or Favicon etc.)
- // is a valid one and it's before the "modtime".
- //
- // A check for !modtime && err == nil is necessary to make sure that
- // it's not modified since, because it may return false but without even
- // had the chance to check the client-side (request) header due to some errors,
- // like the HTTP Method is not "GET" or "HEAD" or if the "modtime" is zero
- // or if parsing time from the header failed. See `ErrPreconditionFailed` too.
- //
- // It's mostly used internally, e.g. `context#WriteWithExpiration`.
- func (ctx *Context) CheckIfModifiedSince(modtime time.Time) (bool, error) {
- if method := ctx.Method(); method != http.MethodGet && method != http.MethodHead {
- return false, fmt.Errorf("method: %w", ErrPreconditionFailed)
- }
- ims := ctx.GetHeader(IfModifiedSinceHeaderKey)
- if ims == "" || IsZeroTime(modtime) {
- return false, fmt.Errorf("zero time: %w", ErrPreconditionFailed)
- }
- t, err := ParseTime(ctx, ims)
- if err != nil {
- return false, err
- }
- // sub-second precision, so
- // use mtime < t+1s instead of mtime <= t to check for unmodified.
- if modtime.UTC().Before(t.Add(1 * time.Second)) {
- return false, nil
- }
- return true, nil
- }
- // WriteNotModified sends a 304 "Not Modified" status code to the client,
- // it makes sure that the content type, the content length headers
- // and any "ETag" are removed before the response sent.
- //
- // It's mostly used internally on core/router/fs.go and context methods.
- func (ctx *Context) WriteNotModified() {
- // RFC 7232 section 4.1:
- // a sender SHOULD NOT generate representation metadata other than the
- // above listed fields unless said metadata exists for the purpose of
- // guiding cache updates (e.g.," Last-Modified" might be useful if the
- // response does not have an ETag field).
- h := ctx.ResponseWriter().Header()
- delete(h, ContentTypeHeaderKey)
- delete(h, ContentLengthHeaderKey)
- if h.Get(ETagHeaderKey) != "" {
- delete(h, LastModifiedHeaderKey)
- }
- ctx.StatusCode(http.StatusNotModified)
- }
- // WriteWithExpiration works like `Write` but it will check if a resource is modified,
- // based on the "modtime" input argument,
- // otherwise sends a 304 status code in order to let the client-side render the cached content.
- func (ctx *Context) WriteWithExpiration(body []byte, modtime time.Time) (int, error) {
- if modified, err := ctx.CheckIfModifiedSince(modtime); !modified && err == nil {
- ctx.WriteNotModified()
- return 0, nil
- }
- ctx.SetLastModified(modtime)
- return ctx.writer.Write(body)
- }
- // StreamWriter registers the given stream writer for populating
- // response body.
- //
- // Access to context's and/or its' members is forbidden from writer.
- //
- // This function may be used in the following cases:
- //
- // - if response body is too big (more than iris.LimitRequestBodySize(if set)).
- // - if response body is streamed from slow external sources.
- // - if response body must be streamed to the client in chunks.
- // (aka `http server push`).
- func (ctx *Context) StreamWriter(writer func(w io.Writer) error) error {
- cancelCtx := ctx.Request().Context()
- notifyClosed := cancelCtx.Done()
- for {
- select {
- // response writer forced to close, exit.
- case <-notifyClosed:
- return cancelCtx.Err()
- default:
- if err := writer(ctx.writer); err != nil {
- return err
- }
- ctx.writer.Flush()
- }
- }
- }
- // +------------------------------------------------------------+
- // | Body Writers with compression |
- // +------------------------------------------------------------+
- // ClientSupportsEncoding reports whether the
- // client expects one of the given "encodings" compression.
- //
- // Note, this method just reports back the first valid encoding it sees,
- // meaning that request accept-encoding offers don't matter here.
- // See `CompressWriter` too.
- func (ctx *Context) ClientSupportsEncoding(encodings ...string) bool {
- if len(encodings) == 0 {
- return false
- }
- if h := ctx.GetHeader(AcceptEncodingHeaderKey); h != "" {
- for _, v := range strings.Split(h, ",") {
- for _, encoding := range encodings {
- if strings.Contains(v, encoding) {
- return true
- }
- }
- }
- }
- return false
- }
- // CompressWriter enables or disables the compress response writer.
- // if the client expects a valid compression algorithm then this
- // will change the response writer to a compress writer instead.
- // All future write and rich write methods will respect this option.
- // Usage:
- //
- // app.Use(func(ctx iris.Context){
- // err := ctx.CompressWriter(true)
- // ctx.Next()
- // })
- //
- // The recommendation is to compress data as much as possible and therefore to use this field,
- // but some types of resources, such as jpeg images, are already compressed.
- // Sometimes, using additional compression doesn't reduce payload size and
- // can even make the payload longer.
- func (ctx *Context) CompressWriter(enable bool) error {
- switch w := ctx.writer.(type) {
- case *CompressResponseWriter:
- if enable {
- return nil
- }
- w.Disabled = true
- case *ResponseRecorder:
- if enable {
- // If it's a recorder which already wraps the compress, exit.
- if _, ok := w.ResponseWriter.(*CompressResponseWriter); ok {
- return nil
- }
- // Keep the Recorder as ctx.writer.
- // Wrap the existing net/http response writer
- // with the compressed writer and
- // replace the recorder's response writer
- // reference with that compressed one.
- // Fixes an issue when Record is called before CompressWriter.
- cw, err := AcquireCompressResponseWriter(w.ResponseWriter, ctx.request, -1)
- if err != nil {
- return err
- }
- w.ResponseWriter = cw
- } else {
- cw, ok := w.ResponseWriter.(*CompressResponseWriter)
- if ok {
- cw.Disabled = true
- }
- }
- default:
- if !enable {
- return nil
- }
- cw, err := AcquireCompressResponseWriter(w, ctx.request, -1)
- if err != nil {
- return err
- }
- ctx.writer = cw
- }
- return nil
- }
- // CompressReader accepts a boolean, which, if set to true
- // it wraps the request body reader with a reader which decompresses request data before read.
- // If the "enable" input argument is false then the request body will reset to the default one.
- //
- // Useful when incoming request data are compressed.
- // All future calls of `ctx.GetBody/ReadXXX/UnmarshalBody` methods will respect this option.
- //
- // Usage:
- //
- // app.Use(func(ctx iris.Context){
- // err := ctx.CompressReader(true)
- // ctx.Next()
- // })
- //
- // More:
- //
- // if cr, ok := ctx.Request().Body.(*CompressReader); ok {
- // cr.Src // the original request body
- // cr.Encoding // the compression algorithm.
- // }
- //
- // It returns `ErrRequestNotCompressed` if client's request data are not compressed
- // (or empty)
- // or `ErrNotSupportedCompression` if server missing the decompression algorithm.
- func (ctx *Context) CompressReader(enable bool) error {
- cr, ok := ctx.request.Body.(*CompressReader)
- if enable {
- if ok {
- // already called.
- return nil
- }
- encoding := ctx.GetHeader(ContentEncodingHeaderKey)
- if encoding == IDENTITY {
- // no transformation whatsoever, return nil error and
- // don't wrap the body reader.
- return nil
- }
- r, err := NewCompressReader(ctx.request.Body, encoding)
- if err != nil {
- return err
- }
- ctx.request.Body = r
- } else if ok {
- ctx.request.Body = cr.Src
- }
- return nil
- }
- // +------------------------------------------------------------+
- // | Rich Body Content Writers/Renderers |
- // +------------------------------------------------------------+
- // ViewEngine registers a view engine for the current chain of handlers.
- // It overrides any previously registered engines, including the application's root ones.
- // Note that, because performance is everything,
- // the "engine" MUST be already ready-to-use,
- // meaning that its `Load` method should be called once before this method call.
- //
- // To register a view engine per-group of groups too see `Party.RegisterView` instead.
- func (ctx *Context) ViewEngine(engine ViewEngine) {
- ctx.values.Set(ctx.app.ConfigurationReadOnly().GetViewEngineContextKey(), engine)
- }
- // ViewLayout sets the "layout" option if and when .View
- // is being called afterwards, in the same request.
- // Useful when need to set or/and change a layout based on the previous handlers in the chain.
- //
- // Note that the 'layoutTmplFile' argument can be set to iris.NoLayout
- // to disable the layout for a specific view render action,
- // it disables the engine's configuration's layout property.
- //
- // Look .ViewData and .View too.
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/view/context-view-data/
- func (ctx *Context) ViewLayout(layoutTmplFile string) {
- ctx.values.Set(ctx.app.ConfigurationReadOnly().GetViewLayoutContextKey(), layoutTmplFile)
- }
- // ViewData saves one or more key-value pair in order to be passed if and when .View
- // is being called afterwards, in the same request.
- // Useful when need to set or/and change template data from previous hanadlers in the chain.
- //
- // If .View's "binding" argument is not nil and it's not a type of map
- // then these data are being ignored, binding has the priority, so the main route's handler can still decide.
- // If binding is a map or iris.Map then these data are being added to the view data
- // and passed to the template.
- //
- // After .View, the data are not destroyed, in order to be re-used if needed (again, in the same request as everything else),
- // to clear the view data, developers can call:
- // ctx.Set(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), nil)
- //
- // If 'key' is empty then the value is added as it's (struct or map) and developer is unable to add other value.
- //
- // Look .ViewLayout and .View too.
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/view/context-view-data/
- func (ctx *Context) ViewData(key string, value interface{}) {
- viewDataContextKey := ctx.app.ConfigurationReadOnly().GetViewDataContextKey()
- if key == "" {
- ctx.values.Set(viewDataContextKey, value)
- return
- }
- v := ctx.values.Get(viewDataContextKey)
- if v == nil {
- ctx.values.Set(viewDataContextKey, Map{key: value})
- return
- }
- if data, ok := v.(Map); ok {
- data[key] = value
- }
- }
- // GetViewData returns the values registered by `context#ViewData`.
- // The return value is `map[string]interface{}`, this means that
- // if a custom struct registered to ViewData then this function
- // will try to parse it to map, if failed then the return value is nil
- // A check for nil is always a good practise if different
- // kind of values or no data are registered via `ViewData`.
- //
- // Similarly to `viewData := ctx.Values().Get("iris.view.data")` or
- // `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`.
- func (ctx *Context) GetViewData() map[string]interface{} {
- if v := ctx.values.Get(ctx.app.ConfigurationReadOnly().GetViewDataContextKey()); v != nil {
- // if pure map[string]interface{}
- if viewData, ok := v.(Map); ok {
- return viewData
- }
- // if struct, convert it to map[string]interface{}
- if structs.IsStruct(v) {
- return structs.Map(v)
- }
- }
- // if no values found, then return nil
- return nil
- }
- // FallbackViewProvider is an interface which can be registered to the `Party.FallbackView`
- // or `Context.FallbackView` methods to handle fallback views.
- // See FallbackView, FallbackViewLayout and FallbackViewFunc.
- type FallbackViewProvider interface {
- FallbackView(ctx *Context, err ErrViewNotExist) error
- } /* Notes(@kataras): If ever requested, this fallback logic (of ctx, error) can go to all necessary methods.
- I've designed with a bit more complexity here instead of a simple filename fallback in order to give
- the freedom to the developer to do whatever he/she wants with that template/layout not exists error,
- e.g. have a list of fallbacks views to loop through until succeed or fire a different error than the default.
- We also provide some helpers for common fallback actions (FallbackView, FallbackViewLayout).
- This naming was chosen in order to be easy to follow up with the previous view-relative context features.
- Also note that here we catch a specific error, we want the developer
- to be aware of the rest template errors (e.g. when a template having parsing issues).
- */
- // FallbackViewFunc is a function that can be registered
- // to handle view fallbacks. It accepts the Context and
- // a special error which contains information about the previous template error.
- // It implements the FallbackViewProvider interface.
- //
- // See `Context.View` method.
- type FallbackViewFunc func(ctx *Context, err ErrViewNotExist) error
- // FallbackView completes the FallbackViewProvider interface.
- func (fn FallbackViewFunc) FallbackView(ctx *Context, err ErrViewNotExist) error {
- return fn(ctx, err)
- }
- var (
- _ FallbackViewProvider = FallbackView("")
- _ FallbackViewProvider = FallbackViewLayout("")
- )
- // FallbackView is a helper to register a single template filename as a fallback
- // when the provided tempate filename was not found.
- type FallbackView string
- // FallbackView completes the FallbackViewProvider interface.
- func (f FallbackView) FallbackView(ctx *Context, err ErrViewNotExist) error {
- if err.IsLayout { // Not responsible to render layouts.
- return err
- }
- // ctx.StatusCode(200) // Let's keep the previous status code here, developer can change it anyways.
- return ctx.View(string(f), err.Data)
- }
- // FallbackViewLayout is a helper to register a single template filename as a fallback
- // layout when the provided layout filename was not found.
- type FallbackViewLayout string
- // FallbackView completes the FallbackViewProvider interface.
- func (f FallbackViewLayout) FallbackView(ctx *Context, err ErrViewNotExist) error {
- if !err.IsLayout {
- // Responsible to render layouts only.
- return err
- }
- ctx.ViewLayout(string(f))
- return ctx.View(err.Name, err.Data)
- }
- const fallbackViewOnce = "iris.fallback.view.once"
- func (ctx *Context) fireFallbackViewOnce(err ErrViewNotExist) error {
- // Note(@kataras): this is our way to keep the same View method for
- // both fallback and normal views, remember, we export the whole
- // Context functionality to the end-developer through the fallback view provider.
- if ctx.values.Get(fallbackViewOnce) != nil {
- return err
- }
- v := ctx.values.Get(ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey())
- if v == nil {
- return err
- }
- providers, ok := v.([]FallbackViewProvider)
- if !ok {
- return err
- }
- ctx.values.Set(fallbackViewOnce, struct{}{})
- var pErr error
- for _, provider := range providers {
- pErr = provider.FallbackView(ctx, err)
- if pErr != nil {
- if vErr, ok := pErr.(ErrViewNotExist); ok {
- // This fallback view does not exist or it's not responsible to handle,
- // try the next.
- pErr = vErr
- continue
- }
- }
- // If OK then we found the correct fallback.
- // If the error was a parse error and not a template not found
- // then exit and report the pErr error.
- break
- }
- return pErr
- }
- // FallbackView registers one or more fallback views for a template or a template layout.
- // When View cannot find the given filename to execute then this "provider"
- // is responsible to handle the error or render a different view.
- //
- // Usage:
- //
- // FallbackView(iris.FallbackView("fallback.html"))
- // FallbackView(iris.FallbackViewLayout("layouts/fallback.html"))
- // OR
- // FallbackView(iris.FallbackViewFunc(ctx iris.Context, err iris.ErrViewNotExist) error {
- // err.Name is the previous template name.
- // err.IsLayout reports whether the failure came from the layout template.
- // err.Data is the template data provided to the previous View call.
- // [...custom logic e.g. ctx.View("fallback", err.Data)]
- // })
- func (ctx *Context) FallbackView(providers ...FallbackViewProvider) {
- key := ctx.app.ConfigurationReadOnly().GetFallbackViewContextKey()
- if key == "" {
- return
- }
- v := ctx.values.Get(key)
- if v == nil {
- ctx.values.Set(key, providers)
- return
- }
- // Can register more than one.
- storedProviders, ok := v.([]FallbackViewProvider)
- if !ok {
- return
- }
- storedProviders = append(storedProviders, providers...)
- ctx.values.Set(key, storedProviders)
- }
- // View renders a template based on the registered view engine(s).
- // First argument accepts the filename, relative to the view engine's Directory and Extension,
- // i.e: if directory is "./templates" and want to render the "./templates/users/index.html"
- // then you pass the "users/index.html" as the filename argument.
- //
- // The second optional argument can receive a single "view model".
- // If "optionalViewModel" exists, even if it's nil, overrides any previous `ViewData` calls.
- // If second argument is missing then binds the data through previous `ViewData` calls (e.g. middleware).
- //
- // Look .ViewData and .ViewLayout too.
- //
- // Examples: https://github.com/kataras/iris/tree/main/_examples/view
- func (ctx *Context) View(filename string, optionalViewModel ...interface{}) error {
- ctx.ContentType(ContentHTMLHeaderValue)
- err := ctx.renderView(filename, optionalViewModel...)
- if err != nil {
- if errNotExists, ok := err.(ErrViewNotExist); ok {
- err = ctx.fireFallbackViewOnce(errNotExists)
- }
- }
- if err != nil {
- if ctx.IsDebug() {
- // send the error back to the client, when debug mode.
- ctx.StopWithError(http.StatusInternalServerError, err)
- } else {
- ctx.SetErrPrivate(err)
- ctx.StopWithStatus(http.StatusInternalServerError)
- }
- }
- return err
- }
- func (ctx *Context) renderView(filename string, optionalViewModel ...interface{}) error {
- cfg := ctx.app.ConfigurationReadOnly()
- layout := ctx.values.GetString(cfg.GetViewLayoutContextKey())
- var bindingData interface{}
- if len(optionalViewModel) > 0 /* Don't do it: can break a lot of servers: && optionalViewModel[0] != nil */ {
- // a nil can override the existing data or model sent by `ViewData`.
- bindingData = optionalViewModel[0]
- } else {
- bindingData = ctx.values.Get(cfg.GetViewDataContextKey())
- }
- if key := cfg.GetViewEngineContextKey(); key != "" {
- if engineV := ctx.values.Get(key); engineV != nil {
- if engine, ok := engineV.(ViewEngine); ok {
- return engine.ExecuteWriter(ctx, filename, layout, bindingData)
- }
- }
- }
- return ctx.app.View(ctx, filename, layout, bindingData)
- }
- const (
- // ContentBinaryHeaderValue header value for binary data.
- ContentBinaryHeaderValue = "application/octet-stream"
- // ContentWebassemblyHeaderValue header value for web assembly files.
- ContentWebassemblyHeaderValue = "application/wasm"
- // ContentHTMLHeaderValue is the string of text/html response header's content type value.
- ContentHTMLHeaderValue = "text/html"
- // ContentJSONHeaderValue header value for JSON data.
- ContentJSONHeaderValue = "application/json"
- // ContentJSONProblemHeaderValue header value for JSON API problem error.
- // Read more at: https://tools.ietf.org/html/rfc7807
- ContentJSONProblemHeaderValue = "application/problem+json"
- // ContentXMLProblemHeaderValue header value for XML API problem error.
- // Read more at: https://tools.ietf.org/html/rfc7807
- ContentXMLProblemHeaderValue = "application/problem+xml"
- // ContentJavascriptHeaderValue header value for JSONP & Javascript data.
- ContentJavascriptHeaderValue = "text/javascript"
- // ContentTextHeaderValue header value for Text data.
- ContentTextHeaderValue = "text/plain"
- // ContentXMLHeaderValue header value for XML data.
- ContentXMLHeaderValue = "text/xml"
- // ContentXMLUnreadableHeaderValue obselete header value for XML.
- ContentXMLUnreadableHeaderValue = "application/xml"
- // ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
- ContentMarkdownHeaderValue = "text/markdown"
- // ContentYAMLHeaderValue header value for YAML data.
- ContentYAMLHeaderValue = "application/x-yaml"
- // ContentYAMLTextHeaderValue header value for YAML plain text.
- ContentYAMLTextHeaderValue = "text/yaml"
- // ContentProtobufHeaderValue header value for Protobuf messages data.
- ContentProtobufHeaderValue = "application/x-protobuf"
- // ContentMsgPackHeaderValue header value for MsgPack data.
- ContentMsgPackHeaderValue = "application/msgpack"
- // ContentMsgPack2HeaderValue alternative header value for MsgPack data.
- ContentMsgPack2HeaderValue = "application/x-msgpack"
- // ContentFormHeaderValue header value for post form data.
- ContentFormHeaderValue = "application/x-www-form-urlencoded"
- // ContentFormMultipartHeaderValue header value for post multipart form data.
- ContentFormMultipartHeaderValue = "multipart/form-data"
- // ContentMultipartRelatedHeaderValue header value for multipart related data.
- ContentMultipartRelatedHeaderValue = "multipart/related"
- // ContentGRPCHeaderValue Content-Type header value for gRPC.
- ContentGRPCHeaderValue = "application/grpc"
- )
- // Binary writes out the raw bytes as binary data.
- func (ctx *Context) Binary(data []byte) (int, error) {
- ctx.ContentType(ContentBinaryHeaderValue)
- return ctx.Write(data)
- }
- // Text writes out a string as plain text.
- func (ctx *Context) Text(format string, args ...interface{}) (int, error) {
- ctx.ContentType(ContentTextHeaderValue)
- return ctx.Writef(format, args...)
- }
- // HTML writes out a string as text/html.
- func (ctx *Context) HTML(format string, args ...interface{}) (int, error) {
- ctx.ContentType(ContentHTMLHeaderValue)
- return ctx.Writef(format, args...)
- }
- // ProtoMarshalOptions is a type alias for protojson.MarshalOptions.
- type ProtoMarshalOptions = protojson.MarshalOptions
- // JSON contains the options for the JSON (Context's) Renderer.
- type JSON struct {
- // content-specific
- UnescapeHTML bool `yaml:"UnescapeHTML"`
- Indent string `yaml:"Indent"`
- Prefix string `yaml:"Prefix"`
- ASCII bool `yaml:"ASCII"` // if true writes with unicode to ASCII content.
- Secure bool `yaml:"Secure"` // if true then it prepends a "while(1);" when Go slice (to JSON Array) value.
- // proto.Message specific marshal options.
- Proto ProtoMarshalOptions `yaml:"ProtoMarshalOptions"`
- // If true and json writing failed then the error handler is skipped
- // and it just returns to the caller.
- //
- // See StopWithJSON and x/errors package.
- OmitErrorHandler bool `yaml:"OmitErrorHandler"`
- }
- // DefaultJSONOptions is the optional settings that are being used
- // inside `Context.JSON`.
- var DefaultJSONOptions = JSON{}
- // IsDefault reports whether this JSON options structure holds the default values.
- func (j *JSON) IsDefault() bool {
- return j.UnescapeHTML == DefaultJSONOptions.UnescapeHTML &&
- j.Indent == DefaultJSONOptions.Indent &&
- j.Prefix == DefaultJSONOptions.Prefix &&
- j.ASCII == DefaultJSONOptions.ASCII &&
- j.Secure == DefaultJSONOptions.Secure &&
- j.Proto == DefaultJSONOptions.Proto
- }
- // JSONP contains the options for the JSONP (Context's) Renderer.
- type JSONP struct {
- // content-specific
- Indent string
- Callback string
- OmitErrorHandler bool // See JSON.OmitErrorHandler.
- }
- // XML contains the options for the XML (Context's) Renderer.
- type XML struct {
- // content-specific
- Indent string
- Prefix string
- OmitErrorHandler bool // See JSON.OmitErrorHandler.
- }
- // Markdown contains the options for the Markdown (Context's) Renderer.
- type Markdown struct {
- // content-specific
- Sanitize bool
- OmitErrorHandler bool // See JSON.OmitErrorHandler.
- //
- // Library-specific.
- // E.g. Flags: html.CommonFlags | html.HrefTargetBlank
- RenderOptions html.RendererOptions
- }
- var (
- newLineB = []byte("\n")
- // the html codes for unescaping.
- ltHex = []byte("\\u003c")
- lt = []byte("<")
- gtHex = []byte("\\u003e")
- gt = []byte(">")
- andHex = []byte("\\u0026")
- and = []byte("&")
- // secure JSON.
- jsonArrayPrefix = []byte("[")
- jsonArraySuffix = []byte("]")
- secureJSONPrefix = []byte("while(1);")
- )
- func (ctx *Context) handleSpecialJSONResponseValue(v interface{}, options *JSON) (bool, int, error) {
- if ctx.app.ConfigurationReadOnly().GetEnableProtoJSON() {
- if m, ok := v.(proto.Message); ok {
- protoJSON := ProtoMarshalOptions{}
- if options != nil {
- protoJSON = options.Proto
- }
- result, err := protoJSON.Marshal(m)
- if err != nil {
- return true, 0, err
- }
- n, err := ctx.writer.Write(result)
- return true, n, err
- }
- }
- if ctx.app.ConfigurationReadOnly().GetEnableEasyJSON() {
- if easyObject, ok := v.(easyjson.Marshaler); ok {
- noEscapeHTML := false
- if options != nil {
- noEscapeHTML = !options.UnescapeHTML
- }
- jw := jwriter.Writer{NoEscapeHTML: noEscapeHTML}
- easyObject.MarshalEasyJSON(&jw)
- n, err := jw.DumpTo(ctx.writer)
- return true, n, err
- }
- }
- return false, 0, nil
- }
- // WriteJSON marshals the given interface object and writes the JSON response to the 'writer'.
- var WriteJSON = func(ctx *Context, v interface{}, options *JSON) error {
- if !options.Secure && !options.ASCII && options.Prefix == "" {
- // jsoniterConfig := jsoniter.Config{
- // EscapeHTML: !options.UnescapeHTML,
- // IndentionStep: 4,
- // }.Froze()
- // enc := jsoniterConfig.NewEncoder(ctx.writer)
- // err = enc.Encode(v)
- //
- // enc := gojson.NewEncoder(ctx.writer)
- // enc.SetEscapeHTML(!options.UnescapeHTML)
- // enc.SetIndent(options.Prefix, options.Indent)
- // err = enc.EncodeContext(ctx, v)
- enc := json.NewEncoder(ctx.writer)
- enc.SetEscapeHTML(!options.UnescapeHTML)
- enc.SetIndent(options.Prefix, options.Indent)
- return enc.Encode(v)
- }
- var (
- result []byte
- err error
- )
- if indent := options.Indent; indent != "" {
- result, err = json.MarshalIndent(v, "", indent)
- result = append(result, newLineB...)
- } else {
- result, err = json.Marshal(v)
- }
- if err != nil {
- return err
- }
- prependSecure := false
- if options.Secure {
- if bytes.HasPrefix(result, jsonArrayPrefix) {
- if options.Indent == "" {
- prependSecure = bytes.HasSuffix(result, jsonArraySuffix)
- } else {
- prependSecure = bytes.HasSuffix(bytes.TrimRightFunc(result, func(r rune) bool {
- return r == '\n' || r == '\r'
- }), jsonArraySuffix)
- }
- }
- }
- if options.UnescapeHTML {
- result = bytes.ReplaceAll(result, ltHex, lt)
- result = bytes.ReplaceAll(result, gtHex, gt)
- result = bytes.ReplaceAll(result, andHex, and)
- }
- if prependSecure {
- result = append(secureJSONPrefix, result...)
- }
- if options.ASCII {
- if len(result) > 0 {
- buf := new(bytes.Buffer)
- for _, s := range string(result) {
- char := string(s)
- if s >= 128 {
- char = fmt.Sprintf("\\u%04x", int64(s))
- }
- buf.WriteString(char)
- }
- result = buf.Bytes()
- }
- }
- if prefix := options.Prefix; prefix != "" {
- result = append(stringToBytes(prefix), result...)
- }
- _, err = ctx.Write(result)
- return err
- }
- // See https://golang.org/src/strings/builder.go#L45
- // func bytesToString(b []byte) string {
- // return unsafe.String(unsafe.SliceData(b), len(b))
- // }
- func stringToBytes(s string) []byte {
- return unsafe.Slice(unsafe.StringData(s), len(s))
- }
- type (
- // ErrorHandler describes a context error handler which applies on
- // JSON, JSONP, Protobuf, MsgPack, XML, YAML and Markdown write errors.
- //
- // It does NOT modify or handle the result of Context.GetErr at all,
- // use a custom middleware instead if you want to handle the handler-provided custom errors (Context.SetErr)
- //
- // An ErrorHandler can be registered once via Application.SetErrorHandler method to override the default behavior.
- // The default behavior is to simply send status internal code error
- // without a body back to the client.
- //
- // See Application.SetContextErrorHandler for more.
- ErrorHandler interface {
- HandleContextError(ctx *Context, err error)
- }
- // ErrorHandlerFunc a function shortcut for ErrorHandler interface.
- ErrorHandlerFunc func(ctx *Context, err error)
- )
- // HandleContextError completes the ErrorHandler interface.
- func (h ErrorHandlerFunc) HandleContextError(ctx *Context, err error) {
- h(ctx, err)
- }
- func (ctx *Context) handleContextError(err error) {
- if err == nil {
- return
- }
- if errHandler := ctx.app.GetContextErrorHandler(); errHandler != nil {
- errHandler.HandleContextError(ctx, err)
- } else {
- if ctx.IsDebug() {
- ctx.app.Logger().Error(err)
- }
- ctx.StatusCode(http.StatusInternalServerError)
- }
- // keep the error non nil so the caller has control over further actions.
- }
- // Render writes the response headers and calls the given renderer "r" to render data.
- // This method can be used while migrating from other frameworks.
- func (ctx *Context) Render(statusCode int, r interface {
- // Render should push data with custom content type to the client.
- Render(http.ResponseWriter) error
- // WriteContentType writes custom content type to the response.
- WriteContentType(w http.ResponseWriter)
- }) {
- ctx.StatusCode(statusCode)
- if statusCode >= 100 && statusCode <= 199 || statusCode == http.StatusNoContent || statusCode == http.StatusNotModified {
- r.WriteContentType(ctx.writer)
- return
- }
- if err := r.Render(ctx.writer); err != nil {
- ctx.StopWithError(http.StatusInternalServerError, err)
- }
- }
- // Component is the interface which all components must implement.
- // A component is a struct which can be rendered to a writer.
- // It's being used by the `Context.RenderComponent` method.
- // An example of compatible Component is a templ.Component.
- type Component interface {
- Render(context.Context, io.Writer) error
- }
- // RenderComponent renders a component to the client.
- // It sets the "Content-Type" header to "text/html; charset=utf-8".
- // It reports any component render errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) RenderComponent(component Component) error {
- ctx.ContentType("text/html; charset=utf-8")
- err := component.Render(ctx.Request().Context(), ctx.ResponseWriter())
- if err != nil {
- ctx.handleContextError(err)
- }
- return err
- }
- // JSON marshals the given "v" value to JSON and writes the response to the client.
- // Look the Configuration.EnableProtoJSON and EnableEasyJSON too.
- //
- // It reports any JSON parser or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- //
- // Customize the behavior of every `Context.JSON“ can be achieved
- // by modifying the package-level `WriteJSON` function on program initilization.
- func (ctx *Context) JSON(v interface{}, opts ...JSON) (err error) {
- var options *JSON
- if len(opts) > 0 {
- options = &opts[0]
- } else {
- // If no options are given safely read the already-initialized value.
- options = &DefaultJSONOptions
- }
- if err = ctx.writeJSON(v, options); err != nil {
- // if no options are given or OmitErrorHandler is false
- // then call the error handler (which may lead to a cycle if the error handler fails to write JSON too).
- if !options.OmitErrorHandler {
- ctx.handleContextError(err)
- }
- }
- return
- }
- func (ctx *Context) writeJSON(v interface{}, options *JSON) error {
- ctx.ContentType(ContentJSONHeaderValue)
- // After content type given and before everything else, try handle proto or easyjson, no matter the performance mode.
- if handled, _, err := ctx.handleSpecialJSONResponseValue(v, options); handled {
- return err
- }
- return WriteJSON(ctx, v, options)
- }
- var finishCallbackB = []byte(");")
- // WriteJSONP marshals the given interface object and writes the JSONP response to the writer.
- var WriteJSONP = func(ctx *Context, v interface{}, options *JSONP) (err error) {
- if callback := options.Callback; callback != "" {
- _, err = ctx.Write(stringToBytes(callback + "("))
- if err != nil {
- return err
- }
- defer func() {
- if err == nil {
- ctx.Write(finishCallbackB)
- }
- }()
- }
- err = WriteJSON(ctx, v, &JSON{
- Indent: options.Indent,
- OmitErrorHandler: options.OmitErrorHandler,
- })
- return err
- }
- // DefaultJSONPOptions is the optional settings that are being used
- // inside `ctx.JSONP`.
- var DefaultJSONPOptions = JSONP{}
- // JSONP marshals the given "v" value to JSON and sends the response to the client.
- //
- // It reports any JSON parser or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) JSONP(v interface{}, opts ...JSONP) (err error) {
- var options *JSONP
- if len(opts) > 0 {
- options = &opts[0]
- } else {
- options = &DefaultJSONPOptions
- }
- ctx.ContentType(ContentJavascriptHeaderValue)
- if err = WriteJSONP(ctx, v, options); err != nil {
- if !options.OmitErrorHandler {
- ctx.handleContextError(err)
- }
- }
- return
- }
- type xmlMapEntry struct {
- XMLName xml.Name
- Value interface{} `xml:",chardata"`
- }
- // XMLMap wraps a map[string]interface{} to compatible xml marshaler,
- // in order to be able to render maps as XML on the `Context.XML` method.
- //
- // Example: `Context.XML(XMLMap("Root", map[string]interface{}{...})`.
- func XMLMap(elementName string, v Map) xml.Marshaler {
- return xmlMap{
- entries: v,
- elementName: elementName,
- }
- }
- type xmlMap struct {
- entries Map
- elementName string
- }
- // MarshalXML marshals a map to XML.
- func (m xmlMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
- if len(m.entries) == 0 {
- return nil
- }
- start.Name = xml.Name{Local: m.elementName}
- err := e.EncodeToken(start)
- if err != nil {
- return err
- }
- for k, v := range m.entries {
- err = e.Encode(xmlMapEntry{XMLName: xml.Name{Local: k}, Value: v})
- if err != nil {
- return err
- }
- }
- return e.EncodeToken(start.End())
- }
- // WriteXML marshals the given interface object and writes the XML response to the writer.
- var WriteXML = func(ctx *Context, v interface{}, options *XML) error {
- if prefix := options.Prefix; prefix != "" {
- _, err := ctx.Write(stringToBytes(prefix))
- if err != nil {
- return err
- }
- }
- encoder := xml.NewEncoder(ctx.writer)
- encoder.Indent("", options.Indent)
- if err := encoder.Encode(v); err != nil {
- return err
- }
- return encoder.Flush()
- }
- // DefaultXMLOptions is the optional settings that are being used
- // from `ctx.XML`.
- var DefaultXMLOptions = XML{}
- // XML marshals the given interface object and writes the XML response to the client.
- // To render maps as XML see the `XMLMap` package-level function.
- //
- // It reports any XML parser or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) XML(v interface{}, opts ...XML) (err error) {
- var options *XML
- if len(opts) > 0 {
- options = &opts[0]
- } else {
- options = &DefaultXMLOptions
- }
- ctx.ContentType(ContentXMLHeaderValue)
- if err = WriteXML(ctx, v, options); err != nil {
- if !options.OmitErrorHandler {
- ctx.handleContextError(err)
- }
- }
- return
- }
- // Problem writes a JSON or XML problem response.
- // Order of Problem fields are not always rendered the same.
- //
- // Behaves exactly like the `Context.JSON` method
- // but with default ProblemOptions.JSON indent of " " and
- // a response content type of "application/problem+json" instead.
- //
- // Use the options.RenderXML and XML fields to change this behavior and
- // send a response of content type "application/problem+xml" instead.
- //
- // Read more at: https://github.com/kataras/iris/blob/main/_examples/routing/http-errors.
- func (ctx *Context) Problem(v interface{}, opts ...ProblemOptions) error {
- options := DefaultProblemOptions
- if len(opts) > 0 {
- options = opts[0]
- // Currently apply only if custom options passsed, otherwise,
- // with the current settings, it's not required.
- // This may change in the future though.
- options.Apply(ctx)
- }
- if p, ok := v.(Problem); ok {
- // if !p.Validate() {
- // ctx.StatusCode(http.StatusInternalServerError)
- // return ErrNotValidProblem
- // }
- p.updateURIsToAbs(ctx)
- code, _ := p.getStatus()
- if code == 0 { // get the current status code and set it to the problem.
- code = ctx.GetStatusCode()
- ctx.StatusCode(code)
- } else {
- // send the problem's status code
- ctx.StatusCode(code)
- }
- if options.RenderXML {
- ctx.contentTypeOnce(ContentXMLProblemHeaderValue, "")
- // Problem is an xml Marshaler already, don't use `XMLMap`.
- return ctx.XML(v, options.XML)
- }
- }
- ctx.contentTypeOnce(ContentJSONProblemHeaderValue, "")
- return ctx.writeJSON(v, &options.JSON)
- }
- var sanitizer = bluemonday.UGCPolicy()
- // WriteMarkdown parses the markdown to html and writes these contents to the writer.
- var WriteMarkdown = func(ctx *Context, markdownB []byte, options *Markdown) error {
- out := markdown.NormalizeNewlines(markdownB)
- renderer := html.NewRenderer(options.RenderOptions)
- doc := markdown.Parse(out, nil)
- out = markdown.Render(doc, renderer)
- if options.Sanitize {
- out = sanitizer.SanitizeBytes(out)
- }
- _, err := ctx.Write(out)
- return err
- }
- // DefaultMarkdownOptions is the optional settings that are being used
- // from `WriteMarkdown` and `ctx.Markdown`.
- var DefaultMarkdownOptions = Markdown{}
- // Markdown parses the markdown to html and renders its result to the client.
- //
- // It reports any Markdown parser or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) Markdown(markdownB []byte, opts ...Markdown) (err error) {
- var options *Markdown
- if len(opts) > 0 {
- options = &opts[0]
- } else {
- options = &DefaultMarkdownOptions
- }
- ctx.ContentType(ContentHTMLHeaderValue)
- if err = WriteMarkdown(ctx, markdownB, options); err != nil {
- if !options.OmitErrorHandler {
- ctx.handleContextError(err)
- }
- }
- return
- }
- // WriteYAML sends YAML response to the client.
- var WriteYAML = func(ctx *Context, v interface{}, indentSpace int) error {
- encoder := yaml.NewEncoder(ctx.writer)
- encoder.SetIndent(indentSpace)
- if err := encoder.Encode(v); err != nil {
- return err
- }
- return encoder.Close()
- }
- // YAML marshals the given "v" value using the yaml marshaler and writes the result to the client.
- //
- // It reports any YAML parser or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) YAML(v interface{}) error {
- ctx.ContentType(ContentYAMLHeaderValue)
- err := WriteYAML(ctx, v, 0)
- if err != nil {
- ctx.handleContextError(err)
- return err
- }
- return nil
- }
- // TextYAML calls the Context.YAML method but with the text/yaml content type instead.
- func (ctx *Context) TextYAML(v interface{}) error {
- ctx.ContentType(ContentYAMLTextHeaderValue)
- err := WriteYAML(ctx, v, 4)
- if err != nil {
- ctx.handleContextError(err)
- return err
- }
- return nil
- }
- // Protobuf marshals the given "v" value of proto Message and writes its result to the client.
- //
- // It reports any protobuf parser or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) Protobuf(v proto.Message) (int, error) {
- out, err := proto.Marshal(v)
- if err != nil {
- ctx.handleContextError(err)
- return 0, err
- }
- ctx.ContentType(ContentProtobufHeaderValue)
- n, err := ctx.Write(out)
- if err != nil {
- ctx.handleContextError(err)
- }
- return n, err
- }
- // MsgPack marshals the given "v" value of msgpack format and writes its result to the client.
- //
- // It reports any message pack or write errors back to the caller.
- // Look the Application.SetContextErrorHandler to override the
- // default status code 500 with a custom error response.
- func (ctx *Context) MsgPack(v interface{}) (int, error) {
- out, err := msgpack.Marshal(v)
- if err != nil {
- ctx.handleContextError(err)
- }
- ctx.ContentType(ContentMsgPackHeaderValue)
- n, err := ctx.Write(out)
- if err != nil {
- ctx.handleContextError(err)
- }
- return n, err
- }
- // +-----------------------------------------------------------------------+
- // | Content Νegotiation |
- // | https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation | |
- // +-----------------------------------------------------------------------+
- // ErrContentNotSupported returns from the `Negotiate` method
- // when server responds with 406.
- var ErrContentNotSupported = errors.New("unsupported content")
- // ContentSelector is the interface which structs can implement
- // to manually choose a content based on the negotiated mime (content type).
- // It can be passed to the `Context.Negotiate` method.
- //
- // See the `N` struct too.
- type ContentSelector interface {
- SelectContent(mime string) interface{}
- }
- // ContentNegotiator is the interface which structs can implement
- // to override the `Context.Negotiate` default implementation and
- // manually respond to the client based on a manuall call of `Context.Negotiation().Build()`
- // to get the final negotiated mime and charset.
- // It can be passed to the `Context.Negotiate` method.
- type ContentNegotiator interface {
- // mime and charset can be retrieved by:
- // mime, charset := Context.Negotiation().Build()
- // Pass this method to `Context.Negotiate` method
- // to write custom content.
- // Overriding the existing behavior of Context.Negotiate for selecting values based on
- // content types, although it can accept any custom mime type with []byte.
- // Content type is already set.
- // Use it with caution, 99.9% you don't need this but it's here for extreme cases.
- Negotiate(ctx *Context) (int, error)
- }
- // N is a struct which can be passed on the `Context.Negotiate` method.
- // It contains fields which should be filled based on the `Context.Negotiation()`
- // server side values. If no matched mime then its "Other" field will be sent,
- // which should be a string or []byte.
- // It completes the `ContentSelector` interface.
- type N struct {
- Text, HTML string
- Markdown []byte
- Binary []byte
- JSON interface{}
- Problem Problem
- JSONP interface{}
- XML interface{}
- YAML interface{}
- Protobuf interface{}
- MsgPack interface{}
- Other []byte // custom content types.
- }
- var _ ContentSelector = N{}
- // SelectContent returns a content based on the matched negotiated "mime".
- func (n N) SelectContent(mime string) interface{} {
- switch mime {
- case ContentTextHeaderValue:
- return n.Text
- case ContentHTMLHeaderValue:
- return n.HTML
- case ContentMarkdownHeaderValue:
- return n.Markdown
- case ContentBinaryHeaderValue:
- return n.Binary
- case ContentJSONHeaderValue:
- return n.JSON
- case ContentJSONProblemHeaderValue:
- return n.Problem
- case ContentJavascriptHeaderValue:
- return n.JSONP
- case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
- return n.XML
- case ContentYAMLHeaderValue:
- return n.YAML
- case ContentProtobufHeaderValue:
- return n.Protobuf
- case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue:
- return n.MsgPack
- default:
- return n.Other
- }
- }
- const negotiationContextKey = "iris.negotiation_builder"
- // Negotiation creates once and returns the negotiation builder
- // to build server-side available prioritized content
- // for specific content type(s), charset(s) and encoding algorithm(s).
- //
- // See `Negotiate` method too.
- func (ctx *Context) Negotiation() *NegotiationBuilder {
- if n := ctx.values.Get(negotiationContextKey); n != nil {
- return n.(*NegotiationBuilder)
- }
- acceptBuilder := NegotiationAcceptBuilder{}
- acceptBuilder.accept = parseHeader(ctx.GetHeader("Accept"))
- acceptBuilder.charset = parseHeader(ctx.GetHeader("Accept-Charset"))
- n := &NegotiationBuilder{Accept: acceptBuilder}
- ctx.values.Set(negotiationContextKey, n)
- return n
- }
- func parseHeader(headerValue string) []string {
- in := strings.Split(headerValue, ",")
- out := make([]string, 0, len(in))
- for _, value := range in {
- // remove any spaces and quality values such as ;q=0.8.
- v := strings.TrimSpace(strings.Split(value, ";")[0])
- if v != "" {
- out = append(out, v)
- }
- }
- return out
- }
- // Negotiate used for serving different representations of a resource at the same URI.
- //
- // The "v" can be a single `N` struct value.
- // The "v" can be any value completes the `ContentSelector` interface.
- // The "v" can be any value completes the `ContentNegotiator` interface.
- // The "v" can be any value of struct(JSON, JSONP, XML, YAML, Protobuf, MsgPack) or
- // string(TEXT, HTML) or []byte(Markdown, Binary) or []byte with any matched mime type.
- //
- // If the "v" is nil, the `Context.Negotitation()` builder's
- // content will be used instead, otherwise "v" overrides builder's content
- // (server mime types are still retrieved by its registered, supported, mime list)
- //
- // Set mime type priorities by `Negotiation().JSON().XML().HTML()...`.
- // Set charset priorities by `Negotiation().Charset(...)`.
- // Set encoding algorithm priorities by `Negotiation().Encoding(...)`.
- // Modify the accepted by
- // `Negotiation().Accept./Override()/.XML().JSON().Charset(...).Encoding(...)...`.
- //
- // It returns `ErrContentNotSupported` when not matched mime type(s).
- //
- // Resources:
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
- //
- // Supports the above without quality values.
- //
- // Read more at: https://github.com/kataras/iris/tree/main/_examples/response-writer/content-negotiation
- func (ctx *Context) Negotiate(v interface{}) (int, error) {
- contentType, charset, encoding, content := ctx.Negotiation().Build()
- if v == nil {
- v = content
- }
- if contentType == "" {
- // If the server cannot serve any matching set,
- // it SHOULD send back a 406 (Not Acceptable) error code.
- ctx.StatusCode(http.StatusNotAcceptable)
- return -1, ErrContentNotSupported
- }
- if charset == "" {
- charset = ctx.app.ConfigurationReadOnly().GetCharset()
- }
- if encoding != "" {
- ctx.CompressWriter(true)
- }
- ctx.contentTypeOnce(contentType, charset)
- if n, ok := v.(ContentNegotiator); ok {
- return n.Negotiate(ctx)
- }
- if s, ok := v.(ContentSelector); ok {
- v = s.SelectContent(contentType)
- }
- // switch v := value.(type) {
- // case []byte:
- // if contentType == ContentMarkdownHeaderValue {
- // return ctx.Markdown(v)
- // }
- // return ctx.Write(v)
- // case string:
- // return ctx.WriteString(v)
- // default:
- // make it switch by content-type only, but we lose custom mime types capability that way:
- // ^ solved with []byte on default case and
- // ^ N.Other and
- // ^ ContentSelector and ContentNegotiator interfaces.
- switch contentType {
- case ContentTextHeaderValue, ContentHTMLHeaderValue:
- return ctx.WriteString(v.(string))
- case ContentMarkdownHeaderValue:
- err := ctx.Markdown(v.([]byte))
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentJSONHeaderValue:
- err := ctx.JSON(v)
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue:
- err := ctx.Problem(v)
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentJavascriptHeaderValue:
- err := ctx.JSONP(v)
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue:
- err := ctx.XML(v)
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentYAMLHeaderValue:
- err := ctx.YAML(v)
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentYAMLTextHeaderValue:
- err := ctx.TextYAML(v)
- if err != nil {
- return 0, err
- }
- return ctx.writer.Written(), nil
- case ContentProtobufHeaderValue:
- msg, ok := v.(proto.Message)
- if !ok {
- return -1, ErrContentNotSupported
- }
- return ctx.Protobuf(msg)
- case ContentMsgPackHeaderValue, ContentMsgPack2HeaderValue:
- return ctx.MsgPack(v)
- default:
- // maybe "Other" or v is []byte or string but not a built-in framework mime,
- // for custom content types,
- // panic if not correct usage.
- switch vv := v.(type) {
- case []byte:
- return ctx.Write(vv)
- case string:
- return ctx.WriteString(vv)
- default:
- ctx.StatusCode(http.StatusNotAcceptable)
- return -1, ErrContentNotSupported
- }
- }
- }
- // NegotiationBuilder returns from the `Context.Negotitation`
- // and can be used inside chain of handlers to build server-side
- // mime type(s), charset(s) and encoding algorithm(s)
- // that should match with the client's
- // Accept, Accept-Charset and Accept-Encoding headers (by-default).
- // To modify the client's accept use its "Accept" field
- // which it's the `NegotitationAcceptBuilder`.
- //
- // See the `Negotiate` method too.
- type NegotiationBuilder struct {
- Accept NegotiationAcceptBuilder
- mime []string // we need order.
- contents map[string]interface{} // map to the "mime" and content should be rendered if that mime requested.
- charset []string
- encoding []string
- }
- // MIME registers a mime type and optionally the value that should be rendered
- // through `Context.Negotiate` when this mime type is accepted by client.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) MIME(mime string, content interface{}) *NegotiationBuilder {
- mimes := parseHeader(mime) // if contains more than one sep by commas ",".
- if content == nil {
- n.mime = append(n.mime, mimes...)
- return n
- }
- if n.contents == nil {
- n.contents = make(map[string]interface{})
- }
- for _, m := range mimes {
- n.mime = append(n.mime, m)
- n.contents[m] = content
- }
- return n
- }
- // Text registers the "text/plain" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "text/plain" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Text(v ...string) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentTextHeaderValue, content)
- }
- // HTML registers the "text/html" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "text/html" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) HTML(v ...string) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentHTMLHeaderValue, content)
- }
- // Markdown registers the "text/markdown" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "text/markdown" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Markdown(v ...[]byte) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v
- }
- return n.MIME(ContentMarkdownHeaderValue, content)
- }
- // Binary registers the "application/octet-stream" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "application/octet-stream" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Binary(v ...[]byte) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentBinaryHeaderValue, content)
- }
- // JSON registers the "application/json" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "application/json" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) JSON(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentJSONHeaderValue, content)
- }
- // Problem registers the "application/problem+json" or "application/problem+xml" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "application/problem+json" or the "application/problem+xml" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Problem(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentJSONProblemHeaderValue+","+ContentXMLProblemHeaderValue, content)
- }
- // JSONP registers the "text/javascript" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "javascript/javascript" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) JSONP(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentJavascriptHeaderValue, content)
- }
- // XML registers the "text/xml" and "application/xml" content types and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts one of the "text/xml" or "application/xml" content types.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) XML(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentXMLHeaderValue+","+ContentXMLUnreadableHeaderValue, content)
- }
- // YAML registers the "application/x-yaml" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "application/x-yaml" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) YAML(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentYAMLHeaderValue, content)
- }
- // TextYAML registers the "text/yaml" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "application/x-yaml" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) TextYAML(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentYAMLTextHeaderValue, content)
- }
- // Protobuf registers the "application/x-protobuf" content type and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts the "application/x-protobuf" content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Protobuf(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentProtobufHeaderValue, content)
- }
- // MsgPack registers the "application/x-msgpack" and "application/msgpack" content types and, optionally,
- // a value that `Context.Negotiate` will render
- // when a client accepts one of the "application/x-msgpack" or "application/msgpack" content types.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) MsgPack(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME(ContentMsgPackHeaderValue+","+ContentMsgPack2HeaderValue, content)
- }
- // Any registers a wildcard that can match any client's accept content type.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Any(v ...interface{}) *NegotiationBuilder {
- var content interface{}
- if len(v) > 0 {
- content = v[0]
- }
- return n.MIME("*", content)
- }
- // Charset overrides the application's config's charset (which defaults to "utf-8")
- // that a client should match for
- // (through Accept-Charset header or custom through `NegotitationBuilder.Accept.Override().Charset(...)` call).
- // Do not set it if you don't know what you're doing.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Charset(charset ...string) *NegotiationBuilder {
- n.charset = append(n.charset, charset...)
- return n
- }
- // Encoding registers one or more encoding algorithms by name, i.e gzip, deflate, br, snappy, s2.
- // that a client should match for (through Accept-Encoding header).
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) Encoding(encoding ...string) *NegotiationBuilder {
- n.encoding = append(n.encoding, encoding...)
- return n
- }
- // EncodingGzip registers the "gzip" encoding algorithm
- // that a client should match for (through Accept-Encoding header or call of Accept.Encoding(enc)).
- //
- // It will make resources to served by "gzip" if Accept-Encoding contains the "gzip" as well.
- //
- // Returns itself for recursive calls.
- func (n *NegotiationBuilder) EncodingGzip() *NegotiationBuilder {
- return n.Encoding(GzipHeaderValue)
- }
- // Build calculates the client's and server's mime type(s), charset(s) and encoding
- // and returns the final content type, charset and encoding that server should render
- // to the client. It does not clear the fields, use the `Clear` method if neeeded.
- //
- // The returned "content" can be nil if the matched "contentType" does not provide any value,
- // in that case the `Context.Negotiate(v)` must be called with a non-nil value.
- func (n *NegotiationBuilder) Build() (contentType, charset, encoding string, content interface{}) {
- contentType = negotiationMatch(n.Accept.accept, n.mime)
- charset = negotiationMatch(n.Accept.charset, n.charset)
- encoding = negotiationMatch(n.Accept.encoding, n.encoding)
- if n.contents != nil {
- if data, ok := n.contents[contentType]; ok {
- content = data
- }
- }
- return
- }
- // Clear clears the prioritized mime type(s), charset(s) and any contents
- // relative to those mime type(s).
- // The "Accept" field is stay as it is, use its `Override` method
- // to clear out the client's accepted mime type(s) and charset(s).
- func (n *NegotiationBuilder) Clear() *NegotiationBuilder {
- n.mime = n.mime[0:0]
- n.contents = nil
- n.charset = n.charset[0:0]
- return n
- }
- // NegotiationAcceptBuilder builds the accepted mime types and charset
- //
- // and "Accept-Charset" headers respectfully.
- // The default values are set by the client side, server can append or override those.
- // The end result will be challenged with runtime preffered set of content types and charsets.
- //
- // See the `Negotiate` method too.
- type NegotiationAcceptBuilder struct {
- // initialized with "Accept" request header values.
- accept []string
- // initialized with "Accept-Charset" request header. and if was empty then the
- // application's default (which defaults to utf-8).
- charset []string
- // initialized with "Accept-Encoding" request header values.
- encoding []string
- // To support override in request life cycle.
- // We need slice when data is the same format
- // for one or more mime types,
- // i.e text/xml and obselete application/xml.
- lastAccept []string
- lastCharset []string
- lastEncoding []string
- }
- // Override clears the default values for accept and accept charset.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Override() *NegotiationAcceptBuilder {
- // when called first.
- n.accept = n.accept[0:0]
- n.charset = n.charset[0:0]
- n.encoding = n.encoding[0:0]
- // when called after.
- if len(n.lastAccept) > 0 {
- n.accept = append(n.accept, n.lastAccept...)
- n.lastAccept = n.lastAccept[0:0]
- }
- if len(n.lastCharset) > 0 {
- n.charset = append(n.charset, n.lastCharset...)
- n.lastCharset = n.lastCharset[0:0]
- }
- if len(n.lastEncoding) > 0 {
- n.encoding = append(n.encoding, n.lastEncoding...)
- n.lastEncoding = n.lastEncoding[0:0]
- }
- return n
- }
- // MIME adds accepted client's mime type(s).
- // Returns itself.
- func (n *NegotiationAcceptBuilder) MIME(mimeType ...string) *NegotiationAcceptBuilder {
- n.lastAccept = mimeType
- n.accept = append(n.accept, mimeType...)
- return n
- }
- // Text adds the "text/plain" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Text() *NegotiationAcceptBuilder {
- return n.MIME(ContentTextHeaderValue)
- }
- // HTML adds the "text/html" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) HTML() *NegotiationAcceptBuilder {
- return n.MIME(ContentHTMLHeaderValue)
- }
- // Markdown adds the "text/markdown" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Markdown() *NegotiationAcceptBuilder {
- return n.MIME(ContentMarkdownHeaderValue)
- }
- // Binary adds the "application/octet-stream" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Binary() *NegotiationAcceptBuilder {
- return n.MIME(ContentBinaryHeaderValue)
- }
- // JSON adds the "application/json" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) JSON() *NegotiationAcceptBuilder {
- return n.MIME(ContentJSONHeaderValue)
- }
- // Problem adds the "application/problem+json" and "application/problem-xml"
- // as accepted client content types.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Problem() *NegotiationAcceptBuilder {
- return n.MIME(ContentJSONProblemHeaderValue, ContentXMLProblemHeaderValue)
- }
- // JSONP adds the "text/javascript" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) JSONP() *NegotiationAcceptBuilder {
- return n.MIME(ContentJavascriptHeaderValue)
- }
- // XML adds the "text/xml" and "application/xml" as accepted client content types.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) XML() *NegotiationAcceptBuilder {
- return n.MIME(ContentXMLHeaderValue, ContentXMLUnreadableHeaderValue)
- }
- // YAML adds the "application/x-yaml" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) YAML() *NegotiationAcceptBuilder {
- return n.MIME(ContentYAMLHeaderValue)
- }
- // TextYAML adds the "text/yaml" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) TextYAML() *NegotiationAcceptBuilder {
- return n.MIME(ContentYAMLTextHeaderValue)
- }
- // Protobuf adds the "application/x-protobuf" as accepted client content type.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Protobuf() *NegotiationAcceptBuilder {
- return n.MIME(ContentYAMLHeaderValue)
- }
- // MsgPack adds the "application/msgpack" and "application/x-msgpack" as accepted client content types.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) MsgPack() *NegotiationAcceptBuilder {
- return n.MIME(ContentYAMLHeaderValue)
- }
- // Charset adds one or more client accepted charsets.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Charset(charset ...string) *NegotiationAcceptBuilder {
- n.lastCharset = charset
- n.charset = append(n.charset, charset...)
- return n
- }
- // Encoding adds one or more client accepted encoding algorithms.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) Encoding(encoding ...string) *NegotiationAcceptBuilder {
- n.lastEncoding = encoding
- n.encoding = append(n.encoding, encoding...)
- return n
- }
- // EncodingGzip adds the "gzip" as accepted encoding.
- // Returns itself.
- func (n *NegotiationAcceptBuilder) EncodingGzip() *NegotiationAcceptBuilder {
- return n.Encoding(GzipHeaderValue)
- }
- // +------------------------------------------------------------+
- // | Serve files |
- // +------------------------------------------------------------+
- // ServeContent replies to the request using the content in the
- // provided ReadSeeker. The main benefit of ServeContent over io.Copy
- // is that it handles Range requests properly, sets the MIME type, and
- // handles If-Match, If-Unmodified-Since, If-None-Match, If-Modified-Since,
- // and If-Range requests.
- //
- // If the response's Content-Type header is not set, ServeContent
- // first tries to deduce the type from name's file extension.
- //
- // The name is otherwise unused; in particular it can be empty and is
- // never sent in the response.
- //
- // If modtime is not the zero time or Unix epoch, ServeContent
- // includes it in a Last-Modified header in the response. If the
- // request includes an If-Modified-Since header, ServeContent uses
- // modtime to decide whether the content needs to be sent at all.
- //
- // The content's Seek method must work: ServeContent uses
- // a seek to the end of the content to determine its size.
- //
- // If the caller has set w's ETag header formatted per RFC 7232, section 2.3,
- // ServeContent uses it to handle requests using If-Match, If-None-Match, or If-Range.
- //
- // Note that *os.File implements the io.ReadSeeker interface.
- // Note that compression can be registered
- // through `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`.
- func (ctx *Context) ServeContent(content io.ReadSeeker, filename string, modtime time.Time) {
- ctx.ServeContentWithRate(content, filename, modtime, 0, 0)
- }
- // rateReadSeeker is a io.ReadSeeker that is rate limited by
- // the given token bucket. Each token in the bucket
- // represents one byte. See "golang.org/x/time/rate" package.
- type rateReadSeeker struct {
- io.ReadSeeker
- ctx context.Context
- limiter *rate.Limiter
- }
- func (rs *rateReadSeeker) Read(buf []byte) (int, error) {
- n, err := rs.ReadSeeker.Read(buf)
- if n <= 0 {
- return n, err
- }
- err = rs.limiter.WaitN(rs.ctx, n)
- return n, err
- }
- // ServeContentWithRate same as `ServeContent` but it can throttle the speed of reading
- // and though writing the "content" to the client.
- func (ctx *Context) ServeContentWithRate(content io.ReadSeeker, filename string, modtime time.Time, limit float64, burst int) {
- if limit > 0 {
- content = &rateReadSeeker{
- ReadSeeker: content,
- ctx: ctx.request.Context(),
- limiter: rate.NewLimiter(rate.Limit(limit), burst),
- }
- }
- http.ServeContent(ctx.writer, ctx.request, filename, modtime, content)
- }
- // ServeFile replies to the request with the contents of the named
- // file or directory.
- //
- // If the provided file or directory name is a relative path, it is
- // interpreted relative to the current directory and may ascend to
- // parent directories. If the provided name is constructed from user
- // input, it should be sanitized before calling `ServeFile`.
- //
- // Use it when you want to serve assets like css and javascript files.
- // If client should confirm and save the file use the `SendFile` instead.
- // Note that compression can be registered
- // through `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`.
- func (ctx *Context) ServeFile(filename string) error {
- return ctx.ServeFileWithRate(filename, 0, 0)
- }
- // ServeFileWithRate same as `ServeFile` but it can throttle the speed of reading
- // and though writing the file to the client.
- func (ctx *Context) ServeFileWithRate(filename string, limit float64, burst int) error {
- f, err := os.Open(filename)
- if err != nil {
- ctx.StatusCode(http.StatusNotFound)
- return err
- }
- defer f.Close()
- st, err := f.Stat()
- if err != nil {
- code := http.StatusInternalServerError
- if os.IsNotExist(err) {
- code = http.StatusNotFound
- }
- if os.IsPermission(err) {
- code = http.StatusForbidden
- }
- ctx.StatusCode(code)
- return err
- }
- if st.IsDir() {
- return ctx.ServeFile(path.Join(filename, "index.html"))
- }
- ctx.ServeContentWithRate(f, st.Name(), st.ModTime(), limit, burst)
- return nil
- }
- // SendFile sends a file as an attachment, that is downloaded and saved locally from client.
- // Note that compression can be registered
- // through `ctx.CompressWriter(true)` or `app.Use(iris.Compression)`.
- // Use `ServeFile` if a file should be served as a page asset instead.
- func (ctx *Context) SendFile(src string, destName string) error {
- return ctx.SendFileWithRate(src, destName, 0, 0)
- }
- // SendFileWithRate same as `SendFile` but it can throttle the speed of reading
- // and though writing the file to the client.
- func (ctx *Context) SendFileWithRate(src, destName string, limit float64, burst int) error {
- if destName == "" {
- destName = filepath.Base(src)
- }
- ctx.writer.Header().Set(ContentDispositionHeaderKey, MakeDisposition(destName))
- return ctx.ServeFileWithRate(src, limit, burst)
- }
- // MakeDisposition generates an HTTP Content-Disposition field-value.
- // Similar solution followed by: Spring(Java), Symfony(PHP) and Ruby on Rails frameworks too.
- //
- // Fixes CVE-2020-5398. Reported by motoyasu-saburi.
- func MakeDisposition(filename string) string {
- return `attachment; filename*=UTF-8''` + url.QueryEscape(filename)
- } /*
- // Found at: https://stackoverflow.com/questions/53069040/checking-a-string-contains-only-ascii-characters
- // A faster (better, more idiomatic) version, which avoids unnecessary rune conversions.
- func isASCII(s string) bool {
- for i := 0; i < len(s); i++ {
- if s[i] > unicode.MaxASCII {
- return false
- }
- }
- return true
- }
- func isRFC5987AttrChar(c rune) bool {
- return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
- c == '!' || c == '#' || c == '$' || c == '&' || c == '+' || c == '-' ||
- c == '.' || c == '^' || c == '_' || c == '`' || c == '|' || c == '~'
- }
- */
- // +------------------------------------------------------------+
- // | Cookies |
- // +------------------------------------------------------------+
- // Set of Cookie actions for `CookieOption`.
- const (
- OpCookieGet uint8 = iota
- OpCookieSet
- OpCookieDel
- )
- // CookieOption is the type of function that is accepted on
- // context's methods like `SetCookieKV`, `RemoveCookie` and `SetCookie`
- // as their (last) variadic input argument to amend the to-be-sent cookie.
- //
- // The "op" is the operation code, 0 is GET, 1 is SET and 2 is REMOVE.
- type CookieOption func(ctx *Context, c *http.Cookie, op uint8)
- // CookieIncluded reports whether the "cookie.Name" is in the list of "cookieNames".
- // Notes:
- // If "cookieNames" slice is empty then it returns true,
- // If "cookie.Name" is empty then it returns false.
- func CookieIncluded(cookie *http.Cookie, cookieNames []string) bool {
- if cookie.Name == "" {
- return false
- }
- if len(cookieNames) > 0 {
- for _, name := range cookieNames {
- if cookie.Name == name {
- return true
- }
- }
- return false
- }
- return true
- }
- // var cookieNameSanitizer = strings.NewReplacer("\n", "-", "\r", "-")
- //
- // func sanitizeCookieName(n string) string {
- // return cookieNameSanitizer.Replace(n)
- // }
- // CookieOverride is a CookieOption which overrides the cookie explicitly to the given "cookie".
- //
- // Usage:
- // ctx.RemoveCookie("the_cookie_name", iris.CookieOverride(&http.Cookie{Domain: "example.com"}))
- func CookieOverride(cookie *http.Cookie) CookieOption { // The "Cookie" word method name is reserved as it's used as an alias.
- return func(_ *Context, c *http.Cookie, op uint8) {
- if op == OpCookieGet {
- return
- }
- *cookie = *c
- }
- }
- // CookieDomain is a CookieOption which sets the cookie's Domain field.
- // If empty then the current domain is used.
- //
- // Usage:
- // ctx.RemoveCookie("the_cookie_name", iris.CookieDomain("example.com"))
- func CookieDomain(domain string) CookieOption {
- return func(_ *Context, c *http.Cookie, op uint8) {
- if op == OpCookieGet {
- return
- }
- c.Domain = domain
- }
- }
- // CookieAllowReclaim accepts the Context itself.
- // If set it will add the cookie to (on `CookieSet`, `CookieSetKV`, `CookieUpsert`)
- // or remove the cookie from (on `CookieRemove`) the Request object too.
- func CookieAllowReclaim(cookieNames ...string) CookieOption {
- return func(ctx *Context, c *http.Cookie, op uint8) {
- if op == OpCookieGet {
- return
- }
- if !CookieIncluded(c, cookieNames) {
- return
- }
- switch op {
- case OpCookieSet:
- // perform upsert on request cookies or is it too much and not worth the cost?
- ctx.Request().AddCookie(c)
- case OpCookieDel:
- cookies := ctx.Request().Cookies()
- ctx.Request().Header.Del("Cookie")
- for i, v := range cookies {
- if v.Name != c.Name {
- ctx.Request().AddCookie(cookies[i])
- }
- }
- }
- }
- }
- // CookieAllowSubdomains set to the Cookie Options
- // in order to allow subdomains to have access to the cookies.
- // It sets the cookie's Domain field (if was empty) and
- // it also sets the cookie's SameSite to lax mode too.
- func CookieAllowSubdomains(cookieNames ...string) CookieOption {
- return func(ctx *Context, c *http.Cookie, _ uint8) {
- if c.Domain != "" {
- return // already set.
- }
- if !CookieIncluded(c, cookieNames) {
- return
- }
- c.Domain = ctx.Domain()
- c.SameSite = http.SameSiteLaxMode // allow subdomain sharing.
- }
- }
- // CookieSameSite sets a same-site rule for cookies to set.
- // SameSite allows a server to define a cookie attribute making it impossible for
- // the browser to send this cookie along with cross-site requests. The main
- // goal is to mitigate the risk of cross-origin information leakage, and provide
- // some protection against cross-site request forgery attacks.
- //
- // See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
- func CookieSameSite(sameSite http.SameSite) CookieOption {
- return func(_ *Context, c *http.Cookie, op uint8) {
- if op == OpCookieSet {
- c.SameSite = sameSite
- }
- }
- }
- // CookieSecure sets the cookie's Secure option if the current request's
- // connection is using TLS. See `CookieHTTPOnly` too.
- func CookieSecure(ctx *Context, c *http.Cookie, op uint8) {
- if op == OpCookieSet {
- if ctx.Request().TLS != nil {
- c.Secure = true
- }
- }
- }
- // CookieHTTPOnly is a `CookieOption`.
- // Use it to set the cookie's HttpOnly field to false or true.
- // HttpOnly field defaults to true for `RemoveCookie` and `SetCookieKV`.
- // See `CookieSecure` too.
- func CookieHTTPOnly(httpOnly bool) CookieOption {
- return func(_ *Context, c *http.Cookie, op uint8) {
- if op == OpCookieSet {
- c.HttpOnly = httpOnly
- }
- }
- }
- // CookiePath is a `CookieOption`.
- // Use it to change the cookie's Path field.
- func CookiePath(path string) CookieOption {
- return func(_ *Context, c *http.Cookie, op uint8) {
- if op > OpCookieGet { // on set and remove.
- c.Path = path
- }
- }
- }
- // CookieCleanPath is a `CookieOption`.
- // Use it to clear the cookie's Path field, exactly the same as `CookiePath("")`.
- func CookieCleanPath(_ *Context, c *http.Cookie, op uint8) {
- if op > OpCookieGet {
- c.Path = ""
- }
- }
- // CookieExpires is a `CookieOption`.
- // Use it to change the cookie's Expires and MaxAge fields by passing the lifetime of the cookie.
- func CookieExpires(durFromNow time.Duration) CookieOption {
- return func(_ *Context, c *http.Cookie, op uint8) {
- if op == OpCookieSet {
- c.Expires = time.Now().Add(durFromNow)
- c.MaxAge = int(durFromNow.Seconds())
- }
- }
- }
- // SecureCookie should encodes and decodes
- // authenticated and optionally encrypted cookie values.
- // See `CookieEncoding` package-level function.
- type SecureCookie interface {
- // Encode should encode the cookie value.
- // Should accept the cookie's name as its first argument
- // and as second argument the cookie value ptr.
- // Should return an encoded value or an empty one if encode operation failed.
- // Should return an error if encode operation failed.
- //
- // Note: Errors are not printed, so you have to know what you're doing,
- // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
- // You either need to provide exactly that amount or you derive the key from what you type in.
- //
- // See `Decode` too.
- Encode(cookieName string, cookieValue interface{}) (string, error)
- // Decode should decode the cookie value.
- // Should accept the cookie's name as its first argument,
- // as second argument the encoded cookie value and as third argument the decoded value ptr.
- // Should return a decoded value or an empty one if decode operation failed.
- // Should return an error if decode operation failed.
- //
- // Note: Errors are not printed, so you have to know what you're doing,
- // and remember: if you use AES it only supports key sizes of 16, 24 or 32 bytes.
- // You either need to provide exactly that amount or you derive the key from what you type in.
- //
- // See `Encode` too.
- Decode(cookieName string, cookieValue string, cookieValuePtr interface{}) error
- }
- // CookieEncoding accepts a value which implements `Encode` and `Decode` methods.
- // It calls its `Encode` on `Context.SetCookie, UpsertCookie, and SetCookieKV` methods.
- // And on `Context.GetCookie` method it calls its `Decode`.
- // If "cookieNames" slice is not empty then only cookies
- // with that `Name` will be encoded on set and decoded on get, that way you can encrypt
- // specific cookie names (like the session id) and let the rest of the cookies "insecure".
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/securecookie
- func CookieEncoding(encoding SecureCookie, cookieNames ...string) CookieOption {
- if encoding == nil {
- return func(_ *Context, _ *http.Cookie, _ uint8) {}
- }
- return func(ctx *Context, c *http.Cookie, op uint8) {
- if op == OpCookieDel {
- return
- }
- if !CookieIncluded(c, cookieNames) {
- return
- }
- switch op {
- case OpCookieSet:
- // Should encode, it's a write to the client operation.
- newVal, err := encoding.Encode(c.Name, c.Value)
- if err != nil {
- ctx.Application().Logger().Error(err)
- c.Value = ""
- } else {
- c.Value = newVal
- }
- return
- case OpCookieGet:
- // Should decode, it's a read from the client operation.
- if err := encoding.Decode(c.Name, c.Value, &c.Value); err != nil {
- c.Value = ""
- }
- }
- }
- }
- const cookieOptionsContextKey = "iris.cookie.options"
- // AddCookieOptions adds cookie options for `SetCookie`,
- // `SetCookieKV, UpsertCookie` and `RemoveCookie` methods
- // for the current request. It can be called from a middleware before
- // cookies sent or received from the next Handler in the chain.
- //
- // Available builtin Cookie options are:
- // - CookieOverride
- // - CookieDomain
- // - CookieAllowReclaim
- // - CookieAllowSubdomains
- // - CookieSecure
- // - CookieHTTPOnly
- // - CookieSameSite
- // - CookiePath
- // - CookieCleanPath
- // - CookieExpires
- // - CookieEncoding
- //
- // Example at: https://github.com/kataras/iris/tree/main/_examples/cookies/securecookie
- func (ctx *Context) AddCookieOptions(options ...CookieOption) {
- if len(options) == 0 {
- return
- }
- if v := ctx.values.Get(cookieOptionsContextKey); v != nil {
- if opts, ok := v.([]CookieOption); ok {
- options = append(opts, options...)
- }
- }
- ctx.values.Set(cookieOptionsContextKey, options)
- }
- func (ctx *Context) applyCookieOptions(c *http.Cookie, op uint8, override []CookieOption) {
- if v := ctx.values.Get(cookieOptionsContextKey); v != nil {
- if options, ok := v.([]CookieOption); ok {
- for _, opt := range options {
- opt(ctx, c, op)
- }
- }
- }
- // The function's ones should be called last, so they can override
- // the stored ones (i.e by a prior middleware).
- for _, opt := range override {
- opt(ctx, c, op)
- }
- }
- // ClearCookieOptions clears any previously registered cookie options.
- // See `AddCookieOptions` too.
- func (ctx *Context) ClearCookieOptions() {
- ctx.values.Remove(cookieOptionsContextKey)
- }
- // SetCookie adds a cookie.
- // Use of the "options" is not required, they can be used to amend the "cookie".
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/basic
- func (ctx *Context) SetCookie(cookie *http.Cookie, options ...CookieOption) {
- ctx.applyCookieOptions(cookie, OpCookieSet, options)
- http.SetCookie(ctx.writer, cookie)
- }
- const setCookieHeaderKey = "Set-Cookie"
- // UpsertCookie adds a cookie to the response like `SetCookie` does
- // but it will also perform a replacement of the cookie
- // if already set by a previous `SetCookie` call.
- // It reports whether the cookie is new (true) or an existing one was updated (false).
- func (ctx *Context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) bool {
- ctx.applyCookieOptions(cookie, OpCookieSet, options)
- header := ctx.ResponseWriter().Header()
- if cookies := header[setCookieHeaderKey]; len(cookies) > 0 {
- s := cookie.Name + "=" // name=?value
- existingUpdated := false
- for i, c := range cookies {
- if strings.HasPrefix(c, s) {
- if existingUpdated { // fixes #1877
- // remove any duplicated.
- cookies[i] = ""
- header[setCookieHeaderKey] = cookies
- continue
- }
- // We need to update the Set-Cookie (to update the expiration or any other cookie's properties).
- // Probably the cookie is set and then updated in the first session creation
- // (e.g. UpdateExpiration, see https://github.com/kataras/iris/issues/1485).
- cookies[i] = cookie.String()
- header[setCookieHeaderKey] = cookies
- existingUpdated = true
- }
- }
- if existingUpdated {
- return false // existing one updated.
- }
- }
- header.Add(setCookieHeaderKey, cookie.String())
- return true
- }
- // SetCookieKVExpiration is 365 days by-default
- // you can change it or simple, use the SetCookie for more control.
- //
- // See `CookieExpires` and `AddCookieOptions` for more.
- var SetCookieKVExpiration = 8760 * time.Hour
- // SetCookieKV adds a cookie, requires the name(string) and the value(string).
- //
- // By default it expires after 365 days and it is added to the root URL path,
- // use the `CookieExpires` and `CookiePath` to modify them.
- // Alternatively: ctx.SetCookie(&http.Cookie{...}) or ctx.AddCookieOptions(...)
- //
- // If you want to set custom the path:
- // ctx.SetCookieKV(name, value, iris.CookiePath("/custom/path/cookie/will/be/stored"))
- //
- // If you want to be visible only to current request path:
- // (note that client should be responsible for that if server sent an empty cookie's path, all browsers are compatible)
- // ctx.SetCookieKV(name, value, iris.CookieCleanPath/iris.CookiePath(""))
- // More:
- //
- // iris.CookieExpires(time.Duration)
- // iris.CookieHTTPOnly(false)
- //
- // Examples: https://github.com/kataras/iris/tree/main/_examples/cookies/basic
- func (ctx *Context) SetCookieKV(name, value string, options ...CookieOption) {
- c := &http.Cookie{}
- c.Path = "/"
- c.Name = name
- c.Value = url.QueryEscape(value)
- c.HttpOnly = true
- // MaxAge=0 means no 'Max-Age' attribute specified.
- // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
- // MaxAge>0 means Max-Age attribute present and given in seconds
- c.Expires = time.Now().Add(SetCookieKVExpiration)
- c.MaxAge = int(time.Until(c.Expires).Seconds())
- ctx.SetCookie(c, options...)
- }
- // GetCookie returns cookie's value by its name
- // returns empty string if nothing was found.
- //
- // If you want more than the value then:
- // cookie, err := ctx.GetRequestCookie("name")
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/basic
- func (ctx *Context) GetCookie(name string, options ...CookieOption) string {
- c, err := ctx.GetRequestCookie(name, options...)
- if err != nil {
- return ""
- }
- value, _ := url.QueryUnescape(c.Value)
- return value
- }
- // GetRequestCookie returns the request cookie including any context's cookie options (stored or given by this method).
- func (ctx *Context) GetRequestCookie(name string, options ...CookieOption) (*http.Cookie, error) {
- c, err := ctx.request.Cookie(name)
- if err != nil {
- return nil, err
- }
- ctx.applyCookieOptions(c, OpCookieGet, options)
- return c, nil
- }
- var (
- // CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
- CookieExpireDelete = memstore.ExpireDelete
- // CookieExpireUnlimited indicates that does expires after 24 years.
- CookieExpireUnlimited = time.Now().AddDate(24, 10, 10)
- )
- // RemoveCookie deletes a cookie by its name and path = "/".
- // Tip: change the cookie's path to the current one by: RemoveCookie("the_cookie_name", iris.CookieCleanPath)
- //
- // If you intend to remove a cookie with a specific domain and value, please ensure to pass these values explicitly:
- //
- // ctx.RemoveCookie("the_cookie_name", iris.CookieDomain("example.com"), iris.CookiePath("/"))
- //
- // OR use a Cookie value instead:
- //
- // ctx.RemoveCookie("the_cookie_name", iris.CookieOverride(&http.Cookie{Domain: "example.com", Path: "/"}))
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/cookies/basic
- func (ctx *Context) RemoveCookie(name string, options ...CookieOption) {
- c := &http.Cookie{Path: "/"}
- // Send the cookie back to the client
- ctx.applyCookieOptions(c, OpCookieDel, options)
- c.Name = name
- c.Value = ""
- c.HttpOnly = true
- // Set the cookie expiration date to a past time
- c.Expires = CookieExpireDelete
- c.MaxAge = -1 // RFC says 1 second, but let's do it -1 to make sure is working.
- http.SetCookie(ctx.writer, c)
- }
- // VisitAllCookies takes a visitor function which is called
- // on each (request's) cookies' name and value.
- func (ctx *Context) VisitAllCookies(visitor func(name string, value string)) {
- for _, cookie := range ctx.request.Cookies() {
- visitor(cookie.Name, cookie.Value)
- }
- }
- var maxAgeExp = regexp.MustCompile(`maxage=(\d+)`)
- // MaxAge returns the "cache-control" request header's value
- // seconds as int64
- // if header not found or parse failed then it returns -1.
- func (ctx *Context) MaxAge() int64 {
- header := ctx.GetHeader(CacheControlHeaderKey)
- if header == "" {
- return -1
- }
- m := maxAgeExp.FindStringSubmatch(header)
- if len(m) == 2 {
- if v, err := strconv.Atoi(m[1]); err == nil {
- return int64(v)
- }
- }
- return -1
- }
- // +------------------------------------------------------------+
- // | Advanced: Response Recorder |
- // +------------------------------------------------------------+
- // Record transforms the context's basic and direct responseWriter to a *ResponseRecorder
- // which can be used to reset the body, reset headers, get the body,
- // get & set the status code at any time and more.
- func (ctx *Context) Record() {
- switch w := ctx.writer.(type) {
- case *ResponseRecorder:
- default:
- recorder := AcquireResponseRecorder()
- recorder.BeginRecord(w)
- ctx.ResetResponseWriter(recorder)
- }
- }
- // Recorder returns the context's ResponseRecorder
- // if not recording then it starts recording and returns the new context's ResponseRecorder
- func (ctx *Context) Recorder() *ResponseRecorder {
- ctx.Record()
- return ctx.writer.(*ResponseRecorder)
- }
- // IsRecording returns the response recorder and a true value
- // when the response writer is recording the status code, body, headers and so on,
- // else returns nil and false.
- func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
- // NOTE:
- // two return values in order to minimize the if statement:
- // if (Recording) then writer = Recorder()
- // instead we do: recorder,ok = Recording()
- rr, ok := ctx.writer.(*ResponseRecorder)
- return rr, ok
- }
- // Exec calls the framewrok's ServeHTTPC
- // based on this context but with a changed method and path
- // like it was requested by the user, but it is not.
- //
- // Offline means that the route is registered to the iris and have all features that a normal route has
- // BUT it isn't available by browsing, its handlers executed only when other handler's context call them
- // it can validate paths, has sessions, path parameters and all.
- //
- // You can find the Route by app.GetRoute("theRouteName")
- // you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName")
- // that will set a name to the route and returns its RouteInfo instance for further usage.
- //
- // It doesn't changes the global state, if a route was "offline" it remains offline.
- //
- // app.None(...) and app.GetRoutes().Offline(route)/.Online(route, method)
- //
- // Example: https://github.com/kataras/iris/tree/main/_examples/routing/route-state
- //
- // User can get the response by simple using rec := ctx.Recorder(); rec.Body()/rec.StatusCode()/rec.Header().
- //
- // context's Values and the Session are kept in order to be able to communicate via the result route.
- //
- // It's for extreme use cases, 99% of the times will never be useful for you.
- func (ctx *Context) Exec(method string, path string) {
- if path == "" {
- return
- }
- if method == "" {
- method = "GET"
- }
- // backup the handlers
- backupHandlers := ctx.handlers[0:]
- backupPos := ctx.currentHandlerIndex
- req := ctx.request
- // backup the request path information
- backupPath := req.URL.Path
- backupMethod := req.Method
- // don't backupValues := ctx.values.ReadOnly()
- // set the request to be align with the 'againstRequestPath'
- req.RequestURI = path
- req.URL.Path = path
- req.Method = method
- // [values stays]
- // reset handlers
- ctx.handlers = ctx.handlers[0:0]
- ctx.currentHandlerIndex = 0
- // execute the route from the (internal) context router
- // this way we keep the sessions and the values
- ctx.app.ServeHTTPC(ctx)
- // set the request back to its previous state
- req.RequestURI = backupPath
- req.URL.Path = backupPath
- req.Method = backupMethod
- // set back the old handlers and the last known index
- ctx.handlers = backupHandlers
- ctx.currentHandlerIndex = backupPos
- }
- // RouteExists reports whether a particular route exists
- // It will search from the current subdomain of context's host, if not inside the root domain.
- func (ctx *Context) RouteExists(method, path string) bool {
- return ctx.app.RouteExists(ctx, method, path)
- }
- const (
- reflectValueContextKey = "iris.context.reflect_value"
- // ControllerContextKey returns the context key from which
- // the `Context.Controller` method returns the store's value.
- ControllerContextKey = "iris.controller.reflect_value"
- )
- // ReflectValue caches and returns a []reflect.Value{reflect.ValueOf(ctx)}.
- // It's just a helper to maintain variable inside the context itself.
- func (ctx *Context) ReflectValue() []reflect.Value {
- if v := ctx.values.Get(reflectValueContextKey); v != nil {
- return v.([]reflect.Value)
- }
- v := []reflect.Value{reflect.ValueOf(ctx)}
- ctx.values.Set(reflectValueContextKey, v)
- return v
- }
- var emptyValue reflect.Value
- // Controller returns a reflect Value of the custom Controller from which this handler executed.
- // It will return a Kind() == reflect.Invalid if the handler was not executed from within a controller.
- func (ctx *Context) Controller() reflect.Value {
- if v := ctx.values.Get(ControllerContextKey); v != nil {
- return v.(reflect.Value)
- }
- return emptyValue
- }
- // DependenciesContextKey is the context key for the context's value
- // to keep the serve-time static dependencies raw values.
- const DependenciesContextKey = "iris.dependencies"
- // DependenciesMap is the type which context serve-time
- // struct dependencies are stored with.
- type DependenciesMap map[reflect.Type]reflect.Value
- // RegisterDependency registers a struct or slice
- // or pointer to struct dependency at request-time
- // for the next handler in the chain. One value per type.
- // Note that it's highly recommended to register
- // your dependencies before server ran
- // through Party.ConfigureContainer or mvc.Application.Register
- // in sake of minimum performance cost.
- //
- // See `UnregisterDependency` too.
- func (ctx *Context) RegisterDependency(v interface{}) {
- if v == nil {
- return
- }
- val, ok := v.(reflect.Value)
- if !ok {
- val = reflect.ValueOf(v)
- }
- cv := ctx.values.Get(DependenciesContextKey)
- if cv != nil {
- m, ok := cv.(DependenciesMap)
- if !ok {
- return
- }
- m[val.Type()] = val
- return
- }
- ctx.values.Set(DependenciesContextKey, DependenciesMap{
- val.Type(): val,
- })
- }
- // UnregisterDependency removes a dependency based on its type.
- // Reports whether a dependency with that type was found and removed successfully.
- func (ctx *Context) UnregisterDependency(typ reflect.Type) bool {
- cv := ctx.values.Get(DependenciesContextKey)
- if cv != nil {
- m, ok := cv.(DependenciesMap)
- if ok {
- delete(m, typ)
- return true
- }
- }
- return false
- }
- // Application returns the iris app instance which belongs to this context.
- // Worth to notice that this function returns an interface
- // of the Application, which contains methods that are safe
- // to be executed at serve-time. The full app's fields
- // and methods are not available here for the developer's safety.
- func (ctx *Context) Application() Application {
- return ctx.app
- }
- // IsDebug reports whether the application runs with debug log level.
- // It is a shortcut of Application.IsDebug().
- func (ctx *Context) IsDebug() bool {
- return ctx.app.IsDebug()
- }
- // SetErr is just a helper that sets an error value
- // as a context value, it does nothing more.
- // Also, by-default this error's value is written to the client
- // on failures when no registered error handler is available (see `Party.On(Any)ErrorCode`).
- // See `GetErr` to retrieve it back.
- //
- // To remove an error simply pass nil.
- //
- // Note that, if you want to stop the chain
- // with an error see the `StopWithError/StopWithPlainError` instead.
- func (ctx *Context) SetErr(err error) {
- if err == nil {
- ctx.values.Remove(errorContextKey)
- return
- }
- ctx.values.Set(errorContextKey, err)
- }
- // GetErr is a helper which retrieves
- // the error value stored by `SetErr`.
- //
- // Note that, if an error was stored by `SetErrPrivate`
- // then it returns the underline/original error instead
- // of the internal error wrapper.
- func (ctx *Context) GetErr() error {
- _, err := ctx.GetErrPublic()
- return err
- }
- // ErrPrivate if provided then the error saved in context
- // should NOT be visible to the client no matter what.
- type ErrPrivate interface {
- error
- IrisPrivateError()
- }
- // An internal wrapper for the `SetErrPrivate` method.
- type privateError struct{ error }
- func (e privateError) IrisPrivateError() {}
- // PrivateError accepts an error and returns a wrapped private one.
- func PrivateError(err error) ErrPrivate {
- if err == nil {
- return nil
- }
- errPrivate, ok := err.(ErrPrivate)
- if !ok {
- errPrivate = privateError{err}
- }
- return errPrivate
- }
- const errorContextKey = "iris.context.error"
- // SetErrPrivate sets an error that it's only accessible through `GetErr`
- // and it should never be sent to the client.
- //
- // Same as ctx.SetErr with an error that completes the `ErrPrivate` interface.
- // See `GetErrPublic` too.
- func (ctx *Context) SetErrPrivate(err error) {
- ctx.SetErr(PrivateError(err))
- }
- // GetErrPublic reports whether the stored error
- // can be displayed to the client without risking
- // to expose security server implementation to the client.
- //
- // If the error is not nil, it is always the original one.
- func (ctx *Context) GetErrPublic() (bool, error) {
- if v := ctx.values.Get(errorContextKey); v != nil {
- switch err := v.(type) {
- case privateError:
- // If it's an error set by SetErrPrivate then unwrap it.
- return false, err.error
- case ErrPrivate:
- return false, err
- case error:
- return true, err
- }
- }
- return false, nil
- }
- // ErrPanicRecovery may be returned from `Context` actions of a `Handler`
- // which recovers from a manual panic.
- type ErrPanicRecovery struct {
- ErrPrivate
- Cause interface{}
- Callers []string // file:line callers.
- Stack []byte // the full debug stack.
- RegisteredHandlers []string // file:line of all registered handlers.
- CurrentHandler string // the handler panic came from.
- }
- // Error implements the Go standard error type.
- func (e *ErrPanicRecovery) Error() string {
- if e.Cause != nil {
- if err, ok := e.Cause.(error); ok {
- return err.Error()
- }
- }
- return fmt.Sprintf("%v\n%s", e.Cause, strings.Join(e.Callers, "\n"))
- }
- // Is completes the internal errors.Is interface.
- func (e *ErrPanicRecovery) Is(err error) bool {
- _, ok := IsErrPanicRecovery(err)
- return ok
- }
- // IsErrPanicRecovery reports whether the given "err" is a type of ErrPanicRecovery.
- func IsErrPanicRecovery(err error) (*ErrPanicRecovery, bool) {
- if err == nil {
- return nil, false
- }
- v, ok := err.(*ErrPanicRecovery)
- return v, ok
- }
- // IsRecovered reports whether this handler has been recovered
- // by the Iris recover middleware.
- func (ctx *Context) IsRecovered() (*ErrPanicRecovery, bool) {
- if ctx.GetStatusCode() == http.StatusInternalServerError {
- // Panic error from recovery middleware is private.
- return IsErrPanicRecovery(ctx.GetErr())
- }
- return nil, false
- }
- const (
- funcsContextPrefixKey = "iris.funcs."
- funcLogoutContextKey = "auth.logout_func"
- )
- // SetFunc registers a custom function to this Request.
- // It's a helper to pass dynamic functions across handlers of the same chain.
- // For a more complete solution please use Dependency Injection instead.
- // This is just an easy to way to pass a function to the
- // next handler like ctx.Values().Set/Get does.
- // Sometimes is faster and easier to pass the object as a request value
- // and cast it when you want to use one of its methods instead of using
- // these `SetFunc` and `CallFunc` methods.
- // This implementation is suitable for functions that may change inside the
- // handler chain and the object holding the method is not predictable.
- //
- // The "name" argument is the custom name of the function,
- // you will use its name to call it later on, e.g. "auth.myfunc".
- //
- // The second, "fn" argument is the raw function/method you want
- // to pass through the next handler(s) of the chain.
- //
- // The last variadic input argument is optionally, if set
- // then its arguments are passing into the function's input arguments,
- // they should be always be the first ones to be accepted by the "fn" inputs,
- // e.g. an object, a receiver or a static service.
- //
- // See its `CallFunc` to call the "fn" on the next handler.
- //
- // Example at:
- // https://github.com/kataras/iris/tree/main/_examples/routing/writing-a-middleware/share-funcs
- func (ctx *Context) SetFunc(name string, fn interface{}, persistenceArgs ...interface{}) {
- f := newFunc(name, fn, persistenceArgs...)
- ctx.values.Set(funcsContextPrefixKey+name, f)
- }
- // GetFunc returns the context function declaration which holds
- // some information about the function registered under the given "name" by
- // the `SetFunc` method.
- func (ctx *Context) GetFunc(name string) (*Func, bool) {
- fn := ctx.values.Get(funcsContextPrefixKey + name)
- if fn == nil {
- return nil, false
- }
- return fn.(*Func), true
- }
- // CallFunc calls the function registered by the `SetFunc`.
- // The input arguments MUST match the expected ones.
- //
- // If the registered function was just a handler
- // or a handler which returns an error
- // or a simple function
- // or a simple function which returns an error
- // then this operation will perform without any serious cost,
- // otherwise reflection will be used instead, which may slow down the overall performance.
- //
- // Retruns ErrNotFound if the function was not registered.
- //
- // For a more complete solution without limiations navigate through
- // the Iris Dependency Injection feature instead.
- func (ctx *Context) CallFunc(name string, args ...interface{}) ([]reflect.Value, error) {
- fn, ok := ctx.GetFunc(name)
- if !ok || fn == nil {
- return nil, ErrNotFound
- }
- return fn.call(ctx, args...)
- }
- // SetLogoutFunc registers a custom logout function that will be
- // available to use inside the next handler(s). The function
- // may be registered multiple times but the last one is the valid.
- // So a logout function may start with basic authentication
- // and other middleware in the chain may change it to a custom sessions logout one.
- // This method uses the `SetFunc` method under the hoods.
- //
- // See `Logout` method too.
- func (ctx *Context) SetLogoutFunc(fn interface{}, persistenceArgs ...interface{}) {
- ctx.SetFunc(funcLogoutContextKey, fn, persistenceArgs...)
- }
- // Logout calls the registered logout function.
- // Returns ErrNotFound if a logout function was not specified
- // by a prior call of `SetLogoutFunc`.
- func (ctx *Context) Logout(args ...interface{}) error {
- _, err := ctx.CallFunc(funcLogoutContextKey, args...)
- return err
- }
- const userContextKey = "iris.user"
- // SetUser sets a value as a User for this request.
- // It's used by auth middlewares as a common
- // method to provide user information to the
- // next handlers in the chain.
- //
- // The "i" input argument can be:
- // - A value which completes the User interface
- // - A map[string]interface{}.
- // - A value which does not complete the whole User interface
- // - A value which does not complete the User interface at all
- // (only its `User().GetRaw` method is available).
- //
- // Look the `User` method to retrieve it.
- func (ctx *Context) SetUser(i interface{}) error {
- if i == nil {
- ctx.values.Remove(userContextKey)
- return nil
- }
- u, ok := i.(User)
- if !ok {
- if m, ok := i.(Map); ok { // it's a map, convert it to a User.
- u = UserMap(m)
- } else {
- // It's a structure, wrap it and let
- // runtime decide the features.
- p := newUserPartial(i)
- if p == nil {
- return ErrNotSupported
- }
- u = p
- }
- }
- ctx.values.Set(userContextKey, u)
- return nil
- }
- // User returns the registered User of this request.
- // To get the original value (even if a value set by SetUser does not implement the User interface)
- // use its GetRaw method.
- // /
- // See `SetUser` too.
- func (ctx *Context) User() User {
- if v := ctx.values.Get(userContextKey); v != nil {
- if u, ok := v.(User); ok {
- return u
- }
- }
- return nil
- }
- // Ensure Iris Context implements the standard Context package, build-time.
- var _ context.Context = (*Context)(nil)
- // Deadline returns the time when work done on behalf of this context
- // should be canceled. Deadline returns ok==false when no deadline is
- // set. Successive calls to Deadline return the same results.
- //
- // Shortcut of Request().Context().Deadline().
- func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
- return ctx.request.Context().Deadline()
- }
- // Done returns a channel that's closed when work done on behalf of this
- // context should be canceled. Done may return nil if this context can
- // never be canceled. Successive calls to Done return the same value.
- // The close of the Done channel may happen asynchronously,
- // after the cancel function returns.
- //
- // WithCancel arranges for Done to be closed when cancel is called;
- // WithDeadline arranges for Done to be closed when the deadline
- // expires; WithTimeout arranges for Done to be closed when the timeout
- // elapses.
- //
- // Done is provided for use in select statements:
- //
- // // Stream generates values with DoSomething and sends them to out
- // // until DoSomething returns an error or ctx.Done is closed.
- // func Stream(ctx context.Context, out chan<- Value) error {
- // for {
- // v, err := DoSomething(ctx)
- // if err != nil {
- // return err
- // }
- // select {
- // case <-ctx.Done():
- // return ctx.Err()
- // case out <- v:
- // }
- // }
- // }
- //
- // See https://blog.golang.org/pipelines for more examples of how to use
- // a Done channel for cancellation.
- //
- // Shortcut of Request().Context().Done().
- func (ctx *Context) Done() <-chan struct{} {
- return ctx.request.Context().Done()
- }
- // If Done is not yet closed, Err returns nil.
- // If Done is closed, Err returns a non-nil error explaining why:
- // Canceled if the context was canceled
- // or DeadlineExceeded if the context's deadline passed.
- // After Err returns a non-nil error, successive calls to Err return the same error.
- //
- // Shortcut of Request().Context().Err().
- func (ctx *Context) Err() error {
- return ctx.request.Context().Err()
- }
- // Value returns the value associated with this context for key, or nil
- // if no value is associated with key. Successive calls to Value with
- // the same key returns the same result.
- //
- // Shortcut of Request().Context().Value(key interface{}) interface{}.
- func (ctx *Context) Value(key interface{}) interface{} {
- if keyStr, ok := key.(string); ok { // check if the key is a type of string, which can be retrieved by the mem store.
- if entry, exists := ctx.values.GetEntry(keyStr); exists {
- return entry.ValueRaw
- }
- }
- // otherwise return the chained value.
- return ctx.request.Context().Value(key)
- }
- const idContextKey = "iris.context.id"
- // SetID sets an ID, any value, to the Request Context.
- // If possible the "id" should implement a `String() string` method
- // so it can be rendered on `Context.String` method.
- //
- // See `GetID` and `middleware/requestid` too.
- func (ctx *Context) SetID(id interface{}) {
- ctx.values.Set(idContextKey, id)
- }
- // GetID returns the Request Context's ID.
- // It returns nil if not given by a prior `SetID` call.
- // See `middleware/requestid` too.
- func (ctx *Context) GetID() interface{} {
- return ctx.values.Get(idContextKey)
- }
- // String returns the string representation of this request.
- //
- // It returns the Context's ID given by a `SetID`call,
- // followed by the client's IP and the method:uri.
- func (ctx *Context) String() string {
- id := ctx.GetID()
- if id != nil {
- if stringer, ok := id.(fmt.Stringer); ok {
- id = stringer.String()
- }
- }
- return fmt.Sprintf("[%v] %s ▶ %s:%s", id, ctx.RemoteAddr(), ctx.Method(), ctx.Request().RequestURI)
- }
|