diff --git a/go.mod b/go.mod index 587b0ce81..8fb0e93cb 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,6 @@ require ( github.com/google/cel-go v0.26.1 github.com/google/uuid v1.6.0 github.com/gorilla/feeds v1.2.0 - github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.13.4 @@ -85,7 +84,6 @@ require ( github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/soheilhy/cmux v0.1.5 github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/sys v0.36.0 // indirect diff --git a/go.sum b/go.sum index 78986a869..c6e7b022e 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,9 @@ cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14= connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I= @@ -46,10 +44,6 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.6 h1:p3jIvqYwUZgu/XYeI48bJxOhvm47 github.com/aws/aws-sdk-go-v2/service/sts v1.38.6/go.mod h1:WtKK+ppze5yKPkZ0XwqIVWD4beCwv056ZbPQNoeHqM8= github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE= github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -59,38 +53,24 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= @@ -99,22 +79,14 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= -github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= @@ -131,16 +103,13 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= -github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -148,9 +117,6 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= -github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= @@ -166,13 +132,9 @@ github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -184,8 +146,6 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -200,116 +160,51 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0= golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4= golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g= google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= modernc.org/cc/v4 v4.26.4 h1:jPhG8oNjtTYuP2FA4YefTJ/wioNUGALmGuEWt7SUR6s= modernc.org/cc/v4 v4.26.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= diff --git a/server/auth/authenticator.go b/server/auth/authenticator.go index e494115a7..aa163dcc1 100644 --- a/server/auth/authenticator.go +++ b/server/auth/authenticator.go @@ -195,3 +195,46 @@ func validateAccessToken(token string, tokens []*storepb.AccessTokensUserSetting } return false } + +// UpdateSessionLastAccessed updates the last accessed time for a session. +// This implements sliding expiration - sessions remain valid as long as they're used. +// Should be called after successful session-based authentication. +func (a *Authenticator) UpdateSessionLastAccessed(ctx context.Context, userID int32, sessionID string) { + // Fire-and-forget update; failures are logged but don't block the request + _ = a.store.UpdateUserSessionLastAccessed(ctx, userID, sessionID, timestamppb.Now()) +} + +// AuthResult contains the result of an authentication attempt. +type AuthResult struct { + User *store.User + SessionID string // Non-empty if authenticated via session cookie + AccessToken string // Non-empty if authenticated via JWT +} + +// Authenticate tries to authenticate using the provided credentials. +// It tries session cookie first, then JWT token. +// Returns nil if no valid credentials are provided. +// On successful session auth, it also updates the session sliding expiration. +func (a *Authenticator) Authenticate(ctx context.Context, sessionCookie, authHeader string) *AuthResult { + // Try session cookie authentication first + if sessionCookie != "" { + user, err := a.AuthenticateBySession(ctx, sessionCookie) + if err == nil && user != nil { + _, sessionID, parseErr := ParseSessionCookieValue(sessionCookie) + if parseErr == nil && sessionID != "" { + a.UpdateSessionLastAccessed(ctx, user.ID, sessionID) + } + return &AuthResult{User: user, SessionID: sessionID} + } + } + + // Try JWT token authentication + if token := ExtractBearerToken(authHeader); token != "" { + user, err := a.AuthenticateByJWT(ctx, token) + if err == nil && user != nil { + return &AuthResult{User: user, AccessToken: token} + } + } + + return nil +} diff --git a/server/auth/context.go b/server/auth/context.go index dfcf946e6..7069ad38d 100644 --- a/server/auth/context.go +++ b/server/auth/context.go @@ -1,6 +1,10 @@ package auth -import "context" +import ( + "context" + + "github.com/usememos/memos/store" +) // ContextKey is the key type for context values. // Using a custom type prevents collisions with other packages. @@ -47,3 +51,22 @@ func GetAccessToken(ctx context.Context) string { } return "" } + +// SetUserInContext sets the authenticated user's information in the context. +// This is a simpler alternative to AuthorizeAndSetContext for cases where +// authorization is handled separately (e.g., HTTP middleware). +// +// Parameters: +// - user: The authenticated user +// - sessionID: Set if authenticated via session cookie (empty string otherwise) +// - accessToken: Set if authenticated via JWT token (empty string otherwise) +func SetUserInContext(ctx context.Context, user *store.User, sessionID, accessToken string) context.Context { + ctx = context.WithValue(ctx, UserIDContextKey, user.ID) + if sessionID != "" { + ctx = context.WithValue(ctx, SessionIDContextKey, sessionID) + } + if accessToken != "" { + ctx = context.WithValue(ctx, AccessTokenContextKey, accessToken) + } + return ctx +} diff --git a/server/router/api/v1/acl.go b/server/router/api/v1/acl.go deleted file mode 100644 index 9594d4795..000000000 --- a/server/router/api/v1/acl.go +++ /dev/null @@ -1,134 +0,0 @@ -package v1 - -// gRPC Authentication Interceptor -// -// This file implements the authentication interceptor for gRPC requests. -// It extracts credentials from gRPC metadata and delegates to the shared Authenticator. -// -// Authentication flow: -// 1. Extract session cookie or bearer token from metadata -// 2. Validate credentials using Authenticator -// 3. Check authorization (admin-only methods) -// 4. Set user context and proceed with request -// -// For public methods (defined in acl_config.go), authentication is skipped. - -import ( - "context" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - - "github.com/usememos/memos/server/auth" - "github.com/usememos/memos/store" -) - -// GRPCAuthInterceptor is the authentication interceptor for gRPC server. -// It validates incoming requests and sets user context for authenticated requests. -type GRPCAuthInterceptor struct { - authenticator *auth.Authenticator -} - -// NewGRPCAuthInterceptor creates a new gRPC authentication interceptor. -func NewGRPCAuthInterceptor(store *store.Store, secret string) *GRPCAuthInterceptor { - return &GRPCAuthInterceptor{ - authenticator: auth.NewAuthenticator(store, secret), - } -} - -// AuthenticationInterceptor is the unary interceptor for gRPC API. -// -// Authentication strategy (in priority order): -// 1. Session Cookie: "user_session" cookie with format "{userID}-{sessionID}" -// 2. Bearer Token: "Authorization: Bearer {jwt_token}" header -// 3. Public Methods: Allow without auth if method is in public allowlist -// 4. Reject: Return Unauthenticated error -// -// On successful authentication, context values are set: -// - auth.UserIDContextKey: The authenticated user's ID -// - auth.SessionIDContextKey: Session ID (cookie auth only) -// - auth.AccessTokenContextKey: JWT token (bearer auth only). -func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - md, ok := metadata.FromIncomingContext(ctx) - if !ok { - // If metadata is missing, only allow public methods - if IsPublicMethod(serverInfo.FullMethod) { - return handler(ctx, request) - } - return nil, status.Errorf(codes.Unauthenticated, "failed to parse metadata from incoming context") - } - - // Try session cookie authentication - if sessionCookie := extractSessionCookieFromMetadata(md); sessionCookie != "" { - user, err := in.authenticator.AuthenticateBySession(ctx, sessionCookie) - if err == nil && user != nil { - _, sessionID, err := auth.ParseSessionCookieValue(sessionCookie) - if err != nil { - // This should not happen since AuthenticateBySession already validated the cookie - // but handle it gracefully anyway - sessionID = "" - } - ctx, err = in.authenticator.AuthorizeAndSetContext(ctx, serverInfo.FullMethod, user, sessionID, "", IsAdminOnlyMethod) - if err != nil { - return nil, toGRPCError(err, codes.PermissionDenied) - } - return handler(ctx, request) - } - } - - // Try bearer token authentication - if token := extractBearerTokenFromMetadata(md); token != "" { - user, err := in.authenticator.AuthenticateByJWT(ctx, token) - if err == nil && user != nil { - ctx, err = in.authenticator.AuthorizeAndSetContext(ctx, serverInfo.FullMethod, user, "", token, IsAdminOnlyMethod) - if err != nil { - return nil, toGRPCError(err, codes.PermissionDenied) - } - return handler(ctx, request) - } - } - - // Allow public methods without authentication - if IsPublicMethod(serverInfo.FullMethod) { - return handler(ctx, request) - } - - return nil, status.Errorf(codes.Unauthenticated, "authentication required") -} - -// toGRPCError converts an error to a gRPC status error with the given code. -// If the error is already a gRPC status error, it is returned as-is. -func toGRPCError(err error, code codes.Code) error { - if err == nil { - return nil - } - if _, ok := status.FromError(err); ok { - return err - } - return status.Errorf(code, "%v", err) -} - -// extractSessionCookieFromMetadata extracts the session cookie value from gRPC metadata. -// Checks both "grpcgateway-cookie" (from gRPC-Gateway) and "cookie" (native gRPC). -// Returns empty string if no session cookie is found. -func extractSessionCookieFromMetadata(md metadata.MD) string { - // gRPC-Gateway puts cookies in "grpcgateway-cookie", native gRPC uses "cookie" - for _, cookieHeader := range append(md.Get("grpcgateway-cookie"), md.Get("cookie")...) { - if cookie := auth.ExtractSessionCookieFromHeader(cookieHeader); cookie != "" { - return cookie - } - } - return "" -} - -// extractBearerTokenFromMetadata extracts JWT token from Authorization header in gRPC metadata. -// Returns empty string if no valid bearer token is found. -func extractBearerTokenFromMetadata(md metadata.MD) string { - authHeaders := md.Get("Authorization") - if len(authHeaders) == 0 { - return "" - } - return auth.ExtractBearerToken(authHeaders[0]) -} diff --git a/server/router/api/v1/acl_config.go b/server/router/api/v1/acl_config.go index 5f8fbef9f..522fd2db6 100644 --- a/server/router/api/v1/acl_config.go +++ b/server/router/api/v1/acl_config.go @@ -1,56 +1,40 @@ package v1 -// Access Control List (ACL) Configuration +// PublicMethods defines API endpoints that don't require authentication. +// All other endpoints require a valid session or access token. // -// This file defines which API methods require authentication and which require admin privileges. -// Used by both gRPC and Connect interceptors to enforce access control. +// This is the SINGLE SOURCE OF TRUTH for public endpoints. +// Both Connect interceptor and gRPC-Gateway interceptor use this map. // -// Method names follow the gRPC full method format: "/{package}.{service}/{method}" -// Example: "/memos.api.v1.MemoService/CreateMemo" +// Format: Full gRPC procedure path as returned by req.Spec().Procedure (Connect) +// or info.FullMethod (gRPC interceptor). +var PublicMethods = map[string]struct{}{ + // Auth Service - login flow must be accessible without auth + "/memos.api.v1.AuthService/CreateSession": {}, + "/memos.api.v1.AuthService/GetCurrentSession": {}, -// publicMethods lists methods that can be called without authentication. -// These are typically read-only endpoints for public content or login-related endpoints. -var publicMethods = map[string]bool{ - // Instance info - needed before login - "/memos.api.v1.InstanceService/GetInstanceProfile": true, - "/memos.api.v1.InstanceService/GetInstanceSetting": true, + // Instance Service - needed before login to show instance info + "/memos.api.v1.InstanceService/GetInstanceProfile": {}, + "/memos.api.v1.InstanceService/GetInstanceSetting": {}, - // Auth - login/session endpoints - "/memos.api.v1.AuthService/CreateSession": true, - "/memos.api.v1.AuthService/GetCurrentSession": true, + // User Service - public user profiles and stats + "/memos.api.v1.UserService/GetUser": {}, + "/memos.api.v1.UserService/GetUserAvatar": {}, + "/memos.api.v1.UserService/GetUserStats": {}, + "/memos.api.v1.UserService/ListAllUserStats": {}, + "/memos.api.v1.UserService/SearchUsers": {}, - // User - public user info and registration - "/memos.api.v1.UserService/CreateUser": true, // Registration (also admin-only when not first user) - "/memos.api.v1.UserService/GetUser": true, - "/memos.api.v1.UserService/GetUserAvatar": true, - "/memos.api.v1.UserService/GetUserStats": true, - "/memos.api.v1.UserService/ListAllUserStats": true, - "/memos.api.v1.UserService/SearchUsers": true, + // Identity Provider Service - SSO buttons on login page + "/memos.api.v1.IdentityProviderService/ListIdentityProviders": {}, - // Identity providers - needed for SSO login - "/memos.api.v1.IdentityProviderService/ListIdentityProviders": true, - - // Memo - public memo access - "/memos.api.v1.MemoService/GetMemo": true, - "/memos.api.v1.MemoService/ListMemos": true, - - // Attachment - public attachment access - "/memos.api.v1.AttachmentService/GetAttachmentBinary": true, + // Memo Service - public memos (visibility filtering done in service layer) + "/memos.api.v1.MemoService/GetMemo": {}, + "/memos.api.v1.MemoService/ListMemos": {}, } -// adminOnlyMethods lists methods that require admin (Host or Admin role) privileges. -// Regular users cannot call these methods even if authenticated. -var adminOnlyMethods = map[string]bool{ - "/memos.api.v1.UserService/CreateUser": true, // Admin creates users (except first user registration) - "/memos.api.v1.InstanceService/UpdateInstanceSetting": true, -} - -// IsPublicMethod returns true if the method can be called without authentication. -func IsPublicMethod(fullMethodName string) bool { - return publicMethods[fullMethodName] -} - -// IsAdminOnlyMethod returns true if the method requires admin privileges. -func IsAdminOnlyMethod(fullMethodName string) bool { - return adminOnlyMethods[fullMethodName] +// IsPublicMethod checks if a procedure path is public (no authentication required). +// Returns true for public methods, false for protected methods. +func IsPublicMethod(procedure string) bool { + _, ok := PublicMethods[procedure] + return ok } diff --git a/server/router/api/v1/acl_config_test.go b/server/router/api/v1/acl_config_test.go new file mode 100644 index 000000000..3613f9f36 --- /dev/null +++ b/server/router/api/v1/acl_config_test.go @@ -0,0 +1,96 @@ +package v1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestPublicMethodsArePublic verifies that methods in PublicMethods are recognized as public. +func TestPublicMethodsArePublic(t *testing.T) { + publicMethods := []string{ + // Auth Service + "/memos.api.v1.AuthService/CreateSession", + "/memos.api.v1.AuthService/GetCurrentSession", + // Instance Service + "/memos.api.v1.InstanceService/GetInstanceProfile", + "/memos.api.v1.InstanceService/GetInstanceSetting", + // User Service + "/memos.api.v1.UserService/GetUser", + "/memos.api.v1.UserService/GetUserAvatar", + "/memos.api.v1.UserService/GetUserStats", + "/memos.api.v1.UserService/ListAllUserStats", + "/memos.api.v1.UserService/SearchUsers", + // Identity Provider Service + "/memos.api.v1.IdentityProviderService/ListIdentityProviders", + // Memo Service + "/memos.api.v1.MemoService/GetMemo", + "/memos.api.v1.MemoService/ListMemos", + } + + for _, method := range publicMethods { + t.Run(method, func(t *testing.T) { + assert.True(t, IsPublicMethod(method), "Expected %s to be public", method) + }) + } +} + +// TestProtectedMethodsRequireAuth verifies that non-public methods are recognized as protected. +func TestProtectedMethodsRequireAuth(t *testing.T) { + protectedMethods := []string{ + // Auth Service - logout requires auth + "/memos.api.v1.AuthService/DeleteSession", + // Instance Service - admin operations + "/memos.api.v1.InstanceService/UpdateInstanceSetting", + // User Service - modification operations + "/memos.api.v1.UserService/ListUsers", + "/memos.api.v1.UserService/UpdateUser", + "/memos.api.v1.UserService/DeleteUser", + // Memo Service - write operations + "/memos.api.v1.MemoService/CreateMemo", + "/memos.api.v1.MemoService/UpdateMemo", + "/memos.api.v1.MemoService/DeleteMemo", + // Attachment Service - write operations + "/memos.api.v1.AttachmentService/CreateAttachment", + "/memos.api.v1.AttachmentService/DeleteAttachment", + // Shortcut Service + "/memos.api.v1.ShortcutService/CreateShortcut", + "/memos.api.v1.ShortcutService/ListShortcuts", + "/memos.api.v1.ShortcutService/UpdateShortcut", + "/memos.api.v1.ShortcutService/DeleteShortcut", + // Activity Service + "/memos.api.v1.ActivityService/GetActivity", + } + + for _, method := range protectedMethods { + t.Run(method, func(t *testing.T) { + assert.False(t, IsPublicMethod(method), "Expected %s to require auth", method) + }) + } +} + +// TestUnknownMethodsRequireAuth verifies that unknown methods default to requiring auth. +func TestUnknownMethodsRequireAuth(t *testing.T) { + unknownMethods := []string{ + "/unknown.Service/Method", + "/memos.api.v1.UnknownService/Method", + "", + "invalid", + } + + for _, method := range unknownMethods { + t.Run(method, func(t *testing.T) { + assert.False(t, IsPublicMethod(method), "Unknown method %q should require auth", method) + }) + } +} + +// TestPublicMethodsMapConsistency verifies that PublicMethods map matches test expectations. +func TestPublicMethodsMapConsistency(t *testing.T) { + // Ensure the PublicMethods map has the expected number of entries + expectedCount := 13 + actualCount := len(PublicMethods) + assert.Equal(t, expectedCount, actualCount, + "PublicMethods map has %d entries, expected %d. Update this test if public methods changed intentionally.", + actualCount, expectedCount) +} diff --git a/server/router/api/v1/connect_interceptors.go b/server/router/api/v1/connect_interceptors.go index d4fda4c57..9f3d6b363 100644 --- a/server/router/api/v1/connect_interceptors.go +++ b/server/router/api/v1/connect_interceptors.go @@ -2,17 +2,62 @@ package v1 import ( "context" + "errors" "fmt" "log/slog" "runtime/debug" "connectrpc.com/connect" - "github.com/pkg/errors" + pkgerrors "github.com/pkg/errors" + "google.golang.org/grpc/metadata" "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) +// MetadataInterceptor converts Connect HTTP headers to gRPC metadata. +// +// This ensures service methods can use metadata.FromIncomingContext() to access +// headers like User-Agent, X-Forwarded-For, etc., regardless of whether the +// request came via Connect RPC or gRPC-Gateway. +type MetadataInterceptor struct{} + +// NewMetadataInterceptor creates a new metadata interceptor. +func NewMetadataInterceptor() *MetadataInterceptor { + return &MetadataInterceptor{} +} + +func (*MetadataInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { + return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { + // Convert HTTP headers to gRPC metadata + header := req.Header() + md := metadata.MD{} + + // Copy important headers for client info extraction + if ua := header.Get("User-Agent"); ua != "" { + md.Set("user-agent", ua) + } + if xff := header.Get("X-Forwarded-For"); xff != "" { + md.Set("x-forwarded-for", xff) + } + if xri := header.Get("X-Real-Ip"); xri != "" { + md.Set("x-real-ip", xri) + } + + // Set metadata in context so services can use metadata.FromIncomingContext() + ctx = metadata.NewIncomingContext(ctx, md) + return next(ctx, req) + } +} + +func (*MetadataInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc { + return next +} + +func (*MetadataInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { + return next +} + // LoggingInterceptor logs Connect RPC requests with appropriate log levels. // // Log levels: @@ -61,7 +106,7 @@ func (*LoggingInterceptor) classifyError(err error) (slog.Level, string) { } var connectErr *connect.Error - if !errors.As(err, &connectErr) { + if !pkgerrors.As(err, &connectErr) { return slog.LevelError, "unknown error" } @@ -99,7 +144,7 @@ func (in *RecoveryInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFu defer func() { if r := recover(); r != nil { in.logPanic(req.Spec().Procedure, r) - err = connect.NewError(connect.CodeInternal, errors.New("internal server error")) + err = connect.NewError(connect.CodeInternal, pkgerrors.New("internal server error")) } }() return next(ctx, req) @@ -127,9 +172,8 @@ func (in *RecoveryInterceptor) logPanic(procedure string, panicValue any) { // AuthInterceptor handles authentication for Connect handlers. // -// It reuses the same authentication logic as GRPCAuthInterceptor by delegating -// to a shared Authenticator instance. This ensures consistent authentication -// behavior across both gRPC and Connect protocols. +// It enforces authentication for all endpoints except those listed in PublicMethods. +// Role-based authorization (admin checks) remains in the service layer. type AuthInterceptor struct { authenticator *auth.Authenticator } @@ -143,45 +187,23 @@ func NewAuthInterceptor(store *store.Store, secret string) *AuthInterceptor { func (in *AuthInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc { return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) { - procedure := req.Spec().Procedure header := req.Header() + sessionCookie := auth.ExtractSessionCookieFromHeader(header.Get("Cookie")) + authHeader := header.Get("Authorization") - // Try session cookie authentication first - if sessionCookie := auth.ExtractSessionCookieFromHeader(header.Get("Cookie")); sessionCookie != "" { - user, err := in.authenticator.AuthenticateBySession(ctx, sessionCookie) - if err == nil && user != nil { - _, sessionID, err := auth.ParseSessionCookieValue(sessionCookie) - if err != nil { - // This should not happen since AuthenticateBySession already validated the cookie - // but handle it gracefully anyway - sessionID = "" - } - ctx, err = in.authenticator.AuthorizeAndSetContext(ctx, procedure, user, sessionID, "", IsAdminOnlyMethod) - if err != nil { - return nil, convertAuthError(err) - } - return next(ctx, req) - } + result := in.authenticator.Authenticate(ctx, sessionCookie, authHeader) + + // Enforce authentication for non-public methods + if result == nil && !IsPublicMethod(req.Spec().Procedure) { + return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("authentication required")) } - // Try JWT token authentication - if accessToken := auth.ExtractBearerToken(header.Get("Authorization")); accessToken != "" { - user, err := in.authenticator.AuthenticateByJWT(ctx, accessToken) - if err == nil && user != nil { - ctx, err = in.authenticator.AuthorizeAndSetContext(ctx, procedure, user, "", accessToken, IsAdminOnlyMethod) - if err != nil { - return nil, convertAuthError(err) - } - return next(ctx, req) - } + // Set user in context (may be nil for public endpoints) + if result != nil { + ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken) } - // Allow public methods without authentication - if IsPublicMethod(procedure) { - return next(ctx, req) - } - - return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("authentication required")) + return next(ctx, req) } } @@ -192,17 +214,3 @@ func (*AuthInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) co func (*AuthInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc { return next } - -// convertAuthError converts authentication/authorization errors to Connect errors. -func convertAuthError(err error) error { - if err == nil { - return nil - } - // Check if it's already a Connect error - var connectErr *connect.Error - if errors.As(err, &connectErr) { - return err - } - // Default to permission denied for auth errors - return connect.NewError(connect.CodePermissionDenied, err) -} diff --git a/server/router/api/v1/logger_interceptor.go b/server/router/api/v1/logger_interceptor.go deleted file mode 100644 index c8f97d6e8..000000000 --- a/server/router/api/v1/logger_interceptor.go +++ /dev/null @@ -1,53 +0,0 @@ -package v1 - -import ( - "context" - "fmt" - "log/slog" - - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" -) - -type LoggerInterceptor struct { - logStacktrace bool -} - -func NewLoggerInterceptor(logStacktrace bool) *LoggerInterceptor { - return &LoggerInterceptor{logStacktrace: logStacktrace} -} - -func (in *LoggerInterceptor) LoggerInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - resp, err := handler(ctx, request) - in.loggerInterceptorDo(ctx, serverInfo.FullMethod, err) - return resp, err -} - -func (in *LoggerInterceptor) loggerInterceptorDo(ctx context.Context, fullMethod string, err error) { - st := status.Convert(err) - var logLevel slog.Level - var logMsg string - switch st.Code() { - case codes.OK: - logLevel = slog.LevelInfo - logMsg = "OK" - case codes.Unauthenticated, codes.OutOfRange, codes.PermissionDenied, codes.NotFound: - logLevel = slog.LevelInfo - logMsg = "client error" - case codes.Internal, codes.Unknown, codes.DataLoss, codes.Unavailable, codes.DeadlineExceeded: - logLevel = slog.LevelError - logMsg = "server error" - default: - logLevel = slog.LevelError - logMsg = "unknown error" - } - logAttrs := []slog.Attr{slog.String("method", fullMethod)} - if err != nil { - logAttrs = append(logAttrs, slog.String("error", err.Error())) - if in.logStacktrace { - logAttrs = append(logAttrs, slog.String("stacktrace", fmt.Sprintf("%v", err))) - } - } - slog.LogAttrs(ctx, logLevel, logMsg, logAttrs...) -} diff --git a/server/router/api/v1/v1.go b/server/router/api/v1/v1.go index 22442aace..7fcf2461c 100644 --- a/server/router/api/v1/v1.go +++ b/server/router/api/v1/v1.go @@ -2,8 +2,6 @@ package v1 import ( "context" - "fmt" - "math" "net/http" "connectrpc.com/connect" @@ -11,20 +9,15 @@ import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "golang.org/x/sync/semaphore" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/health/grpc_health_v1" - "google.golang.org/grpc/reflection" "github.com/usememos/memos/internal/profile" "github.com/usememos/memos/plugin/markdown" v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/server/auth" "github.com/usememos/memos/store" ) type APIV1Service struct { - grpc_health_v1.UnimplementedHealthServer - v1pb.UnimplementedInstanceServiceServer v1pb.UnimplementedAuthServiceServer v1pb.UnimplementedUserServiceServer @@ -39,82 +32,86 @@ type APIV1Service struct { Store *store.Store MarkdownService markdown.Service - grpcServer *grpc.Server - // thumbnailSemaphore limits concurrent thumbnail generation to prevent memory exhaustion thumbnailSemaphore *semaphore.Weighted } -func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store, grpcServer *grpc.Server) *APIV1Service { - grpc.EnableTracing = true +func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store) *APIV1Service { markdownService := markdown.NewService( markdown.WithTagExtension(), ) - apiv1Service := &APIV1Service{ + return &APIV1Service{ Secret: secret, Profile: profile, Store: store, MarkdownService: markdownService, - grpcServer: grpcServer, thumbnailSemaphore: semaphore.NewWeighted(3), // Limit to 3 concurrent thumbnail generations } - grpc_health_v1.RegisterHealthServer(grpcServer, apiv1Service) - v1pb.RegisterInstanceServiceServer(grpcServer, apiv1Service) - v1pb.RegisterAuthServiceServer(grpcServer, apiv1Service) - v1pb.RegisterUserServiceServer(grpcServer, apiv1Service) - v1pb.RegisterMemoServiceServer(grpcServer, apiv1Service) - v1pb.RegisterAttachmentServiceServer(grpcServer, apiv1Service) - v1pb.RegisterShortcutServiceServer(grpcServer, apiv1Service) - v1pb.RegisterActivityServiceServer(grpcServer, apiv1Service) - v1pb.RegisterIdentityProviderServiceServer(grpcServer, apiv1Service) - reflection.Register(grpcServer) - return apiv1Service } -// RegisterGateway registers the gRPC-Gateway with the given Echo instance. +// RegisterGateway registers the gRPC-Gateway and Connect handlers with the given Echo instance. func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Echo) error { - var target string - if len(s.Profile.UNIXSock) == 0 { - addr := s.Profile.Addr - if addr == "" { - addr = "localhost" + // Auth middleware for gRPC-Gateway - runs after routing, has access to method name. + // Uses the same PublicMethods config as the Connect AuthInterceptor. + authenticator := auth.NewAuthenticator(s.Store, s.Secret) + gatewayAuthMiddleware := func(next runtime.HandlerFunc) runtime.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) { + ctx := r.Context() + + // Get the RPC method name from context (set by grpc-gateway after routing) + rpcMethod, _ := runtime.RPCMethod(ctx) + + // Extract credentials from HTTP headers + var sessionCookie string + if cookie, err := r.Cookie("user_session"); err == nil { + sessionCookie = cookie.Value + } + authHeader := r.Header.Get("Authorization") + + result := authenticator.Authenticate(ctx, sessionCookie, authHeader) + + // Enforce authentication for non-public methods + if result == nil && !IsPublicMethod(rpcMethod) { + http.Error(w, `{"code": 16, "message": "authentication required"}`, http.StatusUnauthorized) + return + } + + // Set user in context (may be nil for public endpoints) + if result != nil { + ctx = auth.SetUserInContext(ctx, result.User, result.SessionID, result.AccessToken) + r = r.WithContext(ctx) + } + + next(w, r, pathParams) } - target = fmt.Sprintf("%s:%d", addr, s.Profile.Port) - } else { - target = fmt.Sprintf("unix:%s", s.Profile.UNIXSock) - } - conn, err := grpc.NewClient( - target, - grpc.WithTransportCredentials(insecure.NewCredentials()), - grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32)), - ) - if err != nil { - return err } - gwMux := runtime.NewServeMux() - if err := v1pb.RegisterInstanceServiceHandler(ctx, gwMux, conn); err != nil { + // Create gRPC-Gateway mux with auth middleware. + gwMux := runtime.NewServeMux( + runtime.WithMiddlewares(gatewayAuthMiddleware), + ) + if err := v1pb.RegisterInstanceServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterAuthServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterAuthServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterUserServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterUserServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterMemoServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterMemoServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterAttachmentServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterAttachmentServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterShortcutServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterShortcutServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterActivityServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterActivityServiceHandlerServer(ctx, gwMux, s); err != nil { return err } - if err := v1pb.RegisterIdentityProviderServiceHandler(ctx, gwMux, conn); err != nil { + if err := v1pb.RegisterIdentityProviderServiceHandlerServer(ctx, gwMux, s); err != nil { return err } gwGroup := echoServer.Group("") @@ -127,6 +124,7 @@ func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Ech // Connect handlers for browser clients (replaces grpc-web). logStacktraces := s.Profile.IsDev() connectInterceptors := connect.WithInterceptors( + NewMetadataInterceptor(), // Convert HTTP headers to gRPC metadata first NewLoggingInterceptor(logStacktraces), NewRecoveryInterceptor(logStacktraces), NewAuthInterceptor(s.Store, s.Secret), diff --git a/server/server.go b/server/server.go index ace7415c1..a1fbc1ffe 100644 --- a/server/server.go +++ b/server/server.go @@ -4,20 +4,15 @@ import ( "context" "fmt" "log/slog" - "math" "net" "net/http" "runtime" - "runtime/debug" "time" "github.com/google/uuid" - grpcrecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/pkg/errors" - "github.com/soheilhy/cmux" - "google.golang.org/grpc" "github.com/usememos/memos/internal/profile" storepb "github.com/usememos/memos/proto/gen/store" @@ -35,7 +30,6 @@ type Server struct { Store *store.Store echoServer *echo.Echo - grpcServer *grpc.Server runnerCancelFuncs []context.CancelFunc } @@ -73,20 +67,7 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store rootGroup := echoServer.Group("") - // Log full stacktraces if we're in dev - logStacktraces := profile.IsDev() - - grpcServer := grpc.NewServer( - // Override the maximum receiving message size to math.MaxInt32 for uploading large attachments. - grpc.MaxRecvMsgSize(math.MaxInt32), - grpc.ChainUnaryInterceptor( - apiv1.NewLoggerInterceptor(logStacktraces).LoggerInterceptor, - newRecoveryInterceptor(logStacktraces), - apiv1.NewGRPCAuthInterceptor(store, secret).AuthenticationInterceptor, - )) - s.grpcServer = grpcServer - - apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store, grpcServer) + apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store) // Register HTTP file server routes BEFORE gRPC-Gateway to ensure proper range request handling for Safari. // This uses native HTTP serving (http.ServeContent) instead of gRPC for video/audio files. @@ -103,26 +84,6 @@ func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store return s, nil } -func newRecoveryInterceptor(logStacktraces bool) grpc.UnaryServerInterceptor { - var recoveryOptions []grpcrecovery.Option - if logStacktraces { - recoveryOptions = append(recoveryOptions, grpcrecovery.WithRecoveryHandler(func(p any) error { - if p == nil { - return nil - } - - switch val := p.(type) { - case runtime.Error: - return &stacktraceError{err: val, stacktrace: debug.Stack()} - default: - return nil - } - })) - } - - return grpcrecovery.UnaryServerInterceptor(recoveryOptions...) -} - func (s *Server) Start(ctx context.Context) error { var address, network string if len(s.Profile.UNIXSock) == 0 { @@ -137,25 +98,13 @@ func (s *Server) Start(ctx context.Context) error { return errors.Wrap(err, "failed to listen") } - muxServer := cmux.New(listener) + // Start Echo server directly (no cmux needed - all traffic is HTTP). + s.echoServer.Listener = listener go func() { - grpcListener := muxServer.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) - if err := s.grpcServer.Serve(grpcListener); err != nil { - slog.Error("failed to serve gRPC", "error", err) - } - }() - go func() { - httpListener := muxServer.Match(cmux.HTTP1Fast(http.MethodPatch)) - s.echoServer.Listener = httpListener - if err := s.echoServer.Start(address); err != nil { + if err := s.echoServer.Start(address); err != nil && err != http.ErrServerClosed { slog.Error("failed to start echo server", "error", err) } }() - go func() { - if err := muxServer.Serve(); err != nil { - slog.Error("mux server listen error", "error", err) - } - }() s.StartBackgroundRunners(ctx) return nil @@ -179,9 +128,6 @@ func (s *Server) Shutdown(ctx context.Context) { slog.Error("failed to shutdown server", slog.String("error", err.Error())) } - // Shutdown gRPC server. - s.grpcServer.GracefulStop() - // Close database connection. if err := s.Store.Close(); err != nil { slog.Error("failed to close database", slog.String("error", err.Error())) @@ -234,23 +180,3 @@ func (s *Server) getOrUpsertInstanceBasicSetting(ctx context.Context) (*storepb. } return instanceBasicSetting, nil } - -// stacktraceError wraps an underlying error and captures the stacktrace. It -// implements fmt.Formatter, so it'll be rendered when invoked by something like -// `fmt.Sprint("%v", err)`. -type stacktraceError struct { - err error - stacktrace []byte -} - -func (e *stacktraceError) Error() string { - return e.err.Error() -} - -func (e *stacktraceError) Unwrap() error { - return e.err -} - -func (e *stacktraceError) Format(f fmt.State, _ rune) { - f.Write(e.stacktrace) -}