0!DktzFOAmnPwL(zq4v_b!0&?B1-S5UbOC0q}W{AfANi@?fY2UVN?4>p;g zf(DUUvi_lCt=k6bpvfYP;(a6V{6(WdN#vVQGsZ5K7u7-#v<2sl{`p*b)-%tRC7%Z& z`38V=nje~y_8h+mN`*6;=vaI~LfC2sYjeWR^T691fy61L%Y#`!Ju8llE|>)aCK0lt zJ2|yk5Q!6LYGqUAOZX3%bNK=`6d3%eyi$Br`){UJre*}-U&suKlWK{$F)^#2nKLu?N z?rR4qUUttKK&wzSa})+w9nIv#>M(j7Uvwuj-# LRv@m&60+h8~-(E{BccRzndV(jwbRI z{9AP)8Jd6f6VNMdj}H7lLASIb+-N{Qu8zmL6wuS6XQ+a+o&=}2TBuvRIQr(*CoR&l z1{pverfb}3g}aD3S?uM$fd7TE9q-hnl)jq!(iNI__0v!|dZym6w%SXnYUk(UXj%JT z!twxFT1#g7=u`ow4TOMT3;p!@MY(U}R9@(Fo*-}(Y}brF#3j(ojip;Eg!9z$01MMG zB=5pQL4i2Qi0vD~BtH1p{1Z>t^?w$4ne4)6c;f5@%q8A%r))U&Q$dL-*KkTSB}*TY zFWfWt&WA!8<_R_zecjSwW85HYehsNdA4&I%(;-7wYU6D$t%dS}Cf*zAd_K`*0mpXD z^Mk$_RU1mN9wk<$t)}j7aEhMmdQQHNzp=H{(a}t>=t$h7X`l&{tQ2BUWf0Zn4}KW~ zJst}@7!KLZcDCLXEFHR+<_zmL^$R|e@}X)D$-4@>osfFv?k-$V$@W(cjg2?(IzTzZ ztkhG$vu~&O8>^piD3`mdB z=&U(M!G{ z2J29L6c2K4`?j>U d!R1 zps=wu2DAwWt`N;L+Q#w8vX2!peo@%Xs3e7TyY2Hpz`3^QayZKCpZN&qg;(E+IjiEP ze!`(SmQF+3qp7C&6yzNaV{hHk0d&QbGp?v;N|kb70y&V-33x A 43t>!g{)tUe=Vma20N6jVCoE( aq3r1cZ6Rf)vOLTTt)p*4T!xB-4X7A)l+^PihoakD&4{J z6wp*kc~mhmCLo_y**%S_F$^ehy751wf;a5rlYxH@ZH5k|_;f9|Uz&@BRE<$Rv+R?0 zx-FX6m4)TiN~$wUuW`tx!@60RY{+^XBL@6`lisQ3u6#!3FgT{l9}fSYV@mhpj=UWp z$WIhDHk#8rXL%S cVp?$3M1 gjdclE9c{Q*`f6y zy+E>bu)9Um*qXZr*~xM4FZ8%`6J!5LK*8PkFKr7-MNQ<`MS67LxW_=a`iHOGH$PMv z&ZQEU7!xKKLiNQm5`oH@L`NrrrZu*XVn9a4>qiZ}YJ^Zb=b6(0|5Mc)%uch~c>?-2 zHd`umjQOg0-;mr1pV%v>?t#~705*5nc0V>)_H~!n8@98SW6eGEdD=~Yeb(7ir~dcr z#1u=e*(qbl)*mxJ@)KV2FL-xflwTSJaP!4=CdEBS*(d=C$s6xl+0<>l(EJ3CMGzqh z)D@G{*2M3aqi^E)uInnBZ$F&N6!8l@Z9KtBkPf{UnXvCyaNm|?>GiTNv(Q7cot2cC zIo@X(jyz*FO<(`DFIT(?X~OM7Z#h-5!hz-?(BhNRzam`aIFp>;v0J7{PQG~&nrDj{ z-JH}8IcX5$a(GL;f0lx2clhNW&ho=7vQD-mnU6zjiUrSRJ?b!_ox8B6FEKo0Ysy)N zKC% ^y+#%kxueuHnxrxz#m{v!O=$(t-d&N+Wjew?1`r`a?I$-BIC2lH rY- zeGDSfIb$Ds*o|EOMrP)F*~eZLkYxV_HSK#7I%c^R>B6gxSZ4~n76&6f#v2Kp907Q? z&DfNT7LJGWZ1ql!3+5k*&)HsY+WE@XcZhQ5{kUQW!$uN)hO1*;(^nE87DQoyu@Ipw znXR>-jkI>YV3ECtr5^JRvqI5NSnvB_9dhiVc6V2Agu3vek6@Mje)d3-8Qwu@bI1qH zKeuj|pH+W0YC=p`e#3v1d-qO0F4~QclVPAbXfGafZ)9?vtK8R00e jYDZxPuhfM;FVCdV4m9< L){ zDA;Fv!)2wv-jO%= %3_ zjg~1Sgl3|QZMKHAzQ&}!)0;#+@^|nP7UVE *R{SxXo2C!-h*QzZ# z@PK)*_eJQE@F@C*!s+qT3^>O}X&DnZ_j2EGuiw#VAWcYbVRgb}_0^YCOa!#5SkIM= zha6Xs-Q~kTEa&DZ6OW>T9LXfJH7_~th10Ty_$^I0<97)|Nh|4T42kjiA{WTxCnWu0 zOOrpr(2lLmR{M@&+A~LSYUubW><>y*L!Kmhe$B2 FL?|$;opS`_O5vU}z9s*EITw z%C*{U7!u@rvK4nP4K#B5UQ=fAGy3ve)pngdTP>R~CLnM>ta(m$n)=V#MyYdG-{g29 zauc<6KOizB9}g RQphcpC zCt2$vWpCTynBLkNe}eNbAd9ODT^So@Sbg_*9tjiAD3G$mrhh|L$L%(-7kQPs>bm@@ zXJ`F0ZD_t)Jac!_W5gpSlFYBQVH>o7O8-XCc{;y}VQNqxBW1=5!h?-rumnc5e-&J{ zftoKAAo1Xbgq%f_(4vq421?bwTV4e+)B4K!ekRLSO7cmLIUDs3gRi{*dnZzsB^?tz zh|ICm+mX`tsug1)Yah98e~tRhqmI7(Gg3-{JlJ|C$eHC8A}H$lli=jf>DfE7JSIjp z-yXsSeY*K4F~9!JL1D`cv~16u_+I4?$_SRQ=vaqE)0WhpV)?N!KT`z#IrP>Ix|kvM znPO5FdDG+R+11ARhpnTtM*}`&d;dT}FyZzdSDW?I2O}P)HW$Le?xA+|X30h9K6P8C z;Nl!Bf^}4eqOkCvz|!W|w;pBLSdA8~^97!s(b4xC=wV=NQgq~iS~{~nP3?7_9r_=f z;)8$N4&hg{o#|$rHOO@mqC~r oZ4;C%!Y))WU4KSg*dT0$8O^jTm6D9)10~LpFm^N`Rh@XRM)6E2?OCX z%U7N&yrQ=tKB`_Acep&9vj@-`ww=l K%&p>S`afFNF@Snts($eZO=N{h_;)RJO0F-cj UHw+>{5uKZk@{CHd;LJ`0$0U1Um#B zUPq1F<>E_;F&4U<(R6Z2Q0B9u0;0S^^ZjOSm6$$<8n5Zqg7Slu&{^KA z0G$EBM4q#{of@WWM1!fNVi(L1uwyfrE(`r*=JurLr`4ev6Xfp(Fw|e(@bvg{{lWb= z91BZFLdR5ewrPA)q?zwu2ou$@kj|VKi_fzJqGfZ>aO3oh?#Ho#d0XJ>HP+l zfNC)T8*zRLW~#HSQzxdL&1l9YMMjxb=wevZtQbr^4hNiiAttyxItI-1z~nq0p3RTe zWBqL*;Uik2@7}ywZVlP0=**@P&Yp~0NPszg=9rEXEU5qIwW*B%JpZC_=~NaIqXQaZ zJlt2lUM0Kn5`;Z+Dqynu+D^b3{pHQT6tj$U2v>}frf$|)W$}X@88AVn&`>$*-aF#t z0S2u2NCD)pnvmfJ<=2>m?8ZI6tRFl+LqfeXf7Q;w__7H{ijH%onF5=aH4fBxcD?&S zd=AKAFU}+zX9C*Fz?434ir$%rEV&%Nc0*;`I%oS#L91#`4-l2ahcchS2jjAqQR#jM z1zn-$5RVh_*
x_I!pA$1gSy zX9F-lMA8RFUnyZ>Q_U18OwJ1>rK_)Y#ar1S?>cZzL>oBGZX8ONv&kLI0`gyb=#n$v z!4^Z0@IN-)?W5VRwOXAZ@3L8evX -X7Jopk3)*u^VTv$NRGB |j&B{hU#cpK?^p?bYIe0M5-T5tB(lZ+uFffX=*sb-xs<&NvmGU@Wzz@7yOep!qq zL7OE>xWM~AVRWrza+%|}KvNV9cf%8H;7|3d+p~4n-be4BfhjFZpf@xF14&{{y @4f}e``LLKAo>H*N)g9-|te>)U}j;)c+F yA6^Sg126nZXO)G6@te0RWkH~Q&%&nSXB zIF 5mq)M9fQ$=H6Z`rMZB9`S~H%B5I}h85T5m0S*Ydng^To*{ 9+LThy1T#zvW2- zBk#@^-qyGdIl1{`ObzbR6ya?V0*%q%)rQwc4eH-$&DdYfdUth70(s!K#5{Qh7duY_ z?_g?1lsOtBE>G7zf!7!~g7I5;tTrMST$-_N{cwpe0rSxm@Bvu5goh3T54O~r5p3|% z2y&8T?84xy2IHL@p?`=GCRMuf7t7D1H6w0At3P1t8>~rk_o#x)SF*)c8?oT6&y_mx z`3QkTDh8&40o$WGE3KJ|e4>J;EF@DGKuQ6C!2SjJj~-a8Wguc*$wp9@d*4hwL?jn? z-JSOZS+Y`dCnIVBO@&9TujOKf q5GqUuLFUiE@TzciM>Pgzuxt)Gl=3QIpUA&23)tPo zt %p55&c0QXM=x2;QvaMib;iFeQn&kv xRgjPvdu~C_Hn4?D4qJRQ z=(%X7i)$l_t@K (IBYY7(ZO1J9YGt5*JeR@lEGLXmO%p*(Op&`nQ7?}h)LbkF`P*N6 zb0kQzuy+d2d)rSiU!3;I0&zB-+X4!#!3?%6`_)cxSJZf2a2hzGS$O`6$2=VA6&T^E zdG>g3XNYngjhD;6id)!qC0HSM1;n$qq=6dUuEqOFu9GE55pH*KB0E{$ zHmTrpgMmrMPvl6jzJ0J8o@zaLm%{|CaQp~(jYzB<<*Y%4E_=sd_vuG7y|+_NolHEI zmdu8MdYF4ITk(Dxz*h&SpKsU5fH7`(7Qp*@yrYMr%B%kLaMR4v%;;^}vd;=#6k<#> z2J6Rn9S3E?+4GQWD-y<@cww&}k90XY!Qg>E3MPc5T3}9G-)D~>@RACSqq%`Mdfr3q z@U2D;Le`|*WOYL1K`F4mR!l6g!>{sVbs}KAWcbP8(#yN2$Uf#W0k^Xm Sv}SXgzab!H|228Q7vu7 zl(7d_zdFEspM|!oen1kL`YuPpnFl`{%pg{Pjt>EnC+c0CXpCk0p?JES40KBMguPdB zx}4sXgCeAQE4sj9eh-N-@B1YnC47D;*9Dqj^}zr<`}4t_7&NM_J}Xhgy3hD)D(trg zS0102sv4`R&)iV!l=ime0u^31sh5{$q>IklZhsh|#v>R;Cy|BkKEq@DHXXjJ9e+-H zRK8>H_#O#f)?Xg~mg#Z=Gu-;e7dY_6r#%~B7dT9a-hzqiay_r39AT!>W(7YBSuO_} z-;?pPQ)CI{M1&8xbHeH4?Wa== bFoUbyv+^x4-m zB5dZIDb5!#%Lxqk&GCkE{7%EjaP>oh?cs}A{z}hfvN`6zNUAv`|3;i+ZpUSK+40YU zhq@>{9gA#NK46o7O&lq>gy-JN7r|%+Wq}j~g}@hkDt^_AKj9J_7T9SM_gq{{D>uPN zI2zw)uwfufPI-ksO|XYif(V9iRy%OVRPb!W+I9+Y(MU=7w&s=%x<4}5r7s0F89@%N zx@;&QbM>X!;4S#zewjaY{x5FjD^yL1O_v`qTlId^*plnM+2dSr>-%6xa;Cg&&U15y zfzwiza%%4e6pv{RY|RM%=#123S 5kAH7c+DM)b%17(U z^UQe4tP*`n=lM5UV-kS}t?I&@jww#DBOn!PVUT8RGybp$zDzEqh}HxWac0 zAZgY|Q(Vot(irGogL(#VLB OrY{9+o{rl?;1^E zgQ$tAo@3kM`)WUhy+i&J+!E^?37^nrZfX6n5zv5?2T}CjrADsiJ2dNg=5|Hs#y{YG zK}`MJ+qw!Hpj6_5PQ8$dN`Z$Qfb#O5f5DC1K)*V*y2z@q2mkdn{qxd8q(_3o{ZyEv zef*w{jB>B7Q=Y$u6ICtmrxNtJH7Uncjh}pq+y7<(`m&R=XUd5|S?9T=Fgx(6Ng8Me zvJN=9+TTtEcKt^vzmaV==cT|k&+Ts;1Y|9oh+`|JJiCj7Zxz> zM=&O$Hr@oyU6r_v4JgGUr~FYrEB~dEdVd@1(KHgN7pybz3QJt3s9ppsP2?>d(>BF4 zGT>J9<0lf+F3V>Dl-^Pi9uj?Wjt15)c}LWJyyg#SB6-g?&F!D29IQ-6{&?ZnFl+=t zo)rZjO-norSwy)Ii1fy|B}q2(6D3;wK|kjh!FnbdTx%K^A~7*B1H4K79%qeb4!v z)uri1Rx<>tGO9p2k8l?MX|!4Oac4ZeIG~i*^JH|Ph5WS^CjYL?%Q63Z|J$z7^@Ed} z4R{L`X_Be97|@)deW;p~XNt=HJ>{TRcO2(ioxHg(4Q_oDlO_k<@78gy9|Fl$!$$w! zzn>O-_s-L&FFaenn|$psA==dw?2aA3k9cIJ93<+QtV&;Gv%o8N>O-llJd}!y39 67EFCbq<`hZhx~I&1l4=i0=Ysv zWytTXwWsN@^j~G*xIg@t8E6i%v-2YQwm)CSTT7AOyMO xPctsBVs;!FoYRxe=(MbY%DJ3XluYaEn2|@!ei4AefreC z7}sz~rYDgrp(^)&ILTYNA9>q)1^x13{SSD2_+Urt@g2Eoe}24+G~A_!UOEh$ntVi| z*3sYj@Si??YV@B3o;!^H+TLN36mdueOFWJ2V9nHF>Q0!xTh4_5#-#VQS>!Rj^nFI1 zAw%B#oH?v~yyDbLwP5av3oST)tLsyg8)dj+Ev2Z$?lWkJ=KId_ogP7Wbv{5C)rk*G z)7cp7?|^;k@vzsxkH&@8fOc*>r1>i3z4`n!IA$@iRIY1fQf~)Ir1T^%RURG314){G zPh&TPSba|T$Q4-iQ+M5Am3_laujvDC&Habb&){X$7+Gq^LKjUQ07uZeR;DBPv+S!` z%Bw%~?Dfh)d!bfKf`#6$c;A`6DXc3N3tlz;6Y{I7nbzPrFhUDL9Akdbkm_0u5@XmI zi*KbVf1X89G}+6BOObbSclRt3jn*)mg<4|)<7G8>xZ>ZVzMSwNj`W?ic2>iJSS!73 z8Bfa?7`uB?aRWbnl45|xP(l=6^fO_{7ZZP$Ze?%XqY6LgEi&=s<|xq624aMIh0hwb zl;%~wMU??Po4e)0P9ph?OlD92v&O^!)^PgMhKH2MdnsiRGMto}qbAD27(Wp2wS}e? zdP01q i!M4!BMlWa7>? z$;0y|78c=rnml<}vAhp}F|VV|6l*5Khi!Ew^wXy>pbIuH|M=gGq}Qy~2BMl;F|y=g z(mO(1!3oRTtzFzn%0UrNNUgCL&(pB(xaLt`pRnhD%aGb*Jni?I7Or$%i5gBa$*z>N z>n0n!6R)ERf+A&Ue~yW!cPe0iEJ2oBCS3q)rFE1#U~Xv*=m(tLvkeY>QwHgdtRDh? zln7g%=PtDLoSE~VPa^q~bUVD2gWho56iwcl8l=)}*tj ^UL~Itd6>zA2P%a&3tH#(dzDH@ zS`fq_>(u?TpCaXy3&a$N-+(vQKAV@}`(%~ZjNCinjuiP98QjCsA3V9l5@=fpm>{c} zjVSQd=dGEF9dHD5ltfQlc<4PA`#v)TV`> zO^yrT(By~V Ic5S9|aW$B4?{+8$QFT-K$;%N<;$u_~fB5;C znk4`D*r+vGlqPDNjXlzMy0P_#P&p?fQ?dH(yFiNcYbj*f76@Mw@w}z7JE{N1hhO%c zF^2HrM=tTca ^s9xJb0OmM+BjE8!dlgSy=thcgX~gMAj9DVcbpivh9u?zVCXC zyJBJ{&ezf?&o5t$!P5My#YO;W2s&5Zvt5wB5gF5tn89bjmm%XlgZ4Ej+XyIyJO1{o z+KrKE^41V)bMHl5yGhFR>3s7C8Ak5HNm$*64=?cdfv-z7-;}XCs+wq~hQhIGoOHAL zvozg!DT?pWg%e4iejw3ib@gzSxcIqX3BM!X1W3TZK1$axDTO11H}eJ{7!CWYU2Ap3 z^6Fsm0~()MvR{Et8(Vw>MW4qDSLiQwz3$y;`sP^^CRDZL+Vb0=I`O}}4m5l4pJpmn zF*g~w6WR_MaG!7qNv}I_JdN(^Pxd92Bbgx^VQ^pFtZhmf(5yp0McDBKx3Auck01LA z224Ou2YLv*iQ)1CP=*~COiTcqTehQYX 4bmM_lF|+G(A}K%==WWm-*Ao>UdVRuwO7nJ<``pc_A?6i)fE4xc3E`j*%Z@0 z1;J#3y j8lnEbvUa#R4+AcQ_&PEB*5aiT1arSg>M+LLf~h@m9n6zMh-g z&9?=^NJjp<7oFeGQ$J%*bT{7GM)|;A75mMgrj|u?p>GSCG&qXY)>A9my|Kbw_?R}@ zmo$zA`PIK5x|Em(uxYZj5+XPM_!r5g{>^xZt&n>}EcN1xQ!A?Q&Cc-*m+=M_w8fSy z0;>jtt~M+;Il-{!Fdn+ZEM_8J=YIgP1_dhB|4xPTTd)@yxYGO6OeHZeh?!k@c*zon zWgYp=Jl`_@WU01;m3?e`N8~j30eTdbzI8&QTx>?5c;Cm%m5?ge1*$k(@1z80b9E9Y z8fW3yFXeyZd;HelER$yjc8lSyK}h9YVGIalIyl4F9i(x*+4^-x|E@D-g=Qm@H)iGh zojo324o4?^AdzsLZaa3=PL#1^$;c%+@V)?#)c<#(Fme#44*Fup=FQzJOaclQ9<6Ut zb#M~CvoJz0?cfo-to| 3|0}L+O5+SiWCnAH zSGcN?C9+pRC(!!e(4J}d+_M;0aRWC5xeG1)POK0OHL6YcGd;_$pR!CYJm8-3&;E{3 zcA&{>?6`k#gU92fYASm-J+XkV@1<=diA^!f-#b44cgOcvnJwK05P9Q}E5^LMd@15+ z`RQWS7eZ(j;p%9^mHp4b+93S}dIN#2NQnK#jCfU_7R1GRDM6m<0|!!jia9{4J2lr6 z?&aULAxHbWHR9pK`l*e&GfFfow<$qh%tMz p$5vtprG5fHef|*10qoNi?O4u_1SA4V MJLVW3arjysCRF8>9LHmBn6B!hc{ zXt=R~0R9{F&o6?%!-D>6-oH}-#v1y;>Tmo7_r!>u`TLfCFE0CgoT1l>Y5xsl&}&0$ z`~E&1da*^+-(mm1d*e?`E-+wr@(+Jr;Ays=6%V<5kJ#?3U6Vrtuxh5(_Xv%s{a$se z`7&xKOL7<)9y))5e?e&MxBL6<%DWzc1jup-UT5tZJmNrLdfO#Y$f!Wz$`5;59O=z5 zNFZ*VOqN=!GDv^QA(EIs&aTtP$F!@yyhD41hJoP&oqnuW1o$BfFIULkP$I{&hJ}Vp z37g20TbD5!+L)sE6LHb1kK<$n=_*nB$@?PGkk`>ANb`FUha-#yfd3dXkn}=Kg654N zD_s2y?!ZU0phc!jT76nPX`B%>4H~|5eIa@-3?JX+FUE-(-e+~wd=)vKO<(4!Dw!8` zop#Tg!OQ3Ifz5-T-IJ$E#B&CNFe=!Aw$gBX_7YEUW_l1Etr$lf;`x2N$>N=+&Pjh- z=&vg#9^_Q?rM}+~W_w7s5V ^K$VIIc*>JK&pSP6@<9m!7iI~LZZ;0aC z1y3ZMmv?W-weJHScSI;QDk0Yb`@Fs{g+_%!{fSG~3!b5^uA7%^n1>BugIRRX^a`B* zQNG@@W%faZmV <&BQMd04c)I&zzV8C z&fr4&5PS9C#Kg_al9RKhk-s;2VYCr<(%te `d-T)Y>esqF|}My zO`BO}W)93zQhqnQa+q$V6r6Wo{YH|@nl}3kL$msZyoL9zaDUa)dX+)w6d6WrG>8zC zRX-G-D>f|{Xto`&0lMbgG3vO`Ju E=r}gRUW}|Q_-!p zcO((N1btpMmPSU-`zIWD204EBOR^L+t*G$ nitgXtrPz71mU6-&A1S)u>o#6)>sJ=@# zkg@d!HJYFqS94W~MhD{tT%1~@h!2j>$dq_o)Vy`ZB?+fwrIWr6yXj~DSye+sk4OOU z{ud^Q$C!vQ;gT_#TohzdDXI5hHd#K63QCpBi3Qg@)iOWcAGMgwKG{r55w({#dl<() zf5&QU!kz$;xGasD#}ROTg&*t8<#lI1^|X0>BdOK={Nzu`mHCAL>E~C;yz;SczIIf| z#DMT1eXud)*_ @=K|~U9rvQ6$EMBR-GjG32-ORxdg6kq(NQ{7i z-#&M35}EEMe6U>(iiARyDwc3;`}ekZUISZ>fr0%ULOjT`hiys_c*$% s1@qHAA-E^$)hCX+ RDNA}x9q49bx!H0k@6BtDz>xD+W!3sMrh>OtgMlClS>ite<&bX zEf9LyDMYKh+X7bR+N#?_mUv!sYyOeAI7b5_-FUOg2m*7qBE8a^3asKp21LS=pIF#p z`K#Noe*B(A%dq}qHvH}~)h_~aTRGL`k4hcWMY6@*RiEE42d(W#;aMtt(`r2<@T@Nm z)yCoHh>Q=-18ilif7yzcXroSbYZ( WC JUS8oo#O62L%m*Kk3U-A z?ORRlq-=G#m&>|}-)c`f (lIOEh?Bgt~trSv#xaUB~`tke^V<+$&cI5Ty)qg9G^f{koU2F;)iyo zS@*Na+g80QRlzh}Zd9KIH4MRRkS!apI22=)Fk;&POfeA#TfzO9|AV>B$Z61c{(Fn4 za`wv~ytKR2Ttf%@E*E@SSg{Ug9+diD`9B)MHuB6D`=H67u>T~S)pmcdXq#2!a=X5# z2o&$N^2sO*@d5R^Z`=pE g9@N8q>uw`Y9n^P8q+QgXa z8Pse(KhBP9GPt8pYCR1csym`sKi<>h;XgH9TzRhw3fvg|7aKRJiZ5DX9;cAz7iFWN zDKc#Z&NSkOX=A&N!7prN{u8g59XpW|CQ5>;6DjbC5+`;ZzPT)WU%Ysx6ohhlSNN#m zsTU!=>(w!&_simM bL`m z3(v%nH^smjezg6Rl;-XzO>iiP)PdFh>P1kmNRet67>#!mvC<4Xvoy}2@qawEC)A3i zLeF7pi!`J+AX7z~{_8=yC;KjiLv`FfE0*5GKB3`qfn&LD?k8NRYfo;IX13HJUQ7t_ zv}uH<0%rs+egsch9~ONuLlYlAevp+9O4f}|ldm6$V@I%zXD(D159cD!!Lq-Sf)Sxu ze}$3$8Q=#P(xE?2Kjv~j{C!)ElB>{C5tE;JsaEJOe5W_ht=?y|^7!7+X-_j2m5if8 zha7~pkgXllD*|a96Y8@w?qOgH8{ql74*=)WWrfoG&cdb4Q!N~^!Uqex$l(haD;>_A z!ky7VEFX^_gvx0BE1RAjbz-iFl$*1yI(qAZ>KE{|sPYnNWk-7S44+WO5Fh);J{hro zs$%}W5_kRDQWZ>CJ~h#a=kKQ5wwL3sPVR)vRLiY&TN||(G+_* zbF^_O>X)Ffv2Vku)S!%UZm#x>m@qM@F5%G6NADDTgL8ptV<1Q}G-~YHY?>ky zFq@^}7y?$Eouk@24!d7=y`qwJ(XSSx2@E8u xj z4u>bpx9qZ8pk%ZU-h0jDNu1c(mP6r<>nx`=uQQ>7LF?G@T+C4oh_Av&TnT}m28A14 zj6&8YZBZauZGcoiBt^CTiFDm!GK<5rp*&FJ& OX_d#^v-cy+7)y;Icg(Je!P3$ z(FfZg%LH^@9O>rV(gMOm6l*{Cb-XWudDvX;RBWtJsQ6#q_iN@s7Dh8Yya5W|$yeF$ z2R|7p(|@!15hzYLO2PA1{koAO=y?Ol?Z%9#-%nj%B3M54-ij4&QF)pRncB&?l>Cn7 zrCC _kV( z51d2DcM7#0pWJw!yw1dB^__VphX`G4_3s17PuiyoDo<7!*DOg^MH9MXBVrsj*NtTz zFZ#v;2tR5z5b%0*-GjK`o76xW!px7W{{;DEtXXxy_U-W(k`3qLP@X~#QW6;U+Qf;r z+EM88zNCcNqoC8yARi@lkvt1!(Mj`7+EtU}wL|Co7VLdF3|yQT7q9Y-_6P2hZZ{W@ z(w#sMUz=~`ruU29Qy=2*yt_6H9eUVeLVtP^V3o`6Fd$?yTuzx38T q$Mc z>zj}#%GK~ds7Q8?-<_S&Nj{2YjJUj<3N&9WdauR+V4S6McA6xZlPnRYt5SC`K%5Of zmQLVBM$LucG@CK%yp-+q5`zS%V&amzsLo1Jo3``6l*{bfN^rVh#l*#$yFmJSw&hqA zB1?^xBGY~pU!k5`h`i(W{`dx8#OlC$ib1c{^#~3Ap;>eb3s()`t}3HDoP9wD(h@V* z>067RlNv{?tBD7V*`{>$V3?1SZ9JYQtiFVk&yDX)?kU+AZZ( GnbYwrE4uR?M=t0GfdJ8tbjX@5`c6oh6>;$c4}N6scq} z5v_f!yP_Fs>k01>W)vPE#$qwr1f&&qbVU}gpZcGb&XWnMZ8#NKAiur7 zBC@@1>pkp!SS2mhUsqPieo8b!kBSs+I3aX6sn3+y@gRQXPaUBC@xg9sqR2ukYV>4| z`NhiEe3Xn}1qbh=6X3ZPq8B^;* T?Vrz?CR^2(ZUUMR1!DcO~C}GuaymVyTH?e-;^SYf;JuXz-@GvCH z!UNEh{q&ZxCsX+8#}RxAs{VHukZ;M>s&+Xp4c;y}N^Ll$H9bCeY17Gv{s5|JRIa{) zmtSDwxMVK@GE4s-t1(uaL8a`A?N7<=z5tp-kwaugjRs`1YX`i#_=vvW2G}Q2(JNl3 zg3ZwjSxSy;6hK1zBNOv6fq;SxEnAlLL=a;#kvPEbKlicV3N5V^l=j)z+8_}%N39fs zio S?Wv;DNCg1C*5i9cyyzwu@|Rc1a>h(e3h?GL$d+lwDRW?~p*FKCcv$ zH IL`M2{im&8N82J;S=L nA3&5U3Q6z|FI-o4 nlK!BeqaS>mu1EYhMRR A<2@IbCh+37iyI+dZckH(i7WHQX*E6HMl%kr_gaK77fv21U)w z&sb0Vfj$M^E*R>;UAgNMF82~^gzeWo*yrgjY8Bja$~7$9qF3Lxf@a%2@uE{&pZfL_ zXoTeaSY05Oy3jND9bMEIEGvP=j>#q{GkDy${m17enFCneZ_(4K*4WrxPB0cd3m4i; zssN2 v6O3uHKWIV>;I$ARS;O#R-o99Qv|*bKYH zGn&7rIK;m_T=4=(^W}x_H$mH&>p~+sx9mUisjWV;wkW_mvYu(Y==5%v6_flI0+$En z*N#1WJ;f#nxma&_;(R_G_MGP&0$>`l+{rY~t<^AEwbfLuhWzt=?>9bcbDxmh=#H1a zx4?FE3)W>475>0+D1g>eR5BJg }=VJn`oJ3h_d);mP3>g;0sUz z?7T)0U*DVt8iMh&H`Gi>?G*x1;&JmR2JCzv>>|XRb; +BU?5Wa6dL~4?u`4I~ z@Ikh7A?rh0`;D?`!$Zu>VOwyvo>f*_fljcMNSE=+)pmqsZ^A+^$pOJPe@Q_+kffkg zlUaFt#}bWE1!0l vlLm9MYt&2uMis=Pa-^cA!EOXV?*Psmgj2<=IwXELysZu!w z0POh(L20~^fIiOf+^kfruPVP@VG{&6aqA sTs?|$SM5-HYF0avs2&tATbL_U<5Yr+%I{tYt+*^z@boTwSMnxqmQna zk`Rze!uG)Jy<#+b;w55bQ^r_nB8_lvp@Q$&w4Yib1lYC=EG{G;LxYwReu;}>G#$vf zq!%?+fzjnqoQQ*#cGAbeP4w5}RrDs+%o`~IFXLXoKU5Qcd@dF5cd6fBIx(_l*k3&o z^Q**Av*qNAJ-&6-m-0D|3OxFR(Zs{qmuViK{oD5yo8Ux18NNI}Id>{~ggRTg JhsI|a7nle75Qe8x(P5DeG=&t9{eWlEr7x_lQk zW`29yM7-!!Amr;l7IydgW`QL8iUkCw)Hb_`X5S*z*-%>q20&}cq)L#v@v5kvf&%>< zV?=Ru5^c$Ep3=GPK=RO}=!K}5^YfN5A=RdGleI-!wataT()5&gwPZsW!E<+(gcsdI z2O5HMAC$fqYxp}0$-Ihn*uX_14?DNi!;qnN`)~zYMhhNVa;3k~e}kaZctga2UyJ;- z-&8*7`=VIF)W^isB6`CrILGb>J-+W7TKxf>g4vgNw=g$? v(0 zdhkOO6oMT?SLg~tqfn~((vJrL#a)$2oMwV>5xIxG$5|n29B6JS|A?Qa4}N9HqTdSL zitL>vAb$4yq^RRQP96!cTG;2J+U1udFovEHO616Y?-JRG{~}A>Jp<=6Uiz&-eJ4yo zwlg}{Z2|;(Y;%4>*aDmWAf0(RuJOc+J6lBV97a%~YBWT@kKm ojYNS`S_Cs? z@aki`IWtKEk+j&-Y3&!7-DU?86WdO#K7Uxtt`rdh#nuKqs%SEw2p2G?hj3M!Mmt9d z!0y7B&-`&wsSs>{WwXrH=){>#!`BO(Ez8$4B0Kyp2g&2SDm!~7ArCSLtLuHSuP1+? zgL4qlQvOabis~)cUVB@} M?bt{UcdG2esm>+dII%;(>~Q{a{*o)Oz>p5lOW zCHawhI18<&OWfAzLA%Y0Tg8pI {wMvsZJ*dLxS_?_%Ip5~Sor(8S z8%=gDnDd)cS)OZuVO$pcAV1+pA*{p~o&R+7#A(Ig1Zp&m!qZ_; LmxUYYwQVx==f7X~W+z!^dJ*pp3vK$G_Lm6V^d#tlyZ(jyykVX5}sJpHIfxRnc& zt5z E)<#0IlCjy+7OXwE~n^p_$stAi9ED=6oqbeYrcYwGHScRu~1ya@fk z*Qc}bAztp>$^Z^WW~=+NLH$Nsv~-y$#Rdx~mfkMfljM3$_vMjAanS!+@jWjeU)E~S zMSs0qmVfLq?9rSpzVwyHnd?mY^I3yFIT;-sm%4#%9AZ>tcT*~}n!Y4c;>Y!l;4TNB z*?Kl#EpIW? jb*P26m-`ap1Y%~ BT#C*0j^#gC)CvdE}E@VDV^r57H zCTXSCy~2wJYqg3zMT*rz*$h{Pgjdp*U~v&?`We0rHH!RkItil-jZ%;RCWP#ZzI^~@ zL`F~}@Ek|ZUFHDTf4iJ_%h_UhH#g%9rma)EL=k2NnJ5~mKr z*s9HJa^)8>mHMEJ}86C418|^ZT|#)W>;yc{9?-I8YIR@@CHc823|*tv_HIi(T$N zcHW!>@M&oLK+ZKbW==o~2Kve3u}=iZ%HAhCX5|8SDdtKQju-$2;6@Mr>Oy7Q`=+Wz zOm>UC82@l$Y!&qtMX4&Raq~>t{NPH!vdG1jkO76GLJ#wYg2`jvVNMJwHu4s?j*Bfy zbSGqdj$sns{f~o#qFxXgCrE>2dd0a?QR}aTo?u$W!XCUM+(D(IkHwt}V9wj*#rI`| zZhE{xDjDYm7n9!2YTVri%V(HyWK&U)92OU#!wl4>2^PdLQlpw@&A6yJxZpl{gh$K$ z#6${?LZ~azzb^Fr{l$LlpwLlfb=s@q Po zqnxZR=KU|J=V9s@<_i@ab2y_%Us@yXHoAgi%{9vk<;Q#mcX0N+8)O@$vdY6A2{o9~ z70m5^6gfE5;%FC)zM# pAXowMwX2dbc-=j9dAM`Fv-Q@2>O74-{0pU z5M(ecX5y_0s7({ccMBgM6>L}>6h%u{ykM I2qwH3HV{hGI7V{t cVOz}e Q{f+TNd)D z+4O+yO4M2s2VVt{{A)HJnsNXN+6$aH%<--znb{8+kDET#$>wgfHH%?QY6-ljuVd%V z=mwnmR&&>lUSNHUv!yaFkhzqbY$vl9lHOG}4a(~SiBj`j>lwFNgqVpo^du-Zr5&(1 z7{Wq@uxNcx5H1@WcK1blXe`}ek9SR3v|}_wY9Nwt3RL paRhL#^gnV``ZC&ay_Nz4dyO7%Yb-OLzO8bFgcdT(bcj+I z1>J^0S@0~c8L#T} z27F^eRO$+x z=<$0Fu2853xGq}-ZwF8 zZ#Hy*)c_N{3LrXxP>|>P6-j#EV?-Pz*tD_#s;pxYZ?zlYw#YIvU0K8AyumAoB1dh^ z69EPf;hQEwieKd%Wc$7>k;hM_^iK|TSG#AFSZ`=T;Lh*{BD=@sqQLIHVL+^am9fd# zy!#Co>O%B#=cw^jU@CRK+2D2rR8gF0h3vBYiVTb1W6;vPP$#@qR<$O-%xH;OqB-0+ zuiFvP$*#^#YNXs3dJs0ZDM0O0@s$I y#7%fpspPd4WVklBje3@5q#G81t?cK5`SMXl9X5gi`l$4CVbl2w$ _xwrpsf9$ht;_!NiMzvbvx>@!zrl*Rfcs zI`Ip!;S+9OiiLIUxfCgUknS{rjXai>lEY#eX&8I1f{fZZCPY-{`4j~R79@=j6StfN z3SO>DWr&;W(oj%P{ZTJXzTs@uPnv@dgFwf&A&l=UZ@*pty_U&ZsKS|)dg2;L%dm!v zl=C*rsK|SctNkUEGRU&vJDy_}+ieWpeNnpXf)j_peY VSCpc?gZq&>5#_3| zs+J`nJ&Fp1KBy{y&A1y5P> L+;3d~pw;6$QDDcVzmfdbT72SWSxA(d=nSJL)G z+b^9R8_Pkz7c}29B_b`c^z~3VOeOdaMx6&4+c8}B@J@W7X91SyWFiXBDUpM45_3Uk zx#lR%_I%(Qh4cu>z8)90bNH>P)$yxKwvb!S+lZ}1jST}o?(=2tY)QKAKw>`z+!-%0 z*4##9z6(+gYo$4Y@XF@aoUGc=CKwI=Yz#E%`BOx@d73^8BobNk{c2udo?IWy&UEpV zCm-?zfo;eLXq=VCYi_iYzR#YE QjW(ZX;L| =&bwT`4{dY}$e6L6K<+nr z! @-jOFHZ349dq GJF14em>#b4d_$K6tAi<07Z;fqPhZIyS zm*`&Ynj#XYG8SLC%TDBr3W`KIFFC$j?Wlfck10$4$%XJ`IHwy&Jhwjv4 $LZ$XhtynY~ZZVCBv>h7~nF?oqfv@YE|_R}(g0!s`5# z*i1f;p^H={(40R|(l}RG<-~1(e55u52eu9EfA4QDXT3?_cHa>^R8>{KT1Gz=seGZI zu=quxf+7d&eECJK$e_0fgbA5zeK=a)z>B)p*1O$Pmgzndr2KtBTW=lVDI0Pru{;zg zMf%~7kw@Un_q_bIo-G<}17;33d3PI6;&cH4-B^`Mzs?jrji8F}D4at$*xGL0S7q}% zk3bUNEaDd3@&on83Q?-4{kdLCN2IfgT8%eZwhQx0Ff`?PMsLd`z?qF^a@y-QQ}Zf3 zP9>O$pkb&XMZNN>h#fXznRIG$t*bT~><5xIa?FnrTYY5e3{<_QPd}kKaF?e_sWG?~ zXbH!gNaW;tAR-J1TdEzu(*I#uaLK*jK-z|-57|4lsbL2I^xK6rGV#+ ^+#jh`uZ!HjD;p^^G!R`XSrO60P!djz6z`6K9YS8en?<)&hVUo?0f!!0 9&>OJGGbWQA1fm@g)&H@2q_x)0E*4^ z&!y0CH9gL%wGL< @Nsifx3?do<_?+MI{6}`Tk5^${1bSva(|qXd+Jxu<(Q(*B zJo%tqqn!cR9JO73Kp%Gk5n$T)6hev}wk T{*t6_$^Lf^Kn6c>~M~ z=Hq^FU%HK|{;b-_YTAF;Vx1nw#m+zx0@kA!Z5j`h8bzu`>zT`4LUzH5;MBxlU_-Jo zF-aowMb(isUi$c0XG-Y=&d!!mF1xF%0Fnvln^@X+>o%_?el9cWir2VL!s#|9)Om5w zX(o3s%7R}c7{*}KN&tj!7W;X0n=UrXRP- c)QN`zIjHGkH%a5DYvSX%XX$SjIi7s 4ffYxMuctoWd31HK9>C 5toAE&UiZ4e zr77SzfQ}tUI~DZEJ!`k_2c{H6IU#5rx>n !@7MvN3Saa1pbOG7^c!b2>!4h<)~M zPRqD6uzr`@H1(JjP>KD(X`qy^sXtY$h)9L96YOP+Ft?yhMn#2|9qZtZ^*T&NBXjNc zp}|t^RZ{Apnz|a`Fp-8E^!nsFeP(v-TE(UhBPJ@fS!*p62~5Mcpw6qjeEL;Vq99<4 z;&D7-mDl%3`ABz|3}HTA+AN#nZRh^E`Df?76DT=>Z&33(b5Hs+o?W+M5yW73riFi= zn?B&)BhApvI4rC3wQkEn$ViAS@U}!^y~C=b$NP!CsF(R@X=;=HjTlmp>o;g?LceiY2)-ptbPl$j|p`WG4mWCQBQ$gz+ z +TjVSV zY^wm(SB~;*`#7*PXamYj J?+t#jF0vR7;#U zC54QDBLL Z0RZ#OuC>(lBB9yGDbE>XRr?F&g<#V_ECceC zsdH%8OLlmB6oTDG;zzmjW!RulM9_mfunP5zZ6szIosnInlnA(!P^#i*MFTCS@GXAg zu<7+q!(risQ`q 3xG}+ z!k5dxJk@W~NVQFG$p8(=Y<+^uaHBz=a154fd56>jCyw=WVCRd+hK?}^r(OMGeUyf0 z*$b9W*50<@9{>p|-jAAbh~wxcv2v+)JeC0w*^!Z#f^xxC8|K^7)P3e#kxYfZA$Dtw zA{|e1RTHOXS61M#p7k~S`h6qzlVF<2OW~%tH1L-8Ej@`0mq8EEuP%s^ParcnEys78 zKzVY0TodA%*P-y!r=d-dei0}}!6xCtsVsr{wa0htHLObtpR0Aw0x}6;S8IzkkQd-E zEh?6SHtrdPrYdrtY^24+7RldJ`r}OU=IWbT_)U0uZg$xjYa^F2YVmLbhn~@K0co%5 zxu1PyO<(ixr8l!@yFX%pGxsy#QZh>?01dy^hNHDqS wg M&g7WMhkbD7D_`xnE{-k%)8cpSZuS;TAe^QVH^kt$2XRqYmW!b Y15;e)kQv0~p=|f4D-v0GoB%tr<@^j_MQL9k6Ovc}n1E zYVFy%B>?FHTvTP^h_GhdcU2OSoSOnU2(EJcbI()lcsw<~4g-T+BqjD gfLB1)gjXPGlUc1YK za#tK7Y8fbu8|z&nJBM@c>MFj-oq6J)KO8tYHle2a9t913XIJ1U63smF>C5|cBcjQY zT{b#OlQ^u%7T_>o@Z*kB*lc#X0L{|60Wzi8@=KT)@c-syNgYv93Vk4$Utl6&OzeOf z@auoNk87W9=Vw?wSJ=ov>tCaji|#VNbS^MGJj`o!LX{t2@FbSF^4DQPfD+pIXd3PK z(;&%KTIef)?GE9{Z#yVnof7I^%lRbrFl4}&3uKkD3Om3~5&QXujLJ=B>V8B9jmed+ z>Kdi#(`w9Hpi$X#^;f 7Hm8Rf{r) zmX~ _4_!-0MO!7ym_BK$#Cc&*Zb^uFTD zxdjC37@F#n5P>fB9j61X8`3o z>KSo~ucs&A8E=u`VFnIPv!6PdyHgAw+6d=d?n%5e!xSW|fPp0H6pppjO3vAO{d=5= zZb>thgWpk%X8i@Gx^;U_^ W zVrV}E04Lpd_Z0koSja&l7mzcjQFJbI#j@9JRw_s%?F4G(vOhN+p}z7~ZH=C;r%_`- zL(%u&K^a%55fIfX@{~%DLm)B|gRqIVIeiFwszmpNn`=zJkXMEA)+K}cHM!;%EBGJ+ z;8)Q5&dcwLB!$V`fM0l%ES?`&w$S&~N8O1kr=~2D=A@z|B2y}Bs>IHCse 9!Wc@Kh$7*%ZrUMaS|FTFRUs{Zo<|9dby%6XY5@o z-^bp+Q_40GbjKNEkg{+D S}qlcR{fQEnLJ}7ZaE$49%>N5WE=Tj|b2yj69SYq(7#sx|>jP zu%T-lw)E9`RbXHfL{t{?P*UfUhqU5=-kmJf3I0NZ?}*YkH?DVKz{0Ltx|d4GM>s6| zbU=~Q%iF@YRmr7^)dL2k+j>Xv(g1$LMfe41*1;f-Lj>My9=`bw5L}w{_JPEWQ0$2_ zUYH{m5A1!Q`KNJrBcz{M^c8S81_ol!EohM$*doL;uSIXy2~3u`e~_0_`7JgAG*Tj0 z^;={E1|VlP-ciCdkhTK53pOwq3qI(~&1-hBhHw%c*nplJSm;Xu2YI@>4L~Tun^P^QsmK}SE)2Lr(i60Gu$3pV>9J@!LN>$l7Y0Ec)BBr{`S@6z<+*4s2F}CU#oC^A z$QRpDaF}U9KR}Z0A`neJ$SMo)PW|e7+f4~L#_;iLC6LRb=+(tS+hU$Kd$bml-}=A_ zQd+dzqSD<8Bn%olPditg42n!s_T4Yb#7~3t4?3qPvrHC*j!$iphFl @F1UmU47M* zpaTgtwFw30KVXZJ7rGAtB^U)iz5X>30EyYNJ#r#ghA>%X!{G$I0B*(OR1Q*r(V4yj z^A9bBZ3WJswyl88{q=GywP#& ^ET{9b*({u|Je-Q(UoptU7 zE_n9;HZi{2@D=)Os>58Qy92N`6~FG BnHjE2Z}*^Tp(7_dupkJfF>Eb1au6zrW)Iek6}&| z{}5k#W3QJL6e|IGG2q)Pn*X{RwWJ^D#Vf6pgyuB0b|19mEV286Bh=7zFh>!L@_%O2 zLOGYG+=eg!s7jEndoUWHWA{CNhc&b+dlG{OU6XW%agyNjp=w}Ft8^ZwdOPp_3=l9b zj-$X%zTs!AlLK%G?>kb^C9D3oJsZQ+18NLiKL`K~3-%}Vq+lCb5+4Btr+j2M*Ds{F zP+3Aq4FlR-7!CMbaBKBPgS*QL>1Qwqir07(rj1ze(ngIE0l#w>Y=&|U3o8dQI}-*8 zHAioKVjT8be7wgFmOKwhGj~2duqjkzo;}tjkBlAdGrF4jRk71Z4ma@#Bu)QbThz@Q zo8CIHwtFY@Y*pwvEcgb~e^MvlZ((ZiDJB@0 Date: Fri, 2 Jan 2026 14:42:24 +0100 Subject: [PATCH 06/68] Automated README update (#166) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3d21d941..805fd3ac 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ - +
-
Secure Proxy for Signal Messenger REST API
+Secure Proxy for Signal CLI REST API
token-based authentication, From 92f0abd0ec772271e8edd040e36612d9ec75f9ed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 14:46:48 +0100 Subject: [PATCH 07/68] Automated README update (#167) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 805fd3ac..e295796c 100644 --- a/README.md +++ b/README.md @@ -320,7 +320,7 @@ api: ### Endpoints -Since Secured Signal API is just a proxy you can use all the [Signal REST API](https://github.com/bbernhard/signal-cli-rest-api/blob/master/doc/EXAMPLES.md) endpoints except for… +Since Secured Signal API is just a proxy you can use all the [Signal CLI REST API](https://github.com/bbernhard/signal-cli-rest-api/blob/master/doc/EXAMPLES.md) endpoints except for… | Endpoint | | | :-------------------- | ------------------ | From d8398e439a42642d11e6fdf91941da2517dba8d6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Jan 2026 15:38:06 +0100 Subject: [PATCH 08/68] Automated README update (#168) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e295796c..6e11788c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -
+
![]()
Secure Proxy for Signal CLI REST API
From 086982f0818da4a849e47942c863f8bbb7ae1666 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:49:29 +0100 Subject: [PATCH 09/68] feat: Path Auth (#170) --- internals/proxy/middlewares/auth.go | 219 ++++++++++++++++------ internals/proxy/middlewares/common.go | 9 - internals/proxy/middlewares/middleware.go | 2 +- 3 files changed, 163 insertions(+), 67 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 9d0f2725..98ffa6a2 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -3,11 +3,14 @@ package middlewares import ( "context" "encoding/base64" + "errors" "maps" "net/http" + "net/url" "slices" "strings" + "github.com/codeshelldev/gotl/pkg/logger" log "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/secured-signal-api/internals/config" ) @@ -17,79 +20,157 @@ var Auth Middleware = Middleware{ Use: authHandler, } -func authHandler(next http.Handler) http.Handler { - tokenKeys := maps.Keys(config.ENV.CONFIGS) - tokens := slices.Collect(tokenKeys) +type AuthMethod struct { + Name string + Authenticate func(req *http.Request, tokens []string) (bool, error) +} - if tokens == nil { - tokens = []string{} - } +var BearerAuth = AuthMethod { + Name: "Bearer", + Authenticate: func(req *http.Request, tokens []string) (bool, error) { + header := req.Header.Get("Authorization") - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if len(tokens) <= 0 { - next.ServeHTTP(w, req) - return + headerParts := strings.SplitN(header, " ", 2) + + if len(headerParts) != 2 { + return false, nil } - authHeader := req.Header.Get("Authorization") + if strings.ToLower(headerParts[0]) == "bearer" { + if isValidToken(tokens, headerParts[1]) { + return true, nil + } + + return false, errors.New("invalid Bearer token") + } - authQuery := req.URL.Query().Get("@authorization") + return false, nil + }, +} - var authType authType = None +var BasicAuth = AuthMethod { + Name: "Basic", + Authenticate: func(req *http.Request, tokens []string) (bool, error) { + header := req.Header.Get("Authorization") - var authToken string + if strings.TrimSpace(header) == "" { + return false, nil + } - success := false + headerParts := strings.SplitN(header, " ", 2) - if authHeader != "" { - authBody := strings.Split(authHeader, " ") + if len(headerParts) != 2 { + return false, nil + } - authType = getAuthType(authBody[0]) - authToken = authBody[1] + if strings.ToLower(headerParts[0]) == "basic" { + base64Bytes, err := base64.StdEncoding.DecodeString(headerParts[1]) - switch authType { - case Bearer: - if isValidToken(tokens, authToken) { - success = true - } - case Basic: - basicAuthBody, err := base64.StdEncoding.DecodeString(authToken) + if err != nil { + log.Error("Could not decode Basic auth payload: ", err.Error()) + return false, errors.New("invalid base64 in Basic auth") + } - if err != nil { - log.Error("Could not decode Basic Auth Payload: ", err.Error()) - } + parts := strings.SplitN(string(base64Bytes), ":", 2) - basicAuth := string(basicAuthBody) - basicAuthParts := strings.Split(basicAuth, ":") + if len(parts) != 2 { + return false, errors.New("Basic auth must be user:password") + } - user := "api" - authToken = basicAuthParts[1] + user, password := parts[0], parts[1] - if basicAuthParts[0] == user && isValidToken(tokens, authToken) { - success = true - } + if strings.ToLower(user) == "api" && isValidToken(tokens, password) { + return true, nil } - } else if authQuery != "" { - authType = Query + return false, errors.New("invalid user:password") + } - authToken = strings.TrimSpace(authQuery) + return false, nil + }, +} - if isValidToken(tokens, authToken) { - success = true +var QueryAuth = AuthMethod { + Name: "Query", + Authenticate: func(req *http.Request, tokens []string) (bool, error) { + const authQuery = "@authorization" - modifiedQuery := req.URL.Query() + auth := req.URL.Query().Get(authQuery) - modifiedQuery.Del("@authorization") + if strings.TrimSpace(auth) == "" { + return false, nil + } - req.URL.RawQuery = modifiedQuery.Encode() - } + if isValidToken(tokens, auth) { + query := req.URL.Query() + + query.Del(authQuery) + + req.URL.RawQuery = query.Encode() + + return true, nil + } + + return false, errors.New("invalid Query token") + }, +} + +var PathAuth = AuthMethod { + Name: "Path", + Authenticate: func(req *http.Request, tokens []string) (bool, error) { + parts := strings.Split(req.URL.Path, "/") + + if len(parts) == 0 { + return false, nil + } + + unescaped, err := url.PathUnescape(parts[1]) + + if err != nil { + return false, nil + } + + auth, exists := strings.CutPrefix(unescaped, "auth=") + + if !exists { + return false, nil + } + + if isValidToken(tokens, auth) { + return true, nil + } + + return false, errors.New("invalid Path token") + }, +} + +func authHandler(next http.Handler) http.Handler { + tokenKeys := maps.Keys(config.ENV.CONFIGS) + tokens := slices.Collect(tokenKeys) + + if tokens == nil { + tokens = []string{} + } + + var authChain = NewAuthChain(). + Use(BearerAuth). + Use(BasicAuth). + Use(QueryAuth). + Use(PathAuth) + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if len(tokens) <= 0 { + next.ServeHTTP(w, req) + return } + var authToken string + + success, _ := authChain.Eval(req, tokens) + if !success { w.Header().Set("WWW-Authenticate", "Basic realm=\"Login Required\", Bearer realm=\"Access Token Required\"") - log.Warn("User failed ", string(authType), " Auth") http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -101,17 +182,41 @@ func authHandler(next http.Handler) http.Handler { }) } -func getAuthType(str string) authType { - switch str { - case "Bearer": - return Bearer - case "Basic": - return Basic - default: - return None - } -} - func isValidToken(tokens []string, match string) bool { return slices.Contains(tokens, match) } + +type AuthChain struct { + methods []AuthMethod +} + +func NewAuthChain() *AuthChain { + return &AuthChain{} +} + +func (chain *AuthChain) Use(method AuthMethod) *AuthChain { + chain.methods = append(chain.methods, method) + + logger.Debug("Registered ", method.Name, " auth") + + return chain +} + +func (chain *AuthChain) Eval(req *http.Request, tokens []string) (bool, error) { + var err error + var success bool + + for _, method := range chain.methods { + success, err = method.Authenticate(req, tokens) + + if err != nil { + logger.Warn("User failed ", method.Name, " auth: ", err.Error()) + } + + if success { + return success, nil + } + } + + return false, err +} \ No newline at end of file diff --git a/internals/proxy/middlewares/common.go b/internals/proxy/middlewares/common.go index 6984b471..1f2a9878 100644 --- a/internals/proxy/middlewares/common.go +++ b/internals/proxy/middlewares/common.go @@ -11,15 +11,6 @@ type Context struct { Next http.Handler } -type authType string - -const ( - Bearer authType = "Bearer" - Basic authType = "Basic" - Query authType = "Query" - None authType = "None" -) - type contextKey string const tokenKey contextKey = "token" diff --git a/internals/proxy/middlewares/middleware.go b/internals/proxy/middlewares/middleware.go index cc222e35..3588e161 100644 --- a/internals/proxy/middlewares/middleware.go +++ b/internals/proxy/middlewares/middleware.go @@ -22,7 +22,7 @@ func NewChain() *Chain { func (chain *Chain) Use(middleware Middleware) *Chain { chain.middlewares = append(chain.middlewares, middleware) - logger.Debug("Registered ", middleware.Name) + logger.Debug("Registered ", middleware.Name, " middleware") return chain } From 7136afb2918390da17a806325004800eb5b2d255 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 2 Jan 2026 19:53:35 +0100 Subject: [PATCH 10/68] update PR and issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 16 +++++++++++--- .../ISSUE_TEMPLATE/documentation_request.md | 8 +++++-- .github/ISSUE_TEMPLATE/feature_request.md | 8 +++++-- .github/PULL_REQUEST_TEMPLATE.md | 22 +++++++++++++++---- .github/PULL_REQUEST_TEMPLATE/bug_fix.md | 18 --------------- .../PULL_REQUEST_TEMPLATE/feature_addition.md | 18 --------------- 6 files changed, 43 insertions(+), 47 deletions(-) delete mode 100644 .github/PULL_REQUEST_TEMPLATE/bug_fix.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE/feature_addition.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 446cc833..c13ef00b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,44 +1,54 @@ --- name: Bug report about: Create a report to help us improve -title: '' +title: "" labels: bug -assignees: '' +assignees: "" --- ### Description + ### To Reproduce + Steps to reproduce the behavior: + 1. Start Container 2. Send Request 3. [...] ### Expected behavior + ### Container logs + + ``` ``` + ### Config files + + ```yaml ``` ### Additional Context + diff --git a/.github/ISSUE_TEMPLATE/documentation_request.md b/.github/ISSUE_TEMPLATE/documentation_request.md index 9b5ebb59..698f476e 100644 --- a/.github/ISSUE_TEMPLATE/documentation_request.md +++ b/.github/ISSUE_TEMPLATE/documentation_request.md @@ -1,22 +1,26 @@ --- name: Documentation request about: Suggest an idea for the Documentation -title: '' +title: "" labels: documentation -assignees: '' +assignees: "" --- ### Description + ### Solution + ### Examples + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 417eb41f..2403b871 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,27 +1,31 @@ --- name: Feature request about: Suggest an idea for this project -title: '' +title: "" labels: enhancement -assignees: '' +assignees: "" --- ### Description + ### Solution + ### Alternatives + ### Additional Context + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b034c9dc..084d082a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,19 +1,33 @@ ### Summary + ### Changes + -* list of changes + +- list of changes ### Checklist + + + - [ ] PR tested - [ ] Docs updated (if applicable) ### Related -- Docs PR: -- Code PR: -- Issues: + + + +- Docs PR: +- Code PR: +- Issues: diff --git a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md b/.github/PULL_REQUEST_TEMPLATE/bug_fix.md deleted file mode 100644 index 6a7c1286..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/bug_fix.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Bug fix -about: Submit a Bugfix -title: '' -labels: bug -assignees: '' - ---- - -### Issue -Fixes # (issue) - -### Changes -* Describe the fix clearly -* Mention root cause if known - -### Verification -- [ ] Tested locally diff --git a/.github/PULL_REQUEST_TEMPLATE/feature_addition.md b/.github/PULL_REQUEST_TEMPLATE/feature_addition.md deleted file mode 100644 index 5534b232..00000000 --- a/.github/PULL_REQUEST_TEMPLATE/feature_addition.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -name: Feature addition -about: Add a Feature -title: '' -labels: enhancement -assignees: '' - ---- - -### Summary -Describe the new feature and motivation. - -### Changes -* List major additions - -### Checklist -- [ ] Feature tested -- [ ] Docs updated From f9f855d0f68910d846ab7909a536dfe30ace6502 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:12:48 +0100 Subject: [PATCH 11/68] Oops, fixed templates again --- .github/PULL_REQUEST_TEMPLATE.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 084d082a..725bf5ce 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,10 +14,6 @@ List of all changes below. ### Checklist - - - [ ] PR tested - [ ] Docs updated (if applicable) @@ -29,5 +25,4 @@ Only include this section in the Code PR. --> - Docs PR: -- Code PR: - Issues: From ce40491d08a165ef01963e37cce96228edfe5630 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:19:26 +0100 Subject: [PATCH 12/68] feat: Body auth (#172) --- internals/proxy/middlewares/auth.go | 61 ++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 98ffa6a2..11b0eadd 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -12,6 +12,7 @@ import ( "github.com/codeshelldev/gotl/pkg/logger" log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" ) @@ -22,12 +23,12 @@ var Auth Middleware = Middleware{ type AuthMethod struct { Name string - Authenticate func(req *http.Request, tokens []string) (bool, error) + Authenticate func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) } var BearerAuth = AuthMethod { Name: "Bearer", - Authenticate: func(req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { header := req.Header.Get("Authorization") headerParts := strings.SplitN(header, " ", 2) @@ -50,7 +51,7 @@ var BearerAuth = AuthMethod { var BasicAuth = AuthMethod { Name: "Basic", - Authenticate: func(req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { header := req.Header.Get("Authorization") if strings.TrimSpace(header) == "" { @@ -90,9 +91,50 @@ var BasicAuth = AuthMethod { }, } +var BodyAuth = AuthMethod { + Name: "Body", + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { + const authField = "auth" + + body, err := request.GetReqBody(req) + + if err != nil { + return false, nil + } + + body.Write(req) + + if body.Empty { + return false, nil + } + + value, exists := body.Data[authField] + + if !exists { + return false, nil + } + + auth, ok := value.(string) + + if !ok { + return false, nil + } + + if isValidToken(tokens, auth) { + delete(body.Data, authField) + + body.Write(req) + + return true, nil + } + + return false, errors.New("invalid Body token") + }, +} + var QueryAuth = AuthMethod { Name: "Query", - Authenticate: func(req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { const authQuery = "@authorization" auth := req.URL.Query().Get(authQuery) @@ -117,7 +159,7 @@ var QueryAuth = AuthMethod { var PathAuth = AuthMethod { Name: "Path", - Authenticate: func(req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { parts := strings.Split(req.URL.Path, "/") if len(parts) == 0 { @@ -155,6 +197,7 @@ func authHandler(next http.Handler) http.Handler { var authChain = NewAuthChain(). Use(BearerAuth). Use(BasicAuth). + Use(BodyAuth). Use(QueryAuth). Use(PathAuth) @@ -166,11 +209,11 @@ func authHandler(next http.Handler) http.Handler { var authToken string - success, _ := authChain.Eval(req, tokens) + success, _ := authChain.Eval(w, req, tokens) if !success { + logger.Warn("User failed to provide auth") w.Header().Set("WWW-Authenticate", "Basic realm=\"Login Required\", Bearer realm=\"Access Token Required\"") - http.Error(w, "Unauthorized", http.StatusUnauthorized) return } @@ -202,12 +245,12 @@ func (chain *AuthChain) Use(method AuthMethod) *AuthChain { return chain } -func (chain *AuthChain) Eval(req *http.Request, tokens []string) (bool, error) { +func (chain *AuthChain) Eval(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { var err error var success bool for _, method := range chain.methods { - success, err = method.Authenticate(req, tokens) + success, err = method.Authenticate(w, req, tokens) if err != nil { logger.Warn("User failed ", method.Name, " auth: ", err.Error()) From 61873bc59db161bc0b231c4cc14af108e7f703c7 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 3 Jan 2026 13:12:49 +0100 Subject: [PATCH 13/68] feat: Port per Token Config (#174) --- internals/config/loader.go | 22 ++-- internals/config/tokens.go | 12 +-- internals/proxy/middlewares/auth.go | 97 ++++++++--------- internals/proxy/middlewares/common.go | 2 - internals/proxy/middlewares/endpoints.go | 4 +- internals/proxy/middlewares/log.go | 10 +- internals/proxy/middlewares/mapping.go | 10 +- internals/proxy/middlewares/message.go | 12 +-- internals/proxy/middlewares/policy.go | 6 +- internals/proxy/middlewares/port.go | 54 ++++++++++ internals/proxy/middlewares/template.go | 18 ++-- internals/proxy/proxy.go | 1 + internals/server/server.go | 127 +++++++++++++++++++++++ main.go | 47 +++++---- utils/docker/docker.go | 20 ++-- 15 files changed, 316 insertions(+), 126 deletions(-) create mode 100644 internals/proxy/middlewares/port.go create mode 100644 internals/server/server.go diff --git a/internals/config/loader.go b/internals/config/loader.go index dbdfaab9..802304dd 100644 --- a/internals/config/loader.go +++ b/internals/config/loader.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/codeshelldev/gotl/pkg/configutils" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/gotl/pkg/stringutils" "github.com/codeshelldev/secured-signal-api/internals/config/structure" @@ -63,13 +63,13 @@ func Load() { InitTokens() - log.Info("Finished Loading Configuration") + logger.Info("Finished Loading Configuration") } func Log() { - log.Dev("Loaded Config:", mainConf.Layer.Get("")) - log.Dev("Loaded Token Configs:", tokenConf.Layer.Get("")) - log.Dev("Parsed Configs: ", ENV) + logger.Dev("Loaded Config:", mainConf.Layer.Get("")) + logger.Dev("Loaded Token Configs:", tokenConf.Layer.Get("")) + logger.Dev("Parsed Configs: ", ENV) } func Clear() { @@ -102,7 +102,7 @@ func Normalize(id string, config *configutils.Config, path string, structure any old, ok := data.(map[string]any) if !ok { - log.Warn("Could not load `"+path+"`") + logger.Warn("Could not load `"+path+"`") return } @@ -124,7 +124,7 @@ func Normalize(id string, config *configutils.Config, path string, structure any func InitReload() { reload := func(path string) { - log.Debug(path, " changed, reloading...") + logger.Debug(path, " changed, reloading...") Load() Log() } @@ -145,16 +145,16 @@ func InitConfig() { } func LoadDefaults() { - log.Debug("Loading defaults ", ENV.DEFAULTS_PATH) + logger.Debug("Loading defaults ", ENV.DEFAULTS_PATH) _, err := defaultsConf.LoadFile(ENV.DEFAULTS_PATH, yaml.Parser()) if err != nil { - log.Warn("Could not Load Defaults", ENV.DEFAULTS_PATH) + logger.Warn("Could not Load Defaults", ENV.DEFAULTS_PATH) } } func LoadConfig() { - log.Debug("Loading Config ", ENV.CONFIG_PATH) + logger.Debug("Loading Config ", ENV.CONFIG_PATH) _, err := userConf.LoadFile(ENV.CONFIG_PATH, yaml.Parser()) if err != nil { @@ -166,7 +166,7 @@ func LoadConfig() { return } - log.Error("Could not Load Config ", ENV.CONFIG_PATH, ": ", err.Error()) + logger.Error("Could not Load Config ", ENV.CONFIG_PATH, ": ", err.Error()) } } diff --git a/internals/config/tokens.go b/internals/config/tokens.go index 470bfb97..bf37a16b 100644 --- a/internals/config/tokens.go +++ b/internals/config/tokens.go @@ -4,18 +4,18 @@ import ( "strconv" "github.com/codeshelldev/gotl/pkg/configutils" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/secured-signal-api/internals/config/structure" "github.com/knadh/koanf/parsers/yaml" ) func LoadTokens() { - log.Debug("Loading Configs in ", ENV.TOKENS_DIR) + logger.Debug("Loading Configs in ", ENV.TOKENS_DIR) err := tokenConf.LoadDir("tokenconfigs", ENV.TOKENS_DIR, ".yml", yaml.Parser()) if err != nil { - log.Error("Could not Load Configs in ", ENV.TOKENS_DIR, ": ", err.Error()) + logger.Error("Could not Load Configs in ", ENV.TOKENS_DIR, ": ", err.Error()) } tokenConf.TemplateConfig() @@ -57,9 +57,9 @@ func InitTokens() { } if len(apiTokens) <= 0 { - log.Warn("No API Tokens provided this is NOT recommended") + logger.Warn("No API Tokens provided this is NOT recommended") - log.Info("Disabling Security Features due to incomplete Congfiguration") + logger.Info("Disabling Security Features due to incomplete Congfiguration") ENV.INSECURE = true @@ -69,7 +69,7 @@ func InitTokens() { } if len(apiTokens) > 0 { - log.Debug("Registered " + strconv.Itoa(len(apiTokens)) + " Tokens") + logger.Debug("Registered " + strconv.Itoa(len(apiTokens)) + " Tokens") } } diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 11b0eadd..23967ed9 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/codeshelldev/gotl/pkg/logger" - log "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" ) @@ -21,103 +20,105 @@ var Auth Middleware = Middleware{ Use: authHandler, } +const tokenKey contextKey = "token" + type AuthMethod struct { Name string - Authenticate func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) + Authenticate func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) } var BearerAuth = AuthMethod { Name: "Bearer", - Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { header := req.Header.Get("Authorization") headerParts := strings.SplitN(header, " ", 2) if len(headerParts) != 2 { - return false, nil + return "", nil } if strings.ToLower(headerParts[0]) == "bearer" { if isValidToken(tokens, headerParts[1]) { - return true, nil + return headerParts[1], nil } - return false, errors.New("invalid Bearer token") + return "", errors.New("invalid Bearer token") } - return false, nil + return "", nil }, } var BasicAuth = AuthMethod { Name: "Basic", - Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { header := req.Header.Get("Authorization") if strings.TrimSpace(header) == "" { - return false, nil + return "", nil } headerParts := strings.SplitN(header, " ", 2) if len(headerParts) != 2 { - return false, nil + return "", nil } if strings.ToLower(headerParts[0]) == "basic" { base64Bytes, err := base64.StdEncoding.DecodeString(headerParts[1]) if err != nil { - log.Error("Could not decode Basic auth payload: ", err.Error()) - return false, errors.New("invalid base64 in Basic auth") + logger.Error("Could not decode Basic auth payload: ", err.Error()) + return "", errors.New("invalid base64 in Basic auth") } parts := strings.SplitN(string(base64Bytes), ":", 2) if len(parts) != 2 { - return false, errors.New("Basic auth must be user:password") + return "", errors.New("Basic auth must be user:password") } user, password := parts[0], parts[1] if strings.ToLower(user) == "api" && isValidToken(tokens, password) { - return true, nil + return password, nil } - return false, errors.New("invalid user:password") + return "", errors.New("invalid user:password") } - return false, nil + return "", nil }, } var BodyAuth = AuthMethod { Name: "Body", - Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { const authField = "auth" body, err := request.GetReqBody(req) if err != nil { - return false, nil + return "", nil } body.Write(req) if body.Empty { - return false, nil + return "", nil } value, exists := body.Data[authField] if !exists { - return false, nil + return "", nil } auth, ok := value.(string) if !ok { - return false, nil + return "", nil } if isValidToken(tokens, auth) { @@ -125,22 +126,22 @@ var BodyAuth = AuthMethod { body.Write(req) - return true, nil + return auth, nil } - return false, errors.New("invalid Body token") + return "", errors.New("invalid Body token") }, } var QueryAuth = AuthMethod { Name: "Query", - Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { const authQuery = "@authorization" auth := req.URL.Query().Get(authQuery) if strings.TrimSpace(auth) == "" { - return false, nil + return "", nil } if isValidToken(tokens, auth) { @@ -150,39 +151,39 @@ var QueryAuth = AuthMethod { req.URL.RawQuery = query.Encode() - return true, nil + return auth, nil } - return false, errors.New("invalid Query token") + return "", errors.New("invalid Query token") }, } var PathAuth = AuthMethod { Name: "Path", - Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { + Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { parts := strings.Split(req.URL.Path, "/") if len(parts) == 0 { - return false, nil + return "", nil } unescaped, err := url.PathUnescape(parts[1]) if err != nil { - return false, nil + return "", nil } auth, exists := strings.CutPrefix(unescaped, "auth=") if !exists { - return false, nil + return "", nil } if isValidToken(tokens, auth) { - return true, nil + return auth, nil } - return false, errors.New("invalid Path token") + return "", errors.New("invalid Path token") }, } @@ -207,24 +208,26 @@ func authHandler(next http.Handler) http.Handler { return } - var authToken string - - success, _ := authChain.Eval(w, req, tokens) + token, _ := authChain.Eval(w, req, tokens) - if !success { - logger.Warn("User failed to provide auth") - w.Header().Set("WWW-Authenticate", "Basic realm=\"Login Required\", Bearer realm=\"Access Token Required\"") - http.Error(w, "Unauthorized", http.StatusUnauthorized) + if token == "" { + onUnauthorized(w) return } - ctx := context.WithValue(req.Context(), tokenKey, authToken) + ctx := context.WithValue(req.Context(), tokenKey, token) req = req.WithContext(ctx) next.ServeHTTP(w, req) }) } +func onUnauthorized(w http.ResponseWriter) { + w.Header().Set("WWW-Authenticate", "Basic realm=\"Login Required\", Bearer realm=\"Access Token Required\"") + + http.Error(w, "Unauthorized", http.StatusUnauthorized) +} + func isValidToken(tokens []string, match string) bool { return slices.Contains(tokens, match) } @@ -245,21 +248,21 @@ func (chain *AuthChain) Use(method AuthMethod) *AuthChain { return chain } -func (chain *AuthChain) Eval(w http.ResponseWriter, req *http.Request, tokens []string) (bool, error) { +func (chain *AuthChain) Eval(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { var err error - var success bool + var token string for _, method := range chain.methods { - success, err = method.Authenticate(w, req, tokens) + token, err = method.Authenticate(w, req, tokens) if err != nil { logger.Warn("User failed ", method.Name, " auth: ", err.Error()) } - if success { - return success, nil + if token != "" { + return token, nil } } - return false, err + return "", err } \ No newline at end of file diff --git a/internals/proxy/middlewares/common.go b/internals/proxy/middlewares/common.go index 1f2a9878..4cb4e6c1 100644 --- a/internals/proxy/middlewares/common.go +++ b/internals/proxy/middlewares/common.go @@ -13,8 +13,6 @@ type Context struct { type contextKey string -const tokenKey contextKey = "token" - func getConfigByReq(req *http.Request) *structure.CONFIG { token := req.Context().Value(tokenKey).(string) diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index bac6130a..363c1d5b 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -6,7 +6,7 @@ import ( "slices" "strings" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" ) var Endpoints Middleware = Middleware{ @@ -27,7 +27,7 @@ func endpointsHandler(next http.Handler) http.Handler { reqPath := req.URL.Path if isBlocked(reqPath, endpoints) { - log.Warn("User tried to access blocked endpoint: ", reqPath) + logger.Warn("User tried to access blocked endpoint: ", reqPath) http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index c786535f..edcfbd47 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -3,7 +3,7 @@ package middlewares import ( "net/http" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/gotl/pkg/request" ) @@ -14,15 +14,15 @@ var Logging Middleware = Middleware{ func loggingHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if !log.IsDev() { - log.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + if !logger.IsDev() { + logger.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) } else { body, _ := request.GetReqBody(req) if body.Data != nil && !body.Empty { - log.Dev(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) + logger.Dev(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) } else { - log.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + logger.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) } } diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index 0f92c4c7..74b1cba3 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -4,7 +4,7 @@ import ( "net/http" jsonutils "github.com/codeshelldev/gotl/pkg/jsonutils" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" request "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config/structure" ) @@ -32,7 +32,7 @@ func mappingHandler(next http.Handler) http.Handler { body, err := request.GetReqBody(req) if err != nil { - log.Error("Could not get Request Body: ", err.Error()) + logger.Error("Could not get Request Body: ", err.Error()) http.Error(w, "Bad Request: invalid body", http.StatusBadRequest) } @@ -65,12 +65,12 @@ func mappingHandler(next http.Handler) http.Handler { err := body.Write(req) if err != nil { - log.Error("Could not write to Request Body: ", err.Error()) - http.Error(w, "Internal Error", http.StatusInternalServerError) + logger.Error("Could not write to Request Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - log.Debug("Applied Data Aliasing: ", body.Data) + logger.Debug("Applied Data Aliasing: ", body.Data) } next.ServeHTTP(w, req) diff --git a/internals/proxy/middlewares/message.go b/internals/proxy/middlewares/message.go index 8af1a0b0..c5543074 100644 --- a/internals/proxy/middlewares/message.go +++ b/internals/proxy/middlewares/message.go @@ -3,7 +3,7 @@ package middlewares import ( "net/http" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" request "github.com/codeshelldev/gotl/pkg/request" ) @@ -30,7 +30,7 @@ func messageHandler(next http.Handler) http.Handler { body, err := request.GetReqBody(req) if err != nil { - log.Error("Could not get Request Body: ", err.Error()) + logger.Error("Could not get Request Body: ", err.Error()) http.Error(w, "Bad Request: invalid body", http.StatusBadRequest) } @@ -47,7 +47,7 @@ func messageHandler(next http.Handler) http.Handler { newData, err := TemplateMessage(messageTemplate, bodyData, headerData, variables) if err != nil { - log.Error("Error Templating Message: ", err.Error()) + logger.Error("Error Templating Message: ", err.Error()) } if newData["message"] != bodyData["message"] && newData["message"] != "" && newData["message"] != nil { @@ -63,12 +63,12 @@ func messageHandler(next http.Handler) http.Handler { err := body.Write(req) if err != nil { - log.Error("Could not write to Request Body: ", err.Error()) - http.Error(w, "Internal Error", http.StatusInternalServerError) + logger.Error("Could not write to Request Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - log.Debug("Applied Message Templating: ", body.Data) + logger.Debug("Applied Message Templating: ", body.Data) } next.ServeHTTP(w, req) diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 2415129b..9371696f 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -5,7 +5,7 @@ import ( "net/http" "reflect" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" request "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config/structure" "github.com/codeshelldev/secured-signal-api/utils/requestkeys" @@ -29,7 +29,7 @@ func policyHandler(next http.Handler) http.Handler { body, err := request.GetReqBody(req) if err != nil { - log.Error("Could not get Request Body: ", err.Error()) + logger.Error("Could not get Request Body: ", err.Error()) http.Error(w, "Bad Request: invalid body", http.StatusBadRequest) } @@ -42,7 +42,7 @@ func policyHandler(next http.Handler) http.Handler { shouldBlock, field := doBlock(body.Data, headerData, policies) if shouldBlock { - log.Warn("User tried to use blocked field: ", field) + logger.Warn("User tried to use blocked field: ", field) http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/internals/proxy/middlewares/port.go b/internals/proxy/middlewares/port.go new file mode 100644 index 00000000..f7f6151f --- /dev/null +++ b/internals/proxy/middlewares/port.go @@ -0,0 +1,54 @@ +package middlewares + +import ( + "errors" + "net" + "net/http" + "strings" + + "github.com/codeshelldev/gotl/pkg/logger" +) + +var Port Middleware = Middleware{ + Name: "Port", + Use: portHandler, +} + +func portHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + conf := getConfigByReq(req) + + allowedPort := conf.SERVICE.PORT + + if strings.TrimSpace(allowedPort) == "" { + next.ServeHTTP(w, req) + return + } + + port, err := getPort(req) + + if err != nil { + logger.Error("Could not get Port: ", err.Error()) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + if port != allowedPort { + logger.Warn("User tried using Token on wrong Port") + onUnauthorized(w) + return + } + }) +} + +func getPort(req *http.Request) (string, error) { + addr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr) + + if !ok { + return "", errors.New("no local addr in context") + } + + _, port, err := net.SplitHostPort(addr.String()) + + return port, err +} \ No newline at end of file diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index eac13b5a..4577df46 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -8,7 +8,7 @@ import ( "strings" jsonutils "github.com/codeshelldev/gotl/pkg/jsonutils" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" query "github.com/codeshelldev/gotl/pkg/query" request "github.com/codeshelldev/gotl/pkg/request" templating "github.com/codeshelldev/gotl/pkg/templating" @@ -33,7 +33,7 @@ func templateHandler(next http.Handler) http.Handler { body, err := request.GetReqBody(req) if err != nil { - log.Error("Could not get Request Body: ", err.Error()) + logger.Error("Could not get Request Body: ", err.Error()) http.Error(w, "Bad Request: invalid body", http.StatusBadRequest) } @@ -49,7 +49,7 @@ func templateHandler(next http.Handler) http.Handler { bodyData, modified, err = TemplateBody(body.Data, headerData, variables) if err != nil { - log.Error("Error Templating JSON: ", err.Error()) + logger.Error("Error Templating JSON: ", err.Error()) } if modified { @@ -63,7 +63,7 @@ func templateHandler(next http.Handler) http.Handler { req.URL.RawQuery, bodyData, modified, err = TemplateQuery(req.URL, bodyData, variables) if err != nil { - log.Error("Error Templating Query: ", err.Error()) + logger.Error("Error Templating Query: ", err.Error()) } if modified { @@ -77,12 +77,12 @@ func templateHandler(next http.Handler) http.Handler { err := body.Write(req) if err != nil { - log.Error("Could not write to Request Body: ", err.Error()) - http.Error(w, "Internal Error", http.StatusInternalServerError) + logger.Error("Could not write to Request Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - log.Debug("Applied Body Templating: ", body.Data) + logger.Debug("Applied Body Templating: ", body.Data) } if req.URL.Path != "" { @@ -91,11 +91,11 @@ func templateHandler(next http.Handler) http.Handler { req.URL.Path, modified, err = TemplatePath(req.URL, variables) if err != nil { - log.Error("Error Templating Path: ", err.Error()) + logger.Error("Error Templating Path: ", err.Error()) } if modified { - log.Debug("Applied Path Templating: ", req.URL.Path) + logger.Debug("Applied Path Templating: ", req.URL.Path) } } diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index 960930e2..c549e7d9 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -34,6 +34,7 @@ func (proxy Proxy) Init() http.Handler { Use(m.Logging). Use(m.Server). Use(m.Auth). + Use(m.Port). Use(m.Template). Use(m.Endpoints). Use(m.Mapping). diff --git a/internals/server/server.go b/internals/server/server.go new file mode 100644 index 00000000..680c0046 --- /dev/null +++ b/internals/server/server.go @@ -0,0 +1,127 @@ +package server + +import ( + "context" + "errors" + "net" + "net/http" + "sort" + "strconv" + "strings" + "sync" + + "github.com/codeshelldev/gotl/pkg/logger" +) + +type Server struct { + Host string + Ports []string + Handler http.Handler + Listeners map[string]*http.Server +} + +func Create(handler http.Handler, host string, ports ...string) *Server { + return &Server{ + Host: host, + Ports: ports, + Handler: handler, + Listeners: map[string]*http.Server{}, + } +} + +func (server *Server) ListenAndServer() { + var wg sync.WaitGroup + stopCh := make(chan struct{}) + + for _, port := range server.Ports { + addr := server.Host + ":" + port + listener, err := net.Listen("tcp", addr) + + if err != nil { + logger.Error("Error listening on ", port, ": ", err.Error()) + continue + } + + srv := &http.Server{ + Addr: server.Host + ":" + port, + Handler: server.Handler, + } + + wg.Add(1) + + go func(s *http.Server, l net.Listener, p string) { + defer wg.Done() + + logger.Debug("Listener on port ", port, " started") + + server.Listeners[port] = s + + err := s.Serve(l) + + if err != nil && err != http.ErrServerClosed { + logger.Error("Listener on port ", port, " exited with ", err.Error()) + } + }(srv, listener, port) + } + + go func() { + wg.Wait() + close(stopCh) + }() + + <- stopCh +} + +func (server *Server) Shutdown(ctx context.Context) error { + var errs []error + + for port, s := range server.Listeners { + logger.Debug("Shutting down listener on ", port) + + err := s.Shutdown(ctx) + + if err != nil { + errs = append(errs, err) + } + } + + return errors.Join(errs...) +} + +func PortsToRangeString(ports []string) string { + if len(ports) == 0 { + return "" + } + + sort.Strings(ports) + + result := []string{} + + end, _ := strconv.Atoi(ports[0]) + start, _ := strconv.Atoi(ports[0]) + + for i := 1; i < len(ports); i++ { + port, _ := strconv.Atoi(ports[i]) + + if port == end + 1 { + end = port + } else { + if start == end { + result = append(result, strconv.Itoa(start)) + } else { + result = append(result, strconv.Itoa(start) + "-" + strconv.Itoa(end)) + } + + start = port + end = port + } + } + + if start == end { + result = append(result, strconv.Itoa(start)) + } else { + result = append(result, strconv.Itoa(start) + "-" + strconv.Itoa(end)) + } + + return strings.Join(result, ",") +} \ No newline at end of file diff --git a/main.go b/main.go index 7ba52a94..5d3cbfb4 100644 --- a/main.go +++ b/main.go @@ -1,12 +1,14 @@ package main import ( - "net/http" "os" + "slices" + "strings" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" config "github.com/codeshelldev/secured-signal-api/internals/config" reverseProxy "github.com/codeshelldev/secured-signal-api/internals/proxy" + httpServer "github.com/codeshelldev/secured-signal-api/internals/server" docker "github.com/codeshelldev/secured-signal-api/utils/docker" ) @@ -15,19 +17,19 @@ var proxy reverseProxy.Proxy func main() { logLevel := os.Getenv("LOG_LEVEL") - log.Init(logLevel) + logger.Init(logLevel) docker.Init() config.Load() - if config.DEFAULT.SERVICE.LOG_LEVEL != log.Level() { - log.Init(config.DEFAULT.SERVICE.LOG_LEVEL) + if config.DEFAULT.SERVICE.LOG_LEVEL != logger.Level() { + logger.Init(config.DEFAULT.SERVICE.LOG_LEVEL) } - log.Info("Initialized Logger with Level of ", log.Level()) + logger.Info("Initialized Logger with Level of ", logger.Level()) - log.Info(` + logger.Info(` [1;34m┌────────────────────────────────────────────────┐[0m [1;34m│[0m [1;32m 🎄 Happy Holidays! 🎄 [0m [1;34m│[0m @@ -42,9 +44,9 @@ func main() { [1;34m└────────────────────────────────────────────────┘[0m `) - if log.Level() == "dev" { - log.Dev("Welcome back Developer!") - log.Dev("CTRL+S config to Print to Console") + if logger.Level() == "dev" { + logger.Dev("Welcome back Developer!") + logger.Dev("CTRL+S config to Print to Console") } config.Log() @@ -53,23 +55,28 @@ func main() { handler := proxy.Init() - log.Info("Initialized Middlewares") + logger.Info("Initialized Middlewares") - addr := "0.0.0.0:" + config.DEFAULT.SERVICE.PORT + ports := []string{} - log.Info("Server Listening on ", addr) + for _, config := range config.ENV.CONFIGS { + port := strings.TrimSpace(config.SERVICE.PORT) - server := &http.Server{ - Addr: addr, - Handler: handler, + if port != "" && !slices.Contains(ports, port) { + ports = append(ports, port) + } } - stop := docker.Run(func() { - err := server.ListenAndServe() + server := httpServer.Create(handler, "0.0.0.0", ports...) - if err != nil && err != http.ErrServerClosed { - log.Fatal("Server error: ", err.Error()) + stop := docker.Run(func() { + if logger.IsDebug() && len(ports) > 1 { + logger.Debug("Server started with ", len(ports), " listeners on ", httpServer.PortsToRangeString(ports)) + } else { + logger.Info("Server listening on ", httpServer.PortsToRangeString(ports)) } + + server.ListenAndServer() }) <-stop diff --git a/utils/docker/docker.go b/utils/docker/docker.go index 21b4f2ff..2f3de3db 100644 --- a/utils/docker/docker.go +++ b/utils/docker/docker.go @@ -2,16 +2,16 @@ package docker import ( "context" - "net/http" "os" "time" "github.com/codeshelldev/gotl/pkg/docker" - log "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/gotl/pkg/logger" + "github.com/codeshelldev/secured-signal-api/internals/server" ) func Init() { - log.Info("Running ", os.Getenv("IMAGE_TAG"), " Image") + logger.Info("Running ", os.Getenv("IMAGE_TAG"), " Image") } func Run(main func()) chan os.Signal { @@ -19,15 +19,15 @@ func Run(main func()) chan os.Signal { } func Exit(code int) { - log.Info("Exiting...") + logger.Info("Exiting...") docker.Exit(code) } -func Shutdown(server *http.Server) { - log.Info("Shutdown signal received") +func Shutdown(server *server.Server) { + logger.Info("Shutdown signal received") - log.Sync() + logger.Sync() ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -35,10 +35,10 @@ func Shutdown(server *http.Server) { err := server.Shutdown(ctx) if err != nil { - log.Fatal("Server shutdown failed: ", err.Error()) + logger.Fatal("Server shutdown failed: ", err.Error()) - log.Info("Server exited forcefully") + logger.Info("Server exited forcefully") } else { - log.Info("Server exited gracefully") + logger.Info("Server exited gracefully") } } From 8434b8bee26ad18d10d3ea637c9f0b2ae50da179 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 3 Jan 2026 18:49:37 +0100 Subject: [PATCH 14/68] feat: Log Level per Token (#176) --- go.mod | 4 +- go.sum | 4 +- internals/config/structure/structure.go | 1 + internals/config/tokens.go | 41 ++++++++- internals/proxy/middlewares/auth.go | 100 +++++++++++++--------- internals/proxy/middlewares/common.go | 30 ++++++- internals/proxy/middlewares/endpoints.go | 4 +- internals/proxy/middlewares/log.go | 47 ++++++++-- internals/proxy/middlewares/mapping.go | 3 +- internals/proxy/middlewares/message.go | 6 +- internals/proxy/middlewares/middleware.go | 8 +- internals/proxy/middlewares/policy.go | 3 +- internals/proxy/middlewares/port.go | 4 +- internals/proxy/middlewares/template.go | 3 +- internals/proxy/proxy.go | 3 +- 15 files changed, 195 insertions(+), 66 deletions(-) diff --git a/go.mod b/go.mod index ed4ec3ea..8fc41b9c 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,9 @@ module github.com/codeshelldev/secured-signal-api go 1.25.5 -require github.com/codeshelldev/gotl v0.0.9 +require github.com/codeshelldev/gotl v0.0.10 -require go.uber.org/zap v1.27.1 // indirect +require go.uber.org/zap v1.27.1 require ( github.com/fsnotify/fsnotify v1.9.0 // indirect diff --git a/go.sum b/go.sum index e272a597..72f06e3e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/codeshelldev/gotl v0.0.9 h1:cdLA6XzPt+f4RIW24Yx3dqBbRAq5JO0obzuwhaOgsEo= -github.com/codeshelldev/gotl v0.0.9/go.mod h1:rDkJma6eQSRfCr7ieX9/esn3/uAWNzjHfpjlr9j6FFk= +github.com/codeshelldev/gotl v0.0.10 h1:/2HOPXTlG1HplbXvkB1cZG6NQlGHCiZfWR1pMj6X55M= +github.com/codeshelldev/gotl v0.0.10/go.mod h1:rDkJma6eQSRfCr7ieX9/esn3/uAWNzjHfpjlr9j6FFk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index ba2ce910..b276d57d 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -11,6 +11,7 @@ type ENV struct { } type CONFIG struct { + NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` //TODO: deprecate overrides for tkconfigs diff --git a/internals/config/tokens.go b/internals/config/tokens.go index bf37a16b..6fb44532 100644 --- a/internals/config/tokens.go +++ b/internals/config/tokens.go @@ -1,7 +1,10 @@ package config import ( + "path/filepath" + "reflect" "strconv" + "strings" "github.com/codeshelldev/gotl/pkg/configutils" "github.com/codeshelldev/gotl/pkg/logger" @@ -12,7 +15,7 @@ import ( func LoadTokens() { logger.Debug("Loading Configs in ", ENV.TOKENS_DIR) - err := tokenConf.LoadDir("tokenconfigs", ENV.TOKENS_DIR, ".yml", yaml.Parser()) + err := tokenConf.LoadDir("tokenconfigs", ENV.TOKENS_DIR, ".yml", yaml.Parser(), setTokenConfigName) if err != nil { logger.Error("Could not Load Configs in ", ENV.TOKENS_DIR, ": ", err.Error()) @@ -84,3 +87,39 @@ func parseTokenConfigs(configArray []structure.CONFIG) map[string]structure.CONF return configs } + +func getSchemeTagByPointer(config any, tag string, fieldPointer any) string { + v := reflect.ValueOf(config) + if v.Kind() == reflect.Pointer { + v = v.Elem() + } + + fieldValue := reflect.ValueOf(fieldPointer).Elem() + + for i := 0; i < v.NumField(); i++ { + if v.Field(i).Addr().Interface() == fieldValue.Addr().Interface() { + field := v.Type().Field(i) + + return field.Tag.Get(tag) + } + } + + return "" +} + +func setTokenConfigName(config *configutils.Config, p string) { + schema := structure.CONFIG{ + NAME: "", + } + + nameField := getSchemeTagByPointer(&schema, "koanf", &schema.NAME) + + filename := filepath.Base(p) + filenameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename)) + + name := config.Layer.String(nameField) + + if strings.TrimSpace(name) == "" { + config.Layer.Set(nameField, filenameWithoutExt) + } +} \ No newline at end of file diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 23967ed9..a6757d33 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -1,7 +1,6 @@ package middlewares import ( - "context" "encoding/base64" "errors" "maps" @@ -21,13 +20,67 @@ var Auth Middleware = Middleware{ } const tokenKey contextKey = "token" +const isAuthKey contextKey = "isAuthenticated" + +func authHandler(next http.Handler) http.Handler { + tokenKeys := maps.Keys(config.ENV.CONFIGS) + tokens := slices.Collect(tokenKeys) + + if tokens == nil { + tokens = []string{} + } + + var authChain = NewAuthChain(). + Use(BearerAuth). + Use(BasicAuth). + Use(BodyAuth). + Use(QueryAuth). + Use(PathAuth) + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if len(tokens) <= 0 { + next.ServeHTTP(w, req) + return + } + + token, _ := authChain.Eval(w, req, tokens) + + if token == "" { + onUnauthorized(w) + + req = setContext(req, isAuthKey, false) + } else { + req = setContext(req, isAuthKey, true) + req = setContext(req, tokenKey, token) + } + + next.ServeHTTP(w, req) + }) +} + +var InternalAuthRequirement Middleware = Middleware{ + Name: "_Auth_Requirement", + Use: authRequirementHandler, +} + +func authRequirementHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + isAuthenticated := getContext[bool](req, isAuthKey) + + if !isAuthenticated { + return + } + + next.ServeHTTP(w, req) + }) +} type AuthMethod struct { Name string Authenticate func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) } -var BearerAuth = AuthMethod { +var BearerAuth = AuthMethod{ Name: "Bearer", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { header := req.Header.Get("Authorization") @@ -50,7 +103,7 @@ var BearerAuth = AuthMethod { }, } -var BasicAuth = AuthMethod { +var BasicAuth = AuthMethod{ Name: "Basic", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { header := req.Header.Get("Authorization") @@ -92,7 +145,7 @@ var BasicAuth = AuthMethod { }, } -var BodyAuth = AuthMethod { +var BodyAuth = AuthMethod{ Name: "Body", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { const authField = "auth" @@ -133,7 +186,7 @@ var BodyAuth = AuthMethod { }, } -var QueryAuth = AuthMethod { +var QueryAuth = AuthMethod{ Name: "Query", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { const authQuery = "@authorization" @@ -158,7 +211,7 @@ var QueryAuth = AuthMethod { }, } -var PathAuth = AuthMethod { +var PathAuth = AuthMethod{ Name: "Path", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { parts := strings.Split(req.URL.Path, "/") @@ -187,41 +240,6 @@ var PathAuth = AuthMethod { }, } -func authHandler(next http.Handler) http.Handler { - tokenKeys := maps.Keys(config.ENV.CONFIGS) - tokens := slices.Collect(tokenKeys) - - if tokens == nil { - tokens = []string{} - } - - var authChain = NewAuthChain(). - Use(BearerAuth). - Use(BasicAuth). - Use(BodyAuth). - Use(QueryAuth). - Use(PathAuth) - - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if len(tokens) <= 0 { - next.ServeHTTP(w, req) - return - } - - token, _ := authChain.Eval(w, req, tokens) - - if token == "" { - onUnauthorized(w) - return - } - - ctx := context.WithValue(req.Context(), tokenKey, token) - req = req.WithContext(ctx) - - next.ServeHTTP(w, req) - }) -} - func onUnauthorized(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", "Basic realm=\"Login Required\", Bearer realm=\"Access Token Required\"") diff --git a/internals/proxy/middlewares/common.go b/internals/proxy/middlewares/common.go index 4cb4e6c1..cd497e20 100644 --- a/internals/proxy/middlewares/common.go +++ b/internals/proxy/middlewares/common.go @@ -1,8 +1,10 @@ package middlewares import ( + "context" "net/http" + "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" ) @@ -13,10 +15,32 @@ type Context struct { type contextKey string -func getConfigByReq(req *http.Request) *structure.CONFIG { - token := req.Context().Value(tokenKey).(string) +func setContext(req *http.Request, key, value any) *http.Request { + ctx := context.WithValue(req.Context(), key, value) + return req.WithContext(ctx) +} + +func getContext[T any](req *http.Request, key any) T { + value, ok := req.Context().Value(key).(T) + + if !ok { + var zero T + return zero + } + + return value +} + +func getLogger(req *http.Request) *logger.Logger { + return getContext[*logger.Logger](req, loggerKey) +} - return getConfig(token) +func getToken(req *http.Request) string { + return getContext[string](req, tokenKey) +} + +func getConfigByReq(req *http.Request) *structure.CONFIG { + return getConfig(getToken(req)) } func getConfig(token string) *structure.CONFIG { diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index 363c1d5b..707ef844 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -5,8 +5,6 @@ import ( "path" "slices" "strings" - - "github.com/codeshelldev/gotl/pkg/logger" ) var Endpoints Middleware = Middleware{ @@ -16,6 +14,8 @@ var Endpoints Middleware = Middleware{ func endpointsHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + conf := getConfigByReq(req) endpoints := conf.SETTINGS.ACCESS.ENDPOINTS diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index edcfbd47..96fe536c 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -2,30 +2,65 @@ package middlewares import ( "net/http" + "strings" "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/gotl/pkg/request" + "go.uber.org/zap/zapcore" ) -var Logging Middleware = Middleware{ +var RequestLogger Middleware = Middleware{ Name: "Logging", Use: loggingHandler, } +const loggerKey contextKey = "logger" + func loggingHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if !logger.IsDev() { - logger.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + conf := getConfigByReq(req) + + logLevel := conf.SERVICE.LOG_LEVEL + + if strings.TrimSpace(logLevel) == "" { + logLevel = getConfig("").SERVICE.LOG_LEVEL + } + + options := logger.DefaultOptions() + options.EncodeCaller = func(caller zapcore.EntryCaller, enc zapcore.PrimitiveArrayEncoder) { + var name string + + if strings.TrimSpace(conf.NAME) != "" { + name = " " + conf.NAME + } + + enc.AppendString(caller.TrimmedPath() + name) + } + + l, err := logger.New(logLevel, options) + + if err != nil { + logger.Error("Could not create Middleware Logger: ", err.Error()) + } + + if l == nil { + l = logger.Get() + } + + req = setContext(req, loggerKey, l) + + if !l.IsDev() { + l.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) } else { body, _ := request.GetReqBody(req) if body.Data != nil && !body.Empty { - logger.Dev(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) + l.Dev(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) } else { - logger.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + l.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) } } next.ServeHTTP(w, req) }) -} +} \ No newline at end of file diff --git a/internals/proxy/middlewares/mapping.go b/internals/proxy/middlewares/mapping.go index 74b1cba3..14418ead 100644 --- a/internals/proxy/middlewares/mapping.go +++ b/internals/proxy/middlewares/mapping.go @@ -4,7 +4,6 @@ import ( "net/http" jsonutils "github.com/codeshelldev/gotl/pkg/jsonutils" - "github.com/codeshelldev/gotl/pkg/logger" request "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config/structure" ) @@ -16,6 +15,8 @@ var Mapping Middleware = Middleware{ func mappingHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + conf := getConfigByReq(req) variables := conf.SETTINGS.MESSAGE.VARIABLES diff --git a/internals/proxy/middlewares/message.go b/internals/proxy/middlewares/message.go index c5543074..51a1748c 100644 --- a/internals/proxy/middlewares/message.go +++ b/internals/proxy/middlewares/message.go @@ -2,8 +2,8 @@ package middlewares import ( "net/http" + "strings" - "github.com/codeshelldev/gotl/pkg/logger" request "github.com/codeshelldev/gotl/pkg/request" ) @@ -14,6 +14,8 @@ var Message Middleware = Middleware{ func messageHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + conf := getConfigByReq(req) variables := conf.SETTINGS.MESSAGE.VARIABLES @@ -23,7 +25,7 @@ func messageHandler(next http.Handler) http.Handler { variables = getConfig("").SETTINGS.MESSAGE.VARIABLES } - if messageTemplate == "" { + if strings.TrimSpace(messageTemplate) == "" { messageTemplate = getConfig("").SETTINGS.MESSAGE.TEMPLATE } diff --git a/internals/proxy/middlewares/middleware.go b/internals/proxy/middlewares/middleware.go index 3588e161..a6e76861 100644 --- a/internals/proxy/middlewares/middleware.go +++ b/internals/proxy/middlewares/middleware.go @@ -2,6 +2,7 @@ package middlewares import ( "net/http" + "strings" "github.com/codeshelldev/gotl/pkg/logger" ) @@ -22,7 +23,12 @@ func NewChain() *Chain { func (chain *Chain) Use(middleware Middleware) *Chain { chain.middlewares = append(chain.middlewares, middleware) - logger.Debug("Registered ", middleware.Name, " middleware") + if strings.HasPrefix(middleware.Name, "_") { + logger.Dev("Registered ", middleware.Name, " middleware") + } else { + logger.Debug("Registered ", middleware.Name, " middleware") + } + return chain } diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index 9371696f..bb00ab23 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -5,7 +5,6 @@ import ( "net/http" "reflect" - "github.com/codeshelldev/gotl/pkg/logger" request "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config/structure" "github.com/codeshelldev/secured-signal-api/utils/requestkeys" @@ -18,6 +17,8 @@ var Policy Middleware = Middleware{ func policyHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + conf := getConfigByReq(req) policies := conf.SETTINGS.ACCESS.FIELD_POLICIES diff --git a/internals/proxy/middlewares/port.go b/internals/proxy/middlewares/port.go index f7f6151f..716e04a3 100644 --- a/internals/proxy/middlewares/port.go +++ b/internals/proxy/middlewares/port.go @@ -5,8 +5,6 @@ import ( "net" "net/http" "strings" - - "github.com/codeshelldev/gotl/pkg/logger" ) var Port Middleware = Middleware{ @@ -16,6 +14,8 @@ var Port Middleware = Middleware{ func portHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + conf := getConfigByReq(req) allowedPort := conf.SERVICE.PORT diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index 4577df46..f2eccb91 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -8,7 +8,6 @@ import ( "strings" jsonutils "github.com/codeshelldev/gotl/pkg/jsonutils" - "github.com/codeshelldev/gotl/pkg/logger" query "github.com/codeshelldev/gotl/pkg/query" request "github.com/codeshelldev/gotl/pkg/request" templating "github.com/codeshelldev/gotl/pkg/templating" @@ -22,6 +21,8 @@ var Template Middleware = Middleware{ func templateHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + conf := getConfigByReq(req) variables := conf.SETTINGS.MESSAGE.VARIABLES diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index c549e7d9..1ee5def7 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -31,9 +31,10 @@ func Create(targetUrl string) Proxy { func (proxy Proxy) Init() http.Handler { handler := m.NewChain(). - Use(m.Logging). Use(m.Server). Use(m.Auth). + Use(m.RequestLogger). + Use(m.InternalAuthRequirement). Use(m.Port). Use(m.Template). Use(m.Endpoints). From 57d062b76db45b49faa76d47ac03e4b5de205b77 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:13:17 +0100 Subject: [PATCH 15/68] feat: Rate Limiting (#178) --- go.mod | 1 + go.sum | 2 + internals/config/structure/structure.go | 14 ++-- internals/proxy/middlewares/ratelimit.go | 84 ++++++++++++++++++++++++ internals/proxy/proxy.go | 1 + 5 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 internals/proxy/middlewares/ratelimit.go diff --git a/go.mod b/go.mod index 8fc41b9c..495eafa0 100644 --- a/go.mod +++ b/go.mod @@ -20,4 +20,5 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sys v0.39.0 // indirect + golang.org/x/time v0.14.0 ) diff --git a/go.sum b/go.sum index 72f06e3e..1e267259 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index b276d57d..3b39cab0 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -30,11 +30,11 @@ type API struct { } type SETTINGS struct { - ACCESS ACCESS_SETTINGS `koanf:"access"` - MESSAGE MESSAGE_SETTINGS `koanf:"message"` + ACCESS ACCESS `koanf:"access"` + MESSAGE MESSAGE `koanf:"message"` } -type MESSAGE_SETTINGS struct { +type MESSAGE struct { VARIABLES map[string]any `koanf:"variables" childtransform:"upper"` FIELD_MAPPINGS map[string][]FieldMapping `koanf:"fieldmappings" childtransform:"default"` TEMPLATE string `koanf:"template"` @@ -45,12 +45,18 @@ type FieldMapping struct { Score int `koanf:"score"` } -type ACCESS_SETTINGS struct { +type ACCESS struct { ENDPOINTS []string `koanf:"endpoints"` FIELD_POLICIES map[string]FieldPolicy `koanf:"fieldpolicies" childtransform:"default"` + RATE_LIMITING RateLimiting `koanf:"ratelimiting"` } type FieldPolicy struct { Value any `koanf:"value"` Action string `koanf:"action"` +} + +type RateLimiting struct { + Limit int `koanf:"limit"` + Period string `koanf:"period"` } \ No newline at end of file diff --git a/internals/proxy/middlewares/ratelimit.go b/internals/proxy/middlewares/ratelimit.go new file mode 100644 index 00000000..775b3c00 --- /dev/null +++ b/internals/proxy/middlewares/ratelimit.go @@ -0,0 +1,84 @@ +package middlewares + +import ( + "net/http" + "strings" + "time" + + "golang.org/x/time/rate" +) + +var RateLimit Middleware = Middleware{ + Name: "Rate Limiting", + Use: ratelimitHandler, +} + +type TokenLimiter struct { + limiter *rate.Limiter +} + +func NewTokenLimiter(limit int, period time.Duration) *TokenLimiter { + r := rate.Every(period / time.Duration(limit)) + + return &TokenLimiter{ + limiter: rate.NewLimiter(r, limit), + } +} + +func (t *TokenLimiter) Allow() bool { + return t.limiter.Allow() +} + +var tokenLimiters = map[string]*TokenLimiter{} + +func ratelimitHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + rateLimiting := conf.SETTINGS.ACCESS.RATE_LIMITING + + limit := rateLimiting.Limit + + if limit == 0 { + limit = getConfig("").SETTINGS.ACCESS.RATE_LIMITING.Limit + } + + periodStr := rateLimiting.Period + + if strings.TrimSpace(periodStr) == "" { + periodStr = conf.SETTINGS.ACCESS.RATE_LIMITING.Period + } + + if strings.TrimSpace(periodStr) != "" && limit != 0 { + period, err := time.ParseDuration(periodStr) + + if err != nil { + logger.Error("Could not parse Duration: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + token := getToken(req) + + tokenLimiter, exists := tokenLimiters[token] + + if !exists { + tokenLimiter = NewTokenLimiter(limit, period) + tokenLimiters[token] = tokenLimiter + } + + if !tokenLimiter.Allow() { + logger.Warn("Token exceeded Rate Limit") + + http.Error(w, "Too Many Requests", http.StatusTooManyRequests) + w.Header().Set("Retry-After", "60") + + return + } + } + + next.ServeHTTP(w, req) + }) +} \ No newline at end of file diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index 1ee5def7..aae6345f 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -36,6 +36,7 @@ func (proxy Proxy) Init() http.Handler { Use(m.RequestLogger). Use(m.InternalAuthRequirement). Use(m.Port). + Use(m.RateLimit). Use(m.Template). Use(m.Endpoints). Use(m.Mapping). From 30ac47698d7050b2ae30425dbbcbe516de8f3e46 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 18:31:47 +0100 Subject: [PATCH 16/68] default: allow all --- internals/proxy/middlewares/endpoints.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index 707ef844..bd34c228 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -60,8 +60,8 @@ func matchesPattern(endpoint, pattern string) bool { func isBlocked(endpoint string, endpoints []string) bool { if len(endpoints) == 0 { - // default: block all - return true + // default: allow all + return false } allowed, blocked := getEndpoints(endpoints) @@ -92,6 +92,6 @@ func isBlocked(endpoint string, endpoints []string) bool { return false } - // no match -> default: block all - return true + // default: allow all + return false } From 534e5bbfdf843d284fbaa95c69dfbc4b09e093cf Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:07:17 +0100 Subject: [PATCH 17/68] maybe make INSECURE configurable --- internals/proxy/middlewares/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index a6757d33..058b363d 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -38,7 +38,7 @@ func authHandler(next http.Handler) http.Handler { Use(PathAuth) return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - if len(tokens) <= 0 { + if config.ENV.INSECURE || len(tokens) <= 0 { next.ServeHTTP(w, req) return } From 5ee203c8859b1ecbd92867ed91045ac679071a0a Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:15:47 +0100 Subject: [PATCH 18/68] DEPRECATION: @authorization => @auth (#184) --- internals/proxy/middlewares/auth.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 058b363d..49518ebd 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -189,9 +189,9 @@ var BodyAuth = AuthMethod{ var QueryAuth = AuthMethod{ Name: "Query", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { - const authQuery = "@authorization" + const authQuery = "auth" - auth := req.URL.Query().Get(authQuery) + auth := req.URL.Query().Get("@" + authQuery) if strings.TrimSpace(auth) == "" { return "", nil @@ -200,7 +200,7 @@ var QueryAuth = AuthMethod{ if isValidToken(tokens, auth) { query := req.URL.Query() - query.Del(authQuery) + query.Del("@" + authQuery) req.URL.RawQuery = query.Encode() From d5182a09fafacf444eea546d0e375eab110fccd1 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:23:29 +0100 Subject: [PATCH 19/68] feat: IP Filtering (#181) --- internals/config/structure/structure.go | 3 + internals/proxy/middlewares/auth.go | 2 +- internals/proxy/middlewares/clientip.go | 40 +++++++ internals/proxy/middlewares/endpoints.go | 8 +- internals/proxy/middlewares/ipfilter.go | 95 +++++++++++++++ internals/proxy/middlewares/log.go | 40 +++++-- internals/proxy/middlewares/policy.go | 2 +- internals/proxy/middlewares/port.go | 2 +- internals/proxy/middlewares/proxy.go | 142 +++++++++++++++++++++++ internals/proxy/middlewares/ratelimit.go | 7 ++ internals/proxy/proxy.go | 4 + 11 files changed, 326 insertions(+), 19 deletions(-) create mode 100644 internals/proxy/middlewares/clientip.go create mode 100644 internals/proxy/middlewares/ipfilter.go create mode 100644 internals/proxy/middlewares/proxy.go diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 3b39cab0..2052c11a 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -49,6 +49,9 @@ type ACCESS struct { ENDPOINTS []string `koanf:"endpoints"` FIELD_POLICIES map[string]FieldPolicy `koanf:"fieldpolicies" childtransform:"default"` RATE_LIMITING RateLimiting `koanf:"ratelimiting"` + IP_FILTER []string `koanf:"ipfilter"` + TRUSTED_IPS []string `koanf:"trustedips"` + TRUSTED_PROXIES []string `koanf:"trustedproxies"` } type FieldPolicy struct { diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 49518ebd..5946f27f 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -274,7 +274,7 @@ func (chain *AuthChain) Eval(w http.ResponseWriter, req *http.Request, tokens [] token, err = method.Authenticate(w, req, tokens) if err != nil { - logger.Warn("User failed ", method.Name, " auth: ", err.Error()) + logger.Warn("Client failed ", method.Name, " auth: ", err.Error()) } if token != "" { diff --git a/internals/proxy/middlewares/clientip.go b/internals/proxy/middlewares/clientip.go new file mode 100644 index 00000000..11126c2a --- /dev/null +++ b/internals/proxy/middlewares/clientip.go @@ -0,0 +1,40 @@ +package middlewares + +import ( + "net" + "net/http" +) + +var InternalClientIP Middleware = Middleware{ + Name: "_Client_IP", + Use: clientIPHandler, +} + +var trustedClientKey contextKey = "isClientTrusted" + +func clientIPHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + rawTrustedIPs := conf.SETTINGS.ACCESS.TRUSTED_IPS + + if rawTrustedIPs == nil { + rawTrustedIPs = getConfig("").SETTINGS.ACCESS.TRUSTED_IPS + } + + ip := getContext[net.IP](req, clientIPKey) + + trustedIPs := parseIPsAndIPNets(rawTrustedIPs) + trusted := isIPInList(ip, trustedIPs) + + if trusted { + logger.Dev("Connection from trusted Client: ", ip.String()) + } + + req = setContext(req, trustedClientKey, trusted) + + next.ServeHTTP(w, req) + }) +} \ No newline at end of file diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index bd34c228..c33e45be 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -26,8 +26,8 @@ func endpointsHandler(next http.Handler) http.Handler { reqPath := req.URL.Path - if isBlocked(reqPath, endpoints) { - logger.Warn("User tried to access blocked endpoint: ", reqPath) + if isEndpointBlocked(reqPath, endpoints) { + logger.Warn("Client tried to access blocked endpoint: ", reqPath) http.Error(w, "Forbidden", http.StatusForbidden) return } @@ -58,8 +58,8 @@ func matchesPattern(endpoint, pattern string) bool { return ok } -func isBlocked(endpoint string, endpoints []string) bool { - if len(endpoints) == 0 { +func isEndpointBlocked(endpoint string, endpoints []string) bool { + if len(endpoints) == 0 || endpoints == nil { // default: allow all return false } diff --git a/internals/proxy/middlewares/ipfilter.go b/internals/proxy/middlewares/ipfilter.go new file mode 100644 index 00000000..b4138e47 --- /dev/null +++ b/internals/proxy/middlewares/ipfilter.go @@ -0,0 +1,95 @@ +package middlewares + +import ( + "net" + "net/http" + "slices" + "strings" +) + +var IPFilter Middleware = Middleware{ + Name: "IP Filter", + Use: ipFilterHandler, +} + +func ipFilterHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + ipFilter := conf.SETTINGS.ACCESS.IP_FILTER + + if ipFilter == nil { + ipFilter = getConfig("").SETTINGS.ACCESS.ENDPOINTS + } + + ip := getContext[net.IP](req, clientIPKey) + + if isIPBlocked(ip, ipFilter) { + logger.Warn("Client IP is blocked by filter: ", ip.String()) + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + next.ServeHTTP(w, req) + }) +} + +func getIPNets(ipNets []string) ([]string, []string) { + blockedIPNets := []string{} + allowedIPNets := []string{} + + for _, ipNet := range ipNets { + ip, block := strings.CutPrefix(ipNet, "!") + + if block { + blockedIPNets = append(blockedIPNets, ip) + } else { + allowedIPNets = append(allowedIPNets, ip) + } + } + + return allowedIPNets, blockedIPNets +} + +func isIPBlocked(ip net.IP, ipfilter []string) (bool) { + if len(ipfilter) == 0 || ipfilter == nil { + // default: allow all + return false + } + + rawAllowed, rawBlocked := getIPNets(ipfilter) + + allowed := parseIPsAndIPNets(rawAllowed) + blocked := parseIPsAndIPNets(rawBlocked) + + isExplicitlyAllowed := slices.ContainsFunc(allowed, func(try *net.IPNet) bool { + return try.Contains(ip) + }) + isExplicitlyBlocked := slices.ContainsFunc(blocked, func(try *net.IPNet) bool { + return try.Contains(ip) + }) + + // explicit allow > block + if isExplicitlyAllowed { + return false + } + + if isExplicitlyBlocked { + return true + } + + // if any allow rules exist, default is deny + if len(allowed) > 0 { + return true + } + + // only blocked ips -> allow anything not blocked + if len(blocked) > 0 { + return false + } + + // default: allow all + return false +} diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index 96fe536c..116702e2 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -1,6 +1,7 @@ package middlewares import ( + "net" "net/http" "strings" @@ -17,6 +18,33 @@ var RequestLogger Middleware = Middleware{ const loggerKey contextKey = "logger" func loggingHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + ip := getContext[net.IP](req, clientIPKey) + + if !logger.IsDev() { + logger.Info(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + } else { + body, _ := request.GetReqBody(req) + + if body.Data != nil && !body.Empty { + logger.Dev(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) + } else { + logger.Info(ip.String(), " ", req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) + } + } + + next.ServeHTTP(w, req) + }) +} + +var InternalMiddlewareLogger Middleware = Middleware{ + Name: "_Middleware_Logger", + Use: middlewareLoggerHandler, +} + +func middlewareLoggerHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { conf := getConfigByReq(req) @@ -49,18 +77,6 @@ func loggingHandler(next http.Handler) http.Handler { req = setContext(req, loggerKey, l) - if !l.IsDev() { - l.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) - } else { - body, _ := request.GetReqBody(req) - - if body.Data != nil && !body.Empty { - l.Dev(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery, body.Data) - } else { - l.Info(req.Method, " ", req.URL.Path, " ", req.URL.RawQuery) - } - } - next.ServeHTTP(w, req) }) } \ No newline at end of file diff --git a/internals/proxy/middlewares/policy.go b/internals/proxy/middlewares/policy.go index bb00ab23..08fbe112 100644 --- a/internals/proxy/middlewares/policy.go +++ b/internals/proxy/middlewares/policy.go @@ -43,7 +43,7 @@ func policyHandler(next http.Handler) http.Handler { shouldBlock, field := doBlock(body.Data, headerData, policies) if shouldBlock { - logger.Warn("User tried to use blocked field: ", field) + logger.Warn("Client tried to use blocked field: ", field) http.Error(w, "Forbidden", http.StatusForbidden) return } diff --git a/internals/proxy/middlewares/port.go b/internals/proxy/middlewares/port.go index 716e04a3..84d30d9f 100644 --- a/internals/proxy/middlewares/port.go +++ b/internals/proxy/middlewares/port.go @@ -34,7 +34,7 @@ func portHandler(next http.Handler) http.Handler { } if port != allowedPort { - logger.Warn("User tried using Token on wrong Port") + logger.Warn("Client tried using Token on wrong Port") onUnauthorized(w) return } diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go new file mode 100644 index 00000000..6762bfe2 --- /dev/null +++ b/internals/proxy/middlewares/proxy.go @@ -0,0 +1,142 @@ +package middlewares + +import ( + "errors" + "net" + "net/http" + "strings" +) + +var InternalProxy Middleware = Middleware{ + Name: "_Proxy", + Use: proxyHandler, +} + +const trustedProxyKey contextKey = "isProxyTrusted" +const clientIPKey contextKey = "clientIP" + +func proxyHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + rawTrustedProxies := conf.SETTINGS.ACCESS.TRUSTED_PROXIES + + if rawTrustedProxies == nil { + rawTrustedProxies = getConfig("").SETTINGS.ACCESS.TRUSTED_PROXIES + } + + var trusted bool + var ip net.IP + + host, _, _ := net.SplitHostPort(req.RemoteAddr) + + ip = net.ParseIP(host) + + if len(rawTrustedProxies) != 0 { + trustedProxies := parseIPsAndIPNets(rawTrustedProxies) + + trusted = isIPInList(ip, trustedProxies) + } + + if trusted { + realIP, err := getRealIP(req) + + if err != nil { + logger.Error("Could not get real IP: ", err.Error()) + } + + if realIP != nil { + ip = realIP + } + } + + req = setContext(req, clientIPKey, ip) + req = setContext(req, trustedProxyKey, trusted) + + next.ServeHTTP(w, req) + }) +} + +func parseIP(str string) (*net.IPNet, error) { + if !strings.Contains(str, "/") { + ip := net.ParseIP(str) + + if ip == nil { + return nil, errors.New("invalid ip: " + str) + } + + var mask net.IPMask + + if ip.To4() != nil { + mask = net.CIDRMask(32, 32) // IPv4 /32 + } else { + mask = net.CIDRMask(128, 128) // IPv6 /128 + } + + return &net.IPNet{IP: ip, Mask: mask}, nil + } + + ip, network, err := net.ParseCIDR(str) + if err != nil { + return nil, err + } + + if !ip.Equal(network.IP) { + var mask net.IPMask + + if ip.To4() != nil { + mask = net.CIDRMask(32, 32) // IPv4 /32 + } else { + mask = net.CIDRMask(128, 128) // IPv6 /128 + } + + return &net.IPNet{IP: ip, Mask: mask}, nil + } + + return network, nil +} + +func parseIPsAndIPNets(array []string) []*net.IPNet { + ipNets := []*net.IPNet{} + + for _, item := range array { + ipNet, err := parseIP(item) + + if err != nil { + continue + } + + ipNets = append(ipNets, ipNet) + } + + return ipNets +} + +func getRealIP(req *http.Request) (net.IP, error) { + XFF := req.Header.Get("X-Forwarded-For") + + if XFF != "" { + ips := strings.Split(XFF, ",") + + realIP := net.ParseIP(strings.TrimSpace(ips[0])) + + if realIP == nil { + return nil, errors.New("malformed x-forwarded-for header") + } + + return realIP, nil + } + + return nil, errors.New("no x-forwarded-for header present") +} + +func isIPInList(ip net.IP, list []*net.IPNet) bool { + for _, net := range list { + if net.Contains(ip) { + return true + } + } + return false +} \ No newline at end of file diff --git a/internals/proxy/middlewares/ratelimit.go b/internals/proxy/middlewares/ratelimit.go index 775b3c00..564f4b4b 100644 --- a/internals/proxy/middlewares/ratelimit.go +++ b/internals/proxy/middlewares/ratelimit.go @@ -35,6 +35,13 @@ func ratelimitHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { logger := getLogger(req) + trusted := getContext[bool](req, trustedClientKey) + + if trusted { + next.ServeHTTP(w, req) + return + } + conf := getConfigByReq(req) rateLimiting := conf.SETTINGS.ACCESS.RATE_LIMITING diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index aae6345f..d532707c 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -33,8 +33,12 @@ func (proxy Proxy) Init() http.Handler { handler := m.NewChain(). Use(m.Server). Use(m.Auth). + Use(m.InternalMiddlewareLogger). + Use(m.InternalProxy). + Use(m.InternalClientIP). Use(m.RequestLogger). Use(m.InternalAuthRequirement). + Use(m.IPFilter). Use(m.Port). Use(m.RateLimit). Use(m.Template). From c376af9ab148a57982f1f218e535260388918c8b Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:31:46 +0100 Subject: [PATCH 20/68] misc: Santa is Dead (#186) --- main.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/main.go b/main.go index 5d3cbfb4..db441a09 100644 --- a/main.go +++ b/main.go @@ -29,21 +29,6 @@ func main() { logger.Info("Initialized Logger with Level of ", logger.Level()) - logger.Info(` - - [1;34m┌────────────────────────────────────────────────┐[0m - [1;34m│[0m [1;32m 🎄 Happy Holidays! 🎄 [0m [1;34m│[0m - [1;34m│[0m [1;34m│[0m - [1;34m│[0m [0;37mThank you for using this project and for all [0m [1;34m│[0m - [1;34m│[0m [0;37mthe downloads, stars, and support this year. [0m [1;34m│[0m - [1;34m│[0m [1;34m│[0m - [1;34m│[0m [1;32mYour support truly means a lot — here's to [0m [1;34m│[0m - [1;34m│[0m [1;32man awesome year ahead! ✨ [0m [1;34m│[0m - [1;34m│[0m [1;34m│[0m - [1;34m│[0m [1;36m - CodeShell [0m [1;34m│[0m - [1;34m└────────────────────────────────────────────────┘[0m - `) - if logger.Level() == "dev" { logger.Dev("Welcome back Developer!") logger.Dev("CTRL+S config to Print to Console") From 973847be49ff10d1ea11b2087630acf66c0aeaf0 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 20:34:19 +0100 Subject: [PATCH 21/68] . --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index db441a09..250604ae 100644 --- a/main.go +++ b/main.go @@ -30,7 +30,7 @@ func main() { logger.Info("Initialized Logger with Level of ", logger.Level()) if logger.Level() == "dev" { - logger.Dev("Welcome back Developer!") + logger.Dev("Welcome back, Developer!") logger.Dev("CTRL+S config to Print to Console") } From 4e89c7c6397e83a3dc005efa367e42d43c739df8 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Tue, 6 Jan 2026 21:11:18 +0100 Subject: [PATCH 22/68] DEPRECATION: .token, .tokens, .overrides (#187) --- internals/config/structure/structure.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 2052c11a..d572d3ec 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,8 +14,7 @@ type CONFIG struct { NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` - //TODO: deprecate overrides for tkconfigs - SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides"` + SETTINGS SETTINGS `koanf:"settings"` } type SERVICE struct { @@ -25,8 +24,7 @@ type SERVICE struct { type API struct { URL string `koanf:"url" env>aliases:".apiurl"` - //TODO: deprecate .token for tkconfigs - TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" token>aliases:".tokens,.token" aliases:"token"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token"` } type SETTINGS struct { From 7181b9da3458eac13c0f5f564bb17bd4996626ad Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 7 Jan 2026 14:20:44 +0100 Subject: [PATCH 23/68] feat: Hostnames (#188) --- internals/config/structure/structure.go | 1 + internals/proxy/middlewares/hostname.go | 46 +++++++++++++++++++++++++ internals/proxy/middlewares/proxy.go | 30 ++++++++++++++-- internals/proxy/proxy.go | 3 +- 4 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 internals/proxy/middlewares/hostname.go diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index d572d3ec..aeed07c0 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -18,6 +18,7 @@ type CONFIG struct { } type SERVICE struct { + HOSTNAMES []string `koanf:"hostnames" env>aliases:".hostnames"` PORT string `koanf:"port" env>aliases:".port"` LOG_LEVEL string `koanf:"loglevel" env>aliases:".loglevel"` } diff --git a/internals/proxy/middlewares/hostname.go b/internals/proxy/middlewares/hostname.go new file mode 100644 index 00000000..1701b1c9 --- /dev/null +++ b/internals/proxy/middlewares/hostname.go @@ -0,0 +1,46 @@ +package middlewares + +import ( + "net/http" + "net/url" + "slices" +) + +var Hostname Middleware = Middleware{ + Name: "Hostname", + Use: hostnameHandler, +} + +func hostnameHandler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + logger := getLogger(req) + + conf := getConfigByReq(req) + + hostnames := conf.SERVICE.HOSTNAMES + + if hostnames == nil { + hostnames = getConfig("").SERVICE.HOSTNAMES + } + + if len(hostnames) > 0 { + URL := getContext[*url.URL](req, originURLKey) + + hostname := URL.Hostname() + + if hostname == "" { + logger.Error("Encountered empty hostname") + http.Error(w, "Bad Request: invalid hostname", http.StatusBadRequest) + return + } + + if !slices.Contains(hostnames, hostname) { + logger.Warn("Client tried using Token with wrong hostname") + onUnauthorized(w) + return + } + } + + next.ServeHTTP(w, req) + }) +} \ No newline at end of file diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go index 6762bfe2..599d403c 100644 --- a/internals/proxy/middlewares/proxy.go +++ b/internals/proxy/middlewares/proxy.go @@ -4,6 +4,7 @@ import ( "errors" "net" "net/http" + "net/url" "strings" ) @@ -14,6 +15,7 @@ var InternalProxy Middleware = Middleware{ const trustedProxyKey contextKey = "isProxyTrusted" const clientIPKey contextKey = "clientIP" +const originURLKey contextKey = "originURL" func proxyHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -32,6 +34,8 @@ func proxyHandler(next http.Handler) http.Handler { host, _, _ := net.SplitHostPort(req.RemoteAddr) + originUrl := req.Proto + "://" + req.URL.Host + ip = net.ParseIP(host) if len(rawTrustedProxies) != 0 { @@ -50,10 +54,30 @@ func proxyHandler(next http.Handler) http.Handler { if realIP != nil { ip = realIP } + + XFHost := req.Header.Get("X-Forwarded-Host") + XFProto := req.Header.Get("X-Forwarded-Proto") + XFPort := req.Header.Get("X-Forwarded-Port") + + if XFHost == "" || XFProto == "" || XFPort == "" { + logger.Warn("Missing X-Forwarded-* headers") + } + + originUrl = XFProto + "://" + XFHost + ":" + XFPort + } + + originURL, err := url.Parse(originUrl) + + if err != nil { + logger.Error("Could not parse Url: ", originUrl) + http.Error(w, "Bad Request: invalid Url", http.StatusBadRequest) + return } - req = setContext(req, clientIPKey, ip) req = setContext(req, trustedProxyKey, trusted) + req = setContext(req, originURLKey, originURL) + + req = setContext(req, clientIPKey, ip) next.ServeHTTP(w, req) }) @@ -123,13 +147,13 @@ func getRealIP(req *http.Request) (net.IP, error) { realIP := net.ParseIP(strings.TrimSpace(ips[0])) if realIP == nil { - return nil, errors.New("malformed x-forwarded-for header") + return nil, errors.New("malformed X-Forwarded-For header") } return realIP, nil } - return nil, errors.New("no x-forwarded-for header present") + return nil, errors.New("no X-Forwarded-For header present") } func isIPInList(ip net.IP, list []*net.IPNet) bool { diff --git a/internals/proxy/proxy.go b/internals/proxy/proxy.go index d532707c..b16335b3 100644 --- a/internals/proxy/proxy.go +++ b/internals/proxy/proxy.go @@ -38,8 +38,9 @@ func (proxy Proxy) Init() http.Handler { Use(m.InternalClientIP). Use(m.RequestLogger). Use(m.InternalAuthRequirement). - Use(m.IPFilter). Use(m.Port). + Use(m.Hostname). + Use(m.IPFilter). Use(m.RateLimit). Use(m.Template). Use(m.Endpoints). From d8a753aeca3e1c8ca04d656728b656b810036d98 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Wed, 7 Jan 2026 19:22:00 +0100 Subject: [PATCH 24/68] update PR template --- .github/PULL_REQUEST_TEMPLATE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 725bf5ce..471b97a3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -14,6 +14,11 @@ List of all changes below. ### Checklist + - [ ] PR tested - [ ] Docs updated (if applicable) From 5e6102e038c933dd64226b28f5577da9aaa18278 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:49:07 +0100 Subject: [PATCH 38/68] add deprecation messages --- internals/config/parser.go | 123 ++++++++++++++++-------- internals/config/structure/structure.go | 8 +- 2 files changed, 89 insertions(+), 42 deletions(-) diff --git a/internals/config/parser.go b/internals/config/parser.go index c63886d6..c8373a5d 100644 --- a/internals/config/parser.go +++ b/internals/config/parser.go @@ -29,50 +29,97 @@ func uppercaseTransform(key string, value any) (string, any) { var onUseFuncs = map[string]func(source string, target configutils.TransformTarget) { "deprecated": func(source string, target configutils.TransformTarget) { - box := pretty.NewAutoBox() - box.MinWidth = 50 - box.PaddingX = 2 - box.PaddingY = 1 - - box.Border.Style = pretty.BorderStyle{ - Color: pretty.Basic(pretty.Yellow), - } - - box.AddBlock(pretty.Block{ - Align: pretty.AlignCenter, - Style: pretty.Style{}, - Segments: []pretty.Segment{ - pretty.TextBlockSegment{ - Text: "🚨 Deprecation 🚨", - Style: pretty.Style{ - Bold: true, - Foreground: pretty.Basic(pretty.Yellow), - }, + deprecationHandler(source, target) + }, +} + +var deprecationHandledMap = map[string]bool{} + +func deprecationHandler(source string, target configutils.TransformTarget) { + handled, _ := deprecationHandledMap[source] + + if handled { + return + } + + deprecationHandledMap[source] = true + + deprecationMessage := target.Source.Tag.Get("deprecation") + + box := pretty.NewAutoBox() + box.MinWidth = 50 + box.PaddingX = 2 + box.PaddingY = 1 + + box.Border.Style = pretty.BorderStyle{ + Color: pretty.Basic(pretty.Yellow), + } + + messageParts := strings.Split(deprecationMessage, "\n") + messageSegments := []pretty.Segment{} + + for _, part := range messageParts { + messageSegments = append(messageSegments, pretty.InlineSegment{ + Items: []pretty.Inline{ + pretty.Span{ + Text: part, }, - pretty.InlineSegment{}, - pretty.TextBlockSegment{ - Text: "Please refrain from using", + }, + }) + } + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Style: pretty.Style{}, + Segments: []pretty.Segment{ + pretty.TextBlockSegment{ + Text: "🚨 Deprecation 🚨", + Style: pretty.Style{ + Bold: true, + Foreground: pretty.Basic(pretty.Yellow), }, - pretty.InlineSegment{}, - pretty.TextBlockSegment{ - Text: "`" + source + "`", - Style: pretty.Style{ - Italic: true, - Bold: true, - Background: pretty.Basic(pretty.Red), - }, + }, + pretty.InlineSegment{}, + pretty.TextBlockSegment{ + Text: "Please refrain from using", + }, + pretty.InlineSegment{}, + pretty.TextBlockSegment{ + Text: "`" + source + "`", + Style: pretty.Style{ + Italic: true, + Bold: true, + Background: pretty.Basic(pretty.Red), }, - pretty.InlineSegment{}, - pretty.InlineSegment{ - Items: []pretty.Inline{ - pretty.Span{ - Text: "as it has been marked as deprecated", + }, + pretty.InlineSegment{}, + pretty.InlineSegment{ + Items: []pretty.Inline{ + pretty.Span{ + Text: "as it has been marked as ", + }, + pretty.Span{ + Text: "deprecated", + Style: pretty.Style{ + Bold: true, }, }, + pretty.Span{ + Text: ":", + }, }, }, - }) + pretty.InlineSegment{}, + }, + }) - fmt.Println(box.Render()) - }, + box.AddBlock(pretty.Block{ + Segments: messageSegments, + Align: pretty.AlignCenter, + Style: pretty.Style{ + Background: pretty.Basic(pretty.BrightBlack), + }, + }) + + fmt.Println(box.Render()) } \ No newline at end of file diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index d770f6ff..1aa2898b 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,8 +14,8 @@ type CONFIG struct { NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` - SETTINGS SETTINGS `koanf:"settings"` -} + SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"We have moved away from using 'overrides' in Token Configs"` +} type SERVICE struct { HOSTNAMES []string `koanf:"hostnames" env>aliases:".hostnames"` @@ -25,13 +25,13 @@ type SERVICE struct { type API struct { URL string `koanf:"url" env>aliases:".apiurl"` - TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens>>deprecated,.token>>deprecated,token>>deprecated" onuse:"token>>deprecated" deprecation:"'tokens' and 'token' will not be at the root anymore\n'api.token' will be removed in favor of 'api.tokens'"` AUTH AUTH `koanf:"auth"` } type AUTH struct { METHODS []string `koanf:"methods" env>aliases:".authmethods"` - TOKENS []Token `koanf:"tokens" aliases:"token"` + TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"'api.auth.token' will be removed in favor of 'api.auth.tokens'"` } type Token struct { From fcdfc1c797e69be6c57251a050560031acf01b39 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 23 Jan 2026 22:54:03 +0100 Subject: [PATCH 39/68] pulled of a perfectly sized --- internals/config/parser.go | 13 +++++++++++++ internals/config/structure/structure.go | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/internals/config/parser.go b/internals/config/parser.go index c8373a5d..0d0e3414 100644 --- a/internals/config/parser.go +++ b/internals/config/parser.go @@ -121,5 +121,18 @@ func deprecationHandler(source string, target configutils.TransformTarget) { }, }) + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.InlineSegment{}, + pretty.TextBlockSegment{ + Text: "Update your config before the next update,\nwhere it will be removed for good", + Style: pretty.Style{ + Italic: true, + }, + }, + }, + }) + fmt.Println(box.Render()) } \ No newline at end of file diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 1aa2898b..6a5fe650 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -31,7 +31,7 @@ type API struct { type AUTH struct { METHODS []string `koanf:"methods" env>aliases:".authmethods"` - TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"'api.auth.token' will be removed in favor of 'api.auth.tokens'"` + TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"'api.auth.token' will change in to 'api.auth.tokens'"` } type Token struct { From d23ef8f060158631df6e62e6555abc6c654dac38 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 15:04:26 +0100 Subject: [PATCH 40/68] imrpvoed dep messages --- go.mod | 6 +-- go.sum | 12 +++--- internals/config/parser.go | 49 +++++++++++++++++-------- internals/config/structure/structure.go | 6 +-- 4 files changed, 46 insertions(+), 27 deletions(-) diff --git a/go.mod b/go.mod index a9b29773..15277194 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,11 @@ module github.com/codeshelldev/secured-signal-api go 1.25.5 require ( - github.com/codeshelldev/gotl/pkg/configutils v0.0.5 + github.com/codeshelldev/gotl/pkg/configutils v0.0.7 github.com/codeshelldev/gotl/pkg/docker v0.0.2 github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 github.com/codeshelldev/gotl/pkg/logger v0.0.6 - github.com/codeshelldev/gotl/pkg/pretty v0.0.8 + github.com/codeshelldev/gotl/pkg/pretty v0.0.9-3 github.com/codeshelldev/gotl/pkg/query v0.0.3 github.com/codeshelldev/gotl/pkg/request v0.0.3 github.com/codeshelldev/gotl/pkg/stringutils v0.0.3 @@ -29,7 +29,7 @@ require ( github.com/knadh/koanf/providers/confmap v1.0.0 // indirect github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect github.com/knadh/koanf/providers/file v1.2.1 // indirect - github.com/knadh/koanf/v2 v2.3.0 // indirect + github.com/knadh/koanf/v2 v2.3.1 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index cad8d300..bdf365c9 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfa github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.3.1 h1:RjM8gnVbFbgI67SBekIC7ihFpyXwRPYWXn9BZActHbw= github.com/clipperhouse/uax29/v2 v2.3.1/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/codeshelldev/gotl/pkg/configutils v0.0.5 h1:Dv9PiiJhfRihnv0f2H8T4ebOBaA7SyISOK7ghJ6oA3k= -github.com/codeshelldev/gotl/pkg/configutils v0.0.5/go.mod h1:WoWMBB8+84ePRnI2m+kbq1Rw8F/9iCWLHkVBsun3Qjc= +github.com/codeshelldev/gotl/pkg/configutils v0.0.7 h1:54zv82v87xCrilbi7gfVFpC37dIpR4a9PXw8pYs1Few= +github.com/codeshelldev/gotl/pkg/configutils v0.0.7/go.mod h1:WoWMBB8+84ePRnI2m+kbq1Rw8F/9iCWLHkVBsun3Qjc= github.com/codeshelldev/gotl/pkg/docker v0.0.2 h1:kpseReocEBoSzWe/tOhUrIrOYeAR/inw3EF2/d+N078= github.com/codeshelldev/gotl/pkg/docker v0.0.2/go.mod h1:odNnlRw4aO1n2hSkDZIaiuSXIoFoVeatmXtF64Yd33U= github.com/codeshelldev/gotl/pkg/ioutils v0.0.2 h1:IRcN2M6H4v59iodw1k7gFX9lirhbVy6RZ4yRtKNcFYg= @@ -12,8 +12,8 @@ github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 h1:ERsjkaWVrsyUZoEunCEeNYDXhua github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2/go.mod h1:oxgKaAoMu6iYVHfgR7AhkK22xbYx4K0KCkyVEfYVoWs= github.com/codeshelldev/gotl/pkg/logger v0.0.6 h1:heo6z6yZm5PpX78vxud9HJNfVU9J46HVlW8T4EOy9nQ= github.com/codeshelldev/gotl/pkg/logger v0.0.6/go.mod h1:pL/I7KYxbGHhyedallZlCkBvoalv9gAWNEYVXbF9BoM= -github.com/codeshelldev/gotl/pkg/pretty v0.0.8 h1:buLobwNqZRlYGnfyFLi7A7z2m7362Wm9k5Y+Tv0tMsI= -github.com/codeshelldev/gotl/pkg/pretty v0.0.8/go.mod h1:2Gk6UBrtkIME2RCSNytS/RJ5lHXYL/MDx0rYRpknobM= +github.com/codeshelldev/gotl/pkg/pretty v0.0.9-3 h1:1zj79w0L+gWiXZhZi8GGzeJwNt3wdhTlV/iDslChDKM= +github.com/codeshelldev/gotl/pkg/pretty v0.0.9-3/go.mod h1:2Gk6UBrtkIME2RCSNytS/RJ5lHXYL/MDx0rYRpknobM= github.com/codeshelldev/gotl/pkg/query v0.0.3 h1:Zy8k0R5HcJS00OMPRHybgFEiwMg7ceLyv6bA0G7NOfs= github.com/codeshelldev/gotl/pkg/query v0.0.3/go.mod h1:kKaPOKXluIid3qeS7xzrmfq3NxIa8/PhKYHM6GRbwJw= github.com/codeshelldev/gotl/pkg/request v0.0.3 h1:maRPHu366NARow8/m1Q8Cw1EU1Uy0pDIn1vlAsOatKM= @@ -38,8 +38,8 @@ github.com/knadh/koanf/providers/env/v2 v2.0.0 h1:Ad5H3eun722u+FvchiIcEIJZsZ2M6o github.com/knadh/koanf/providers/env/v2 v2.0.0/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc= github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.3.0 h1:Qg076dDRFHvqnKG97ZEsi9TAg2/nFTa9hCdcSa1lvlM= -github.com/knadh/koanf/v2 v2.3.0/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.1 h1:2uTWFib/W7LAaAH88C2Qa5woBW/efhhcy23FnkUiyuQ= +github.com/knadh/koanf/v2 v2.3.1/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= diff --git a/internals/config/parser.go b/internals/config/parser.go index 0d0e3414..1f530b17 100644 --- a/internals/config/parser.go +++ b/internals/config/parser.go @@ -44,10 +44,10 @@ func deprecationHandler(source string, target configutils.TransformTarget) { deprecationHandledMap[source] = true - deprecationMessage := target.Source.Tag.Get("deprecation") + msgMap := configutils.ParseTag(target.Source.Tag.Get("deprecation")) box := pretty.NewAutoBox() - box.MinWidth = 50 + box.MinWidth = 60 box.PaddingX = 2 box.PaddingY = 1 @@ -55,19 +55,24 @@ func deprecationHandler(source string, target configutils.TransformTarget) { Color: pretty.Basic(pretty.Yellow), } - messageParts := strings.Split(deprecationMessage, "\n") + messageParts := strings.Split(configutils.GetValueWithSource(source, target.Parent, msgMap), "\n") messageSegments := []pretty.Segment{} for _, part := range messageParts { - messageSegments = append(messageSegments, pretty.InlineSegment{ - Items: []pretty.Inline{ - pretty.Span{ - Text: part, - }, - }, + messageSegments = append(messageSegments, pretty.StyledTextBlockSegment{ + Raw: part, }) } + atRoot := !strings.Contains(source, ".") + refrainPrefix := "" + refrainSuffix := "" + + if atRoot { + refrainPrefix = "⇧ " + refrainSuffix = " (at root)" + } + box.AddBlock(pretty.Block{ Align: pretty.AlignCenter, Style: pretty.Style{}, @@ -84,12 +89,26 @@ func deprecationHandler(source string, target configutils.TransformTarget) { Text: "Please refrain from using", }, pretty.InlineSegment{}, - pretty.TextBlockSegment{ - Text: "`" + source + "`", - Style: pretty.Style{ - Italic: true, - Bold: true, - Background: pretty.Basic(pretty.Red), + pretty.InlineSegment{ + Items: []pretty.Inline{ + pretty.Span{ + Text: refrainPrefix, + Style: pretty.Style{ + Bold: true, + Foreground: pretty.Basic(pretty.BrightWhite), + }, + }, + pretty.Span{ + Text: "`" + source + "`", + Style: pretty.Style{ + Italic: true, + Bold: true, + Background: pretty.Basic(pretty.Red), + }, + }, + pretty.Span{ + Text: refrainSuffix, + }, }, }, pretty.InlineSegment{}, diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 6a5fe650..72639cf8 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,7 +14,7 @@ type CONFIG struct { NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` - SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"We have moved away from using 'overrides' in Token Configs"` + SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,s,fg=orange}\x60overrides\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` } type SERVICE struct { @@ -25,13 +25,13 @@ type SERVICE struct { type API struct { URL string `koanf:"url" env>aliases:".apiurl"` - TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens>>deprecated,.token>>deprecated,token>>deprecated" onuse:"token>>deprecated" deprecation:"'tokens' and 'token' will not be at the root anymore\n'api.token' will be removed in favor of 'api.tokens'"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,s,fg=orange}\x60tokens\x60{/} and {b,s,fg=orange}\x60token\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,s,fg=orange}\x60api.token\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` AUTH AUTH `koanf:"auth"` } type AUTH struct { METHODS []string `koanf:"methods" env>aliases:".authmethods"` - TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"'api.auth.token' will change in to 'api.auth.tokens'"` + TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,s,fg=orange}\x60api.auth.token\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` } type Token struct { From 084380fa3004e0b8e09fd644d5a63b925fef19c3 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:59:34 +0100 Subject: [PATCH 41/68] feat: Regex in Endpoints (#209) --- dev-env.sh | 3 ++- internals/proxy/middlewares/endpoints.go | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/dev-env.sh b/dev-env.sh index 5a44c145..7380de30 100644 --- a/dev-env.sh +++ b/dev-env.sh @@ -62,9 +62,10 @@ cecho "${GREEN}Successfully loaded development environment!${END}" #= Mock server =# #=-----------------------------------=# +MOCK_PORT="8881" + MOCK_BIN="/tmp/mockserver-$MOCK_PORT" MOCK_PID="/tmp/mockserver-$MOCK_PORT.pid" -MOCK_PORT="8881" # Kill mockserver if still running if [ -f "$MOCK_PID" ]; then diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index c85f2899..dea11922 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -3,6 +3,7 @@ package middlewares import ( "net/http" "path" + "regexp" "slices" "strings" ) @@ -54,8 +55,15 @@ func getEndpoints(endpoints []string) ([]string, []string) { } func matchesPattern(endpoint, pattern string) bool { - ok, _ := path.Match(pattern, endpoint) - return ok + re, err := regexp.Compile(pattern) + + if err != nil { + ok, _ := path.Match(pattern, endpoint) + + return ok + } + + return re.MatchString(endpoint) } func isEndpointBlocked(endpoint string, endpoints []string) bool { From 72db0c92cf65e4bbbf6b34ed91469caf0e8e904d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:06:42 +0100 Subject: [PATCH 42/68] feat: Regex in Endpoints (#2) (#212) --- internals/proxy/middlewares/endpoints.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internals/proxy/middlewares/endpoints.go b/internals/proxy/middlewares/endpoints.go index dea11922..2d5dc6f4 100644 --- a/internals/proxy/middlewares/endpoints.go +++ b/internals/proxy/middlewares/endpoints.go @@ -2,7 +2,6 @@ package middlewares import ( "net/http" - "path" "regexp" "slices" "strings" @@ -58,9 +57,7 @@ func matchesPattern(endpoint, pattern string) bool { re, err := regexp.Compile(pattern) if err != nil { - ok, _ := path.Match(pattern, endpoint) - - return ok + return endpoint == pattern } return re.MatchString(endpoint) From 54298c8a4bd128bde1ea3a31c92d0d4cdf09641d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:49:01 +0100 Subject: [PATCH 43/68] update dev workflow --- .github/workflows/docker-image-dev.yml | 147 ++++++++++++++++++++++--- 1 file changed, 129 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 513ef6ce..6877f9b7 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -1,22 +1,133 @@ -name: Build & Push Dev Image +name: Build & Push Image on: - push: - branches: - - dev - paths: - - "**/*.go" + workflow_call: + inputs: + username: + required: false + type: string + default: ${{ github.repository_owner }} + + registry: + required: true + type: string + + image: + required: false + type: string + default: ${{ github.repository }} + + flavor: + required: false + type: string + default: | + latest=true + + tags: + required: false + type: string + default: | + type=semver,pattern=v{{version}} + type=semver,pattern=v{{major}}.{{minor}} + type=semver,pattern=v{{major}} + + context: + required: false + type: string + default: . + + platforms: + required: false + type: string + default: linux/amd64, linux/arm64 + + go-version: + required: false + type: string + default: "1.25.x" -jobs: - update: - uses: codeshelldev/gh-actions/.github/workflows/docker-image-go.yml@main - name: Development Image - with: - registry: ghcr.io - flavor: | - latest=false - tags: | - type=sha - type=raw,value=latest-dev secrets: - GH_PCKG_TOKEN: ${{ secrets.GH_PCKG_TOKEN }} + GH_PCKG_TOKEN: + required: true + +jobs: + build: + runs-on: ubuntu-latest + name: Build & Push + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/go/pkg/mod + ~/.cache/go-build + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Install dependencies + run: go mod tidy + + - name: Run tests + run: go test ./... -v + + - name: Cross-Compile Go binaries + run: | + platforms="${{ inputs.platforms }}" + + IFS=', ' read -ra platforms <<< "$platforms" + + for platform in "${platforms[@]}"; do + IFS='/' read -ra platformData <<< "$platform" + + os="${platformData[0]}" + arch="${platformData[1]}" + + path="dist/$os/$arch/app" + + mkdir -p "$(dirname "$path")" + + echo "Created $path" + + GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -ldflags="-w -s" -o "$path" . + + echo "Built Binaries for $os/$arch" + done + + - name: Login to Registry + uses: docker/login-action@v3 + with: + registry: ${{ inputs.registry }} + username: ${{ inputs.username }} + password: ${{ secrets.GH_PCKG_TOKEN }} + + - name: Setup Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract Labels and Tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ inputs.registry }}/${{ inputs.image }} + flavor: ${{ inputs.flavor }} + tags: ${{ inputs.tags }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: ${{ inputs.context }} + platforms: ${{ inputs.platforms }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: true + build-args: | + IMAGE_TAG=${{ steps.meta.outputs.version }} From 96a39094f130682d24d6587a53408bfb9eead78d Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:51:20 +0100 Subject: [PATCH 44/68] oops, wrong paste --- .github/workflows/docker-image-dev.yml | 179 +++++++++---------------- 1 file changed, 61 insertions(+), 118 deletions(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 6877f9b7..e5123137 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -1,133 +1,76 @@ -name: Build & Push Image +name: Build & Push Dev Image on: - workflow_call: + workflow_dispatch: inputs: - username: - required: false - type: string - default: ${{ github.repository_owner }} - - registry: + base-tag: + description: Base Tag to be used (`type-n` is automatically appended) required: true - type: string - - image: - required: false - type: string - default: ${{ github.repository }} - - flavor: - required: false - type: string - default: | - latest=true - - tags: - required: false - type: string - default: | - type=semver,pattern=v{{version}} - type=semver,pattern=v{{major}}.{{minor}} - type=semver,pattern=v{{major}} - - context: - required: false - type: string - default: . - - platforms: - required: false - type: string - default: linux/amd64, linux/arm64 - - go-version: - required: false - type: string - default: "1.25.x" + default: latest - secrets: - GH_PCKG_TOKEN: + type: + description: Type of Build required: true + type: choice + default: dev + options: + - rc + - beta + - alpha + - dev jobs: - build: + image-tag: runs-on: ubuntu-latest - name: Build & Push + name: Image Tag + outputs: + image_tag: ${{ steps.resolve.outputs.image_tag }} steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: ${{ inputs.go-version }} - - - name: Cache Go modules - uses: actions/cache@v4 - with: - path: | - ~/go/pkg/mod - ~/.cache/go-build - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - - name: Install dependencies - run: go mod tidy - - - name: Run tests - run: go test ./... -v - - - name: Cross-Compile Go binaries + - name: Login to Registry run: | - platforms="${{ inputs.platforms }}" + echo "${{ secrets.GITHUB_TOKEN }}" | \ + docker login ghcr.io -u ${{ github.actor }} --password-stdin - IFS=', ' read -ra platforms <<< "$platforms" - - for platform in "${platforms[@]}"; do - IFS='/' read -ra platformData <<< "$platform" - - os="${platformData[0]}" - arch="${platformData[1]}" - - path="dist/$os/$arch/app" - - mkdir -p "$(dirname "$path")" - - echo "Created $path" - - GOOS="$os" GOARCH="$arch" CGO_ENABLED=0 go build -ldflags="-w -s" -o "$path" . - - echo "Built Binaries for $os/$arch" + - name: Resolve next Image Tag + id: resolve + run: | + set -euo pipefail + + IMAGE="ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}" + PREFIX="${{ inputs.base-tag }}-${{ inputs.type }}-" + + TAGS=$(curl -fsSL \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + "https://ghcr.io/v2/${{ github.repository_owner }}/${{ github.event.repository.name }}/tags/list" \ + | jq -r '.tags[]?') + + MAX=0 + for tag in $TAGS; do + if [[ "$tag" == ${PREFIX}* ]]; then + NUM="${tag#$PREFIX}" + if [[ "$NUM" =~ ^[0-9]+$ ]]; then + (( NUM > MAX )) && MAX=$NUM + fi + fi done - - name: Login to Registry - uses: docker/login-action@v3 - with: - registry: ${{ inputs.registry }} - username: ${{ inputs.username }} - password: ${{ secrets.GH_PCKG_TOKEN }} - - - name: Setup Buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract Labels and Tags - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ inputs.registry }}/${{ inputs.image }} - flavor: ${{ inputs.flavor }} - tags: ${{ inputs.tags }} - - - name: Build and Push Image - uses: docker/build-push-action@v6 - with: - context: ${{ inputs.context }} - platforms: ${{ inputs.platforms }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - push: true - build-args: | - IMAGE_TAG=${{ steps.meta.outputs.version }} + NEXT=$((MAX + 1)) + FINAL_TAG="${PREFIX}${NEXT}" + + echo "Resolved tag: $FINAL_TAG" + echo "image_tag=$FINAL_TAG" >> "$GITHUB_OUTPUT" + + update: + needs: image-tag + uses: codeshelldev/gh-actions/.github/workflows/docker-image-go.yml@main + name: Development Image + with: + registry: ghcr.io + flavor: | + latest=false + tags: | + type=sha + type=raw,value=${{ needs.image-tag }} + secrets: + GH_PCKG_TOKEN: ${{ secrets.GH_PCKG_TOKEN }} From 45c35146629f1bf3d780478d2561808f497a91ea Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:54:45 +0100 Subject: [PATCH 45/68] . --- .github/workflows/docker-image-dev.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index e5123137..7ae43c5a 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -20,9 +20,24 @@ on: - dev jobs: + check-ref: + name: Check Ref + runs-on: ubuntu-latest + + steps: + - name: Fail if @main + if: ${{ github.ref == 'refs/heads/main' }} + run: | + echo "❌ This workflow cannot be run from the main branch." + echo "Please pin to a release, use another branch or commit SHA instead of @main." + exit 1 + + name: Build & Push Dev Image + image-tag: runs-on: ubuntu-latest name: Image Tag + needs: check-ref outputs: image_tag: ${{ steps.resolve.outputs.image_tag }} From 906d50923b89a0c385d1290d6cab4369f726b061 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 20:59:54 +0100 Subject: [PATCH 46/68] fix registry login --- .github/workflows/docker-image-dev.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 7ae43c5a..bf79ad17 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -43,9 +43,11 @@ jobs: steps: - name: Login to Registry - run: | - echo "${{ secrets.GITHUB_TOKEN }}" | \ - docker login ghcr.io -u ${{ github.actor }} --password-stdin + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GH_PCKG_TOKEN }} - name: Resolve next Image Tag id: resolve @@ -56,7 +58,7 @@ jobs: PREFIX="${{ inputs.base-tag }}-${{ inputs.type }}-" TAGS=$(curl -fsSL \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "Authorization: Bearer ${{ secrets.GH_PCKG_TOKEN }}" \ "https://ghcr.io/v2/${{ github.repository_owner }}/${{ github.event.repository.name }}/tags/list" \ | jq -r '.tags[]?') From a9380de08fbd7fe36168261792a18d4d49259052 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:04:27 +0100 Subject: [PATCH 47/68] fix workflow --- .github/workflows/docker-image-dev.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index bf79ad17..674005fe 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -54,12 +54,18 @@ jobs: run: | set -euo pipefail - IMAGE="ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}" PREFIX="${{ inputs.base-tag }}-${{ inputs.type }}-" + IMAGE=${{ github.repository_owner }}/${{ github.event.repository.name }} + + TOKEN="$( + curl "https://ghcr.io/token?scope=repository:${IMAGE}:pull" | + awk -F'"' '$0=$4' + )" + TAGS=$(curl -fsSL \ - -H "Authorization: Bearer ${{ secrets.GH_PCKG_TOKEN }}" \ - "https://ghcr.io/v2/${{ github.repository_owner }}/${{ github.event.repository.name }}/tags/list" \ + -H "Authorization: Bearer ${TOKEN}" \ + "https://ghcr.io/v2/${IMAGE}/tags/list" \ | jq -r '.tags[]?') MAX=0 From 66b6558ca258e904ba998d57b27a5fb645210bb6 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:09:01 +0100 Subject: [PATCH 48/68] fix workflow output --- .github/workflows/docker-image-dev.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 674005fe..5a31ffc0 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -34,7 +34,7 @@ jobs: name: Build & Push Dev Image - image-tag: + resolve-tag: runs-on: ubuntu-latest name: Image Tag needs: check-ref @@ -85,7 +85,7 @@ jobs: echo "image_tag=$FINAL_TAG" >> "$GITHUB_OUTPUT" update: - needs: image-tag + needs: resolve-tag uses: codeshelldev/gh-actions/.github/workflows/docker-image-go.yml@main name: Development Image with: @@ -94,6 +94,6 @@ jobs: latest=false tags: | type=sha - type=raw,value=${{ needs.image-tag }} + type=raw,value=${{ needs.resolve-tag.outputs.image_tag }} secrets: GH_PCKG_TOKEN: ${{ secrets.GH_PCKG_TOKEN }} From e756970feb37889dd61f786bd385e17b8a0ed8c5 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:09:19 +0100 Subject: [PATCH 49/68] . --- .github/workflows/docker-image-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 5a31ffc0..18790501 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -93,7 +93,6 @@ jobs: flavor: | latest=false tags: | - type=sha type=raw,value=${{ needs.resolve-tag.outputs.image_tag }} secrets: GH_PCKG_TOKEN: ${{ secrets.GH_PCKG_TOKEN }} From 614d17bc06eb79ecb81dd7b4c4b9cd6d12318590 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:11:28 +0100 Subject: [PATCH 50/68] change image format --- .github/workflows/docker-image-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker-image-dev.yml b/.github/workflows/docker-image-dev.yml index 18790501..bd7db9fd 100644 --- a/.github/workflows/docker-image-dev.yml +++ b/.github/workflows/docker-image-dev.yml @@ -54,7 +54,7 @@ jobs: run: | set -euo pipefail - PREFIX="${{ inputs.base-tag }}-${{ inputs.type }}-" + PREFIX="${{ inputs.base-tag }}-${{ inputs.type }}" IMAGE=${{ github.repository_owner }}/${{ github.event.repository.name }} From 9bd29c0adda84fd956422286043e6753ab5e76d1 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:26:02 +0100 Subject: [PATCH 51/68] update go to v1.25.6 --- go.mod | 10 +++++----- go.sum | 16 ++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 15277194..148d82e2 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module github.com/codeshelldev/secured-signal-api -go 1.25.5 +go 1.25.6 require ( github.com/codeshelldev/gotl/pkg/configutils v0.0.7 github.com/codeshelldev/gotl/pkg/docker v0.0.2 github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 github.com/codeshelldev/gotl/pkg/logger v0.0.6 - github.com/codeshelldev/gotl/pkg/pretty v0.0.9-3 + github.com/codeshelldev/gotl/pkg/pretty v0.0.9 github.com/codeshelldev/gotl/pkg/query v0.0.3 - github.com/codeshelldev/gotl/pkg/request v0.0.3 + github.com/codeshelldev/gotl/pkg/request v0.0.4 github.com/codeshelldev/gotl/pkg/stringutils v0.0.3 github.com/codeshelldev/gotl/pkg/templating v0.0.3 ) @@ -22,14 +22,14 @@ require ( require ( github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.1 // indirect + github.com/clipperhouse/uax29/v2 v2.4.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect github.com/knadh/koanf/providers/confmap v1.0.0 // indirect github.com/knadh/koanf/providers/env/v2 v2.0.0 // indirect github.com/knadh/koanf/providers/file v1.2.1 // indirect - github.com/knadh/koanf/v2 v2.3.1 // indirect + github.com/knadh/koanf/v2 v2.3.2 // indirect github.com/mattn/go-runewidth v0.0.19 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index bdf365c9..79ab68a7 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.1 h1:RjM8gnVbFbgI67SBekIC7ihFpyXwRPYWXn9BZActHbw= -github.com/clipperhouse/uax29/v2 v2.3.1/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/uax29/v2 v2.4.0 h1:RXqE/l5EiAbA4u97giimKNlmpvkmz+GrBVTelsoXy9g= +github.com/clipperhouse/uax29/v2 v2.4.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/codeshelldev/gotl/pkg/configutils v0.0.7 h1:54zv82v87xCrilbi7gfVFpC37dIpR4a9PXw8pYs1Few= github.com/codeshelldev/gotl/pkg/configutils v0.0.7/go.mod h1:WoWMBB8+84ePRnI2m+kbq1Rw8F/9iCWLHkVBsun3Qjc= github.com/codeshelldev/gotl/pkg/docker v0.0.2 h1:kpseReocEBoSzWe/tOhUrIrOYeAR/inw3EF2/d+N078= @@ -12,12 +12,12 @@ github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2 h1:ERsjkaWVrsyUZoEunCEeNYDXhua github.com/codeshelldev/gotl/pkg/jsonutils v0.0.2/go.mod h1:oxgKaAoMu6iYVHfgR7AhkK22xbYx4K0KCkyVEfYVoWs= github.com/codeshelldev/gotl/pkg/logger v0.0.6 h1:heo6z6yZm5PpX78vxud9HJNfVU9J46HVlW8T4EOy9nQ= github.com/codeshelldev/gotl/pkg/logger v0.0.6/go.mod h1:pL/I7KYxbGHhyedallZlCkBvoalv9gAWNEYVXbF9BoM= -github.com/codeshelldev/gotl/pkg/pretty v0.0.9-3 h1:1zj79w0L+gWiXZhZi8GGzeJwNt3wdhTlV/iDslChDKM= -github.com/codeshelldev/gotl/pkg/pretty v0.0.9-3/go.mod h1:2Gk6UBrtkIME2RCSNytS/RJ5lHXYL/MDx0rYRpknobM= +github.com/codeshelldev/gotl/pkg/pretty v0.0.9 h1:YgAZ0QpV+cUCKeLz5T0XFVByM8BXrJuz5KKLE0a6o1Y= +github.com/codeshelldev/gotl/pkg/pretty v0.0.9/go.mod h1:2Gk6UBrtkIME2RCSNytS/RJ5lHXYL/MDx0rYRpknobM= github.com/codeshelldev/gotl/pkg/query v0.0.3 h1:Zy8k0R5HcJS00OMPRHybgFEiwMg7ceLyv6bA0G7NOfs= github.com/codeshelldev/gotl/pkg/query v0.0.3/go.mod h1:kKaPOKXluIid3qeS7xzrmfq3NxIa8/PhKYHM6GRbwJw= -github.com/codeshelldev/gotl/pkg/request v0.0.3 h1:maRPHu366NARow8/m1Q8Cw1EU1Uy0pDIn1vlAsOatKM= -github.com/codeshelldev/gotl/pkg/request v0.0.3/go.mod h1:vCXIZ2n/XxvEVInBQv9eIh0kQ2353V+WymL8kZ9yrOU= +github.com/codeshelldev/gotl/pkg/request v0.0.4 h1:QyuKTJd/jGrG0XYkFyIc8JmfS4kMlqeTj6kpsYtbXfI= +github.com/codeshelldev/gotl/pkg/request v0.0.4/go.mod h1:vCXIZ2n/XxvEVInBQv9eIh0kQ2353V+WymL8kZ9yrOU= github.com/codeshelldev/gotl/pkg/stringutils v0.0.3 h1:7k/HMnX7me8Kchm41I/M6dp3wXI0XORI3oyS87O0Viw= github.com/codeshelldev/gotl/pkg/stringutils v0.0.3/go.mod h1:/dWlzYoTj23LmpFs+Bpal4tfUDbOVeApIgkLv9gTgUE= github.com/codeshelldev/gotl/pkg/templating v0.0.3 h1:PAz6VN8yGBuZIdR/sDM+TmW1OFykl+I7/Zwa07uMgYA= @@ -38,8 +38,8 @@ github.com/knadh/koanf/providers/env/v2 v2.0.0 h1:Ad5H3eun722u+FvchiIcEIJZsZ2M6o github.com/knadh/koanf/providers/env/v2 v2.0.0/go.mod h1:1g01PE+Ve1gBfWNNw2wmULRP0tc8RJrjn5p2N/jNCIc= github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP3AESOJYp9wM= github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= -github.com/knadh/koanf/v2 v2.3.1 h1:2uTWFib/W7LAaAH88C2Qa5woBW/efhhcy23FnkUiyuQ= -github.com/knadh/koanf/v2 v2.3.1/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= +github.com/knadh/koanf/v2 v2.3.2 h1:Ee6tuzQYFwcZXQpc2MiVeC6qHMandf5SMUJJNoFp/c4= +github.com/knadh/koanf/v2 v2.3.2/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From 10c94447d4781ae648167af222678e157ea00d09 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:26:12 +0100 Subject: [PATCH 52/68] update deprecation --- internals/config/parser.go | 124 ++----------------- internals/config/structure/structure.go | 6 +- utils/deprecation/deprecation.go | 152 ++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 116 deletions(-) create mode 100644 utils/deprecation/deprecation.go diff --git a/internals/config/parser.go b/internals/config/parser.go index 1f530b17..db4b9d45 100644 --- a/internals/config/parser.go +++ b/internals/config/parser.go @@ -1,11 +1,10 @@ package config import ( - "fmt" "strings" "github.com/codeshelldev/gotl/pkg/configutils" - "github.com/codeshelldev/gotl/pkg/pretty" + "github.com/codeshelldev/secured-signal-api/utils/deprecation" ) var transformFuncs = map[string]func(string, any) (string, any) { @@ -33,125 +32,24 @@ var onUseFuncs = map[string]func(source string, target configutils.TransformTarg }, } -var deprecationHandledMap = map[string]bool{} - func deprecationHandler(source string, target configutils.TransformTarget) { - handled, _ := deprecationHandledMap[source] - - if handled { - return - } - - deprecationHandledMap[source] = true - msgMap := configutils.ParseTag(target.Source.Tag.Get("deprecation")) - box := pretty.NewAutoBox() - box.MinWidth = 60 - box.PaddingX = 2 - box.PaddingY = 1 - - box.Border.Style = pretty.BorderStyle{ - Color: pretty.Basic(pretty.Yellow), - } - - messageParts := strings.Split(configutils.GetValueWithSource(source, target.Parent, msgMap), "\n") - messageSegments := []pretty.Segment{} - - for _, part := range messageParts { - messageSegments = append(messageSegments, pretty.StyledTextBlockSegment{ - Raw: part, - }) - } + message := configutils.GetValueWithSource(source, target.Parent, msgMap) atRoot := !strings.Contains(source, ".") - refrainPrefix := "" - refrainSuffix := "" + usingPrefix := "" + usingSuffix := "" if atRoot { - refrainPrefix = "⇧ " - refrainSuffix = " (at root)" + usingPrefix = "⇧ " + usingSuffix = " (at root)" } - box.AddBlock(pretty.Block{ - Align: pretty.AlignCenter, - Style: pretty.Style{}, - Segments: []pretty.Segment{ - pretty.TextBlockSegment{ - Text: "🚨 Deprecation 🚨", - Style: pretty.Style{ - Bold: true, - Foreground: pretty.Basic(pretty.Yellow), - }, - }, - pretty.InlineSegment{}, - pretty.TextBlockSegment{ - Text: "Please refrain from using", - }, - pretty.InlineSegment{}, - pretty.InlineSegment{ - Items: []pretty.Inline{ - pretty.Span{ - Text: refrainPrefix, - Style: pretty.Style{ - Bold: true, - Foreground: pretty.Basic(pretty.BrightWhite), - }, - }, - pretty.Span{ - Text: "`" + source + "`", - Style: pretty.Style{ - Italic: true, - Bold: true, - Background: pretty.Basic(pretty.Red), - }, - }, - pretty.Span{ - Text: refrainSuffix, - }, - }, - }, - pretty.InlineSegment{}, - pretty.InlineSegment{ - Items: []pretty.Inline{ - pretty.Span{ - Text: "as it has been marked as ", - }, - pretty.Span{ - Text: "deprecated", - Style: pretty.Style{ - Bold: true, - }, - }, - pretty.Span{ - Text: ":", - }, - }, - }, - pretty.InlineSegment{}, - }, - }) - - box.AddBlock(pretty.Block{ - Segments: messageSegments, - Align: pretty.AlignCenter, - Style: pretty.Style{ - Background: pretty.Basic(pretty.BrightBlack), - }, + deprecation.Warn(source, deprecation.DeprecationMessage{ + Using: "{b,fg=bright_white}" + usingPrefix + "{/}{b,i,bg=red}`" + source + "`{/}" + usingSuffix, + Message: message, + Fix: "", + Note: "\n{i}Update your config before the next update,{/}\n{i}where it will be removed for good{/}", }) - - box.AddBlock(pretty.Block{ - Align: pretty.AlignCenter, - Segments: []pretty.Segment{ - pretty.InlineSegment{}, - pretty.TextBlockSegment{ - Text: "Update your config before the next update,\nwhere it will be removed for good", - Style: pretty.Style{ - Italic: true, - }, - }, - }, - }) - - fmt.Println(box.Render()) } \ No newline at end of file diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 72639cf8..f67204e6 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,7 +14,7 @@ type CONFIG struct { NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` - SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,s,fg=orange}\x60overrides\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` + SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,fg=orange}\x60{s}overrides{/}\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` } type SERVICE struct { @@ -25,13 +25,13 @@ type SERVICE struct { type API struct { URL string `koanf:"url" env>aliases:".apiurl"` - TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,s,fg=orange}\x60tokens\x60{/} and {b,s,fg=orange}\x60token\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,s,fg=orange}\x60api.token\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,fg=orange}\x60{s}tokens{/}\x60{/} and {b,fg=orange}\x60{s}token{/}\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,fg=orange}\x60{s}api.token{/}\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` AUTH AUTH `koanf:"auth"` } type AUTH struct { METHODS []string `koanf:"methods" env>aliases:".authmethods"` - TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,s,fg=orange}\x60api.auth.token\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` + TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,fg=orange}\x60{s}api.auth.token{/}\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` } type Token struct { diff --git a/utils/deprecation/deprecation.go b/utils/deprecation/deprecation.go new file mode 100644 index 00000000..90f8d9e1 --- /dev/null +++ b/utils/deprecation/deprecation.go @@ -0,0 +1,152 @@ +package deprecation + +import ( + "fmt" + + "github.com/codeshelldev/gotl/pkg/pretty" +) + +type DeprecationMessage struct { + Using string + Message string + Fix string + Note string +} + +var deprecationMap = map[string]DeprecationMessage{} + +func base(id string, title, beforeUsing, afterUsing pretty.Segment, borderStyle pretty.BorderStyle, msg DeprecationMessage) { + _, exists := deprecationMap[id] + + if exists { + return + } + + deprecationMap[id] = msg + + box := pretty.NewAutoBox() + box.MinWidth = 60 + box.PaddingX = 2 + box.PaddingY = 1 + + box.Border.Style = borderStyle + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + title, + pretty.InlineSegment{}, + beforeUsing, + pretty.InlineSegment{}, + pretty.StyledTextBlockSegment{ + Raw: msg.Using, + }, + pretty.InlineSegment{}, + afterUsing, + pretty.InlineSegment{}, + }, + }) + + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.StyledTextBlockSegment{ + Raw: msg.Message, + }, + }, + }) + + if msg.Fix != "" { + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.StyledTextBlockSegment{ + Raw: msg.Fix, + }, + }, + }) + } + + if msg.Note != "" { + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + pretty.StyledTextBlockSegment{ + Raw: msg.Note, + }, + }, + }) + } + + fmt.Println(box.Render()) +} + +func Warn(id string, msg DeprecationMessage) { + base(id, + pretty.TextBlockSegment{ + Text: "🚧 Deprecation 🚧", + Style: pretty.Style{ + Bold: true, + Foreground: pretty.Basic(pretty.Yellow), + }, + }, + pretty.TextBlockSegment{ + Text: "Please refrain from using", + }, + pretty.InlineSegment{ + Items: []pretty.Inline{ + pretty.Span{ + Text: "as it has been marked as ", + }, + pretty.Span{ + Text: "deprecated", + Style: pretty.Style{ + Bold: true, + }, + }, + pretty.Span{ + Text: ":", + }, + }, + }, + pretty.BorderStyle{ + Color: pretty.Basic(pretty.Yellow), + }, + msg, + ) +} + +func Error(id string, msg DeprecationMessage) { + base(id, + pretty.TextBlockSegment{ + Text: "🚨 Breaking Change 🚨", + Style: pretty.Style{ + Bold: true, + Foreground: pretty.Basic(pretty.Red), + }, + }, + pretty.TextBlockSegment{ + Text: "Please stop using", + }, + pretty.InlineSegment{ + Items: []pretty.Inline{ + pretty.Span{ + Text: "as it has been affected in a ", + }, + pretty.Span{ + Text: "breaking change", + Style: pretty.Style{ + Bold: true, + }, + }, + pretty.Span{ + Text: ":", + }, + }, + }, + pretty.BorderStyle{ + Color: pretty.Basic(pretty.Red), + }, + msg, + ) +} \ No newline at end of file From ff3c927d3bed40a2465bd639facf788cbf829353 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:26:32 +0100 Subject: [PATCH 53/68] add `@authorization` breaking change --- internals/proxy/middlewares/auth.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 3078d3da..e73a3fd5 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -13,6 +13,7 @@ import ( "github.com/codeshelldev/gotl/pkg/request" "github.com/codeshelldev/secured-signal-api/internals/config" "github.com/codeshelldev/secured-signal-api/internals/config/structure" + "github.com/codeshelldev/secured-signal-api/utils/deprecation" ) var Auth Middleware = Middleware{ @@ -141,6 +142,22 @@ var QueryAuth = AuthMethod{ auth := req.URL.Query().Get("@" + authQuery) + // todo breaking-start + //* = v1.5.0 + const oldAuthQuery = "authorization" + + if req.URL.Query().Has("@" + oldAuthQuery) { + fullURL, _ := request.ParseRequestURL(req) + urlWithNewAuthQuery := strings.Replace(fullURL.String(), "@" + oldAuthQuery, "@{s,fg=bright_red}" + oldAuthQuery + "{/}{b,fg=green}" + authQuery + "{/}", 1) + + deprecation.Error(req.URL.String(), deprecation.DeprecationMessage{ + Using: "{b,i,bg=red}`@authorization`{/} in the query", + Message: "{b,fg=red}`/?@{s}authorization{/}`{/} has been renamed to {b,fg=green}`/?@auth`{}", + Fix: "\nChange the {b}url{/} to:\n`" + urlWithNewAuthQuery + "`", + }) + } + // todo breaking-end + if strings.TrimSpace(auth) == "" { return "", nil } From 10f1ac5de6dd020ab9779a774086162ff3794a9b Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:30:21 +0100 Subject: [PATCH 54/68] remove Authorization header after auth middleware --- internals/proxy/middlewares/auth.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index e73a3fd5..7d4b2e4b 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -41,6 +41,8 @@ var BearerAuth = AuthMethod{ } if strings.ToLower(headerParts[0]) == "bearer" { + req.Header.Del("Authorization") + if isValidToken(tokens, headerParts[1]) { return headerParts[1], nil } @@ -68,6 +70,8 @@ var BasicAuth = AuthMethod{ } if strings.ToLower(headerParts[0]) == "basic" { + req.Header.Del("Authorization") + base64Bytes, err := base64.StdEncoding.DecodeString(headerParts[1]) if err != nil { From 67f248776a38a4502cc80a70ba45986a15219f81 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:33:13 +0100 Subject: [PATCH 55/68] fix default auth methods not being applied --- internals/proxy/middlewares/auth.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 7d4b2e4b..c9ffa1df 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -146,8 +146,7 @@ var QueryAuth = AuthMethod{ auth := req.URL.Query().Get("@" + authQuery) - // todo breaking-start - //* = v1.5.0 + // BREAKING @authorization Query const oldAuthQuery = "authorization" if req.URL.Query().Has("@" + oldAuthQuery) { @@ -160,7 +159,6 @@ var QueryAuth = AuthMethod{ Fix: "\nChange the {b}url{/} to:\n`" + urlWithNewAuthQuery + "`", }) } - // todo breaking-end if strings.TrimSpace(auth) == "" { return "", nil @@ -284,10 +282,20 @@ func authHandler(next http.Handler) http.Handler { allowedMethods = getConfig("").API.AUTH.METHODS } - if isAuthMethodAllowed(method, token, conf.API.TOKENS, conf.API.AUTH.METHODS, conf.API.AUTH.TOKENS) { + if isAuthMethodAllowed(method, token, conf.API.TOKENS, allowedMethods, conf.API.AUTH.TOKENS) { req = setContext(req, isAuthKey, true) req = setContext(req, tokenKey, token) } else { + // BREAKING Query & Path auth disabled (default) + if (method.Name == "Path" || method.Name == "Query") && conf.API.AUTH.METHODS == nil { + deprecation.Error(method.Name, deprecation.DeprecationMessage{ + Message: "{b}Query{/} and {b}Path{/} auth are {u}disabled{/} by default\nTo be able to use them they must first be enabled", + Fix: "\n{b}Add{/} {b,fg=green}`" + strings.ToLower(method.Name) + "`{/} to {i}`api.auth.methods`{/}:" + + "\napi.auth.methods: [" + strings.Join(append(allowedMethods, "{b,fg=green}" + strings.ToLower(method.Name) + "{/}"), ", ") + "]", + Note: "\n{i}Let us know what you think about this change at\n{i}{u,fg=blue}https://github.com/CodeShellDev/secured-signal-api/discussions/221{/}{/}", + }) + } + logger.Warn("Client tried using disabled auth method: ", method.Name) onUnauthorized(w) From 53fb06c41af0b48253e4059f26b32e2e61810267 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:33:25 +0100 Subject: [PATCH 56/68] more deprecation message (improvements) --- internals/config/structure/structure.go | 4 ++++ utils/deprecation/deprecation.go | 24 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index f67204e6..927be76d 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,6 +14,7 @@ type CONFIG struct { NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` + // DEPRECATION overrides in Token Config SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,fg=orange}\x60{s}overrides{/}\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` } @@ -25,12 +26,15 @@ type SERVICE struct { type API struct { URL string `koanf:"url" env>aliases:".apiurl"` + // DEPRECATION token, tokens in Token Config + // DEPRECATION api.token => api.tokens TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,fg=orange}\x60{s}tokens{/}\x60{/} and {b,fg=orange}\x60{s}token{/}\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,fg=orange}\x60{s}api.token{/}\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` AUTH AUTH `koanf:"auth"` } type AUTH struct { METHODS []string `koanf:"methods" env>aliases:".authmethods"` + // DEPRECATION auth.token => auth.tokens TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,fg=orange}\x60{s}api.auth.token{/}\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` } diff --git a/utils/deprecation/deprecation.go b/utils/deprecation/deprecation.go index 90f8d9e1..1aa3327c 100644 --- a/utils/deprecation/deprecation.go +++ b/utils/deprecation/deprecation.go @@ -36,17 +36,25 @@ func base(id string, title, beforeUsing, afterUsing pretty.Segment, borderStyle Segments: []pretty.Segment{ title, pretty.InlineSegment{}, - beforeUsing, - pretty.InlineSegment{}, - pretty.StyledTextBlockSegment{ - Raw: msg.Using, - }, - pretty.InlineSegment{}, - afterUsing, - pretty.InlineSegment{}, }, }) + if msg.Using != "" { + box.AddBlock(pretty.Block{ + Align: pretty.AlignCenter, + Segments: []pretty.Segment{ + beforeUsing, + pretty.InlineSegment{}, + pretty.StyledTextBlockSegment{ + Raw: msg.Using, + }, + pretty.InlineSegment{}, + afterUsing, + pretty.InlineSegment{}, + }, + }) + } + box.AddBlock(pretty.Block{ Align: pretty.AlignCenter, Segments: []pretty.Segment{ From bc68083da779e990626c3240d29a657cb9b30192 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:23:00 +0100 Subject: [PATCH 57/68] use basic colors (due to limited feats of some consoles) --- internals/config/structure/structure.go | 6 +++--- utils/deprecation/deprecation.go | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 927be76d..aa30ab83 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -15,7 +15,7 @@ type CONFIG struct { SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` // DEPRECATION overrides in Token Config - SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,fg=orange}\x60{s}overrides{/}\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` + SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,fg=yellow}\x60{s}overrides{/}\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` } type SERVICE struct { @@ -28,14 +28,14 @@ type API struct { URL string `koanf:"url" env>aliases:".apiurl"` // DEPRECATION token, tokens in Token Config // DEPRECATION api.token => api.tokens - TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,fg=orange}\x60{s}tokens{/}\x60{/} and {b,fg=orange}\x60{s}token{/}\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,fg=orange}\x60{s}api.token{/}\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` + TOKENS []string `koanf:"tokens" env>aliases:".apitokens,.apitoken" aliases:"token" token>aliases:".tokens,.token" token>onuse:".tokens,.token,token>>deprecated" onuse:"token>>deprecated" deprecation:".tokens,.token>>{b,fg=yellow}\x60{s}tokens{/}\x60{/} and {b,fg=yellow}\x60{s}token{/}\x60{/} will not be at {b}root{/} anymore\nUse {b,fg=green}\x60api.tokens\x60{/} instead|token>>{b,fg=yellow}\x60{s}api.token{/}\x60{/} will be {u}removed{/} in favor of {b,fg=green}\x60api.tokens\x60{/}"` AUTH AUTH `koanf:"auth"` } type AUTH struct { METHODS []string `koanf:"methods" env>aliases:".authmethods"` // DEPRECATION auth.token => auth.tokens - TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,fg=orange}\x60{s}api.auth.token{/}\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` + TOKENS []Token `koanf:"tokens" aliases:"token" onuse:"token>>deprecated" deprecation:"{b,fg=yellow}\x60{s}api.auth.token{/}\x60{/} will be removed\nUse {b,fg=green}\x60api.auth.tokens\x60{/} instead"` } type Token struct { diff --git a/utils/deprecation/deprecation.go b/utils/deprecation/deprecation.go index 1aa3327c..46c086ef 100644 --- a/utils/deprecation/deprecation.go +++ b/utils/deprecation/deprecation.go @@ -95,7 +95,7 @@ func Warn(id string, msg DeprecationMessage) { Text: "🚧 Deprecation 🚧", Style: pretty.Style{ Bold: true, - Foreground: pretty.Basic(pretty.Yellow), + Foreground: pretty.Basic(pretty.BrightYellow), }, }, pretty.TextBlockSegment{ @@ -118,7 +118,7 @@ func Warn(id string, msg DeprecationMessage) { }, }, pretty.BorderStyle{ - Color: pretty.Basic(pretty.Yellow), + Color: pretty.Basic(pretty.BrightYellow), }, msg, ) @@ -130,7 +130,7 @@ func Error(id string, msg DeprecationMessage) { Text: "🚨 Breaking Change 🚨", Style: pretty.Style{ Bold: true, - Foreground: pretty.Basic(pretty.Red), + Foreground: pretty.Basic(pretty.BrightRed), }, }, pretty.TextBlockSegment{ @@ -153,7 +153,7 @@ func Error(id string, msg DeprecationMessage) { }, }, pretty.BorderStyle{ - Color: pretty.Basic(pretty.Red), + Color: pretty.Basic(pretty.BrightRed), }, msg, ) From 5389b3a78337feb41f7dfae1ee8784c9846a6e89 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:23:19 +0100 Subject: [PATCH 58/68] fix `/auth=TOKEN` not being stripped --- internals/proxy/middlewares/auth.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index c9ffa1df..f250e71e 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -183,11 +183,13 @@ var PathAuth = AuthMethod{ Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { parts := strings.Split(req.URL.Path, "/") - if len(parts) == 0 { + if len(parts) <= 1 { return "", nil } - unescaped, err := url.PathUnescape(parts[1]) + parts = parts[1:] + + unescaped, err := url.PathUnescape(parts[0]) if err != nil { return "", nil @@ -199,6 +201,8 @@ var PathAuth = AuthMethod{ return "", nil } + req.URL.Path = "/" + strings.Join(parts[1:], "/") + if isValidToken(tokens, auth) { return auth, nil } From 973d0faf0de918446a26cd96abf796e9f1015ca5 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:18:25 +0100 Subject: [PATCH 59/68] fix mockserver using wrong env for port --- utils/mockserver/mockserver.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/mockserver/mockserver.go b/utils/mockserver/mockserver.go index 18d0e604..6d955627 100644 --- a/utils/mockserver/mockserver.go +++ b/utils/mockserver/mockserver.go @@ -18,7 +18,7 @@ func main() { logLevel = "info" } - port := os.Getenv("PORT") + port := os.Getenv("MOCK_PORT") if strings.TrimSpace(port) == "" { port = "8881" From ed8432bf3f4b5c540e08b0a3b21b69798e77a524 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:23:24 +0100 Subject: [PATCH 60/68] fix additional warn message after client failed at some auth --- internals/proxy/middlewares/auth.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index f250e71e..45e4f3af 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -236,11 +236,10 @@ func (chain *AuthChain) Eval(w http.ResponseWriter, req *http.Request, tokens [] if err != nil { logger.Warn("Client failed ", method.Name, " auth: ", err.Error()) + return AuthMethod{}, "", err } if token != "" { - - return method, token, nil } } From 2ed82f490e8765450e7cf2265852f2cc051699fd Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:46:45 +0100 Subject: [PATCH 61/68] . --- internals/proxy/middlewares/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index 4dae1006..c3c6bd33 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -2,6 +2,7 @@ package middlewares import ( "encoding/base64" + "errors" "net/http" "net/url" "slices" From 6c6721e96c633f31fcfd8603ccd6e4cfd2482255 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:13:27 +0100 Subject: [PATCH 62/68] fix port middleware not serving next middleware when port is OK --- internals/proxy/middlewares/port.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internals/proxy/middlewares/port.go b/internals/proxy/middlewares/port.go index 84d30d9f..fc708178 100644 --- a/internals/proxy/middlewares/port.go +++ b/internals/proxy/middlewares/port.go @@ -38,6 +38,8 @@ func portHandler(next http.Handler) http.Handler { onUnauthorized(w) return } + + next.ServeHTTP(w, req) }) } From 56ea8f96e1af5ea73d950898dcacc52bfd154482 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:14:05 +0100 Subject: [PATCH 63/68] fix main config creating sublogger --- internals/config/loader.go | 2 ++ internals/config/structure/structure.go | 10 +++++++++- internals/config/tokens.go | 2 ++ internals/proxy/middlewares/log.go | 3 ++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/internals/config/loader.go b/internals/config/loader.go index ab81a2a9..1225a60e 100644 --- a/internals/config/loader.go +++ b/internals/config/loader.go @@ -142,6 +142,8 @@ func InitConfig() { mainConf.Layer.Unmarshal("", &config) + config.TYPE = structure.MAIN + ENV.CONFIGS["*"] = &config DEFAULT = ENV.CONFIGS["*"] diff --git a/internals/config/structure/structure.go b/internals/config/structure/structure.go index 95b76c69..2197d146 100644 --- a/internals/config/structure/structure.go +++ b/internals/config/structure/structure.go @@ -14,12 +14,20 @@ type ENV struct { } type CONFIG struct { + TYPE ConfigType NAME string `koanf:"name"` SERVICE SERVICE `koanf:"service"` API API `koanf:"api"` // DEPRECATION overrides in Token Config SETTINGS SETTINGS `koanf:"settings" token>aliases:"overrides" token>onuse:".overrides>>deprecated" deprecation:"{b,fg=yellow}\x60{s}overrides{/}\x60{/} is no longer needed in {b}Token Configs{/}\nUse {b,fg=green}\x60settings\x60{/} instead"` -} +} + +type ConfigType string + +const ( + TOKEN ConfigType = "token" + MAIN ConfigType = "main" +) type SERVICE struct { HOSTNAMES []string `koanf:"hostnames" env>aliases:".hostnames"` diff --git a/internals/config/tokens.go b/internals/config/tokens.go index 06b685ae..54413138 100644 --- a/internals/config/tokens.go +++ b/internals/config/tokens.go @@ -56,6 +56,8 @@ func InitTokens() { for token, config := range config { apiTokens = append(apiTokens, token) + config.TYPE = structure.TOKEN + ENV.CONFIGS[token] = &config } diff --git a/internals/proxy/middlewares/log.go b/internals/proxy/middlewares/log.go index 95557327..1c9e12f5 100644 --- a/internals/proxy/middlewares/log.go +++ b/internals/proxy/middlewares/log.go @@ -7,6 +7,7 @@ import ( "github.com/codeshelldev/gotl/pkg/logger" "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/secured-signal-api/internals/config/structure" ) var RequestLogger Middleware = Middleware{ @@ -49,7 +50,7 @@ func middlewareLoggerHandler(next http.Handler) http.Handler { var logLevel string - if conf != nil { + if conf != nil && conf.TYPE != structure.MAIN { logLevel = conf.SERVICE.LOG_LEVEL } From da4fbfecd4dbdcabb39d33f516d630922bb05615 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 19:43:22 +0100 Subject: [PATCH 64/68] treat `192.168.1.10/24` as `192.168.1.0/24` --- internals/proxy/middlewares/proxy.go | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go index 599d403c..c4caaf89 100644 --- a/internals/proxy/middlewares/proxy.go +++ b/internals/proxy/middlewares/proxy.go @@ -34,7 +34,7 @@ func proxyHandler(next http.Handler) http.Handler { host, _, _ := net.SplitHostPort(req.RemoteAddr) - originUrl := req.Proto + "://" + req.URL.Host + originUrl := req.Proto + "://" + req.Host ip = net.ParseIP(host) @@ -102,23 +102,12 @@ func parseIP(str string) (*net.IPNet, error) { return &net.IPNet{IP: ip, Mask: mask}, nil } - ip, network, err := net.ParseCIDR(str) + _, network, err := net.ParseCIDR(str) + if err != nil { return nil, err } - if !ip.Equal(network.IP) { - var mask net.IPMask - - if ip.To4() != nil { - mask = net.CIDRMask(32, 32) // IPv4 /32 - } else { - mask = net.CIDRMask(128, 128) // IPv6 /128 - } - - return &net.IPNet{IP: ip, Mask: mask}, nil - } - return network, nil } From bc48506d145007c308ef08a5e2ce10085b50a500 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 20:43:12 +0100 Subject: [PATCH 65/68] improve trusted proxy handling --- internals/proxy/middlewares/proxy.go | 179 +++++++++++++++++++++------ 1 file changed, 142 insertions(+), 37 deletions(-) diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go index c4caaf89..7d0f8d3a 100644 --- a/internals/proxy/middlewares/proxy.go +++ b/internals/proxy/middlewares/proxy.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/url" + "slices" "strings" ) @@ -13,10 +14,21 @@ var InternalProxy Middleware = Middleware{ Use: proxyHandler, } -const trustedProxyKey contextKey = "isProxyTrusted" const clientIPKey contextKey = "clientIP" const originURLKey contextKey = "originURL" +type ForwardedEntry struct { + For string + Host string + Proto string +} + +type OriginInfo struct { + IP net.IP + Host string + Proto string +} + func proxyHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { logger := getLogger(req) @@ -29,41 +41,44 @@ func proxyHandler(next http.Handler) http.Handler { rawTrustedProxies = getConfig("").SETTINGS.ACCESS.TRUSTED_PROXIES } - var trusted bool var ip net.IP - - host, _, _ := net.SplitHostPort(req.RemoteAddr) - - originUrl := req.Proto + "://" + req.Host - - ip = net.ParseIP(host) + var originUrl string if len(rawTrustedProxies) != 0 { trustedProxies := parseIPsAndIPNets(rawTrustedProxies) - trusted = isIPInList(ip, trustedProxies) - } - - if trusted { - realIP, err := getRealIP(req) + var forwardedEntries []ForwardedEntry - if err != nil { - logger.Error("Could not get real IP: ", err.Error()) + if req.Header.Get("Forwarded") != "" { + forwardedEntries = parseForwarded(req.Header.Get("Forwarded")) + } else { + forwardedEntries = parseXForwardedHeaders(req.Header) } - if realIP != nil { - ip = realIP + if len(forwardedEntries) != 0 { + originInfo := getOriginFromForwarded(forwardedEntries, trustedProxies) + ip = originInfo.IP + + originUrl = originInfo.Proto + "://" + originInfo.Host } + } - XFHost := req.Header.Get("X-Forwarded-Host") - XFProto := req.Header.Get("X-Forwarded-Proto") - XFPort := req.Header.Get("X-Forwarded-Port") + if ip == nil { + host, _, _ := net.SplitHostPort(req.RemoteAddr) - if XFHost == "" || XFProto == "" || XFPort == "" { - logger.Warn("Missing X-Forwarded-* headers") - } + ip = net.ParseIP(host) + } + + if originUrl == "" { + originUrl = req.Proto + "://" + req.Host - originUrl = XFProto + "://" + XFHost + ":" + XFPort + if !strings.Contains(req.Host, ":") { + if req.Proto == "https" { + originUrl += ":443" + } else { + originUrl += ":80" + } + } } originURL, err := url.Parse(originUrl) @@ -74,7 +89,6 @@ func proxyHandler(next http.Handler) http.Handler { return } - req = setContext(req, trustedProxyKey, trusted) req = setContext(req, originURLKey, originURL) req = setContext(req, clientIPKey, ip) @@ -83,6 +97,44 @@ func proxyHandler(next http.Handler) http.Handler { }) } +func getOriginFromForwarded(entries []ForwardedEntry, trusted []*net.IPNet) OriginInfo { + var origin OriginInfo + + // reverse to place origin client last + slices.Reverse(entries) + + for _, entry := range entries { + ip := parseForIP(entry.For) + + if ip == nil { + continue + } + + // ip not trusted => use as client ip + if !isIPInList(ip, trusted) { + origin.IP = ip + origin.Proto = entry.Proto + origin.Host = entry.Host + break + } + } + + return origin +} + +func parseForIP(value string) net.IP { + value = strings.TrimSpace(value) + value = strings.Trim(value, `"`) + value = strings.Trim(value, "[]") + + host, _, err := net.SplitHostPort(value) + if err == nil { + value = host + } + + return net.ParseIP(value) +} + func parseIP(str string) (*net.IPNet, error) { if !strings.Contains(str, "/") { ip := net.ParseIP(str) @@ -127,22 +179,75 @@ func parseIPsAndIPNets(array []string) []*net.IPNet { return ipNets } -func getRealIP(req *http.Request) (net.IP, error) { - XFF := req.Header.Get("X-Forwarded-For") +func parseXForwardedHeaders(headers http.Header) []ForwardedEntry { + var entries []ForwardedEntry - if XFF != "" { - ips := strings.Split(XFF, ",") - - realIP := net.ParseIP(strings.TrimSpace(ips[0])) + XFF := headers.Get("X-Forwarded-For") + if XFF == "" { + return nil + } - if realIP == nil { - return nil, errors.New("malformed X-Forwarded-For header") - } + parts := strings.Split(XFF, ",") - return realIP, nil - } + XFProto := headers.Get("X-Forwarded-Proto") + XFHost := headers.Get("X-Forwarded-Host") + + for i, part := range parts { + ip := strings.TrimSpace(part) + if ip == "" { + continue + } + + entry := ForwardedEntry{ + For: ip, + } + + if i == 0 { + if XFProto != "" { + entry.Proto = XFProto + } + if XFHost != "" { + entry.Host = XFHost + } + } + + entries = append(entries, entry) + } + + return entries +} + +func parseForwarded(header string) []ForwardedEntry { + var entries []ForwardedEntry + + for part := range strings.SplitSeq(header, ",") { + entry := ForwardedEntry{} + params := strings.SplitSeq(part, ";") + + for param := range params { + keyValuePair := strings.SplitN(strings.TrimSpace(param), "=", 2) + + if len(keyValuePair) != 2 { + continue + } + + key := strings.ToLower(keyValuePair[0]) + value := strings.Trim(keyValuePair[1], `"`) + + switch key { + case "for": + entry.For = value + case "proto": + entry.Proto = value + case "host": + entry.Host = value + } + } + + entries = append(entries, entry) + } - return nil, errors.New("no X-Forwarded-For header present") + return entries } func isIPInList(ip net.IP, list []*net.IPNet) bool { From 71a3251dedcad3a2a4503712cc76402776ce4430 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Fri, 30 Jan 2026 22:33:10 +0100 Subject: [PATCH 66/68] feat: improve trusted proxy (#223) --- internals/proxy/middlewares/proxy.go | 75 ++++++++++------------------ 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/internals/proxy/middlewares/proxy.go b/internals/proxy/middlewares/proxy.go index 7d0f8d3a..b5daad56 100644 --- a/internals/proxy/middlewares/proxy.go +++ b/internals/proxy/middlewares/proxy.go @@ -5,7 +5,6 @@ import ( "net" "net/http" "net/url" - "slices" "strings" ) @@ -14,6 +13,7 @@ var InternalProxy Middleware = Middleware{ Use: proxyHandler, } +const trustedProxyKey contextKey = "isProxyTrusted" const clientIPKey contextKey = "clientIP" const originURLKey contextKey = "originURL" @@ -41,45 +41,36 @@ func proxyHandler(next http.Handler) http.Handler { rawTrustedProxies = getConfig("").SETTINGS.ACCESS.TRUSTED_PROXIES } + var trusted bool var ip net.IP - var originUrl string + + host, _, _ := net.SplitHostPort(req.RemoteAddr) + + originUrl := parseOrigin(req.Proto, req.Host) + + ip = net.ParseIP(host) if len(rawTrustedProxies) != 0 { trustedProxies := parseIPsAndIPNets(rawTrustedProxies) - var forwardedEntries []ForwardedEntry + trusted = isIPInList(ip, trustedProxies) + } + + if trusted { + var forwardedEntries []ForwardedEntry - if req.Header.Get("Forwarded") != "" { + if req.Header.Get("Forwarded") != "" { forwardedEntries = parseForwarded(req.Header.Get("Forwarded")) } else { forwardedEntries = parseXForwardedHeaders(req.Header) } - if len(forwardedEntries) != 0 { - originInfo := getOriginFromForwarded(forwardedEntries, trustedProxies) - ip = originInfo.IP - - originUrl = originInfo.Proto + "://" + originInfo.Host - } - } - - if ip == nil { - host, _, _ := net.SplitHostPort(req.RemoteAddr) - - ip = net.ParseIP(host) - } - - if originUrl == "" { - originUrl = req.Proto + "://" + req.Host + if len(forwardedEntries) != 0 { + ip = parseForIP(forwardedEntries[0].For) - if !strings.Contains(req.Host, ":") { - if req.Proto == "https" { - originUrl += ":443" - } else { - originUrl += ":80" - } - } - } + originUrl = parseOrigin(forwardedEntries[0].Proto, forwardedEntries[0].Host) + } + } originURL, err := url.Parse(originUrl) @@ -89,6 +80,7 @@ func proxyHandler(next http.Handler) http.Handler { return } + req = setContext(req, trustedProxyKey, trusted) req = setContext(req, originURLKey, originURL) req = setContext(req, clientIPKey, ip) @@ -97,29 +89,16 @@ func proxyHandler(next http.Handler) http.Handler { }) } -func getOriginFromForwarded(entries []ForwardedEntry, trusted []*net.IPNet) OriginInfo { - var origin OriginInfo - - // reverse to place origin client last - slices.Reverse(entries) - - for _, entry := range entries { - ip := parseForIP(entry.For) - - if ip == nil { - continue - } - - // ip not trusted => use as client ip - if !isIPInList(ip, trusted) { - origin.IP = ip - origin.Proto = entry.Proto - origin.Host = entry.Host - break +func parseOrigin(proto, host string) string { + if !strings.Contains(host, ":") { + if proto == "https" { + host += ":443" + } else { + host += ":80" } } - return origin + return proto + "://" + host } func parseForIP(value string) net.IP { From d4a3d88ee1aeae6754edb7f0e7c21dddaacd250b Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:15:05 +0100 Subject: [PATCH 67/68] use `@auth=TOKEN` in path auth instead of without `@` --- internals/proxy/middlewares/auth.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internals/proxy/middlewares/auth.go b/internals/proxy/middlewares/auth.go index c3c6bd33..641f45c5 100644 --- a/internals/proxy/middlewares/auth.go +++ b/internals/proxy/middlewares/auth.go @@ -180,6 +180,8 @@ var QueryAuth = AuthMethod{ var PathAuth = AuthMethod{ Name: "Path", Authenticate: func(w http.ResponseWriter, req *http.Request, tokens []string) (string, error) { + const authPath = "auth" + parts := strings.Split(req.URL.Path, "/") if len(parts) <= 1 { @@ -194,7 +196,7 @@ var PathAuth = AuthMethod{ return "", nil } - auth, exists := strings.CutPrefix(unescaped, "auth=") + auth, exists := strings.CutPrefix(unescaped, "@" + authPath + "=") if !exists { return "", nil From 1bcc4719d94f48139cfd868811b2adf1f926d703 Mon Sep 17 00:00:00 2001 From: CodeShell <122738806+CodeShellDev@users.noreply.github.com> Date: Sat, 31 Jan 2026 11:18:30 +0100 Subject: [PATCH 68/68] feat: URL to Body Injection (#224) --- internals/proxy/middlewares/template.go | 71 ++++++++++++++++++------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/internals/proxy/middlewares/template.go b/internals/proxy/middlewares/template.go index af7d1a2e..ef1235be 100644 --- a/internals/proxy/middlewares/template.go +++ b/internals/proxy/middlewares/template.go @@ -10,6 +10,7 @@ import ( jsonutils "github.com/codeshelldev/gotl/pkg/jsonutils" query "github.com/codeshelldev/gotl/pkg/query" request "github.com/codeshelldev/gotl/pkg/request" + "github.com/codeshelldev/gotl/pkg/stringutils" templating "github.com/codeshelldev/gotl/pkg/templating" "github.com/codeshelldev/secured-signal-api/utils/requestkeys" ) @@ -73,32 +74,37 @@ func templateHandler(next http.Handler) http.Handler { } } - if modifiedBody { - body.Data = bodyData + if req.URL.Path != "" { + var modified bool + var templated bool - err := body.Write(req) + req.URL.Path, bodyData, modified, templated, err = TemplatePath(req.URL, bodyData, variables) if err != nil { - logger.Error("Could not write to Request Body: ", err.Error()) - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return + logger.Error("Error Templating Path: ", err.Error()) } - logger.Debug("Applied Body Templating: ", body.Data) + if modified { + logger.Debug("Applied Path Templating: ", req.URL.Path) + } + + if templated { + modifiedBody = true + } } - if req.URL.Path != "" { - var modified bool + if modifiedBody { + body.Data = bodyData - req.URL.Path, modified, err = TemplatePath(req.URL, variables) + err := body.Write(req) if err != nil { - logger.Error("Error Templating Path: ", err.Error()) + logger.Error("Could not write to Request Body: ", err.Error()) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return } - if modified { - logger.Debug("Applied Path Templating: ", req.URL.Path) - } + logger.Debug("Applied Body Templating: ", body.Data) } next.ServeHTTP(w, req) @@ -208,26 +214,55 @@ func TemplateBody(body map[string]any, headers map[string][]string, VARIABLES ma return templatedData, modified, nil } -func TemplatePath(reqUrl *url.URL, VARIABLES any) (string, bool, error) { +func TemplatePath(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, bool, error) { var modified bool + var modifiedBody bool reqPath, err := url.PathUnescape(reqUrl.Path) if err != nil { - return reqUrl.Path, modified, err + return reqUrl.Path, data, false, false, err } reqPath, err = templating.RenderNormalizedTemplate("path", reqPath, VARIABLES) if err != nil { - return reqUrl.Path, modified, err + return reqUrl.Path, data, false, false, err + } + + parts := strings.Split(reqPath, "/") + newParts := []string{} + + for _, part := range parts { + newParts = append(newParts, part) + + keyValuePair := strings.SplitN(part, "=", 2) + + if len(keyValuePair) != 2 { + continue + } + + keyWithoutPrefix, match := strings.CutPrefix(keyValuePair[0], "@") + + if !match { + continue + } + + value := stringutils.ToType(keyValuePair[1]) + + data[keyWithoutPrefix] = value + modifiedBody = true + + newParts = newParts[:len(newParts) - 1] } + reqPath = strings.Join(newParts, "/") + if reqUrl.Path != reqPath { modified = true } - return reqPath, modified, nil + return reqPath, data, modified, modifiedBody, nil } func TemplateQuery(reqUrl *url.URL, data map[string]any, VARIABLES any) (string, map[string]any, bool, error) {