Compare commits

...

13 Commits

Author SHA1 Message Date
e72aa1c853 updated dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-08-19 03:38:56 +02:00
3c02969ffd fix: cache bug
All checks were successful
continuous-integration/drone/push Build is passing
2024-07-14 14:57:11 +02:00
7a3fd4e400 feat: refactor
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
* updated dependencies
* add json indent to replies
* misc refactor
2024-07-14 14:40:59 +02:00
d10f31109b updated main.js
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-11 18:43:21 +02:00
eb5d136207 add interval refresh 2024-06-10 01:14:01 +02:00
32ddcdc269 updated dip
All checks were successful
continuous-integration/drone/push Build is passing
2024-06-05 11:14:02 +02:00
bb2a195007 added favicon
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-14 11:46:29 +02:00
3c797f0850 updated dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2024-05-11 18:24:09 +02:00
715eedf4bf updated dip
* added async memcache actions
* update cors methods
2024-05-11 18:21:36 +02:00
fa74f7ad4c updated dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2024-04-28 13:08:41 +02:00
d7738f79e1 updated dependencies
Some checks failed
continuous-integration/drone/push Build is failing
2024-04-28 13:08:01 +02:00
1a1a8e0f98 updated dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-01 12:03:44 +02:00
5b0f744a75 added cache feature on #memcached
All checks were successful
continuous-integration/drone/push Build is passing
2023-08-13 08:42:44 +02:00
473 changed files with 16390 additions and 9818 deletions

View File

@ -1,6 +1,8 @@
package main package main
import ( import (
"os"
"git.paulbsd.com/paulbsd/dip/src/config" "git.paulbsd.com/paulbsd/dip/src/config"
"git.paulbsd.com/paulbsd/dip/src/dip" "git.paulbsd.com/paulbsd/dip/src/dip"
"git.paulbsd.com/paulbsd/dip/src/ws" "git.paulbsd.com/paulbsd/dip/src/ws"
@ -18,5 +20,11 @@ func main() {
ws := ws.WS{} ws := ws.WS{}
ws.Page.Title = "Public IP Address Service" ws.Page.Title = "Public IP Address Service"
dip.Init() dip.Init()
memcacheconn, ok := os.LookupEnv("MEMCACHECONN")
if ok {
dip.MemcacheConn = memcacheconn
}
ws.RunServer(config, Templates, Static) ws.RunServer(config, Templates, Static)
} }

34
go.mod
View File

@ -1,29 +1,27 @@
module git.paulbsd.com/paulbsd/dip module git.paulbsd.com/paulbsd/dip
go 1.20 go 1.23
require ( require (
github.com/labstack/echo/v4 v4.10.2 github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874
github.com/mattn/go-colorable v0.1.13 // indirect github.com/labstack/echo/v4 v4.12.0
golang.org/x/crypto v0.11.0 // indirect github.com/likexian/whois v1.15.4
golang.org/x/sys v0.10.0 // indirect github.com/likexian/whois-parser v1.24.19
golang.org/x/text v0.11.0 // indirect github.com/oschwald/geoip2-golang v1.11.0
)
require (
github.com/likexian/whois v1.15.0
github.com/likexian/whois-parser v1.24.8
github.com/oschwald/geoip2-golang v1.9.0
) )
require ( require (
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/labstack/gommon v0.4.0 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/likexian/gokit v0.25.13 // indirect github.com/likexian/gokit v0.25.15 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
golang.org/x/net v0.12.0 // indirect golang.org/x/crypto v0.26.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.6.0 // indirect
) )

140
go.sum
View File

@ -1,111 +1,61 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous=
github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/labstack/echo/v4 v4.10.0 h1:5CiyngihEO4HXsz3vVsJn7f8xAlWwRr3aY6Ih280ZKA= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.10.0/go.mod h1:S/T/5fy/GigaXnHTkh0ZGe4LpkkQysvRjFMSUTkDRNQ= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= github.com/likexian/gokit v0.25.15 h1:QjospM1eXhdMMHwZRpMKKAHY/Wig9wgcREmLtf9NslY=
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/likexian/gokit v0.25.15/go.mod h1:S2QisdsxLEHWeD/XI0QMVeggp+jbxYqUxMvSBil7MRg=
github.com/likexian/gokit v0.25.9 h1:rzSQ/dP7Qw+QUzSuWlrLF0AtZS3Di6uO5yWOKhx2Gk4= github.com/likexian/whois v1.15.3 h1:0emFSUSUj98Q12Wer3iM3eROPXjg+CyUBlibGPNbKHw=
github.com/likexian/gokit v0.25.9/go.mod h1:oDDqJUcnnF9uAKuw54F7s6oEG+OJ7eallfDW2dq0A/o= github.com/likexian/whois v1.15.3/go.mod h1:a6sGAAKEb+O3JRBuW2x/QDM80l5hJ07p0+SjQkJ1c+0=
github.com/likexian/gokit v0.25.11 h1:o5H5JgVtBYBQn0QX30zE5+V8j2rgLTDoZWAO8xLFFfY= github.com/likexian/whois v1.15.4 h1:r5En62c+S9HKFgJtdh2WsdmRGTcxE4WUtGBdZkSBXmM=
github.com/likexian/gokit v0.25.11/go.mod h1:IKRQFPah2KQ0HL3iVjaOvFbx2apkSdFicGmsA0A4a1c= github.com/likexian/whois v1.15.4/go.mod h1:rXFTPcQdNlPQBJCQpPWTSIDGzzmgKBftmhdOOcLpwXk=
github.com/likexian/gokit v0.25.13 h1:p2Uw3+6fGG53CwdU2Dz0T6bOycdb2+bAFAa3ymwWVkM= github.com/likexian/whois-parser v1.24.18 h1:Xolieo/uwjNwhmQN/oDDNlwFajHipdHedyPBgzG44kw=
github.com/likexian/gokit v0.25.13/go.mod h1:qQhEWFBEfqLCO3/vOEo2EDKd+EycekVtUK4tex+l2H4= github.com/likexian/whois-parser v1.24.18/go.mod h1:k5zmKRZ7xPg1TLv3BGT4g/LOPRIMhvdNMeB0F53V/jk=
github.com/likexian/whois v1.14.4 h1:yNQVugAHfeoylzyhitcs0vaPksDduvzW46HLjqXhpCg= github.com/likexian/whois-parser v1.24.19 h1:vT8lWhnV8ogkdaYLyef6IvE5VTHVCwlUDG5BUXCx06k=
github.com/likexian/whois v1.14.4/go.mod h1:ijMqjPbtafr5TB49U3Hs3BsJx/93ixhRi/8YnCSK0iY= github.com/likexian/whois-parser v1.24.19/go.mod h1:rAtaofg2luol09H+ogDzGIfcG8ig1NtM5R16uQADDz4=
github.com/likexian/whois v1.14.6 h1:chNF6xkUShMQHq+5tiovVOQFadb9hqrWcaqbtlvbe3o=
github.com/likexian/whois v1.14.6/go.mod h1:upfX0X3o78fvHyFY2zHv46zooPoydUV1msyovvLZTMU=
github.com/likexian/whois v1.15.0 h1:AYYJ5bNUo8Qy2T1Z5GgMp1oIcIlCcTDfg1buYz6TdAE=
github.com/likexian/whois v1.15.0/go.mod h1:456fUTkh+O8F8v09bGdVl7XxBjRaQ4LvYHyVWX5Bxyg=
github.com/likexian/whois-parser v1.24.2 h1:EaNwcXqWBfQPfLD5F6MxAA0o7FnzmGcN3mg7v7h0zfY=
github.com/likexian/whois-parser v1.24.2/go.mod h1:fT0m/HCa4PXuR4ddA5PIE9V7gTWldeiMG/AHpeAhLtg=
github.com/likexian/whois-parser v1.24.6 h1:Zaz4dEDxtLaz9Ot8jEdni4xUHqYqt/6DAhfrZmsUd9s=
github.com/likexian/whois-parser v1.24.6/go.mod h1:MJN8IUeQ55VSG2OCB01a88T6V6VQm/UYxKLQySSqdSc=
github.com/likexian/whois-parser v1.24.7 h1:UF36WfIXXu4BMwaUmWk6YbzolgpzWTGY0JQ4wZ/kumE=
github.com/likexian/whois-parser v1.24.7/go.mod h1:MJN8IUeQ55VSG2OCB01a88T6V6VQm/UYxKLQySSqdSc=
github.com/likexian/whois-parser v1.24.8 h1:41RCpnRXSvlo9U7vPuLORi187UkTURH+8HXUcGdnhZA=
github.com/likexian/whois-parser v1.24.8/go.mod h1:b6STMHHDaSKbd4PzGrP50wWE5NzeBUETa/hT9gI0G9I=
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 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/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,11 +1,14 @@
package dip package dip
import ( import (
"encoding/json"
"fmt" "fmt"
"log" "log"
"net" "net"
"time"
"git.paulbsd.com/paulbsd/dip/src/geoip" "git.paulbsd.com/paulbsd/dip/src/geoip"
"github.com/bradfitz/gomemcache/memcache"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/likexian/whois" "github.com/likexian/whois"
whoisparser "github.com/likexian/whois-parser" whoisparser "github.com/likexian/whois-parser"
@ -13,26 +16,53 @@ import (
) )
const defaultLanguage = "en" const defaultLanguage = "en"
const cacheMaxTime = 604800
var citydb *geoip2.Reader var citydb *geoip2.Reader
var asndb *geoip2.Reader var asndb *geoip2.Reader
var MemcacheConn string = ""
func Init() (err error) { func Init() (err error) {
citydb, asndb, err = geoip.InitGeoIP() citydb, asndb, err = geoip.InitGeoIP()
if err != nil { if err != nil {
log.Println(err) log.Println(err)
log.Println("failed to get geoip database")
} }
return return
} }
// GetIPInfo returns IP address informations // GetIPInfo returns IP address informations
func (ip *IP) GetIPInfo(c echo.Context) (err error) { func (ip *IP) GetIPInfo(c echo.Context) (cached bool, err error) {
if c.Param("ip") != "" { var mcenabled bool
ip.IP = c.Param("ip") var mc *memcache.Client
} else { if MemcacheConn != "" {
mc = memcache.New(MemcacheConn)
mc.Timeout, err = time.ParseDuration("1s")
mcenabled = true
defer mc.Close()
}
ip.IP = c.Param("ip")
if c.Param("ip") == "" {
ip.IP = c.RealIP() ip.IP = c.RealIP()
} }
if mcenabled {
item, err := mc.Get(ip.IP)
if err != nil {
log.Println("error from cache", ip.IP, err)
cached = false
} else {
cachedip := IP{}
err = json.Unmarshal(item.Value, &cachedip)
if err != nil {
log.Println(err)
}
*ip = cachedip
return true, nil
}
}
err = ip.CheckIPAddress() err = ip.CheckIPAddress()
if err != nil { if err != nil {
ip.IP = c.RealIP() ip.IP = c.RealIP()
@ -52,6 +82,16 @@ func (ip *IP) GetIPInfo(c echo.Context) (err error) {
log.Println(err) log.Println(err)
} }
if mcenabled && !cached {
go func() {
dt, err := json.Marshal(*ip)
err = mc.Set(&memcache.Item{Key: ip.IP, Value: dt, Expiration: cacheMaxTime})
if err != nil {
log.Println(err, "test")
}
}()
}
/*err = ip.GetWhois() /*err = ip.GetWhois()
if err != nil { if err != nil {
log.Println(err) log.Println(err)

View File

@ -13,24 +13,30 @@ import (
geoip2 "github.com/oschwald/geoip2-golang" geoip2 "github.com/oschwald/geoip2-golang"
) )
var RootURL = "https://git.paulbsd.com/paulbsd/GeoLite.mmdb/releases/download/%s/%s" const RootURL = "https://git.paulbsd.com/paulbsd/GeoLite.mmdb/releases/download/%s/%s"
var APIUrl = "https://git.paulbsd.com/api/v1/repos/paulbsd/GeoLite.mmdb/releases" const APIUrl = "https://git.paulbsd.com/api/v1/repos/paulbsd/GeoLite.mmdb/releases"
func GetLastVersion() string { const CityFilename = "GeoLite2-City.mmdb"
const ASNFilename = "GeoLite2-ASN.mmdb"
func GetLastVersion() (result string, err error) {
var apiresults []struct { var apiresults []struct {
Tag string `json:"tag_name"` Tag string `json:"tag_name"`
} }
res, err := http.Get(APIUrl) res, err := http.Get(APIUrl)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return
} }
body, err := io.ReadAll(res.Body) body, err := io.ReadAll(res.Body)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return
} }
err = json.Unmarshal(body, &apiresults) err = json.Unmarshal(body, &apiresults)
if err != nil { if err != nil {
log.Println(err) log.Println(err)
return
} }
var tags []string var tags []string
@ -39,14 +45,18 @@ func GetLastVersion() string {
} }
sort.Sort(sort.Reverse(sort.StringSlice(tags))) sort.Sort(sort.Reverse(sort.StringSlice(tags)))
return tags[0] result = tags[0]
return
} }
func InitGeoIP() (citydb *geoip2.Reader, asndb *geoip2.Reader, err error) { func InitGeoIP() (citydb *geoip2.Reader, asndb *geoip2.Reader, err error) {
var version = GetLastVersion() version, err := GetLastVersion()
if err != nil {
return
}
var dbs map[string]string = map[string]string{ var dbs map[string]string = map[string]string{
"city": fmt.Sprintf(RootURL, version, "GeoLite2-City.mmdb"), "city": fmt.Sprintf(RootURL, version, CityFilename),
"asn": fmt.Sprintf(RootURL, version, "GeoLite2-ASN.mmdb")} "asn": fmt.Sprintf(RootURL, version, ASNFilename)}
log.Printf("Fetching GeoLite.mmdb version %s\n", version) log.Printf("Fetching GeoLite.mmdb version %s\n", version)
citydb, err = FetchDB(dbs["city"], version) citydb, err = FetchDB(dbs["city"], version)
if err != nil { if err != nil {

View File

@ -21,6 +21,8 @@ func GetStatic(staticfiles *embed.FS, c echo.Context) (err error) {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJavaScript) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJavaScript)
} else if strings.HasSuffix(name, ".css") { } else if strings.HasSuffix(name, ".css") {
c.Response().Header().Set(echo.HeaderContentType, "text/css") c.Response().Header().Set(echo.HeaderContentType, "text/css")
} else if strings.HasSuffix(name, ".ico") {
c.Response().Header().Set(echo.HeaderContentType, "image/x-icon")
} }
c.Response().Header().Add(echo.HeaderCacheControl, "max-age=172800") c.Response().Header().Add(echo.HeaderCacheControl, "max-age=172800")
return c.String(http.StatusOK, string(content)) return c.String(http.StatusOK, string(content))

View File

@ -22,7 +22,7 @@ func (ws *WS) InitServer(templatefiles *embed.FS, staticfiles *embed.FS) (err er
})) }))
ws.e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ ws.e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"}, AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete}, AllowMethods: []string{http.MethodGet},
})) }))
builtTemplates, _ := templates.BuildTemplates(templatefiles) builtTemplates, _ := templates.BuildTemplates(templatefiles)

View File

@ -8,9 +8,9 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
func (p *Page) GetContent(c echo.Context) (err error) { func (p *Page) GetContent(c echo.Context) (cached bool, err error) {
var ip dip.IP var ip dip.IP
err = ip.GetIPInfo(c) cached, err = ip.GetIPInfo(c)
p.IP = &ip p.IP = &ip
return return
@ -18,7 +18,11 @@ func (p *Page) GetContent(c echo.Context) (err error) {
// Process returns main webpage or a JSON // Process returns main webpage or a JSON
func (p *Page) Process(c echo.Context, querytype string) (err error) { func (p *Page) Process(c echo.Context, querytype string) (err error) {
p.GetContent(c) cached, err := p.GetContent(c)
c.Response().Header().Set("X-Cached", "false")
if cached {
c.Response().Header().Set("X-Cached", "true")
}
if querytype == "json" { if querytype == "json" {
return c.JSONPretty(http.StatusOK, p.IP, " ") return c.JSONPretty(http.StatusOK, p.IP, " ")
} }

BIN
static/images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,26 +1,39 @@
function main() { const dip_ip = document.getElementById("dip_ip");
const dip_hostname = document.getElementById("dip_hostname");
const dip_city = document.getElementById("dip_city");
const dip_country = document.getElementById("dip_country");
const dip_as_number = document.getElementById("dip_as_number");
const dip_as_org = document.getElementById("dip_as_org");
function setData(res) {
dip_ip.innerHTML = res["ip"];
dip_hostname.innerHTML = res["hostname"];
dip_city.innerHTML = res["city"];
dip_country.innerHTML = res["country"];
dip_as_number.innerHTML = res["as"]["number"];
dip_as_org.innerHTML = res["as"]["org"];
}
function updateIP() {
let ip = ""; let ip = "";
let dip_ip = document.getElementById("dip_ip");
let dip_hostname = document.getElementById("dip_hostname");
let dip_city = document.getElementById("dip_city");
let dip_country = document.getElementById("dip_country");
let dip_as_number = document.getElementById("dip_as_number");
let dip_as_org = document.getElementById("dip_as_org");
if (window.location.pathname.length > 4) { if (window.location.pathname.length > 4) {
ip = window.location.pathname.split("/")[1]; ip = window.location.pathname.split("/")[1];
} }
const localdata = localStorage.getItem("data");
if (localdata) {
const data = JSON.parse(localdata);
setData(data);
}
fetch(`/json/${ip}`).then((response) => { fetch(`/json/${ip}`).then((response) => {
response.json().then((res) => { response.json().then((data) => {
dip_ip.innerHTML = res["ip"]; setData(data);
dip_hostname.innerHTML = res["hostname"]; localStorage.setItem("data",JSON.stringify(data));
dip_city.innerHTML = res["city"];
dip_country.innerHTML = res["country"];
dip_as_number.innerHTML = res["as"]["number"];
dip_as_org.innerHTML = res["as"]["org"];
}); });
}); });
} }
main(); updateIP()
setInterval(updateIP,1000);

View File

@ -8,4 +8,5 @@ import (
// //
//go:embed css/* //go:embed css/*
//go:embed js/* //go:embed js/*
//go:embed images/*
var Static embed.FS var Static embed.FS

View File

@ -4,6 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="static/images/favicon.ico"/>
<link rel="stylesheet" href="static/css/main.css" /> <link rel="stylesheet" href="static/css/main.css" />
<link rel="stylesheet" href="static/css/uikit.min.css" /> <link rel="stylesheet" href="static/css/uikit.min.css" />
<link rel="stylesheet" href="static/css/font-awesome.min.css" /> <link rel="stylesheet" href="static/css/font-awesome.min.css" />

9
vendor/github.com/bradfitz/gomemcache/AUTHORS generated vendored Normal file
View File

@ -0,0 +1,9 @@
The following people & companies are the copyright holders of this
package. Feel free to add to this list if you or your employer cares,
otherwise it's implicit from the git log.
Authors:
- Brad Fitzpatrick
- Google, Inc. (from Googlers contributing)
- Anybody else in the git log.

202
vendor/github.com/bradfitz/gomemcache/LICENSE generated vendored Normal file
View File

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,776 @@
/*
Copyright 2011 The gomemcache AUTHORS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package memcache provides a client for the memcached cache server.
package memcache
import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"io"
"net"
"strconv"
"strings"
"sync"
"time"
)
// Similar to:
// https://godoc.org/google.golang.org/appengine/memcache
var (
// ErrCacheMiss means that a Get failed because the item wasn't present.
ErrCacheMiss = errors.New("memcache: cache miss")
// ErrCASConflict means that a CompareAndSwap call failed due to the
// cached value being modified between the Get and the CompareAndSwap.
// If the cached value was simply evicted rather than replaced,
// ErrNotStored will be returned instead.
ErrCASConflict = errors.New("memcache: compare-and-swap conflict")
// ErrNotStored means that a conditional write operation (i.e. Add or
// CompareAndSwap) failed because the condition was not satisfied.
ErrNotStored = errors.New("memcache: item not stored")
// ErrServer means that a server error occurred.
ErrServerError = errors.New("memcache: server error")
// ErrNoStats means that no statistics were available.
ErrNoStats = errors.New("memcache: no statistics available")
// ErrMalformedKey is returned when an invalid key is used.
// Keys must be at maximum 250 bytes long and not
// contain whitespace or control characters.
ErrMalformedKey = errors.New("malformed: key is too long or contains invalid characters")
// ErrNoServers is returned when no servers are configured or available.
ErrNoServers = errors.New("memcache: no servers configured or available")
)
const (
// DefaultTimeout is the default socket read/write timeout.
DefaultTimeout = 500 * time.Millisecond
// DefaultMaxIdleConns is the default maximum number of idle connections
// kept for any single address.
DefaultMaxIdleConns = 2
)
const buffered = 8 // arbitrary buffered channel size, for readability
// resumableError returns true if err is only a protocol-level cache error.
// This is used to determine whether or not a server connection should
// be re-used or not. If an error occurs, by default we don't reuse the
// connection, unless it was just a cache error.
func resumableError(err error) bool {
switch err {
case ErrCacheMiss, ErrCASConflict, ErrNotStored, ErrMalformedKey:
return true
}
return false
}
func legalKey(key string) bool {
if len(key) > 250 {
return false
}
for i := 0; i < len(key); i++ {
if key[i] <= ' ' || key[i] == 0x7f {
return false
}
}
return true
}
var (
crlf = []byte("\r\n")
space = []byte(" ")
resultOK = []byte("OK\r\n")
resultStored = []byte("STORED\r\n")
resultNotStored = []byte("NOT_STORED\r\n")
resultExists = []byte("EXISTS\r\n")
resultNotFound = []byte("NOT_FOUND\r\n")
resultDeleted = []byte("DELETED\r\n")
resultEnd = []byte("END\r\n")
resultOk = []byte("OK\r\n")
resultTouched = []byte("TOUCHED\r\n")
resultClientErrorPrefix = []byte("CLIENT_ERROR ")
versionPrefix = []byte("VERSION")
)
// New returns a memcache client using the provided server(s)
// with equal weight. If a server is listed multiple times,
// it gets a proportional amount of weight.
func New(server ...string) *Client {
ss := new(ServerList)
ss.SetServers(server...)
return NewFromSelector(ss)
}
// NewFromSelector returns a new Client using the provided ServerSelector.
func NewFromSelector(ss ServerSelector) *Client {
return &Client{selector: ss}
}
// Client is a memcache client.
// It is safe for unlocked use by multiple concurrent goroutines.
type Client struct {
// DialContext connects to the address on the named network using the
// provided context.
//
// To connect to servers using TLS (memcached running with "--enable-ssl"),
// use a DialContext func that uses tls.Dialer.DialContext. See this
// package's tests as an example.
DialContext func(ctx context.Context, network, address string) (net.Conn, error)
// Timeout specifies the socket read/write timeout.
// If zero, DefaultTimeout is used.
Timeout time.Duration
// MaxIdleConns specifies the maximum number of idle connections that will
// be maintained per address. If less than one, DefaultMaxIdleConns will be
// used.
//
// Consider your expected traffic rates and latency carefully. This should
// be set to a number higher than your peak parallel requests.
MaxIdleConns int
selector ServerSelector
lk sync.Mutex
freeconn map[string][]*conn
}
// Item is an item to be got or stored in a memcached server.
type Item struct {
// Key is the Item's key (250 bytes maximum).
Key string
// Value is the Item's value.
Value []byte
// Flags are server-opaque flags whose semantics are entirely
// up to the app.
Flags uint32
// Expiration is the cache expiration time, in seconds: either a relative
// time from now (up to 1 month), or an absolute Unix epoch time.
// Zero means the Item has no expiration time.
Expiration int32
// CasID is the compare and swap ID.
//
// It's populated by get requests and then the same value is
// required for a CompareAndSwap request to succeed.
CasID uint64
}
// conn is a connection to a server.
type conn struct {
nc net.Conn
rw *bufio.ReadWriter
addr net.Addr
c *Client
}
// release returns this connection back to the client's free pool
func (cn *conn) release() {
cn.c.putFreeConn(cn.addr, cn)
}
func (cn *conn) extendDeadline() {
cn.nc.SetDeadline(time.Now().Add(cn.c.netTimeout()))
}
// condRelease releases this connection if the error pointed to by err
// is nil (not an error) or is only a protocol level error (e.g. a
// cache miss). The purpose is to not recycle TCP connections that
// are bad.
func (cn *conn) condRelease(err *error) {
if *err == nil || resumableError(*err) {
cn.release()
} else {
cn.nc.Close()
}
}
func (c *Client) putFreeConn(addr net.Addr, cn *conn) {
c.lk.Lock()
defer c.lk.Unlock()
if c.freeconn == nil {
c.freeconn = make(map[string][]*conn)
}
freelist := c.freeconn[addr.String()]
if len(freelist) >= c.maxIdleConns() {
cn.nc.Close()
return
}
c.freeconn[addr.String()] = append(freelist, cn)
}
func (c *Client) getFreeConn(addr net.Addr) (cn *conn, ok bool) {
c.lk.Lock()
defer c.lk.Unlock()
if c.freeconn == nil {
return nil, false
}
freelist, ok := c.freeconn[addr.String()]
if !ok || len(freelist) == 0 {
return nil, false
}
cn = freelist[len(freelist)-1]
c.freeconn[addr.String()] = freelist[:len(freelist)-1]
return cn, true
}
func (c *Client) netTimeout() time.Duration {
if c.Timeout != 0 {
return c.Timeout
}
return DefaultTimeout
}
func (c *Client) maxIdleConns() int {
if c.MaxIdleConns > 0 {
return c.MaxIdleConns
}
return DefaultMaxIdleConns
}
// ConnectTimeoutError is the error type used when it takes
// too long to connect to the desired host. This level of
// detail can generally be ignored.
type ConnectTimeoutError struct {
Addr net.Addr
}
func (cte *ConnectTimeoutError) Error() string {
return "memcache: connect timeout to " + cte.Addr.String()
}
func (c *Client) dial(addr net.Addr) (net.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), c.netTimeout())
defer cancel()
dialerContext := c.DialContext
if dialerContext == nil {
dialer := net.Dialer{
Timeout: c.netTimeout(),
}
dialerContext = dialer.DialContext
}
nc, err := dialerContext(ctx, addr.Network(), addr.String())
if err == nil {
return nc, nil
}
if ne, ok := err.(net.Error); ok && ne.Timeout() {
return nil, &ConnectTimeoutError{addr}
}
return nil, err
}
func (c *Client) getConn(addr net.Addr) (*conn, error) {
cn, ok := c.getFreeConn(addr)
if ok {
cn.extendDeadline()
return cn, nil
}
nc, err := c.dial(addr)
if err != nil {
return nil, err
}
cn = &conn{
nc: nc,
addr: addr,
rw: bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc)),
c: c,
}
cn.extendDeadline()
return cn, nil
}
func (c *Client) onItem(item *Item, fn func(*Client, *bufio.ReadWriter, *Item) error) error {
addr, err := c.selector.PickServer(item.Key)
if err != nil {
return err
}
cn, err := c.getConn(addr)
if err != nil {
return err
}
defer cn.condRelease(&err)
if err = fn(c, cn.rw, item); err != nil {
return err
}
return nil
}
func (c *Client) FlushAll() error {
return c.selector.Each(c.flushAllFromAddr)
}
// Get gets the item for the given key. ErrCacheMiss is returned for a
// memcache cache miss. The key must be at most 250 bytes in length.
func (c *Client) Get(key string) (item *Item, err error) {
err = c.withKeyAddr(key, func(addr net.Addr) error {
return c.getFromAddr(addr, []string{key}, func(it *Item) { item = it })
})
if err == nil && item == nil {
err = ErrCacheMiss
}
return
}
// Touch updates the expiry for the given key. The seconds parameter is either
// a Unix timestamp or, if seconds is less than 1 month, the number of seconds
// into the future at which time the item will expire. Zero means the item has
// no expiration time. ErrCacheMiss is returned if the key is not in the cache.
// The key must be at most 250 bytes in length.
func (c *Client) Touch(key string, seconds int32) (err error) {
return c.withKeyAddr(key, func(addr net.Addr) error {
return c.touchFromAddr(addr, []string{key}, seconds)
})
}
func (c *Client) withKeyAddr(key string, fn func(net.Addr) error) (err error) {
if !legalKey(key) {
return ErrMalformedKey
}
addr, err := c.selector.PickServer(key)
if err != nil {
return err
}
return fn(addr)
}
func (c *Client) withAddrRw(addr net.Addr, fn func(*bufio.ReadWriter) error) (err error) {
cn, err := c.getConn(addr)
if err != nil {
return err
}
defer cn.condRelease(&err)
return fn(cn.rw)
}
func (c *Client) withKeyRw(key string, fn func(*bufio.ReadWriter) error) error {
return c.withKeyAddr(key, func(addr net.Addr) error {
return c.withAddrRw(addr, fn)
})
}
func (c *Client) getFromAddr(addr net.Addr, keys []string, cb func(*Item)) error {
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
if _, err := fmt.Fprintf(rw, "gets %s\r\n", strings.Join(keys, " ")); err != nil {
return err
}
if err := rw.Flush(); err != nil {
return err
}
if err := parseGetResponse(rw.Reader, cb); err != nil {
return err
}
return nil
})
}
// flushAllFromAddr send the flush_all command to the given addr
func (c *Client) flushAllFromAddr(addr net.Addr) error {
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
if _, err := fmt.Fprintf(rw, "flush_all\r\n"); err != nil {
return err
}
if err := rw.Flush(); err != nil {
return err
}
line, err := rw.ReadSlice('\n')
if err != nil {
return err
}
switch {
case bytes.Equal(line, resultOk):
break
default:
return fmt.Errorf("memcache: unexpected response line from flush_all: %q", string(line))
}
return nil
})
}
// ping sends the version command to the given addr
func (c *Client) ping(addr net.Addr) error {
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
if _, err := fmt.Fprintf(rw, "version\r\n"); err != nil {
return err
}
if err := rw.Flush(); err != nil {
return err
}
line, err := rw.ReadSlice('\n')
if err != nil {
return err
}
switch {
case bytes.HasPrefix(line, versionPrefix):
break
default:
return fmt.Errorf("memcache: unexpected response line from ping: %q", string(line))
}
return nil
})
}
func (c *Client) touchFromAddr(addr net.Addr, keys []string, expiration int32) error {
return c.withAddrRw(addr, func(rw *bufio.ReadWriter) error {
for _, key := range keys {
if _, err := fmt.Fprintf(rw, "touch %s %d\r\n", key, expiration); err != nil {
return err
}
if err := rw.Flush(); err != nil {
return err
}
line, err := rw.ReadSlice('\n')
if err != nil {
return err
}
switch {
case bytes.Equal(line, resultTouched):
break
case bytes.Equal(line, resultNotFound):
return ErrCacheMiss
default:
return fmt.Errorf("memcache: unexpected response line from touch: %q", string(line))
}
}
return nil
})
}
// GetMulti is a batch version of Get. The returned map from keys to
// items may have fewer elements than the input slice, due to memcache
// cache misses. Each key must be at most 250 bytes in length.
// If no error is returned, the returned map will also be non-nil.
func (c *Client) GetMulti(keys []string) (map[string]*Item, error) {
var lk sync.Mutex
m := make(map[string]*Item)
addItemToMap := func(it *Item) {
lk.Lock()
defer lk.Unlock()
m[it.Key] = it
}
keyMap := make(map[net.Addr][]string)
for _, key := range keys {
if !legalKey(key) {
return nil, ErrMalformedKey
}
addr, err := c.selector.PickServer(key)
if err != nil {
return nil, err
}
keyMap[addr] = append(keyMap[addr], key)
}
ch := make(chan error, buffered)
for addr, keys := range keyMap {
go func(addr net.Addr, keys []string) {
ch <- c.getFromAddr(addr, keys, addItemToMap)
}(addr, keys)
}
var err error
for _ = range keyMap {
if ge := <-ch; ge != nil {
err = ge
}
}
return m, err
}
// parseGetResponse reads a GET response from r and calls cb for each
// read and allocated Item
func parseGetResponse(r *bufio.Reader, cb func(*Item)) error {
for {
line, err := r.ReadSlice('\n')
if err != nil {
return err
}
if bytes.Equal(line, resultEnd) {
return nil
}
it := new(Item)
size, err := scanGetResponseLine(line, it)
if err != nil {
return err
}
it.Value = make([]byte, size+2)
_, err = io.ReadFull(r, it.Value)
if err != nil {
it.Value = nil
return err
}
if !bytes.HasSuffix(it.Value, crlf) {
it.Value = nil
return fmt.Errorf("memcache: corrupt get result read")
}
it.Value = it.Value[:size]
cb(it)
}
}
// scanGetResponseLine populates it and returns the declared size of the item.
// It does not read the bytes of the item.
func scanGetResponseLine(line []byte, it *Item) (size int, err error) {
pattern := "VALUE %s %d %d %d\r\n"
dest := []interface{}{&it.Key, &it.Flags, &size, &it.CasID}
if bytes.Count(line, space) == 3 {
pattern = "VALUE %s %d %d\r\n"
dest = dest[:3]
}
n, err := fmt.Sscanf(string(line), pattern, dest...)
if err != nil || n != len(dest) {
return -1, fmt.Errorf("memcache: unexpected line in get response: %q", line)
}
return size, nil
}
// Set writes the given item, unconditionally.
func (c *Client) Set(item *Item) error {
return c.onItem(item, (*Client).set)
}
func (c *Client) set(rw *bufio.ReadWriter, item *Item) error {
return c.populateOne(rw, "set", item)
}
// Add writes the given item, if no value already exists for its
// key. ErrNotStored is returned if that condition is not met.
func (c *Client) Add(item *Item) error {
return c.onItem(item, (*Client).add)
}
func (c *Client) add(rw *bufio.ReadWriter, item *Item) error {
return c.populateOne(rw, "add", item)
}
// Replace writes the given item, but only if the server *does*
// already hold data for this key
func (c *Client) Replace(item *Item) error {
return c.onItem(item, (*Client).replace)
}
func (c *Client) replace(rw *bufio.ReadWriter, item *Item) error {
return c.populateOne(rw, "replace", item)
}
// Append appends the given item to the existing item, if a value already
// exists for its key. ErrNotStored is returned if that condition is not met.
func (c *Client) Append(item *Item) error {
return c.onItem(item, (*Client).append)
}
func (c *Client) append(rw *bufio.ReadWriter, item *Item) error {
return c.populateOne(rw, "append", item)
}
// Prepend prepends the given item to the existing item, if a value already
// exists for its key. ErrNotStored is returned if that condition is not met.
func (c *Client) Prepend(item *Item) error {
return c.onItem(item, (*Client).prepend)
}
func (c *Client) prepend(rw *bufio.ReadWriter, item *Item) error {
return c.populateOne(rw, "prepend", item)
}
// CompareAndSwap writes the given item that was previously returned
// by Get, if the value was neither modified or evicted between the
// Get and the CompareAndSwap calls. The item's Key should not change
// between calls but all other item fields may differ. ErrCASConflict
// is returned if the value was modified in between the
// calls. ErrNotStored is returned if the value was evicted in between
// the calls.
func (c *Client) CompareAndSwap(item *Item) error {
return c.onItem(item, (*Client).cas)
}
func (c *Client) cas(rw *bufio.ReadWriter, item *Item) error {
return c.populateOne(rw, "cas", item)
}
func (c *Client) populateOne(rw *bufio.ReadWriter, verb string, item *Item) error {
if !legalKey(item.Key) {
return ErrMalformedKey
}
var err error
if verb == "cas" {
_, err = fmt.Fprintf(rw, "%s %s %d %d %d %d\r\n",
verb, item.Key, item.Flags, item.Expiration, len(item.Value), item.CasID)
} else {
_, err = fmt.Fprintf(rw, "%s %s %d %d %d\r\n",
verb, item.Key, item.Flags, item.Expiration, len(item.Value))
}
if err != nil {
return err
}
if _, err = rw.Write(item.Value); err != nil {
return err
}
if _, err := rw.Write(crlf); err != nil {
return err
}
if err := rw.Flush(); err != nil {
return err
}
line, err := rw.ReadSlice('\n')
if err != nil {
return err
}
switch {
case bytes.Equal(line, resultStored):
return nil
case bytes.Equal(line, resultNotStored):
return ErrNotStored
case bytes.Equal(line, resultExists):
return ErrCASConflict
case bytes.Equal(line, resultNotFound):
return ErrCacheMiss
}
return fmt.Errorf("memcache: unexpected response line from %q: %q", verb, string(line))
}
func writeReadLine(rw *bufio.ReadWriter, format string, args ...interface{}) ([]byte, error) {
_, err := fmt.Fprintf(rw, format, args...)
if err != nil {
return nil, err
}
if err := rw.Flush(); err != nil {
return nil, err
}
line, err := rw.ReadSlice('\n')
return line, err
}
func writeExpectf(rw *bufio.ReadWriter, expect []byte, format string, args ...interface{}) error {
line, err := writeReadLine(rw, format, args...)
if err != nil {
return err
}
switch {
case bytes.Equal(line, resultOK):
return nil
case bytes.Equal(line, expect):
return nil
case bytes.Equal(line, resultNotStored):
return ErrNotStored
case bytes.Equal(line, resultExists):
return ErrCASConflict
case bytes.Equal(line, resultNotFound):
return ErrCacheMiss
}
return fmt.Errorf("memcache: unexpected response line: %q", string(line))
}
// Delete deletes the item with the provided key. The error ErrCacheMiss is
// returned if the item didn't already exist in the cache.
func (c *Client) Delete(key string) error {
return c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
return writeExpectf(rw, resultDeleted, "delete %s\r\n", key)
})
}
// DeleteAll deletes all items in the cache.
func (c *Client) DeleteAll() error {
return c.withKeyRw("", func(rw *bufio.ReadWriter) error {
return writeExpectf(rw, resultDeleted, "flush_all\r\n")
})
}
// Ping checks all instances if they are alive. Returns error if any
// of them is down.
func (c *Client) Ping() error {
return c.selector.Each(c.ping)
}
// Increment atomically increments key by delta. The return value is
// the new value after being incremented or an error. If the value
// didn't exist in memcached the error is ErrCacheMiss. The value in
// memcached must be an decimal number, or an error will be returned.
// On 64-bit overflow, the new value wraps around.
func (c *Client) Increment(key string, delta uint64) (newValue uint64, err error) {
return c.incrDecr("incr", key, delta)
}
// Decrement atomically decrements key by delta. The return value is
// the new value after being decremented or an error. If the value
// didn't exist in memcached the error is ErrCacheMiss. The value in
// memcached must be an decimal number, or an error will be returned.
// On underflow, the new value is capped at zero and does not wrap
// around.
func (c *Client) Decrement(key string, delta uint64) (newValue uint64, err error) {
return c.incrDecr("decr", key, delta)
}
func (c *Client) incrDecr(verb, key string, delta uint64) (uint64, error) {
var val uint64
err := c.withKeyRw(key, func(rw *bufio.ReadWriter) error {
line, err := writeReadLine(rw, "%s %s %d\r\n", verb, key, delta)
if err != nil {
return err
}
switch {
case bytes.Equal(line, resultNotFound):
return ErrCacheMiss
case bytes.HasPrefix(line, resultClientErrorPrefix):
errMsg := line[len(resultClientErrorPrefix) : len(line)-2]
return errors.New("memcache: client error: " + string(errMsg))
}
val, err = strconv.ParseUint(string(line[:len(line)-2]), 10, 64)
if err != nil {
return err
}
return nil
})
return val, err
}
// Close closes any open connections.
//
// It returns the first error encountered closing connections, but always
// closes all connections.
//
// After Close, the Client may still be used.
func (c *Client) Close() error {
c.lk.Lock()
defer c.lk.Unlock()
var ret error
for _, conns := range c.freeconn {
for _, c := range conns {
if err := c.nc.Close(); err != nil && ret == nil {
ret = err
}
}
}
c.freeconn = nil
return ret
}

View File

@ -0,0 +1,129 @@
/*
Copyright 2011 The gomemcache AUTHORS
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package memcache
import (
"hash/crc32"
"net"
"strings"
"sync"
)
// ServerSelector is the interface that selects a memcache server
// as a function of the item's key.
//
// All ServerSelector implementations must be safe for concurrent use
// by multiple goroutines.
type ServerSelector interface {
// PickServer returns the server address that a given item
// should be shared onto.
PickServer(key string) (net.Addr, error)
Each(func(net.Addr) error) error
}
// ServerList is a simple ServerSelector. Its zero value is usable.
type ServerList struct {
mu sync.RWMutex
addrs []net.Addr
}
// staticAddr caches the Network() and String() values from any net.Addr.
type staticAddr struct {
ntw, str string
}
func newStaticAddr(a net.Addr) net.Addr {
return &staticAddr{
ntw: a.Network(),
str: a.String(),
}
}
func (s *staticAddr) Network() string { return s.ntw }
func (s *staticAddr) String() string { return s.str }
// SetServers changes a ServerList's set of servers at runtime and is
// safe for concurrent use by multiple goroutines.
//
// Each server is given equal weight. A server is given more weight
// if it's listed multiple times.
//
// SetServers returns an error if any of the server names fail to
// resolve. No attempt is made to connect to the server. If any error
// is returned, no changes are made to the ServerList.
func (ss *ServerList) SetServers(servers ...string) error {
naddr := make([]net.Addr, len(servers))
for i, server := range servers {
if strings.Contains(server, "/") {
addr, err := net.ResolveUnixAddr("unix", server)
if err != nil {
return err
}
naddr[i] = newStaticAddr(addr)
} else {
tcpaddr, err := net.ResolveTCPAddr("tcp", server)
if err != nil {
return err
}
naddr[i] = newStaticAddr(tcpaddr)
}
}
ss.mu.Lock()
defer ss.mu.Unlock()
ss.addrs = naddr
return nil
}
// Each iterates over each server calling the given function
func (ss *ServerList) Each(f func(net.Addr) error) error {
ss.mu.RLock()
defer ss.mu.RUnlock()
for _, a := range ss.addrs {
if err := f(a); nil != err {
return err
}
}
return nil
}
// keyBufPool returns []byte buffers for use by PickServer's call to
// crc32.ChecksumIEEE to avoid allocations. (but doesn't avoid the
// copies, which at least are bounded in size and small)
var keyBufPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 256)
return &b
},
}
func (ss *ServerList) PickServer(key string) (net.Addr, error) {
ss.mu.RLock()
defer ss.mu.RUnlock()
if len(ss.addrs) == 0 {
return nil, ErrNoServers
}
if len(ss.addrs) == 1 {
return ss.addrs[0], nil
}
bufp := keyBufPool.Get().(*[]byte)
n := copy(*bufp, key)
cs := crc32.ChecksumIEEE((*bufp)[:n])
keyBufPool.Put(bufp)
return ss.addrs[cs%uint32(len(ss.addrs))], nil
}

View File

@ -1,5 +1,110 @@
# Changelog # Changelog
## v4.12.0 - 2024-04-15
**Security**
* Update golang.org/x/net dep because of [GO-2024-2687](https://pkg.go.dev/vuln/GO-2024-2687) by @aldas in https://github.com/labstack/echo/pull/2625
**Enhancements**
* binder: make binding to Map work better with string destinations by @aldas in https://github.com/labstack/echo/pull/2554
* README.md: add Encore as sponsor by @marcuskohlberg in https://github.com/labstack/echo/pull/2579
* Reorder paragraphs in README.md by @aldas in https://github.com/labstack/echo/pull/2581
* CI: upgrade actions/checkout to v4 by @aldas in https://github.com/labstack/echo/pull/2584
* Remove default charset from 'application/json' Content-Type header by @doortts in https://github.com/labstack/echo/pull/2568
* CI: Use Go 1.22 by @aldas in https://github.com/labstack/echo/pull/2588
* binder: allow binding to a nil map by @georgmu in https://github.com/labstack/echo/pull/2574
* Add Skipper Unit Test In BasicBasicAuthConfig and Add More Detail Explanation regarding BasicAuthValidator by @RyoKusnadi in https://github.com/labstack/echo/pull/2461
* fix some typos by @teslaedison in https://github.com/labstack/echo/pull/2603
* fix: some typos by @pomadev in https://github.com/labstack/echo/pull/2596
* Allow ResponseWriters to unwrap writers when flushing/hijacking by @aldas in https://github.com/labstack/echo/pull/2595
* Add SPDX licence comments to files. by @aldas in https://github.com/labstack/echo/pull/2604
* Upgrade deps by @aldas in https://github.com/labstack/echo/pull/2605
* Change type definition blocks to single declarations. This helps copy… by @aldas in https://github.com/labstack/echo/pull/2606
* Fix Real IP logic by @cl-bvl in https://github.com/labstack/echo/pull/2550
* Default binder can use `UnmarshalParams(params []string) error` inter… by @aldas in https://github.com/labstack/echo/pull/2607
* Default binder can bind pointer to slice as struct field. For example `*[]string` by @aldas in https://github.com/labstack/echo/pull/2608
* Remove maxparam dependence from Context by @aldas in https://github.com/labstack/echo/pull/2611
* When route is registered with empty path it is normalized to `/`. by @aldas in https://github.com/labstack/echo/pull/2616
* proxy middleware should use httputil.ReverseProxy for SSE requests by @aldas in https://github.com/labstack/echo/pull/2624
## v4.11.4 - 2023-12-20
**Security**
* Upgrade golang.org/x/crypto to v0.17.0 to fix vulnerability [issue](https://pkg.go.dev/vuln/GO-2023-2402) [#2562](https://github.com/labstack/echo/pull/2562)
**Enhancements**
* Update deps and mark Go version to 1.18 as this is what golang.org/x/* use [#2563](https://github.com/labstack/echo/pull/2563)
* Request logger: add example for Slog https://pkg.go.dev/log/slog [#2543](https://github.com/labstack/echo/pull/2543)
## v4.11.3 - 2023-11-07
**Security**
* 'c.Attachment' and 'c.Inline' should escape filename in 'Content-Disposition' header to avoid 'Reflect File Download' vulnerability. [#2541](https://github.com/labstack/echo/pull/2541)
**Enhancements**
* Tests: refactor context tests to be separate functions [#2540](https://github.com/labstack/echo/pull/2540)
* Proxy middleware: reuse echo request context [#2537](https://github.com/labstack/echo/pull/2537)
* Mark unmarshallable yaml struct tags as ignored [#2536](https://github.com/labstack/echo/pull/2536)
## v4.11.2 - 2023-10-11
**Security**
* Bump golang.org/x/net to prevent CVE-2023-39325 / CVE-2023-44487 HTTP/2 Rapid Reset Attack [#2527](https://github.com/labstack/echo/pull/2527)
* fix(sec): randomString bias introduced by #2490 [#2492](https://github.com/labstack/echo/pull/2492)
* CSRF/RequestID mw: switch math/random usage to crypto/random [#2490](https://github.com/labstack/echo/pull/2490)
**Enhancements**
* Delete unused context in body_limit.go [#2483](https://github.com/labstack/echo/pull/2483)
* Use Go 1.21 in CI [#2505](https://github.com/labstack/echo/pull/2505)
* Fix some typos [#2511](https://github.com/labstack/echo/pull/2511)
* Allow CORS middleware to send Access-Control-Max-Age: 0 [#2518](https://github.com/labstack/echo/pull/2518)
* Bump dependancies [#2522](https://github.com/labstack/echo/pull/2522)
## v4.11.1 - 2023-07-16
**Fixes**
* Fix `Gzip` middleware not sending response code for no content responses (404, 301/302 redirects etc) [#2481](https://github.com/labstack/echo/pull/2481)
## v4.11.0 - 2023-07-14
**Fixes**
* Fixes the proxy middleware concurrency issue of calling the Next() proxy target on Round Robin Balancer [#2409](https://github.com/labstack/echo/pull/2409)
* Fix `group.RouteNotFound` not working when group has attached middlewares [#2411](https://github.com/labstack/echo/pull/2411)
* Fix global error handler return error message when message is an error [#2456](https://github.com/labstack/echo/pull/2456)
* Do not use global timeNow variables [#2477](https://github.com/labstack/echo/pull/2477)
**Enhancements**
* Added a optional config variable to disable centralized error handler in recovery middleware [#2410](https://github.com/labstack/echo/pull/2410)
* refactor: use `strings.ReplaceAll` directly [#2424](https://github.com/labstack/echo/pull/2424)
* Add support for Go1.20 `http.rwUnwrapper` to Response struct [#2425](https://github.com/labstack/echo/pull/2425)
* Check whether is nil before invoking centralized error handling [#2429](https://github.com/labstack/echo/pull/2429)
* Proper colon support in `echo.Reverse` method [#2416](https://github.com/labstack/echo/pull/2416)
* Fix misuses of a vs an in documentation comments [#2436](https://github.com/labstack/echo/pull/2436)
* Add link to slog.Handler library for Echo logging into README.md [#2444](https://github.com/labstack/echo/pull/2444)
* In proxy middleware Support retries of failed proxy requests [#2414](https://github.com/labstack/echo/pull/2414)
* gofmt fixes to comments [#2452](https://github.com/labstack/echo/pull/2452)
* gzip response only if it exceeds a minimal length [#2267](https://github.com/labstack/echo/pull/2267)
* Upgrade packages [#2475](https://github.com/labstack/echo/pull/2475)
## v4.10.2 - 2023-02-22 ## v4.10.2 - 2023-02-22
**Security** **Security**

View File

@ -31,6 +31,6 @@ benchmark: ## Run benchmarks
help: ## Display this help screen help: ## Display this help screen
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
goversion ?= "1.17" goversion ?= "1.19"
test_version: ## Run tests inside Docker with given version (defaults to 1.17 oldest supported). Example: make test_version goversion=1.17 test_version: ## Run tests inside Docker with given version (defaults to 1.19 oldest supported). Example: make test_version goversion=1.19
@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check" @docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check"

View File

@ -3,26 +3,24 @@
[![Sourcegraph](https://sourcegraph.com/github.com/labstack/echo/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/labstack/echo?badge) [![Sourcegraph](https://sourcegraph.com/github.com/labstack/echo/-/badge.svg?style=flat-square)](https://sourcegraph.com/github.com/labstack/echo?badge)
[![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/labstack/echo/v4) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/labstack/echo/v4)
[![Go Report Card](https://goreportcard.com/badge/github.com/labstack/echo?style=flat-square)](https://goreportcard.com/report/github.com/labstack/echo) [![Go Report Card](https://goreportcard.com/badge/github.com/labstack/echo?style=flat-square)](https://goreportcard.com/report/github.com/labstack/echo)
[![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/labstack/echo/echo.yml?style=flat-square)](https://github.com/labstack/echo/actions)
[![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo) [![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo)
[![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://github.com/labstack/echo/discussions) [![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://github.com/labstack/echo/discussions)
[![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack)
[![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE)
## Supported Go versions ## Echo
Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with High performance, extensible, minimalist Go web framework.
older versions.
As of version 4.0.0, Echo is available as a [Go module](https://github.com/golang/go/wiki/Modules). * [Official website](https://echo.labstack.com)
Therefore a Go version capable of understanding /vN suffixed imports is required: * [Quick start](https://echo.labstack.com/docs/quick-start)
* [Middlewares](https://echo.labstack.com/docs/category/middleware)
Any of these versions will allow you to import Echo as `github.com/labstack/echo/v4` which is the recommended Help and questions: [Github Discussions](https://github.com/labstack/echo/discussions)
way of using Echo going forward.
For older versions, please use the latest v3 tag.
## Feature Overview ### Feature Overview
- Optimized HTTP router which smartly prioritize routes - Optimized HTTP router which smartly prioritize routes
- Build robust and scalable RESTful APIs - Build robust and scalable RESTful APIs
@ -38,6 +36,18 @@ For older versions, please use the latest v3 tag.
- Automatic TLS via Lets Encrypt - Automatic TLS via Lets Encrypt
- HTTP/2 support - HTTP/2 support
## Sponsors
<div>
<a href="https://encore.dev" style="display: inline-flex; align-items: center; gap: 10px">
<img src="https://user-images.githubusercontent.com/78424526/214602214-52e0483a-b5fc-4d4c-b03e-0b7b23e012df.svg" height="28px" alt="encore icon"></img>
<b>Encore the platform for building Go-based cloud backends</b>
</a>
</div>
<br/>
Click [here](https://github.com/sponsors/labstack) for more information on sponsorship.
## Benchmarks ## Benchmarks
Date: 2020/11/11<br> Date: 2020/11/11<br>
@ -57,6 +67,7 @@ The benchmarks above were run on an Intel(R) Core(TM) i7-6820HQ CPU @ 2.70GHz
// go get github.com/labstack/echo/{version} // go get github.com/labstack/echo/{version}
go get github.com/labstack/echo/v4 go get github.com/labstack/echo/v4
``` ```
Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with older versions.
### Example ### Example
@ -110,16 +121,13 @@ of middlewares in this list.
| [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger) | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0. | | [github.com/swaggo/echo-swagger](https://github.com/swaggo/echo-swagger) | Automatically generate RESTful API documentation with [Swagger](https://swagger.io/) 2.0. |
| [github.com/ziflex/lecho](https://github.com/ziflex/lecho) | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface. | | [github.com/ziflex/lecho](https://github.com/ziflex/lecho) | [Zerolog](https://github.com/rs/zerolog) logging library wrapper for Echo logger interface. |
| [github.com/brpaz/echozap](https://github.com/brpaz/echozap) | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface. | | [github.com/brpaz/echozap](https://github.com/brpaz/echozap) | Uber´s [Zap](https://github.com/uber-go/zap) logging library wrapper for Echo logger interface. |
| [github.com/samber/slog-echo](https://github.com/samber/slog-echo) | Go [slog](https://pkg.go.dev/golang.org/x/exp/slog) logging library wrapper for Echo logger interface. |
| [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs. | | [github.com/darkweak/souin/plugins/echo](https://github.com/darkweak/souin/tree/master/plugins/echo) | HTTP cache system based on [Souin](https://github.com/darkweak/souin) to automatically get your endpoints cached. It supports some distributed and non-distributed storage systems depending your needs. |
| [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda) | Rapid, easy full-stack web development starter kit built with Echo. | | [github.com/mikestefanello/pagoda](https://github.com/mikestefanello/pagoda) | Rapid, easy full-stack web development starter kit built with Echo. |
| [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo) | ProtoBuf generate Echo server side code | | [github.com/go-woo/protoc-gen-echo](https://github.com/go-woo/protoc-gen-echo) | ProtoBuf generate Echo server side code |
Please send a PR to add your own library here. Please send a PR to add your own library here.
## Help
- [Forum](https://github.com/labstack/echo/discussions)
## Contribute ## Contribute
**Use issues for everything** **Use issues for everything**

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
@ -11,23 +14,28 @@ import (
"strings" "strings"
) )
type ( // Binder is the interface that wraps the Bind method.
// Binder is the interface that wraps the Bind method. type Binder interface {
Binder interface { Bind(i interface{}, c Context) error
Bind(i interface{}, c Context) error }
}
// DefaultBinder is the default implementation of the Binder interface. // DefaultBinder is the default implementation of the Binder interface.
DefaultBinder struct{} type DefaultBinder struct{}
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method. // BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
// Types that don't implement this, but do implement encoding.TextUnmarshaler // Types that don't implement this, but do implement encoding.TextUnmarshaler
// will use that interface instead. // will use that interface instead.
BindUnmarshaler interface { type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param. // UnmarshalParam decodes and assigns a value from an form or query param.
UnmarshalParam(param string) error UnmarshalParam(param string) error
} }
)
// bindMultipleUnmarshaler is used by binder to unmarshal multiple values from request at once to
// type implementing this interface. For example request could have multiple query fields `?a=1&a=2&b=test` in that case
// for `a` following slice `["1", "2"] will be passed to unmarshaller.
type bindMultipleUnmarshaler interface {
UnmarshalParams(params []string) error
}
// BindPathParams binds path params to bindable object // BindPathParams binds path params to bindable object
func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error { func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error {
@ -114,7 +122,7 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
// Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body. // Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body.
// For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues. // For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues.
// The HTTP method check restores pre-v4.1.11 behavior to avoid these problems (see issue #1670) // The HTTP method check restores pre-v4.1.11 behavior to avoid these problems (see issue #1670)
method := c.Request().Method method := c.Request().Method
if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead { if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {
if err = b.BindQueryParams(c, i); err != nil { if err = b.BindQueryParams(c, i); err != nil {
return err return err
@ -131,10 +139,29 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
typ := reflect.TypeOf(destination).Elem() typ := reflect.TypeOf(destination).Elem()
val := reflect.ValueOf(destination).Elem() val := reflect.ValueOf(destination).Elem()
// Map // Support binding to limited Map destinations:
if typ.Kind() == reflect.Map { // - map[string][]string,
// - map[string]string <-- (binds first value from data slice)
// - map[string]interface{}
// You are better off binding to struct but there are user who want this map feature. Source of data for these cases are:
// params,query,header,form as these sources produce string values, most of the time slice of strings, actually.
if typ.Kind() == reflect.Map && typ.Key().Kind() == reflect.String {
k := typ.Elem().Kind()
isElemInterface := k == reflect.Interface
isElemString := k == reflect.String
isElemSliceOfStrings := k == reflect.Slice && typ.Elem().Elem().Kind() == reflect.String
if !(isElemSliceOfStrings || isElemString || isElemInterface) {
return nil
}
if val.IsNil() {
val.Set(reflect.MakeMap(typ))
}
for k, v := range data { for k, v := range data {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) if isElemString {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0]))
} else {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v))
}
} }
return nil return nil
} }
@ -161,14 +188,14 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
} }
structFieldKind := structField.Kind() structFieldKind := structField.Kind()
inputFieldName := typeField.Tag.Get(tag) inputFieldName := typeField.Tag.Get(tag)
if typeField.Anonymous && structField.Kind() == reflect.Struct && inputFieldName != "" { if typeField.Anonymous && structFieldKind == reflect.Struct && inputFieldName != "" {
// if anonymous struct with query/param/form tags, report an error // if anonymous struct with query/param/form tags, report an error
return errors.New("query/param/form tags are not allowed with anonymous struct field") return errors.New("query/param/form tags are not allowed with anonymous struct field")
} }
if inputFieldName == "" { if inputFieldName == "" {
// If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags). // If tag is nil, we inspect if the field is a not BindUnmarshaler struct and try to bind data into it (might contains fields with tags).
// structs that implement BindUnmarshaler are binded only when they have explicit tag // structs that implement BindUnmarshaler are bound only when they have explicit tag
if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct { if _, ok := structField.Addr().Interface().(BindUnmarshaler); !ok && structFieldKind == reflect.Struct {
if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil { if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
return err return err
@ -197,27 +224,46 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
continue continue
} }
// Call this first, in case we're dealing with an alias to an array type // NOTE: algorithm here is not particularly sophisticated. It probably does not work with absurd types like `**[]*int`
if ok, err := unmarshalField(typeField.Type.Kind(), inputValue[0], structField); ok { // but it is smart enough to handle niche cases like `*int`,`*[]string`,`[]*int` .
// try unmarshalling first, in case we're dealing with an alias to an array type
if ok, err := unmarshalInputsToField(typeField.Type.Kind(), inputValue, structField); ok {
if err != nil { if err != nil {
return err return err
} }
continue continue
} }
numElems := len(inputValue) if ok, err := unmarshalInputToField(typeField.Type.Kind(), inputValue[0], structField); ok {
if structFieldKind == reflect.Slice && numElems > 0 { if err != nil {
return err
}
continue
}
// we could be dealing with pointer to slice `*[]string` so dereference it. There are wierd OpenAPI generators
// that could create struct fields like that.
if structFieldKind == reflect.Pointer {
structFieldKind = structField.Elem().Kind()
structField = structField.Elem()
}
if structFieldKind == reflect.Slice {
sliceOf := structField.Type().Elem().Kind() sliceOf := structField.Type().Elem().Kind()
numElems := len(inputValue)
slice := reflect.MakeSlice(structField.Type(), numElems, numElems) slice := reflect.MakeSlice(structField.Type(), numElems, numElems)
for j := 0; j < numElems; j++ { for j := 0; j < numElems; j++ {
if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil { if err := setWithProperType(sliceOf, inputValue[j], slice.Index(j)); err != nil {
return err return err
} }
} }
val.Field(i).Set(slice) structField.Set(slice)
} else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { continue
return err }
if err := setWithProperType(structFieldKind, inputValue[0], structField); err != nil {
return err
} }
} }
return nil return nil
@ -225,7 +271,7 @@ func (b *DefaultBinder) bindData(destination interface{}, data map[string][]stri
func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error {
// But also call it here, in case we're dealing with an array of BindUnmarshalers // But also call it here, in case we're dealing with an array of BindUnmarshalers
if ok, err := unmarshalField(valueKind, val, structField); ok { if ok, err := unmarshalInputToField(valueKind, val, structField); ok {
return err return err
} }
@ -266,35 +312,41 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
return nil return nil
} }
func unmarshalField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) { func unmarshalInputsToField(valueKind reflect.Kind, values []string, field reflect.Value) (bool, error) {
switch valueKind { if valueKind == reflect.Ptr {
case reflect.Ptr: if field.IsNil() {
return unmarshalFieldPtr(val, field) field.Set(reflect.New(field.Type().Elem()))
default: }
return unmarshalFieldNonPtr(val, field) field = field.Elem()
} }
fieldIValue := field.Addr().Interface()
unmarshaler, ok := fieldIValue.(bindMultipleUnmarshaler)
if !ok {
return false, nil
}
return true, unmarshaler.UnmarshalParams(values)
} }
func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) { func unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
fieldIValue := field.Addr().Interface() if valueKind == reflect.Ptr {
if unmarshaler, ok := fieldIValue.(BindUnmarshaler); ok { if field.IsNil() {
return true, unmarshaler.UnmarshalParam(value) field.Set(reflect.New(field.Type().Elem()))
}
field = field.Elem()
} }
if unmarshaler, ok := fieldIValue.(encoding.TextUnmarshaler); ok {
return true, unmarshaler.UnmarshalText([]byte(value)) fieldIValue := field.Addr().Interface()
switch unmarshaler := fieldIValue.(type) {
case BindUnmarshaler:
return true, unmarshaler.UnmarshalParam(val)
case encoding.TextUnmarshaler:
return true, unmarshaler.UnmarshalText([]byte(val))
} }
return false, nil return false, nil
} }
func unmarshalFieldPtr(value string, field reflect.Value) (bool, error) {
if field.IsNil() {
// Initialize the pointer to a nil value
field.Set(reflect.New(field.Type().Elem()))
}
return unmarshalFieldNonPtr(value, field.Elem())
}
func setIntField(value string, bitSize int, field reflect.Value) error { func setIntField(value string, bitSize int, field reflect.Value) error {
if value == "" { if value == "" {
value = "0" value = "0"

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
@ -1236,7 +1239,7 @@ func (b *ValueBinder) durations(sourceParam string, values []string, dest *[]tim
// Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00 // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00
// //
// Note: // Note:
// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder { func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, false, time.Second) return b.unixTime(sourceParam, dest, false, time.Second)
} }
@ -1247,7 +1250,7 @@ func (b *ValueBinder) UnixTime(sourceParam string, dest *time.Time) *ValueBinder
// Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00 // Example: 1609180603 bind to 2020-12-28T18:36:43.000000000+00:00
// //
// Note: // Note:
// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder { func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, true, time.Second) return b.unixTime(sourceParam, dest, true, time.Second)
} }
@ -1257,7 +1260,7 @@ func (b *ValueBinder) MustUnixTime(sourceParam string, dest *time.Time) *ValueBi
// Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00 // Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00
// //
// Note: // Note:
// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder { func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, false, time.Millisecond) return b.unixTime(sourceParam, dest, false, time.Millisecond)
} }
@ -1268,7 +1271,7 @@ func (b *ValueBinder) UnixTimeMilli(sourceParam string, dest *time.Time) *ValueB
// Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00 // Example: 1647184410140 bind to 2022-03-13T15:13:30.140000000+00:00
// //
// Note: // Note:
// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder { func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, true, time.Millisecond) return b.unixTime(sourceParam, dest, true, time.Millisecond)
} }
@ -1280,8 +1283,8 @@ func (b *ValueBinder) MustUnixTimeMilli(sourceParam string, dest *time.Time) *Va
// Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00 // Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00
// //
// Note: // Note:
// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
// * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. // - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.
func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, false, time.Nanosecond) return b.unixTime(sourceParam, dest, false, time.Nanosecond)
} }
@ -1294,8 +1297,8 @@ func (b *ValueBinder) UnixTimeNano(sourceParam string, dest *time.Time) *ValueBi
// Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00 // Example: 999999999 binds to 1970-01-01T00:00:00.999999999+00:00
// //
// Note: // Note:
// * time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal // - time.Time{} (param is empty) and time.Unix(0,0) (param = "0") are not equal
// * Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example. // - Javascript's Number type only has about 53 bits of precision (Number.MAX_SAFE_INTEGER = 9007199254740991). Compare it to 1609180603123456789 in example.
func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder { func (b *ValueBinder) MustUnixTimeNano(sourceParam string, dest *time.Time) *ValueBinder {
return b.unixTime(sourceParam, dest, true, time.Nanosecond) return b.unixTime(sourceParam, dest, true, time.Nanosecond)
} }
@ -1323,7 +1326,7 @@ func (b *ValueBinder) unixTime(sourceParam string, dest *time.Time, valueMustExi
case time.Second: case time.Second:
*dest = time.Unix(n, 0) *dest = time.Unix(n, 0)
case time.Millisecond: case time.Millisecond:
*dest = time.Unix(n/1e3, (n%1e3)*1e6) // TODO: time.UnixMilli(n) exists since Go1.17 switch to that when min version allows *dest = time.UnixMilli(n)
case time.Nanosecond: case time.Nanosecond:
*dest = time.Unix(0, n) *dest = time.Unix(0, n)
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
@ -13,204 +16,216 @@ import (
"sync" "sync"
) )
type ( // Context represents the context of the current HTTP request. It holds request and
// Context represents the context of the current HTTP request. It holds request and // response objects, path, path parameters, data and registered handler.
// response objects, path, path parameters, data and registered handler. type Context interface {
Context interface { // Request returns `*http.Request`.
// Request returns `*http.Request`. Request() *http.Request
Request() *http.Request
// SetRequest sets `*http.Request`. // SetRequest sets `*http.Request`.
SetRequest(r *http.Request) SetRequest(r *http.Request)
// SetResponse sets `*Response`. // SetResponse sets `*Response`.
SetResponse(r *Response) SetResponse(r *Response)
// Response returns `*Response`. // Response returns `*Response`.
Response() *Response Response() *Response
// IsTLS returns true if HTTP connection is TLS otherwise false. // IsTLS returns true if HTTP connection is TLS otherwise false.
IsTLS() bool IsTLS() bool
// IsWebSocket returns true if HTTP connection is WebSocket otherwise false. // IsWebSocket returns true if HTTP connection is WebSocket otherwise false.
IsWebSocket() bool IsWebSocket() bool
// Scheme returns the HTTP protocol scheme, `http` or `https`. // Scheme returns the HTTP protocol scheme, `http` or `https`.
Scheme() string Scheme() string
// RealIP returns the client's network address based on `X-Forwarded-For` // RealIP returns the client's network address based on `X-Forwarded-For`
// or `X-Real-IP` request header. // or `X-Real-IP` request header.
// The behavior can be configured using `Echo#IPExtractor`. // The behavior can be configured using `Echo#IPExtractor`.
RealIP() string RealIP() string
// Path returns the registered path for the handler. // Path returns the registered path for the handler.
Path() string Path() string
// SetPath sets the registered path for the handler. // SetPath sets the registered path for the handler.
SetPath(p string) SetPath(p string)
// Param returns path parameter by name. // Param returns path parameter by name.
Param(name string) string Param(name string) string
// ParamNames returns path parameter names. // ParamNames returns path parameter names.
ParamNames() []string ParamNames() []string
// SetParamNames sets path parameter names. // SetParamNames sets path parameter names.
SetParamNames(names ...string) SetParamNames(names ...string)
// ParamValues returns path parameter values. // ParamValues returns path parameter values.
ParamValues() []string ParamValues() []string
// SetParamValues sets path parameter values. // SetParamValues sets path parameter values.
SetParamValues(values ...string) SetParamValues(values ...string)
// QueryParam returns the query param for the provided name. // QueryParam returns the query param for the provided name.
QueryParam(name string) string QueryParam(name string) string
// QueryParams returns the query parameters as `url.Values`. // QueryParams returns the query parameters as `url.Values`.
QueryParams() url.Values QueryParams() url.Values
// QueryString returns the URL query string. // QueryString returns the URL query string.
QueryString() string QueryString() string
// FormValue returns the form field value for the provided name. // FormValue returns the form field value for the provided name.
FormValue(name string) string FormValue(name string) string
// FormParams returns the form parameters as `url.Values`. // FormParams returns the form parameters as `url.Values`.
FormParams() (url.Values, error) FormParams() (url.Values, error)
// FormFile returns the multipart form file for the provided name. // FormFile returns the multipart form file for the provided name.
FormFile(name string) (*multipart.FileHeader, error) FormFile(name string) (*multipart.FileHeader, error)
// MultipartForm returns the multipart form. // MultipartForm returns the multipart form.
MultipartForm() (*multipart.Form, error) MultipartForm() (*multipart.Form, error)
// Cookie returns the named cookie provided in the request. // Cookie returns the named cookie provided in the request.
Cookie(name string) (*http.Cookie, error) Cookie(name string) (*http.Cookie, error)
// SetCookie adds a `Set-Cookie` header in HTTP response. // SetCookie adds a `Set-Cookie` header in HTTP response.
SetCookie(cookie *http.Cookie) SetCookie(cookie *http.Cookie)
// Cookies returns the HTTP cookies sent with the request. // Cookies returns the HTTP cookies sent with the request.
Cookies() []*http.Cookie Cookies() []*http.Cookie
// Get retrieves data from the context. // Get retrieves data from the context.
Get(key string) interface{} Get(key string) interface{}
// Set saves data in the context. // Set saves data in the context.
Set(key string, val interface{}) Set(key string, val interface{})
// Bind binds the request body into provided type `i`. The default binder // Bind binds path params, query params and the request body into provided type `i`. The default binder
// does it based on Content-Type header. // binds body based on Content-Type header.
Bind(i interface{}) error Bind(i interface{}) error
// Validate validates provided `i`. It is usually called after `Context#Bind()`. // Validate validates provided `i`. It is usually called after `Context#Bind()`.
// Validator must be registered using `Echo#Validator`. // Validator must be registered using `Echo#Validator`.
Validate(i interface{}) error Validate(i interface{}) error
// Render renders a template with data and sends a text/html response with status // Render renders a template with data and sends a text/html response with status
// code. Renderer must be registered using `Echo.Renderer`. // code. Renderer must be registered using `Echo.Renderer`.
Render(code int, name string, data interface{}) error Render(code int, name string, data interface{}) error
// HTML sends an HTTP response with status code. // HTML sends an HTTP response with status code.
HTML(code int, html string) error HTML(code int, html string) error
// HTMLBlob sends an HTTP blob response with status code. // HTMLBlob sends an HTTP blob response with status code.
HTMLBlob(code int, b []byte) error HTMLBlob(code int, b []byte) error
// String sends a string response with status code. // String sends a string response with status code.
String(code int, s string) error String(code int, s string) error
// JSON sends a JSON response with status code. // JSON sends a JSON response with status code.
JSON(code int, i interface{}) error JSON(code int, i interface{}) error
// JSONPretty sends a pretty-print JSON with status code. // JSONPretty sends a pretty-print JSON with status code.
JSONPretty(code int, i interface{}, indent string) error JSONPretty(code int, i interface{}, indent string) error
// JSONBlob sends a JSON blob response with status code. // JSONBlob sends a JSON blob response with status code.
JSONBlob(code int, b []byte) error JSONBlob(code int, b []byte) error
// JSONP sends a JSONP response with status code. It uses `callback` to construct // JSONP sends a JSONP response with status code. It uses `callback` to construct
// the JSONP payload. // the JSONP payload.
JSONP(code int, callback string, i interface{}) error JSONP(code int, callback string, i interface{}) error
// JSONPBlob sends a JSONP blob response with status code. It uses `callback` // JSONPBlob sends a JSONP blob response with status code. It uses `callback`
// to construct the JSONP payload. // to construct the JSONP payload.
JSONPBlob(code int, callback string, b []byte) error JSONPBlob(code int, callback string, b []byte) error
// XML sends an XML response with status code. // XML sends an XML response with status code.
XML(code int, i interface{}) error XML(code int, i interface{}) error
// XMLPretty sends a pretty-print XML with status code. // XMLPretty sends a pretty-print XML with status code.
XMLPretty(code int, i interface{}, indent string) error XMLPretty(code int, i interface{}, indent string) error
// XMLBlob sends an XML blob response with status code. // XMLBlob sends an XML blob response with status code.
XMLBlob(code int, b []byte) error XMLBlob(code int, b []byte) error
// Blob sends a blob response with status code and content type. // Blob sends a blob response with status code and content type.
Blob(code int, contentType string, b []byte) error Blob(code int, contentType string, b []byte) error
// Stream sends a streaming response with status code and content type. // Stream sends a streaming response with status code and content type.
Stream(code int, contentType string, r io.Reader) error Stream(code int, contentType string, r io.Reader) error
// File sends a response with the content of the file. // File sends a response with the content of the file.
File(file string) error File(file string) error
// Attachment sends a response as attachment, prompting client to save the // Attachment sends a response as attachment, prompting client to save the
// file. // file.
Attachment(file string, name string) error Attachment(file string, name string) error
// Inline sends a response as inline, opening the file in the browser. // Inline sends a response as inline, opening the file in the browser.
Inline(file string, name string) error Inline(file string, name string) error
// NoContent sends a response with no body and a status code. // NoContent sends a response with no body and a status code.
NoContent(code int) error NoContent(code int) error
// Redirect redirects the request to a provided URL with status code. // Redirect redirects the request to a provided URL with status code.
Redirect(code int, url string) error Redirect(code int, url string) error
// Error invokes the registered global HTTP error handler. Generally used by middleware. // Error invokes the registered global HTTP error handler. Generally used by middleware.
// A side-effect of calling global error handler is that now Response has been committed (sent to the client) and // A side-effect of calling global error handler is that now Response has been committed (sent to the client) and
// middlewares up in chain can not change Response status code or Response body anymore. // middlewares up in chain can not change Response status code or Response body anymore.
// //
// Avoid using this method in handlers as no middleware will be able to effectively handle errors after that. // Avoid using this method in handlers as no middleware will be able to effectively handle errors after that.
Error(err error) Error(err error)
// Handler returns the matched handler by router. // Handler returns the matched handler by router.
Handler() HandlerFunc Handler() HandlerFunc
// SetHandler sets the matched handler by router. // SetHandler sets the matched handler by router.
SetHandler(h HandlerFunc) SetHandler(h HandlerFunc)
// Logger returns the `Logger` instance. // Logger returns the `Logger` instance.
Logger() Logger Logger() Logger
// SetLogger Set the logger // SetLogger Set the logger
SetLogger(l Logger) SetLogger(l Logger)
// Echo returns the `Echo` instance. // Echo returns the `Echo` instance.
Echo() *Echo Echo() *Echo
// Reset resets the context after request completes. It must be called along // Reset resets the context after request completes. It must be called along
// with `Echo#AcquireContext()` and `Echo#ReleaseContext()`. // with `Echo#AcquireContext()` and `Echo#ReleaseContext()`.
// See `Echo#ServeHTTP()` // See `Echo#ServeHTTP()`
Reset(r *http.Request, w http.ResponseWriter) Reset(r *http.Request, w http.ResponseWriter)
} }
context struct { type context struct {
request *http.Request request *http.Request
response *Response response *Response
path string query url.Values
pnames []string echo *Echo
pvalues []string logger Logger
query url.Values
handler HandlerFunc store Map
store Map lock sync.RWMutex
echo *Echo
logger Logger // following fields are set by Router
lock sync.RWMutex
} // path is route path that Router matched. It is empty string where there is no route match.
) // Route registered with RouteNotFound is considered as a match and path therefore is not empty.
path string
// pnames length is tied to param count for the matched route
pnames []string
// Usually echo.Echo is sizing pvalues but there could be user created middlewares that decide to
// overwrite parameter by calling SetParamNames + SetParamValues.
// When echo.Echo allocated that slice it length/capacity is tied to echo.Echo.maxParam value.
//
// It is important that pvalues size is always equal or bigger to pnames length.
pvalues []string
handler HandlerFunc
}
const ( const (
// ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain. // ContextKeyHeaderAllow is set by Router for getting value for `Allow` header in later stages of handler call chain.
@ -329,13 +344,9 @@ func (c *context) SetParamNames(names ...string) {
c.pnames = names c.pnames = names
l := len(names) l := len(names)
if *c.echo.maxParam < l {
*c.echo.maxParam = l
}
if len(c.pvalues) < l { if len(c.pvalues) < l {
// Keeping the old pvalues just for backward compatibility, but it sounds that doesn't make sense to keep them, // Keeping the old pvalues just for backward compatibility, but it sounds that doesn't make sense to keep them,
// probably those values will be overriden in a Context#SetParamValues // probably those values will be overridden in a Context#SetParamValues
newPvalues := make([]string, l) newPvalues := make([]string, l)
copy(newPvalues, c.pvalues) copy(newPvalues, c.pvalues)
c.pvalues = newPvalues c.pvalues = newPvalues
@ -347,11 +358,11 @@ func (c *context) ParamValues() []string {
} }
func (c *context) SetParamValues(values ...string) { func (c *context) SetParamValues(values ...string) {
// NOTE: Don't just set c.pvalues = values, because it has to have length c.echo.maxParam at all times // NOTE: Don't just set c.pvalues = values, because it has to have length c.echo.maxParam (or bigger) at all times
// It will brake the Router#Find code // It will brake the Router#Find code
limit := len(values) limit := len(values)
if limit > *c.echo.maxParam { if limit > len(c.pvalues) {
limit = *c.echo.maxParam c.pvalues = make([]string, limit)
} }
for i := 0; i < limit; i++ { for i := 0; i < limit; i++ {
c.pvalues[i] = values[i] c.pvalues[i] = values[i]
@ -489,7 +500,7 @@ func (c *context) jsonPBlob(code int, callback string, i interface{}) (err error
} }
func (c *context) json(code int, i interface{}, indent string) error { func (c *context) json(code int, i interface{}, indent string) error {
c.writeContentType(MIMEApplicationJSONCharsetUTF8) c.writeContentType(MIMEApplicationJSON)
c.response.Status = code c.response.Status = code
return c.echo.JSONSerializer.Serialize(c, i, indent) return c.echo.JSONSerializer.Serialize(c, i, indent)
} }
@ -507,7 +518,7 @@ func (c *context) JSONPretty(code int, i interface{}, indent string) (err error)
} }
func (c *context) JSONBlob(code int, b []byte) (err error) { func (c *context) JSONBlob(code int, b []byte) (err error) {
return c.Blob(code, MIMEApplicationJSONCharsetUTF8, b) return c.Blob(code, MIMEApplicationJSON, b)
} }
func (c *context) JSONP(code int, callback string, i interface{}) (err error) { func (c *context) JSONP(code int, callback string, i interface{}) (err error) {
@ -584,8 +595,10 @@ func (c *context) Inline(file, name string) error {
return c.contentDisposition(file, name, "inline") return c.contentDisposition(file, name, "inline")
} }
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func (c *context) contentDisposition(file, name, dispositionType string) error { func (c *context) contentDisposition(file, name, dispositionType string) error {
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name)) c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf(`%s; filename="%s"`, dispositionType, quoteEscaper.Replace(name)))
return c.File(file) return c.File(file)
} }
@ -640,8 +653,8 @@ func (c *context) Reset(r *http.Request, w http.ResponseWriter) {
c.path = "" c.path = ""
c.pnames = nil c.pnames = nil
c.logger = nil c.logger = nil
// NOTE: Don't reset because it has to have length c.echo.maxParam at all times // NOTE: Don't reset because it has to have length c.echo.maxParam (or bigger) at all times
for i := 0; i < *c.echo.maxParam; i++ { for i := 0; i < len(c.pvalues); i++ {
c.pvalues[i] = "" c.pvalues[i] = ""
} }
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
/* /*
Package echo implements high performance, minimalist Go web framework. Package echo implements high performance, minimalist Go web framework.
@ -39,6 +42,7 @@ package echo
import ( import (
stdContext "context" stdContext "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -59,97 +63,95 @@ import (
"golang.org/x/net/http2/h2c" "golang.org/x/net/http2/h2c"
) )
type ( // Echo is the top-level framework instance.
// Echo is the top-level framework instance. //
// // Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these
// Goroutine safety: Do not mutate Echo instance fields after server has started. Accessing these // fields from handlers/middlewares and changing field values at the same time leads to data-races.
// fields from handlers/middlewares and changing field values at the same time leads to data-races. // Adding new routes after the server has been started is also not safe!
// Adding new routes after the server has been started is also not safe! type Echo struct {
Echo struct { filesystem
filesystem common
common // startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get
// startupMutex is mutex to lock Echo instance access during server configuration and startup. Useful for to get // listener address info (on which interface/port was listener bound) without having data races.
// listener address info (on which interface/port was listener binded) without having data races. startupMutex sync.RWMutex
startupMutex sync.RWMutex colorer *color.Color
colorer *color.Color
// premiddleware are middlewares that are run before routing is done. In case a pre-middleware returns // premiddleware are middlewares that are run before routing is done. In case a pre-middleware returns
// an error the router is not executed and the request will end up in the global error handler. // an error the router is not executed and the request will end up in the global error handler.
premiddleware []MiddlewareFunc premiddleware []MiddlewareFunc
middleware []MiddlewareFunc middleware []MiddlewareFunc
maxParam *int maxParam *int
router *Router router *Router
routers map[string]*Router routers map[string]*Router
pool sync.Pool pool sync.Pool
StdLogger *stdLog.Logger StdLogger *stdLog.Logger
Server *http.Server Server *http.Server
TLSServer *http.Server TLSServer *http.Server
Listener net.Listener Listener net.Listener
TLSListener net.Listener TLSListener net.Listener
AutoTLSManager autocert.Manager AutoTLSManager autocert.Manager
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool HideBanner bool
HidePort bool HidePort bool
HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler
Binder Binder Binder Binder
JSONSerializer JSONSerializer JSONSerializer JSONSerializer
Validator Validator Validator Validator
Renderer Renderer Renderer Renderer
Logger Logger Logger Logger
IPExtractor IPExtractor IPExtractor IPExtractor
ListenerNetwork string ListenerNetwork string
// OnAddRouteHandler is called when Echo adds new route to specific host router. // OnAddRouteHandler is called when Echo adds new route to specific host router.
OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc) OnAddRouteHandler func(host string, route Route, handler HandlerFunc, middleware []MiddlewareFunc)
} }
// Route contains a handler and information for matching against requests. // Route contains a handler and information for matching against requests.
Route struct { type Route struct {
Method string `json:"method"` Method string `json:"method"`
Path string `json:"path"` Path string `json:"path"`
Name string `json:"name"` Name string `json:"name"`
} }
// HTTPError represents an error that occurred while handling a request. // HTTPError represents an error that occurred while handling a request.
HTTPError struct { type HTTPError struct {
Code int `json:"-"` Code int `json:"-"`
Message interface{} `json:"message"` Message interface{} `json:"message"`
Internal error `json:"-"` // Stores the error returned by an external dependency Internal error `json:"-"` // Stores the error returned by an external dependency
} }
// MiddlewareFunc defines a function to process middleware. // MiddlewareFunc defines a function to process middleware.
MiddlewareFunc func(next HandlerFunc) HandlerFunc type MiddlewareFunc func(next HandlerFunc) HandlerFunc
// HandlerFunc defines a function to serve HTTP requests. // HandlerFunc defines a function to serve HTTP requests.
HandlerFunc func(c Context) error type HandlerFunc func(c Context) error
// HTTPErrorHandler is a centralized HTTP error handler. // HTTPErrorHandler is a centralized HTTP error handler.
HTTPErrorHandler func(err error, c Context) type HTTPErrorHandler func(err error, c Context)
// Validator is the interface that wraps the Validate function. // Validator is the interface that wraps the Validate function.
Validator interface { type Validator interface {
Validate(i interface{}) error Validate(i interface{}) error
} }
// JSONSerializer is the interface that encodes and decodes JSON to and from interfaces. // JSONSerializer is the interface that encodes and decodes JSON to and from interfaces.
JSONSerializer interface { type JSONSerializer interface {
Serialize(c Context, i interface{}, indent string) error Serialize(c Context, i interface{}, indent string) error
Deserialize(c Context, i interface{}) error Deserialize(c Context, i interface{}) error
} }
// Renderer is the interface that wraps the Render function. // Renderer is the interface that wraps the Render function.
Renderer interface { type Renderer interface {
Render(io.Writer, string, interface{}, Context) error Render(io.Writer, string, interface{}, Context) error
} }
// Map defines a generic map of type `map[string]interface{}`. // Map defines a generic map of type `map[string]interface{}`.
Map map[string]interface{} type Map map[string]interface{}
// Common struct for Echo & Group. // Common struct for Echo & Group.
common struct{} type common struct{}
)
// HTTP methods // HTTP methods
// NOTE: Deprecated, please use the stdlib constants directly instead. // NOTE: Deprecated, please use the stdlib constants directly instead.
@ -168,7 +170,12 @@ const (
// MIME types // MIME types
const ( const (
MIMEApplicationJSON = "application/json" // MIMEApplicationJSON JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259
MIMEApplicationJSON = "application/json"
// Deprecated: Please use MIMEApplicationJSON instead. JSON should be encoded using UTF-8 by default.
// No "charset" parameter is defined for this registration.
// Adding one really has no effect on compliant recipients.
// See RFC 8259, section 8.1. https://datatracker.ietf.org/doc/html/rfc8259#section-8.1
MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8 MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + charsetUTF8
MIMEApplicationJavaScript = "application/javascript" MIMEApplicationJavaScript = "application/javascript"
MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8 MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + charsetUTF8
@ -258,7 +265,7 @@ const (
const ( const (
// Version of Echo // Version of Echo
Version = "4.10.2" Version = "4.12.0"
website = "https://echo.labstack.com" website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo // http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = ` banner = `
@ -273,21 +280,19 @@ ____________________________________O/_______
` `
) )
var ( var methods = [...]string{
methods = [...]string{ http.MethodConnect,
http.MethodConnect, http.MethodDelete,
http.MethodDelete, http.MethodGet,
http.MethodGet, http.MethodHead,
http.MethodHead, http.MethodOptions,
http.MethodOptions, http.MethodPatch,
http.MethodPatch, http.MethodPost,
http.MethodPost, PROPFIND,
PROPFIND, http.MethodPut,
http.MethodPut, http.MethodTrace,
http.MethodTrace, REPORT,
REPORT, }
}
)
// Errors // Errors
var ( var (
@ -340,22 +345,23 @@ var (
ErrInvalidListenerNetwork = errors.New("invalid listener network") ErrInvalidListenerNetwork = errors.New("invalid listener network")
) )
// Error handlers // NotFoundHandler is the handler that router uses in case there was no matching route found. Returns an error that results
var ( // HTTP 404 status code.
NotFoundHandler = func(c Context) error { var NotFoundHandler = func(c Context) error {
return ErrNotFound return ErrNotFound
} }
MethodNotAllowedHandler = func(c Context) error { // MethodNotAllowedHandler is the handler thar router uses in case there was no matching route found but there was
// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed) // another matching routes for that requested URL. Returns an error that results HTTP 405 Method Not Allowed status code.
// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned var MethodNotAllowedHandler = func(c Context) error {
routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string) // See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)
if ok && routerAllowMethods != "" { // response and MAY do so in any other response. For disabled resources an empty Allow header may be returned
c.Response().Header().Set(HeaderAllow, routerAllowMethods) routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)
} if ok && routerAllowMethods != "" {
return ErrMethodNotAllowed c.Response().Header().Set(HeaderAllow, routerAllowMethods)
} }
) return ErrMethodNotAllowed
}
// New creates an instance of Echo. // New creates an instance of Echo.
func New() (e *Echo) { func New() (e *Echo) {
@ -413,7 +419,7 @@ func (e *Echo) Routers() map[string]*Router {
// //
// NOTE: In case errors happens in middleware call-chain that is returning from handler (which did not return an error). // NOTE: In case errors happens in middleware call-chain that is returning from handler (which did not return an error).
// When handler has already sent response (ala c.JSON()) and there is error in middleware that is returning from // When handler has already sent response (ala c.JSON()) and there is error in middleware that is returning from
// handler. Then the error that global error handler received will be ignored because we have already "commited" the // handler. Then the error that global error handler received will be ignored because we have already "committed" the
// response and status code header has been sent to the client. // response and status code header has been sent to the client.
func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) { func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
@ -438,12 +444,18 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
// Issue #1426 // Issue #1426
code := he.Code code := he.Code
message := he.Message message := he.Message
if m, ok := he.Message.(string); ok {
switch m := he.Message.(type) {
case string:
if e.Debug { if e.Debug {
message = Map{"message": m, "error": err.Error()} message = Map{"message": m, "error": err.Error()}
} else { } else {
message = Map{"message": m} message = Map{"message": m}
} }
case json.Marshaler:
// do nothing - this type knows how to format itself to JSON
case error:
message = Map{"message": m.Error()}
} }
// Send response // Send response
@ -614,7 +626,7 @@ func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...) return e.URI(h, params...)
} }
// Reverse generates an URL from route name and provided parameters. // Reverse generates a URL from route name and provided parameters.
func (e *Echo) Reverse(name string, params ...interface{}) string { func (e *Echo) Reverse(name string, params ...interface{}) string {
return e.router.Reverse(name, params...) return e.router.Reverse(name, params...)
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (

View File

@ -1,21 +1,22 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
"net/http" "net/http"
) )
type ( // Group is a set of sub-routes for a specified route. It can be used for inner
// Group is a set of sub-routes for a specified route. It can be used for inner // routes that share a common middleware or functionality that should be separate
// routes that share a common middleware or functionality that should be separate // from the parent echo instance while still inheriting from it.
// from the parent echo instance while still inheriting from it. type Group struct {
Group struct { common
common host string
host string prefix string
prefix string middleware []MiddlewareFunc
middleware []MiddlewareFunc echo *Echo
echo *Echo }
}
)
// Use implements `Echo#Use()` for sub-routes within the Group. // Use implements `Echo#Use()` for sub-routes within the Group.
func (g *Group) Use(middleware ...MiddlewareFunc) { func (g *Group) Use(middleware ...MiddlewareFunc) {
@ -23,10 +24,12 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
if len(g.middleware) == 0 { if len(g.middleware) == 0 {
return return
} }
// Allow all requests to reach the group as they might get dropped if router // group level middlewares are different from Echo `Pre` and `Use` middlewares (those are global). Group level middlewares
// doesn't find a match, making none of the group middleware process. // are only executed if they are added to the Router with route.
g.Any("", NotFoundHandler) // So we register catch all route (404 is a safe way to emulate route match) for this group and now during routing the
g.Any("/*", NotFoundHandler) // Router would find route to match our request path and therefore guarantee the middleware(s) will get executed.
g.RouteNotFound("", NotFoundHandler)
g.RouteNotFound("/*", NotFoundHandler)
} }
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
@ -64,7 +67,7 @@ XFF: "x" "x, a" "x, a, b"
``` ```
In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is In this case, use **first _untrustable_ IP reading from right**. Never use first one reading from left, as it is
configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructre". configurable by client. Here "trustable" means "you are sure the IP address belongs to your infrastructure".
In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`. In above example, if `b` and `c` are trustable, the IP address of the client is `a` for both cases, never be `x`.
In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`. In Echo, use `ExtractIPFromXFFHeader(...TrustOption)`.
@ -225,15 +228,21 @@ func extractIP(req *http.Request) string {
func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor { func ExtractIPFromRealIPHeader(options ...TrustOption) IPExtractor {
checker := newIPChecker(options) checker := newIPChecker(options)
return func(req *http.Request) string { return func(req *http.Request) string {
directIP := extractIP(req)
realIP := req.Header.Get(HeaderXRealIP) realIP := req.Header.Get(HeaderXRealIP)
if realIP != "" { if realIP == "" {
return directIP
}
if checker.trust(net.ParseIP(directIP)) {
realIP = strings.TrimPrefix(realIP, "[") realIP = strings.TrimPrefix(realIP, "[")
realIP = strings.TrimSuffix(realIP, "]") realIP = strings.TrimSuffix(realIP, "]")
if ip := net.ParseIP(realIP); ip != nil && checker.trust(ip) { if rIP := net.ParseIP(realIP); rIP != nil {
return realIP return realIP
} }
} }
return extractIP(req)
return directIP
} }
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (

View File

@ -1,41 +1,41 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
"io"
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
"io"
) )
type ( // Logger defines the logging interface.
// Logger defines the logging interface. type Logger interface {
Logger interface { Output() io.Writer
Output() io.Writer SetOutput(w io.Writer)
SetOutput(w io.Writer) Prefix() string
Prefix() string SetPrefix(p string)
SetPrefix(p string) Level() log.Lvl
Level() log.Lvl SetLevel(v log.Lvl)
SetLevel(v log.Lvl) SetHeader(h string)
SetHeader(h string) Print(i ...interface{})
Print(i ...interface{}) Printf(format string, args ...interface{})
Printf(format string, args ...interface{}) Printj(j log.JSON)
Printj(j log.JSON) Debug(i ...interface{})
Debug(i ...interface{}) Debugf(format string, args ...interface{})
Debugf(format string, args ...interface{}) Debugj(j log.JSON)
Debugj(j log.JSON) Info(i ...interface{})
Info(i ...interface{}) Infof(format string, args ...interface{})
Infof(format string, args ...interface{}) Infoj(j log.JSON)
Infoj(j log.JSON) Warn(i ...interface{})
Warn(i ...interface{}) Warnf(format string, args ...interface{})
Warnf(format string, args ...interface{}) Warnj(j log.JSON)
Warnj(j log.JSON) Error(i ...interface{})
Error(i ...interface{}) Errorf(format string, args ...interface{})
Errorf(format string, args ...interface{}) Errorj(j log.JSON)
Errorj(j log.JSON) Fatal(i ...interface{})
Fatal(i ...interface{}) Fatalj(j log.JSON)
Fatalj(j log.JSON) Fatalf(format string, args ...interface{})
Fatalf(format string, args ...interface{}) Panic(i ...interface{})
Panic(i ...interface{}) Panicj(j log.JSON)
Panicj(j log.JSON) Panicf(format string, args ...interface{})
Panicf(format string, args ...interface{}) }
}
)

View File

@ -1,45 +1,46 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
"encoding/base64" "encoding/base64"
"net/http"
"strconv" "strconv"
"strings" "strings"
"net/http"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // BasicAuthConfig defines the config for BasicAuth middleware.
// BasicAuthConfig defines the config for BasicAuth middleware. type BasicAuthConfig struct {
BasicAuthConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Validator is a function to validate BasicAuth credentials. // Validator is a function to validate BasicAuth credentials.
// Required. // Required.
Validator BasicAuthValidator Validator BasicAuthValidator
// Realm is a string to define realm attribute of BasicAuth. // Realm is a string to define realm attribute of BasicAuth.
// Default value "Restricted". // Default value "Restricted".
Realm string Realm string
} }
// BasicAuthValidator defines a function to validate BasicAuth credentials. // BasicAuthValidator defines a function to validate BasicAuth credentials.
BasicAuthValidator func(string, string, echo.Context) (bool, error) // The function should return a boolean indicating whether the credentials are valid,
) // and an error if any error occurs during the validation process.
type BasicAuthValidator func(string, string, echo.Context) (bool, error)
const ( const (
basic = "basic" basic = "basic"
defaultRealm = "Restricted" defaultRealm = "Restricted"
) )
var ( // DefaultBasicAuthConfig is the default BasicAuth middleware config.
// DefaultBasicAuthConfig is the default BasicAuth middleware config. var DefaultBasicAuthConfig = BasicAuthConfig{
DefaultBasicAuthConfig = BasicAuthConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Realm: defaultRealm,
Realm: defaultRealm, }
}
)
// BasicAuth returns an BasicAuth middleware. // BasicAuth returns an BasicAuth middleware.
// //

View File

@ -1,8 +1,12 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -10,32 +14,28 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // BodyDumpConfig defines the config for BodyDump middleware.
// BodyDumpConfig defines the config for BodyDump middleware. type BodyDumpConfig struct {
BodyDumpConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Handler receives request and response payload. // Handler receives request and response payload.
// Required. // Required.
Handler BodyDumpHandler Handler BodyDumpHandler
} }
// BodyDumpHandler receives the request and response payload. // BodyDumpHandler receives the request and response payload.
BodyDumpHandler func(echo.Context, []byte, []byte) type BodyDumpHandler func(echo.Context, []byte, []byte)
bodyDumpResponseWriter struct { type bodyDumpResponseWriter struct {
io.Writer io.Writer
http.ResponseWriter http.ResponseWriter
} }
)
var ( // DefaultBodyDumpConfig is the default BodyDump middleware config.
// DefaultBodyDumpConfig is the default BodyDump middleware config. var DefaultBodyDumpConfig = BodyDumpConfig{
DefaultBodyDumpConfig = BodyDumpConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, }
}
)
// BodyDump returns a BodyDump middleware. // BodyDump returns a BodyDump middleware.
// //
@ -98,9 +98,16 @@ func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {
} }
func (w *bodyDumpResponseWriter) Flush() { func (w *bodyDumpResponseWriter) Flush() {
w.ResponseWriter.(http.Flusher).Flush() err := responseControllerFlush(w.ResponseWriter)
if err != nil && errors.Is(err, http.ErrNotSupported) {
panic(errors.New("response writer flushing is not supported"))
}
} }
func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack() return responseControllerHijack(w.ResponseWriter)
}
func (w *bodyDumpResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -9,32 +12,27 @@ import (
"github.com/labstack/gommon/bytes" "github.com/labstack/gommon/bytes"
) )
type ( // BodyLimitConfig defines the config for BodyLimit middleware.
// BodyLimitConfig defines the config for BodyLimit middleware. type BodyLimitConfig struct {
BodyLimitConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Maximum allowed size for a request body, it can be specified // Maximum allowed size for a request body, it can be specified
// as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P.
Limit string `yaml:"limit"` Limit string `yaml:"limit"`
limit int64 limit int64
} }
limitedReader struct { type limitedReader struct {
BodyLimitConfig BodyLimitConfig
reader io.ReadCloser reader io.ReadCloser
read int64 read int64
context echo.Context }
}
)
var ( // DefaultBodyLimitConfig is the default BodyLimit middleware config.
// DefaultBodyLimitConfig is the default BodyLimit middleware config. var DefaultBodyLimitConfig = BodyLimitConfig{
DefaultBodyLimitConfig = BodyLimitConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, }
}
)
// BodyLimit returns a BodyLimit middleware. // BodyLimit returns a BodyLimit middleware.
// //
@ -80,7 +78,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
// Based on content read // Based on content read
r := pool.Get().(*limitedReader) r := pool.Get().(*limitedReader)
r.Reset(req.Body, c) r.Reset(req.Body)
defer pool.Put(r) defer pool.Put(r)
req.Body = r req.Body = r
@ -102,9 +100,8 @@ func (r *limitedReader) Close() error {
return r.reader.Close() return r.reader.Close()
} }
func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) { func (r *limitedReader) Reset(reader io.ReadCloser) {
r.reader = reader r.reader = reader
r.context = context
r.read = 0 r.read = 0
} }

View File

@ -1,7 +1,11 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
"bufio" "bufio"
"bytes"
"compress/gzip" "compress/gzip"
"io" "io"
"net" "net"
@ -12,35 +16,50 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // GzipConfig defines the config for Gzip middleware.
// GzipConfig defines the config for Gzip middleware. type GzipConfig struct {
GzipConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Gzip compression level. // Gzip compression level.
// Optional. Default value -1. // Optional. Default value -1.
Level int `yaml:"level"` Level int `yaml:"level"`
}
gzipResponseWriter struct { // Length threshold before gzip compression is applied.
io.Writer // Optional. Default value 0.
http.ResponseWriter //
wroteBody bool // Most of the time you will not need to change the default. Compressing
} // a short response might increase the transmitted data because of the
) // gzip format overhead. Compressing the response will also consume CPU
// and time on the server and the client (for decompressing). Depending on
// your use case such a threshold might be useful.
//
// See also:
// https://webmasters.stackexchange.com/questions/31750/what-is-recommended-minimum-object-size-for-gzip-performance-benefits
MinLength int
}
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
wroteHeader bool
wroteBody bool
minLength int
minLengthExceeded bool
buffer *bytes.Buffer
code int
}
const ( const (
gzipScheme = "gzip" gzipScheme = "gzip"
) )
var ( // DefaultGzipConfig is the default Gzip middleware config.
// DefaultGzipConfig is the default Gzip middleware config. var DefaultGzipConfig = GzipConfig{
DefaultGzipConfig = GzipConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Level: -1,
Level: -1, MinLength: 0,
} }
)
// Gzip returns a middleware which compresses HTTP response using gzip compression // Gzip returns a middleware which compresses HTTP response using gzip compression
// scheme. // scheme.
@ -58,8 +77,12 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
if config.Level == 0 { if config.Level == 0 {
config.Level = DefaultGzipConfig.Level config.Level = DefaultGzipConfig.Level
} }
if config.MinLength < 0 {
config.MinLength = DefaultGzipConfig.MinLength
}
pool := gzipCompressPool(config) pool := gzipCompressPool(config)
bpool := bufferPool()
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -70,7 +93,6 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
res := c.Response() res := c.Response()
res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding) res.Header().Add(echo.HeaderVary, echo.HeaderAcceptEncoding)
if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) { if strings.Contains(c.Request().Header.Get(echo.HeaderAcceptEncoding), gzipScheme) {
res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
i := pool.Get() i := pool.Get()
w, ok := i.(*gzip.Writer) w, ok := i.(*gzip.Writer)
if !ok { if !ok {
@ -78,19 +100,38 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
} }
rw := res.Writer rw := res.Writer
w.Reset(rw) w.Reset(rw)
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw}
buf := bpool.Get().(*bytes.Buffer)
buf.Reset()
grw := &gzipResponseWriter{Writer: w, ResponseWriter: rw, minLength: config.MinLength, buffer: buf}
defer func() { defer func() {
// There are different reasons for cases when we have not yet written response to the client and now need to do so.
// a) handler response had only response code and no response body (ala 404 or redirects etc). Response code need to be written now.
// b) body is shorter than our minimum length threshold and being buffered currently and needs to be written
if !grw.wroteBody { if !grw.wroteBody {
if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme { if res.Header().Get(echo.HeaderContentEncoding) == gzipScheme {
res.Header().Del(echo.HeaderContentEncoding) res.Header().Del(echo.HeaderContentEncoding)
} }
if grw.wroteHeader {
rw.WriteHeader(grw.code)
}
// We have to reset response to it's pristine state when // We have to reset response to it's pristine state when
// nothing is written to body or error is returned. // nothing is written to body or error is returned.
// See issue #424, #407. // See issue #424, #407.
res.Writer = rw res.Writer = rw
w.Reset(io.Discard) w.Reset(io.Discard)
} else if !grw.minLengthExceeded {
// Write uncompressed response
res.Writer = rw
if grw.wroteHeader {
grw.ResponseWriter.WriteHeader(grw.code)
}
grw.buffer.WriteTo(rw)
w.Reset(io.Discard)
} }
w.Close() w.Close()
bpool.Put(buf)
pool.Put(w) pool.Put(w)
}() }()
res.Writer = grw res.Writer = grw
@ -102,7 +143,11 @@ func GzipWithConfig(config GzipConfig) echo.MiddlewareFunc {
func (w *gzipResponseWriter) WriteHeader(code int) { func (w *gzipResponseWriter) WriteHeader(code int) {
w.Header().Del(echo.HeaderContentLength) // Issue #444 w.Header().Del(echo.HeaderContentLength) // Issue #444
w.ResponseWriter.WriteHeader(code)
w.wroteHeader = true
// Delay writing of the header until we know if we'll actually compress the response
w.code = code
} }
func (w *gzipResponseWriter) Write(b []byte) (int, error) { func (w *gzipResponseWriter) Write(b []byte) (int, error) {
@ -110,18 +155,50 @@ func (w *gzipResponseWriter) Write(b []byte) (int, error) {
w.Header().Set(echo.HeaderContentType, http.DetectContentType(b)) w.Header().Set(echo.HeaderContentType, http.DetectContentType(b))
} }
w.wroteBody = true w.wroteBody = true
if !w.minLengthExceeded {
n, err := w.buffer.Write(b)
if w.buffer.Len() >= w.minLength {
w.minLengthExceeded = true
// The minimum length is exceeded, add Content-Encoding header and write the header
w.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
if w.wroteHeader {
w.ResponseWriter.WriteHeader(w.code)
}
return w.Writer.Write(w.buffer.Bytes())
}
return n, err
}
return w.Writer.Write(b) return w.Writer.Write(b)
} }
func (w *gzipResponseWriter) Flush() { func (w *gzipResponseWriter) Flush() {
w.Writer.(*gzip.Writer).Flush() if !w.minLengthExceeded {
if flusher, ok := w.ResponseWriter.(http.Flusher); ok { // Enforce compression because we will not know how much more data will come
flusher.Flush() w.minLengthExceeded = true
w.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
if w.wroteHeader {
w.ResponseWriter.WriteHeader(w.code)
}
w.Writer.Write(w.buffer.Bytes())
} }
w.Writer.(*gzip.Writer).Flush()
_ = responseControllerFlush(w.ResponseWriter)
}
func (w *gzipResponseWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
} }
func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (w *gzipResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack() return responseControllerHijack(w.ResponseWriter)
} }
func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error { func (w *gzipResponseWriter) Push(target string, opts *http.PushOptions) error {
@ -142,3 +219,12 @@ func gzipCompressPool(config GzipConfig) sync.Pool {
}, },
} }
} }
func bufferPool() sync.Pool {
return sync.Pool{
New: func() interface{} {
b := &bytes.Buffer{}
return b
},
}
}

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -13,7 +16,7 @@ type ContextTimeoutConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
// ErrorHandler is a function when error aries in middeware execution. // ErrorHandler is a function when error aries in middleware execution.
ErrorHandler func(err error, c echo.Context) error ErrorHandler func(err error, c echo.Context) error
// Timeout configures a timeout for the middleware, defaults to 0 for no timeout // Timeout configures a timeout for the middleware, defaults to 0 for no timeout

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -9,112 +12,109 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // CORSConfig defines the config for CORS middleware.
// CORSConfig defines the config for CORS middleware. type CORSConfig struct {
CORSConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// AllowOrigins determines the value of the Access-Control-Allow-Origin // AllowOrigins determines the value of the Access-Control-Allow-Origin
// response header. This header defines a list of origins that may access the // response header. This header defines a list of origins that may access the
// resource. The wildcard characters '*' and '?' are supported and are // resource. The wildcard characters '*' and '?' are supported and are
// converted to regex fragments '.*' and '.' accordingly. // converted to regex fragments '.*' and '.' accordingly.
// //
// Security: use extreme caution when handling the origin, and carefully // Security: use extreme caution when handling the origin, and carefully
// validate any logic. Remember that attackers may register hostile domain names. // validate any logic. Remember that attackers may register hostile domain names.
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
// //
// Optional. Default value []string{"*"}. // Optional. Default value []string{"*"}.
// //
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
AllowOrigins []string `yaml:"allow_origins"` AllowOrigins []string `yaml:"allow_origins"`
// AllowOriginFunc is a custom function to validate the origin. It takes the // AllowOriginFunc is a custom function to validate the origin. It takes the
// origin as an argument and returns true if allowed or false otherwise. If // origin as an argument and returns true if allowed or false otherwise. If
// an error is returned, it is returned by the handler. If this option is // an error is returned, it is returned by the handler. If this option is
// set, AllowOrigins is ignored. // set, AllowOrigins is ignored.
// //
// Security: use extreme caution when handling the origin, and carefully // Security: use extreme caution when handling the origin, and carefully
// validate any logic. Remember that attackers may register hostile domain names. // validate any logic. Remember that attackers may register hostile domain names.
// See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // See https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
// //
// Optional. // Optional.
AllowOriginFunc func(origin string) (bool, error) `yaml:"allow_origin_func"` AllowOriginFunc func(origin string) (bool, error) `yaml:"-"`
// AllowMethods determines the value of the Access-Control-Allow-Methods // AllowMethods determines the value of the Access-Control-Allow-Methods
// response header. This header specified the list of methods allowed when // response header. This header specified the list of methods allowed when
// accessing the resource. This is used in response to a preflight request. // accessing the resource. This is used in response to a preflight request.
// //
// Optional. Default value DefaultCORSConfig.AllowMethods. // Optional. Default value DefaultCORSConfig.AllowMethods.
// If `allowMethods` is left empty, this middleware will fill for preflight // If `allowMethods` is left empty, this middleware will fill for preflight
// request `Access-Control-Allow-Methods` header value // request `Access-Control-Allow-Methods` header value
// from `Allow` header that echo.Router set into context. // from `Allow` header that echo.Router set into context.
// //
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
AllowMethods []string `yaml:"allow_methods"` AllowMethods []string `yaml:"allow_methods"`
// AllowHeaders determines the value of the Access-Control-Allow-Headers // AllowHeaders determines the value of the Access-Control-Allow-Headers
// response header. This header is used in response to a preflight request to // response header. This header is used in response to a preflight request to
// indicate which HTTP headers can be used when making the actual request. // indicate which HTTP headers can be used when making the actual request.
// //
// Optional. Default value []string{}. // Optional. Default value []string{}.
// //
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
AllowHeaders []string `yaml:"allow_headers"` AllowHeaders []string `yaml:"allow_headers"`
// AllowCredentials determines the value of the // AllowCredentials determines the value of the
// Access-Control-Allow-Credentials response header. This header indicates // Access-Control-Allow-Credentials response header. This header indicates
// whether or not the response to the request can be exposed when the // whether or not the response to the request can be exposed when the
// credentials mode (Request.credentials) is true. When used as part of a // credentials mode (Request.credentials) is true. When used as part of a
// response to a preflight request, this indicates whether or not the actual // response to a preflight request, this indicates whether or not the actual
// request can be made using credentials. See also // request can be made using credentials. See also
// [MDN: Access-Control-Allow-Credentials]. // [MDN: Access-Control-Allow-Credentials].
// //
// Optional. Default value false, in which case the header is not set. // Optional. Default value false, in which case the header is not set.
// //
// Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`. // Security: avoid using `AllowCredentials = true` with `AllowOrigins = *`.
// See "Exploiting CORS misconfigurations for Bitcoins and bounties", // See "Exploiting CORS misconfigurations for Bitcoins and bounties",
// https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html // https://blog.portswigger.net/2016/10/exploiting-cors-misconfigurations-for.html
// //
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
AllowCredentials bool `yaml:"allow_credentials"` AllowCredentials bool `yaml:"allow_credentials"`
// UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials // UnsafeWildcardOriginWithAllowCredentials UNSAFE/INSECURE: allows wildcard '*' origin to be used with AllowCredentials
// flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header. // flag. In that case we consider any origin allowed and send it back to the client with `Access-Control-Allow-Origin` header.
// //
// This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties) // This is INSECURE and potentially leads to [cross-origin](https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties)
// attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject. // attacks. See: https://github.com/labstack/echo/issues/2400 for discussion on the subject.
// //
// Optional. Default value is false. // Optional. Default value is false.
UnsafeWildcardOriginWithAllowCredentials bool `yaml:"unsafe_wildcard_origin_with_allow_credentials"` UnsafeWildcardOriginWithAllowCredentials bool `yaml:"unsafe_wildcard_origin_with_allow_credentials"`
// ExposeHeaders determines the value of Access-Control-Expose-Headers, which // ExposeHeaders determines the value of Access-Control-Expose-Headers, which
// defines a list of headers that clients are allowed to access. // defines a list of headers that clients are allowed to access.
// //
// Optional. Default value []string{}, in which case the header is not set. // Optional. Default value []string{}, in which case the header is not set.
// //
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Header
ExposeHeaders []string `yaml:"expose_headers"` ExposeHeaders []string `yaml:"expose_headers"`
// MaxAge determines the value of the Access-Control-Max-Age response header. // MaxAge determines the value of the Access-Control-Max-Age response header.
// This header indicates how long (in seconds) the results of a preflight // This header indicates how long (in seconds) the results of a preflight
// request can be cached. // request can be cached.
// // The header is set only if MaxAge != 0, negative value sends "0" which instructs browsers not to cache that response.
// Optional. Default value 0. The header is set only if MaxAge > 0. //
// // Optional. Default value 0 - meaning header is not sent.
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age //
MaxAge int `yaml:"max_age"` // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
} MaxAge int `yaml:"max_age"`
) }
var ( // DefaultCORSConfig is the default CORS middleware config.
// DefaultCORSConfig is the default CORS middleware config. var DefaultCORSConfig = CORSConfig{
DefaultCORSConfig = CORSConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, AllowOrigins: []string{"*"},
AllowOrigins: []string{"*"}, AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete},
AllowMethods: []string{http.MethodGet, http.MethodHead, http.MethodPut, http.MethodPatch, http.MethodPost, http.MethodDelete}, }
}
)
// CORS returns a Cross-Origin Resource Sharing (CORS) middleware. // CORS returns a Cross-Origin Resource Sharing (CORS) middleware.
// See also [MDN: Cross-Origin Resource Sharing (CORS)]. // See also [MDN: Cross-Origin Resource Sharing (CORS)].
@ -150,8 +150,8 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
allowOriginPatterns := []string{} allowOriginPatterns := []string{}
for _, origin := range config.AllowOrigins { for _, origin := range config.AllowOrigins {
pattern := regexp.QuoteMeta(origin) pattern := regexp.QuoteMeta(origin)
pattern = strings.Replace(pattern, "\\*", ".*", -1) pattern = strings.ReplaceAll(pattern, "\\*", ".*")
pattern = strings.Replace(pattern, "\\?", ".", -1) pattern = strings.ReplaceAll(pattern, "\\?", ".")
pattern = "^" + pattern + "$" pattern = "^" + pattern + "$"
allowOriginPatterns = append(allowOriginPatterns, pattern) allowOriginPatterns = append(allowOriginPatterns, pattern)
} }
@ -159,7 +159,11 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
allowMethods := strings.Join(config.AllowMethods, ",") allowMethods := strings.Join(config.AllowMethods, ",")
allowHeaders := strings.Join(config.AllowHeaders, ",") allowHeaders := strings.Join(config.AllowHeaders, ",")
exposeHeaders := strings.Join(config.ExposeHeaders, ",") exposeHeaders := strings.Join(config.ExposeHeaders, ",")
maxAge := strconv.Itoa(config.MaxAge)
maxAge := "0"
if config.MaxAge > 0 {
maxAge = strconv.Itoa(config.MaxAge)
}
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
@ -282,7 +286,7 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
res.Header().Set(echo.HeaderAccessControlAllowHeaders, h) res.Header().Set(echo.HeaderAccessControlAllowHeaders, h)
} }
} }
if config.MaxAge > 0 { if config.MaxAge != 0 {
res.Header().Set(echo.HeaderAccessControlMaxAge, maxAge) res.Header().Set(echo.HeaderAccessControlMaxAge, maxAge)
} }
return c.NoContent(http.StatusNoContent) return c.NoContent(http.StatusNoContent)

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -6,85 +9,80 @@ import (
"time" "time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/gommon/random"
) )
type ( // CSRFConfig defines the config for CSRF middleware.
// CSRFConfig defines the config for CSRF middleware. type CSRFConfig struct {
CSRFConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// TokenLength is the length of the generated token. // TokenLength is the length of the generated token.
TokenLength uint8 `yaml:"token_length"` TokenLength uint8 `yaml:"token_length"`
// Optional. Default value 32. // Optional. Default value 32.
// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used // TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
// to extract token from the request. // to extract token from the request.
// Optional. Default value "header:X-CSRF-Token". // Optional. Default value "header:X-CSRF-Token".
// Possible values: // Possible values:
// - "header:<name>" or "header:<name>:<cut-prefix>" // - "header:<name>" or "header:<name>:<cut-prefix>"
// - "query:<name>" // - "query:<name>"
// - "form:<name>" // - "form:<name>"
// Multiple sources example: // Multiple sources example:
// - "header:X-CSRF-Token,query:csrf" // - "header:X-CSRF-Token,query:csrf"
TokenLookup string `yaml:"token_lookup"` TokenLookup string `yaml:"token_lookup"`
// Context key to store generated CSRF token into context. // Context key to store generated CSRF token into context.
// Optional. Default value "csrf". // Optional. Default value "csrf".
ContextKey string `yaml:"context_key"` ContextKey string `yaml:"context_key"`
// Name of the CSRF cookie. This cookie will store CSRF token. // Name of the CSRF cookie. This cookie will store CSRF token.
// Optional. Default value "csrf". // Optional. Default value "csrf".
CookieName string `yaml:"cookie_name"` CookieName string `yaml:"cookie_name"`
// Domain of the CSRF cookie. // Domain of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookieDomain string `yaml:"cookie_domain"` CookieDomain string `yaml:"cookie_domain"`
// Path of the CSRF cookie. // Path of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookiePath string `yaml:"cookie_path"` CookiePath string `yaml:"cookie_path"`
// Max age (in seconds) of the CSRF cookie. // Max age (in seconds) of the CSRF cookie.
// Optional. Default value 86400 (24hr). // Optional. Default value 86400 (24hr).
CookieMaxAge int `yaml:"cookie_max_age"` CookieMaxAge int `yaml:"cookie_max_age"`
// Indicates if CSRF cookie is secure. // Indicates if CSRF cookie is secure.
// Optional. Default value false. // Optional. Default value false.
CookieSecure bool `yaml:"cookie_secure"` CookieSecure bool `yaml:"cookie_secure"`
// Indicates if CSRF cookie is HTTP only. // Indicates if CSRF cookie is HTTP only.
// Optional. Default value false. // Optional. Default value false.
CookieHTTPOnly bool `yaml:"cookie_http_only"` CookieHTTPOnly bool `yaml:"cookie_http_only"`
// Indicates SameSite mode of the CSRF cookie. // Indicates SameSite mode of the CSRF cookie.
// Optional. Default value SameSiteDefaultMode. // Optional. Default value SameSiteDefaultMode.
CookieSameSite http.SameSite `yaml:"cookie_same_site"` CookieSameSite http.SameSite `yaml:"cookie_same_site"`
// ErrorHandler defines a function which is executed for returning custom errors. // ErrorHandler defines a function which is executed for returning custom errors.
ErrorHandler CSRFErrorHandler ErrorHandler CSRFErrorHandler
} }
// CSRFErrorHandler is a function which is executed for creating custom errors. // CSRFErrorHandler is a function which is executed for creating custom errors.
CSRFErrorHandler func(err error, c echo.Context) error type CSRFErrorHandler func(err error, c echo.Context) error
)
// ErrCSRFInvalid is returned when CSRF check fails // ErrCSRFInvalid is returned when CSRF check fails
var ErrCSRFInvalid = echo.NewHTTPError(http.StatusForbidden, "invalid csrf token") var ErrCSRFInvalid = echo.NewHTTPError(http.StatusForbidden, "invalid csrf token")
var ( // DefaultCSRFConfig is the default CSRF middleware config.
// DefaultCSRFConfig is the default CSRF middleware config. var DefaultCSRFConfig = CSRFConfig{
DefaultCSRFConfig = CSRFConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, TokenLength: 32,
TokenLength: 32, TokenLookup: "header:" + echo.HeaderXCSRFToken,
TokenLookup: "header:" + echo.HeaderXCSRFToken, ContextKey: "csrf",
ContextKey: "csrf", CookieName: "_csrf",
CookieName: "_csrf", CookieMaxAge: 86400,
CookieMaxAge: 86400, CookieSameSite: http.SameSiteDefaultMode,
CookieSameSite: http.SameSiteDefaultMode, }
}
)
// CSRF returns a Cross-Site Request Forgery (CSRF) middleware. // CSRF returns a Cross-Site Request Forgery (CSRF) middleware.
// See: https://en.wikipedia.org/wiki/Cross-site_request_forgery // See: https://en.wikipedia.org/wiki/Cross-site_request_forgery
@ -103,6 +101,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
if config.TokenLength == 0 { if config.TokenLength == 0 {
config.TokenLength = DefaultCSRFConfig.TokenLength config.TokenLength = DefaultCSRFConfig.TokenLength
} }
if config.TokenLookup == "" { if config.TokenLookup == "" {
config.TokenLookup = DefaultCSRFConfig.TokenLookup config.TokenLookup = DefaultCSRFConfig.TokenLookup
} }
@ -132,7 +131,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
token := "" token := ""
if k, err := c.Cookie(config.CookieName); err != nil { if k, err := c.Cookie(config.CookieName); err != nil {
token = random.String(config.TokenLength) // Generate token token = randomString(config.TokenLength)
} else { } else {
token = k.Value // Reuse token token = k.Value // Reuse token
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -9,18 +12,16 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // DecompressConfig defines the config for Decompress middleware.
// DecompressConfig defines the config for Decompress middleware. type DecompressConfig struct {
DecompressConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers // GzipDecompressPool defines an interface to provide the sync.Pool used to create/store Gzip readers
GzipDecompressPool Decompressor GzipDecompressPool Decompressor
} }
)
//GZIPEncoding content-encoding header if set to "gzip", decompress body contents. // GZIPEncoding content-encoding header if set to "gzip", decompress body contents.
const GZIPEncoding string = "gzip" const GZIPEncoding string = "gzip"
// Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers // Decompressor is used to get the sync.Pool used by the middleware to get Gzip readers
@ -28,13 +29,11 @@ type Decompressor interface {
gzipDecompressPool() sync.Pool gzipDecompressPool() sync.Pool
} }
var ( // DefaultDecompressConfig defines the config for decompress middleware
//DefaultDecompressConfig defines the config for decompress middleware var DefaultDecompressConfig = DecompressConfig{
DefaultDecompressConfig = DecompressConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, GzipDecompressPool: &DefaultGzipDecompressPool{},
GzipDecompressPool: &DefaultGzipDecompressPool{}, }
}
)
// DefaultGzipDecompressPool is the default implementation of Decompressor interface // DefaultGzipDecompressPool is the default implementation of Decompressor interface
type DefaultGzipDecompressPool struct { type DefaultGzipDecompressPool struct {
@ -44,12 +43,12 @@ func (d *DefaultGzipDecompressPool) gzipDecompressPool() sync.Pool {
return sync.Pool{New: func() interface{} { return new(gzip.Reader) }} return sync.Pool{New: func() interface{} { return new(gzip.Reader) }}
} }
//Decompress decompresses request body based if content encoding type is set to "gzip" with default config // Decompress decompresses request body based if content encoding type is set to "gzip" with default config
func Decompress() echo.MiddlewareFunc { func Decompress() echo.MiddlewareFunc {
return DecompressWithConfig(DefaultDecompressConfig) return DecompressWithConfig(DefaultDecompressConfig)
} }
//DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config // DecompressWithConfig decompresses request body based if content encoding type is set to "gzip" with config
func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc { func DecompressWithConfig(config DecompressConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Skipper == nil { if config.Skipper == nil {

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build go1.15 //go:build go1.15
// +build go1.15 // +build go1.15
@ -12,139 +15,135 @@ import (
"reflect" "reflect"
) )
type ( // JWTConfig defines the config for JWT middleware.
// JWTConfig defines the config for JWT middleware. type JWTConfig struct {
JWTConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// BeforeFunc defines a function which is executed just before the middleware. // BeforeFunc defines a function which is executed just before the middleware.
BeforeFunc BeforeFunc BeforeFunc BeforeFunc
// SuccessHandler defines a function which is executed for a valid token before middleware chain continues with next // SuccessHandler defines a function which is executed for a valid token before middleware chain continues with next
// middleware or handler. // middleware or handler.
SuccessHandler JWTSuccessHandler SuccessHandler JWTSuccessHandler
// ErrorHandler defines a function which is executed for an invalid token. // ErrorHandler defines a function which is executed for an invalid token.
// It may be used to define a custom JWT error. // It may be used to define a custom JWT error.
ErrorHandler JWTErrorHandler ErrorHandler JWTErrorHandler
// ErrorHandlerWithContext is almost identical to ErrorHandler, but it's passed the current context. // ErrorHandlerWithContext is almost identical to ErrorHandler, but it's passed the current context.
ErrorHandlerWithContext JWTErrorHandlerWithContext ErrorHandlerWithContext JWTErrorHandlerWithContext
// ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandlerWithContext decides to // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandlerWithContext decides to
// ignore the error (by returning `nil`). // ignore the error (by returning `nil`).
// This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality. // This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality.
// In that case you can use ErrorHandlerWithContext to set a default public JWT token value in the request context // In that case you can use ErrorHandlerWithContext to set a default public JWT token value in the request context
// and continue. Some logic down the remaining execution chain needs to check that (public) token value then. // and continue. Some logic down the remaining execution chain needs to check that (public) token value then.
ContinueOnIgnoredError bool ContinueOnIgnoredError bool
// Signing key to validate token. // Signing key to validate token.
// This is one of the three options to provide a token validation key. // This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither user-defined KeyFunc nor SigningKeys is provided. // Required if neither user-defined KeyFunc nor SigningKeys is provided.
SigningKey interface{} SigningKey interface{}
// Map of signing keys to validate token with kid field usage. // Map of signing keys to validate token with kid field usage.
// This is one of the three options to provide a token validation key. // This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither user-defined KeyFunc nor SigningKey is provided. // Required if neither user-defined KeyFunc nor SigningKey is provided.
SigningKeys map[string]interface{} SigningKeys map[string]interface{}
// Signing method used to check the token's signing algorithm. // Signing method used to check the token's signing algorithm.
// Optional. Default value HS256. // Optional. Default value HS256.
SigningMethod string SigningMethod string
// Context key to store user information from the token into context. // Context key to store user information from the token into context.
// Optional. Default value "user". // Optional. Default value "user".
ContextKey string ContextKey string
// Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation. // Claims are extendable claims data defining token content. Used by default ParseTokenFunc implementation.
// Not used if custom ParseTokenFunc is set. // Not used if custom ParseTokenFunc is set.
// Optional. Default value jwt.MapClaims // Optional. Default value jwt.MapClaims
Claims jwt.Claims Claims jwt.Claims
// TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used // TokenLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
// to extract token from the request. // to extract token from the request.
// Optional. Default value "header:Authorization". // Optional. Default value "header:Authorization".
// Possible values: // Possible values:
// - "header:<name>" or "header:<name>:<cut-prefix>" // - "header:<name>" or "header:<name>:<cut-prefix>"
// `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header // `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
// value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we // value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
// want to cut is `<auth-scheme> ` note the space at the end. // want to cut is `<auth-scheme> ` note the space at the end.
// In case of JWT tokens `Authorization: Bearer <token>` prefix we cut is `Bearer `. // In case of JWT tokens `Authorization: Bearer <token>` prefix we cut is `Bearer `.
// If prefix is left empty the whole value is returned. // If prefix is left empty the whole value is returned.
// - "query:<name>" // - "query:<name>"
// - "param:<name>" // - "param:<name>"
// - "cookie:<name>" // - "cookie:<name>"
// - "form:<name>" // - "form:<name>"
// Multiple sources example: // Multiple sources example:
// - "header:Authorization,cookie:myowncookie" // - "header:Authorization,cookie:myowncookie"
TokenLookup string TokenLookup string
// TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context. // TokenLookupFuncs defines a list of user-defined functions that extract JWT token from the given context.
// This is one of the two options to provide a token extractor. // This is one of the two options to provide a token extractor.
// The order of precedence is user-defined TokenLookupFuncs, and TokenLookup. // The order of precedence is user-defined TokenLookupFuncs, and TokenLookup.
// You can also provide both if you want. // You can also provide both if you want.
TokenLookupFuncs []ValuesExtractor TokenLookupFuncs []ValuesExtractor
// AuthScheme to be used in the Authorization header. // AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer". // Optional. Default value "Bearer".
AuthScheme string AuthScheme string
// KeyFunc defines a user-defined function that supplies the public key for a token validation. // KeyFunc defines a user-defined function that supplies the public key for a token validation.
// The function shall take care of verifying the signing algorithm and selecting the proper key. // The function shall take care of verifying the signing algorithm and selecting the proper key.
// A user-defined KeyFunc can be useful if tokens are issued by an external party. // A user-defined KeyFunc can be useful if tokens are issued by an external party.
// Used by default ParseTokenFunc implementation. // Used by default ParseTokenFunc implementation.
// //
// When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored. // When a user-defined KeyFunc is provided, SigningKey, SigningKeys, and SigningMethod are ignored.
// This is one of the three options to provide a token validation key. // This is one of the three options to provide a token validation key.
// The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey. // The order of precedence is a user-defined KeyFunc, SigningKeys and SigningKey.
// Required if neither SigningKeys nor SigningKey is provided. // Required if neither SigningKeys nor SigningKey is provided.
// Not used if custom ParseTokenFunc is set. // Not used if custom ParseTokenFunc is set.
// Default to an internal implementation verifying the signing algorithm and selecting the proper key. // Default to an internal implementation verifying the signing algorithm and selecting the proper key.
KeyFunc jwt.Keyfunc KeyFunc jwt.Keyfunc
// ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token // ParseTokenFunc defines a user-defined function that parses token from given auth. Returns an error when token
// parsing fails or parsed token is invalid. // parsing fails or parsed token is invalid.
// Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library // Defaults to implementation using `github.com/golang-jwt/jwt` as JWT implementation library
ParseTokenFunc func(auth string, c echo.Context) (interface{}, error) ParseTokenFunc func(auth string, c echo.Context) (interface{}, error)
} }
// JWTSuccessHandler defines a function which is executed for a valid token. // JWTSuccessHandler defines a function which is executed for a valid token.
JWTSuccessHandler func(c echo.Context) type JWTSuccessHandler func(c echo.Context)
// JWTErrorHandler defines a function which is executed for an invalid token. // JWTErrorHandler defines a function which is executed for an invalid token.
JWTErrorHandler func(err error) error type JWTErrorHandler func(err error) error
// JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context. // JWTErrorHandlerWithContext is almost identical to JWTErrorHandler, but it's passed the current context.
JWTErrorHandlerWithContext func(err error, c echo.Context) error type JWTErrorHandlerWithContext func(err error, c echo.Context) error
)
// Algorithms // Algorithms
const ( const (
AlgorithmHS256 = "HS256" AlgorithmHS256 = "HS256"
) )
// Errors // ErrJWTMissing is error that is returned when no JWToken was extracted from the request.
var ( var ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt")
ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt")
ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt")
)
var ( // ErrJWTInvalid is error that is returned when middleware could not parse JWT correctly.
// DefaultJWTConfig is the default JWT auth middleware config. var ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt")
DefaultJWTConfig = JWTConfig{
Skipper: DefaultSkipper, // DefaultJWTConfig is the default JWT auth middleware config.
SigningMethod: AlgorithmHS256, var DefaultJWTConfig = JWTConfig{
ContextKey: "user", Skipper: DefaultSkipper,
TokenLookup: "header:" + echo.HeaderAuthorization, SigningMethod: AlgorithmHS256,
TokenLookupFuncs: nil, ContextKey: "user",
AuthScheme: "Bearer", TokenLookup: "header:" + echo.HeaderAuthorization,
Claims: jwt.MapClaims{}, TokenLookupFuncs: nil,
KeyFunc: nil, AuthScheme: "Bearer",
} Claims: jwt.MapClaims{},
) KeyFunc: nil,
}
// JWT returns a JSON Web Token (JWT) auth middleware. // JWT returns a JSON Web Token (JWT) auth middleware.
// //

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -6,69 +9,65 @@ import (
"net/http" "net/http"
) )
type ( // KeyAuthConfig defines the config for KeyAuth middleware.
// KeyAuthConfig defines the config for KeyAuth middleware. type KeyAuthConfig struct {
KeyAuthConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// KeyLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used // KeyLookup is a string in the form of "<source>:<name>" or "<source>:<name>,<source>:<name>" that is used
// to extract key from the request. // to extract key from the request.
// Optional. Default value "header:Authorization". // Optional. Default value "header:Authorization".
// Possible values: // Possible values:
// - "header:<name>" or "header:<name>:<cut-prefix>" // - "header:<name>" or "header:<name>:<cut-prefix>"
// `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header // `<cut-prefix>` is argument value to cut/trim prefix of the extracted value. This is useful if header
// value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we // value has static prefix like `Authorization: <auth-scheme> <authorisation-parameters>` where part that we
// want to cut is `<auth-scheme> ` note the space at the end. // want to cut is `<auth-scheme> ` note the space at the end.
// In case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove is `Basic `. // In case of basic authentication `Authorization: Basic <credentials>` prefix we want to remove is `Basic `.
// - "query:<name>" // - "query:<name>"
// - "form:<name>" // - "form:<name>"
// - "cookie:<name>" // - "cookie:<name>"
// Multiple sources example: // Multiple sources example:
// - "header:Authorization,header:X-Api-Key" // - "header:Authorization,header:X-Api-Key"
KeyLookup string KeyLookup string
// AuthScheme to be used in the Authorization header. // AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer". // Optional. Default value "Bearer".
AuthScheme string AuthScheme string
// Validator is a function to validate key. // Validator is a function to validate key.
// Required. // Required.
Validator KeyAuthValidator Validator KeyAuthValidator
// ErrorHandler defines a function which is executed for an invalid key. // ErrorHandler defines a function which is executed for an invalid key.
// It may be used to define a custom error. // It may be used to define a custom error.
ErrorHandler KeyAuthErrorHandler ErrorHandler KeyAuthErrorHandler
// ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to // ContinueOnIgnoredError allows the next middleware/handler to be called when ErrorHandler decides to
// ignore the error (by returning `nil`). // ignore the error (by returning `nil`).
// This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality. // This is useful when parts of your site/api allow public access and some authorized routes provide extra functionality.
// In that case you can use ErrorHandler to set a default public key auth value in the request context // In that case you can use ErrorHandler to set a default public key auth value in the request context
// and continue. Some logic down the remaining execution chain needs to check that (public) key auth value then. // and continue. Some logic down the remaining execution chain needs to check that (public) key auth value then.
ContinueOnIgnoredError bool ContinueOnIgnoredError bool
} }
// KeyAuthValidator defines a function to validate KeyAuth credentials. // KeyAuthValidator defines a function to validate KeyAuth credentials.
KeyAuthValidator func(auth string, c echo.Context) (bool, error) type KeyAuthValidator func(auth string, c echo.Context) (bool, error)
// KeyAuthErrorHandler defines a function which is executed for an invalid key. // KeyAuthErrorHandler defines a function which is executed for an invalid key.
KeyAuthErrorHandler func(err error, c echo.Context) error type KeyAuthErrorHandler func(err error, c echo.Context) error
)
var (
// DefaultKeyAuthConfig is the default KeyAuth middleware config.
DefaultKeyAuthConfig = KeyAuthConfig{
Skipper: DefaultSkipper,
KeyLookup: "header:" + echo.HeaderAuthorization,
AuthScheme: "Bearer",
}
)
// ErrKeyAuthMissing is error type when KeyAuth middleware is unable to extract value from lookups // ErrKeyAuthMissing is error type when KeyAuth middleware is unable to extract value from lookups
type ErrKeyAuthMissing struct { type ErrKeyAuthMissing struct {
Err error Err error
} }
// DefaultKeyAuthConfig is the default KeyAuth middleware config.
var DefaultKeyAuthConfig = KeyAuthConfig{
Skipper: DefaultSkipper,
KeyLookup: "header:" + echo.HeaderAuthorization,
AuthScheme: "Bearer",
}
// Error returns errors text // Error returns errors text
func (e *ErrKeyAuthMissing) Error() string { func (e *ErrKeyAuthMissing) Error() string {
return e.Err.Error() return e.Err.Error()

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -14,77 +17,73 @@ import (
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
type ( // LoggerConfig defines the config for Logger middleware.
// LoggerConfig defines the config for Logger middleware. type LoggerConfig struct {
LoggerConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Tags to construct the logger format. // Tags to construct the logger format.
// //
// - time_unix // - time_unix
// - time_unix_milli // - time_unix_milli
// - time_unix_micro // - time_unix_micro
// - time_unix_nano // - time_unix_nano
// - time_rfc3339 // - time_rfc3339
// - time_rfc3339_nano // - time_rfc3339_nano
// - time_custom // - time_custom
// - id (Request ID) // - id (Request ID)
// - remote_ip // - remote_ip
// - uri // - uri
// - host // - host
// - method // - method
// - path // - path
// - route // - route
// - protocol // - protocol
// - referer // - referer
// - user_agent // - user_agent
// - status // - status
// - error // - error
// - latency (In nanoseconds) // - latency (In nanoseconds)
// - latency_human (Human readable) // - latency_human (Human readable)
// - bytes_in (Bytes received) // - bytes_in (Bytes received)
// - bytes_out (Bytes sent) // - bytes_out (Bytes sent)
// - header:<NAME> // - header:<NAME>
// - query:<NAME> // - query:<NAME>
// - form:<NAME> // - form:<NAME>
// - custom (see CustomTagFunc field) // - custom (see CustomTagFunc field)
// //
// Example "${remote_ip} ${status}" // Example "${remote_ip} ${status}"
// //
// Optional. Default value DefaultLoggerConfig.Format. // Optional. Default value DefaultLoggerConfig.Format.
Format string `yaml:"format"` Format string `yaml:"format"`
// Optional. Default value DefaultLoggerConfig.CustomTimeFormat. // Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
CustomTimeFormat string `yaml:"custom_time_format"` CustomTimeFormat string `yaml:"custom_time_format"`
// CustomTagFunc is function called for `${custom}` tag to output user implemented text by writing it to buf. // CustomTagFunc is function called for `${custom}` tag to output user implemented text by writing it to buf.
// Make sure that outputted text creates valid JSON string with other logged tags. // Make sure that outputted text creates valid JSON string with other logged tags.
// Optional. // Optional.
CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error) CustomTagFunc func(c echo.Context, buf *bytes.Buffer) (int, error)
// Output is a writer where logs in JSON format are written. // Output is a writer where logs in JSON format are written.
// Optional. Default value os.Stdout. // Optional. Default value os.Stdout.
Output io.Writer Output io.Writer
template *fasttemplate.Template template *fasttemplate.Template
colorer *color.Color colorer *color.Color
pool *sync.Pool pool *sync.Pool
} }
)
var ( // DefaultLoggerConfig is the default Logger middleware config.
// DefaultLoggerConfig is the default Logger middleware config. var DefaultLoggerConfig = LoggerConfig{
DefaultLoggerConfig = LoggerConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` + `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` +
`"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + `"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` +
`"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` + `,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n",
`,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", CustomTimeFormat: "2006-01-02 15:04:05.00000",
CustomTimeFormat: "2006-01-02 15:04:05.00000", colorer: color.New(),
colorer: color.New(), }
}
)
// Logger returns a middleware that logs HTTP requests. // Logger returns a middleware that logs HTTP requests.
func Logger() echo.MiddlewareFunc { func Logger() echo.MiddlewareFunc {

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -6,28 +9,24 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // MethodOverrideConfig defines the config for MethodOverride middleware.
// MethodOverrideConfig defines the config for MethodOverride middleware. type MethodOverrideConfig struct {
MethodOverrideConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Getter is a function that gets overridden method from the request. // Getter is a function that gets overridden method from the request.
// Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride). // Optional. Default values MethodFromHeader(echo.HeaderXHTTPMethodOverride).
Getter MethodOverrideGetter Getter MethodOverrideGetter
} }
// MethodOverrideGetter is a function that gets overridden method from the request // MethodOverrideGetter is a function that gets overridden method from the request
MethodOverrideGetter func(echo.Context) string type MethodOverrideGetter func(echo.Context) string
)
var ( // DefaultMethodOverrideConfig is the default MethodOverride middleware config.
// DefaultMethodOverrideConfig is the default MethodOverride middleware config. var DefaultMethodOverrideConfig = MethodOverrideConfig{
DefaultMethodOverrideConfig = MethodOverrideConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride),
Getter: MethodFromHeader(echo.HeaderXHTTPMethodOverride), }
}
)
// MethodOverride returns a MethodOverride middleware. // MethodOverride returns a MethodOverride middleware.
// MethodOverride middleware checks for the overridden method from the request and // MethodOverride middleware checks for the overridden method from the request and

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -9,14 +12,12 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // Skipper defines a function to skip middleware. Returning true skips processing
// Skipper defines a function to skip middleware. Returning true skips processing // the middleware.
// the middleware. type Skipper func(c echo.Context) bool
Skipper func(c echo.Context) bool
// BeforeFunc defines a function which is executed just before the middleware. // BeforeFunc defines a function which is executed just before the middleware.
BeforeFunc func(c echo.Context) type BeforeFunc func(c echo.Context)
)
func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer { func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
groups := pattern.FindAllStringSubmatch(input, -1) groups := pattern.FindAllStringSubmatch(input, -1)
@ -38,9 +39,9 @@ func rewriteRulesRegex(rewrite map[string]string) map[*regexp.Regexp]string {
rulesRegex := map[*regexp.Regexp]string{} rulesRegex := map[*regexp.Regexp]string{}
for k, v := range rewrite { for k, v := range rewrite {
k = regexp.QuoteMeta(k) k = regexp.QuoteMeta(k)
k = strings.Replace(k, `\*`, "(.*?)", -1) k = strings.ReplaceAll(k, `\*`, "(.*?)")
if strings.HasPrefix(k, `\^`) { if strings.HasPrefix(k, `\^`) {
k = strings.Replace(k, `\^`, "^", -1) k = strings.ReplaceAll(k, `\^`, "^")
} }
k = k + "$" k = k + "$"
rulesRegex[regexp.MustCompile(k)] = v rulesRegex[regexp.MustCompile(k)] = v
@ -53,7 +54,7 @@ func rewriteURL(rewriteRegex map[*regexp.Regexp]string, req *http.Request) error
return nil return nil
} }
// Depending how HTTP request is sent RequestURI could contain Scheme://Host/path or be just /path. // Depending on how HTTP request is sent RequestURI could contain Scheme://Host/path or be just /path.
// We only want to use path part for rewriting and therefore trim prefix if it exists // We only want to use path part for rewriting and therefore trim prefix if it exists
rawURI := req.RequestURI rawURI := req.RequestURI
if rawURI != "" && rawURI[0] != '/' { if rawURI != "" && rawURI[0] != '/' {

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -12,7 +15,6 @@ import (
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -20,101 +22,126 @@ import (
// TODO: Handle TLS proxy // TODO: Handle TLS proxy
type ( // ProxyConfig defines the config for Proxy middleware.
// ProxyConfig defines the config for Proxy middleware. type ProxyConfig struct {
ProxyConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Balancer defines a load balancing technique. // Balancer defines a load balancing technique.
// Required. // Required.
Balancer ProxyBalancer Balancer ProxyBalancer
// Rewrite defines URL path rewrite rules. The values captured in asterisk can be // RetryCount defines the number of times a failed proxied request should be retried
// retrieved by index e.g. $1, $2 and so on. // using the next available ProxyTarget. Defaults to 0, meaning requests are never retried.
// Examples: RetryCount int
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
Rewrite map[string]string
// RegexRewrite defines rewrite rules using regexp.Rexexp with captures // RetryFilter defines a function used to determine if a failed request to a
// Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. // ProxyTarget should be retried. The RetryFilter will only be called when the number
// Example: // of previous retries is less than RetryCount. If the function returns true, the
// "^/old/[0.9]+/": "/new", // request will be retried. The provided error indicates the reason for the request
// "^/api/.+?/(.*)": "/v2/$1", // failure. When the ProxyTarget is unavailable, the error will be an instance of
RegexRewrite map[*regexp.Regexp]string // echo.HTTPError with a Code of http.StatusBadGateway. In all other cases, the error
// will indicate an internal error in the Proxy middleware. When a RetryFilter is not
// specified, all requests that fail with http.StatusBadGateway will be retried. A custom
// RetryFilter can be provided to only retry specific requests. Note that RetryFilter is
// only called when the request to the target fails, or an internal error in the Proxy
// middleware has occurred. Successful requests that return a non-200 response code cannot
// be retried.
RetryFilter func(c echo.Context, e error) bool
// Context key to store selected ProxyTarget into context. // ErrorHandler defines a function which can be used to return custom errors from
// Optional. Default value "target". // the Proxy middleware. ErrorHandler is only invoked when there has been
ContextKey string // either an internal error in the Proxy middleware or the ProxyTarget is
// unavailable. Due to the way requests are proxied, ErrorHandler is not invoked
// when a ProxyTarget returns a non-200 response. In these cases, the response
// is already written so errors cannot be modified. ErrorHandler is only
// invoked after all retry attempts have been exhausted.
ErrorHandler func(c echo.Context, err error) error
// To customize the transport to remote. // Rewrite defines URL path rewrite rules. The values captured in asterisk can be
// Examples: If custom TLS certificates are required. // retrieved by index e.g. $1, $2 and so on.
Transport http.RoundTripper // Examples:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
Rewrite map[string]string
// ModifyResponse defines function to modify response from ProxyTarget. // RegexRewrite defines rewrite rules using regexp.Rexexp with captures
ModifyResponse func(*http.Response) error // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.
} // Example:
// "^/old/[0.9]+/": "/new",
// "^/api/.+?/(.*)": "/v2/$1",
RegexRewrite map[*regexp.Regexp]string
// ProxyTarget defines the upstream target. // Context key to store selected ProxyTarget into context.
ProxyTarget struct { // Optional. Default value "target".
Name string ContextKey string
URL *url.URL
Meta echo.Map
}
// ProxyBalancer defines an interface to implement a load balancing technique. // To customize the transport to remote.
ProxyBalancer interface { // Examples: If custom TLS certificates are required.
AddTarget(*ProxyTarget) bool Transport http.RoundTripper
RemoveTarget(string) bool
Next(echo.Context) *ProxyTarget
}
// TargetProvider defines an interface that gives the opportunity for balancer to return custom errors when selecting target. // ModifyResponse defines function to modify response from ProxyTarget.
TargetProvider interface { ModifyResponse func(*http.Response) error
NextTarget(echo.Context) (*ProxyTarget, error) }
}
commonBalancer struct { // ProxyTarget defines the upstream target.
targets []*ProxyTarget type ProxyTarget struct {
mutex sync.RWMutex Name string
} URL *url.URL
Meta echo.Map
}
// RandomBalancer implements a random load balancing technique. // ProxyBalancer defines an interface to implement a load balancing technique.
randomBalancer struct { type ProxyBalancer interface {
*commonBalancer AddTarget(*ProxyTarget) bool
random *rand.Rand RemoveTarget(string) bool
} Next(echo.Context) *ProxyTarget
}
// RoundRobinBalancer implements a round-robin load balancing technique. // TargetProvider defines an interface that gives the opportunity for balancer
roundRobinBalancer struct { // to return custom errors when selecting target.
*commonBalancer type TargetProvider interface {
i uint32 NextTarget(echo.Context) (*ProxyTarget, error)
} }
)
var ( type commonBalancer struct {
// DefaultProxyConfig is the default Proxy middleware config. targets []*ProxyTarget
DefaultProxyConfig = ProxyConfig{ mutex sync.Mutex
Skipper: DefaultSkipper, }
ContextKey: "target",
} // RandomBalancer implements a random load balancing technique.
) type randomBalancer struct {
commonBalancer
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
type roundRobinBalancer struct {
commonBalancer
// tracking the index on `targets` slice for the next `*ProxyTarget` to be used
i int
}
// DefaultProxyConfig is the default Proxy middleware config.
var DefaultProxyConfig = ProxyConfig{
Skipper: DefaultSkipper,
ContextKey: "target",
}
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler { func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
in, _, err := c.Response().Hijack() in, _, err := c.Response().Hijack()
if err != nil { if err != nil {
c.Set("_error", fmt.Sprintf("proxy raw, hijack error=%v, url=%s", t.URL, err)) c.Set("_error", fmt.Errorf("proxy raw, hijack error=%w, url=%s", err, t.URL))
return return
} }
defer in.Close() defer in.Close()
out, err := net.Dial("tcp", t.URL.Host) out, err := net.Dial("tcp", t.URL.Host)
if err != nil { if err != nil {
c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", t.URL, err))) c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", err, t.URL)))
return return
} }
defer out.Close() defer out.Close()
@ -122,7 +149,7 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
// Write header // Write header
err = r.Write(out) err = r.Write(out)
if err != nil { if err != nil {
c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", t.URL, err))) c.Set("_error", echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", err, t.URL)))
return return
} }
@ -136,39 +163,44 @@ func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
go cp(in, out) go cp(in, out)
err = <-errCh err = <-errCh
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
c.Set("_error", fmt.Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err)) c.Set("_error", fmt.Errorf("proxy raw, copy body error=%w, url=%s", err, t.URL))
} }
}) })
} }
// NewRandomBalancer returns a random proxy balancer. // NewRandomBalancer returns a random proxy balancer.
func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer { func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
b := &randomBalancer{commonBalancer: new(commonBalancer)} b := randomBalancer{}
b.targets = targets b.targets = targets
return b b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
return &b
} }
// NewRoundRobinBalancer returns a round-robin proxy balancer. // NewRoundRobinBalancer returns a round-robin proxy balancer.
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer { func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
b := &roundRobinBalancer{commonBalancer: new(commonBalancer)} b := roundRobinBalancer{}
b.targets = targets b.targets = targets
return b return &b
} }
// AddTarget adds an upstream target to the list. // AddTarget adds an upstream target to the list and returns `true`.
//
// However, if a target with the same name already exists then the operation is aborted returning `false`.
func (b *commonBalancer) AddTarget(target *ProxyTarget) bool { func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
b.mutex.Lock()
defer b.mutex.Unlock()
for _, t := range b.targets { for _, t := range b.targets {
if t.Name == target.Name { if t.Name == target.Name {
return false return false
} }
} }
b.mutex.Lock()
defer b.mutex.Unlock()
b.targets = append(b.targets, target) b.targets = append(b.targets, target)
return true return true
} }
// RemoveTarget removes an upstream target from the list. // RemoveTarget removes an upstream target from the list by name.
//
// Returns `true` on success, `false` if no target with the name is found.
func (b *commonBalancer) RemoveTarget(name string) bool { func (b *commonBalancer) RemoveTarget(name string) bool {
b.mutex.Lock() b.mutex.Lock()
defer b.mutex.Unlock() defer b.mutex.Unlock()
@ -182,21 +214,58 @@ func (b *commonBalancer) RemoveTarget(name string) bool {
} }
// Next randomly returns an upstream target. // Next randomly returns an upstream target.
//
// Note: `nil` is returned in case upstream target list is empty.
func (b *randomBalancer) Next(c echo.Context) *ProxyTarget { func (b *randomBalancer) Next(c echo.Context) *ProxyTarget {
if b.random == nil { b.mutex.Lock()
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond()))) defer b.mutex.Unlock()
if len(b.targets) == 0 {
return nil
} else if len(b.targets) == 1 {
return b.targets[0]
} }
b.mutex.RLock()
defer b.mutex.RUnlock()
return b.targets[b.random.Intn(len(b.targets))] return b.targets[b.random.Intn(len(b.targets))]
} }
// Next returns an upstream target using round-robin technique. // Next returns an upstream target using round-robin technique. In the case
// where a previously failed request is being retried, the round-robin
// balancer will attempt to use the next target relative to the original
// request. If the list of targets held by the balancer is modified while a
// failed request is being retried, it is possible that the balancer will
// return the original failed target.
//
// Note: `nil` is returned in case upstream target list is empty.
func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget { func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget {
b.i = b.i % uint32(len(b.targets)) b.mutex.Lock()
t := b.targets[b.i] defer b.mutex.Unlock()
atomic.AddUint32(&b.i, 1) if len(b.targets) == 0 {
return t return nil
} else if len(b.targets) == 1 {
return b.targets[0]
}
var i int
const lastIdxKey = "_round_robin_last_index"
// This request is a retry, start from the index of the previous
// target to ensure we don't attempt to retry the request with
// the same failed target
if c.Get(lastIdxKey) != nil {
i = c.Get(lastIdxKey).(int)
i++
if i >= len(b.targets) {
i = 0
}
} else {
// This is a first time request, use the global index
if b.i >= len(b.targets) {
b.i = 0
}
i = b.i
b.i++
}
c.Set(lastIdxKey, i)
return b.targets[i]
} }
// Proxy returns a Proxy middleware. // Proxy returns a Proxy middleware.
@ -211,14 +280,26 @@ func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
// ProxyWithConfig returns a Proxy middleware with config. // ProxyWithConfig returns a Proxy middleware with config.
// See: `Proxy()` // See: `Proxy()`
func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc { func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
if config.Balancer == nil {
panic("echo: proxy middleware requires balancer")
}
// Defaults // Defaults
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultProxyConfig.Skipper config.Skipper = DefaultProxyConfig.Skipper
} }
if config.Balancer == nil { if config.RetryFilter == nil {
panic("echo: proxy middleware requires balancer") config.RetryFilter = func(c echo.Context, e error) bool {
if httpErr, ok := e.(*echo.HTTPError); ok {
return httpErr.Code == http.StatusBadGateway
}
return false
}
}
if config.ErrorHandler == nil {
config.ErrorHandler = func(c echo.Context, err error) error {
return err
}
} }
if config.Rewrite != nil { if config.Rewrite != nil {
if config.RegexRewrite == nil { if config.RegexRewrite == nil {
config.RegexRewrite = make(map[*regexp.Regexp]string) config.RegexRewrite = make(map[*regexp.Regexp]string)
@ -229,28 +310,17 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
} }
provider, isTargetProvider := config.Balancer.(TargetProvider) provider, isTargetProvider := config.Balancer.(TargetProvider)
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) { return func(c echo.Context) error {
if config.Skipper(c) { if config.Skipper(c) {
return next(c) return next(c)
} }
req := c.Request() req := c.Request()
res := c.Response() res := c.Response()
var tgt *ProxyTarget
if isTargetProvider {
tgt, err = provider.NextTarget(c)
if err != nil {
return err
}
} else {
tgt = config.Balancer.Next(c)
}
c.Set(config.ContextKey, tgt)
if err := rewriteURL(config.RegexRewrite, req); err != nil { if err := rewriteURL(config.RegexRewrite, req); err != nil {
return err return config.ErrorHandler(c, err)
} }
// Fix header // Fix header
@ -266,19 +336,52 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
req.Header.Set(echo.HeaderXForwardedFor, c.RealIP()) req.Header.Set(echo.HeaderXForwardedFor, c.RealIP())
} }
// Proxy retries := config.RetryCount
switch { for {
case c.IsWebSocket(): var tgt *ProxyTarget
proxyRaw(tgt, c).ServeHTTP(res, req) var err error
case req.Header.Get(echo.HeaderAccept) == "text/event-stream": if isTargetProvider {
default: tgt, err = provider.NextTarget(c)
proxyHTTP(tgt, c, config).ServeHTTP(res, req) if err != nil {
} return config.ErrorHandler(c, err)
if e, ok := c.Get("_error").(error); ok { }
err = e } else {
} tgt = config.Balancer.Next(c)
}
return c.Set(config.ContextKey, tgt)
//If retrying a failed request, clear any previous errors from
//context here so that balancers have the option to check for
//errors that occurred using previous target
if retries < config.RetryCount {
c.Set("_error", nil)
}
// This is needed for ProxyConfig.ModifyResponse and/or ProxyConfig.Transport to be able to process the Request
// that Balancer may have replaced with c.SetRequest.
req = c.Request()
// Proxy
switch {
case c.IsWebSocket():
proxyRaw(tgt, c).ServeHTTP(res, req)
default: // even SSE requests
proxyHTTP(tgt, c, config).ServeHTTP(res, req)
}
err, hasError := c.Get("_error").(error)
if !hasError {
return nil
}
retry := retries > 0 && config.RetryFilter(c, err)
if !retry {
return config.ErrorHandler(c, err)
}
retries--
}
} }
} }
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -9,39 +12,34 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
) )
type ( // RateLimiterStore is the interface to be implemented by custom stores.
// RateLimiterStore is the interface to be implemented by custom stores. type RateLimiterStore interface {
RateLimiterStore interface { // Stores for the rate limiter have to implement the Allow method
// Stores for the rate limiter have to implement the Allow method Allow(identifier string) (bool, error)
Allow(identifier string) (bool, error) }
}
)
type ( // RateLimiterConfig defines the configuration for the rate limiter
// RateLimiterConfig defines the configuration for the rate limiter type RateLimiterConfig struct {
RateLimiterConfig struct { Skipper Skipper
Skipper Skipper BeforeFunc BeforeFunc
BeforeFunc BeforeFunc // IdentifierExtractor uses echo.Context to extract the identifier for a visitor
// IdentifierExtractor uses echo.Context to extract the identifier for a visitor IdentifierExtractor Extractor
IdentifierExtractor Extractor // Store defines a store for the rate limiter
// Store defines a store for the rate limiter Store RateLimiterStore
Store RateLimiterStore // ErrorHandler provides a handler to be called when IdentifierExtractor returns an error
// ErrorHandler provides a handler to be called when IdentifierExtractor returns an error ErrorHandler func(context echo.Context, err error) error
ErrorHandler func(context echo.Context, err error) error // DenyHandler provides a handler to be called when RateLimiter denies access
// DenyHandler provides a handler to be called when RateLimiter denies access DenyHandler func(context echo.Context, identifier string, err error) error
DenyHandler func(context echo.Context, identifier string, err error) error }
}
// Extractor is used to extract data from echo.Context
Extractor func(context echo.Context) (string, error)
)
// errors // Extractor is used to extract data from echo.Context
var ( type Extractor func(context echo.Context) (string, error)
// ErrRateLimitExceeded denotes an error raised when rate limit is exceeded
ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") // ErrRateLimitExceeded denotes an error raised when rate limit is exceeded
// ErrExtractorError denotes an error raised when extractor function is unsuccessful var ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier")
) // ErrExtractorError denotes an error raised when extractor function is unsuccessful
var ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier")
// DefaultRateLimiterConfig defines default values for RateLimiterConfig // DefaultRateLimiterConfig defines default values for RateLimiterConfig
var DefaultRateLimiterConfig = RateLimiterConfig{ var DefaultRateLimiterConfig = RateLimiterConfig{
@ -150,23 +148,24 @@ func RateLimiterWithConfig(config RateLimiterConfig) echo.MiddlewareFunc {
} }
} }
type ( // RateLimiterMemoryStore is the built-in store implementation for RateLimiter
// RateLimiterMemoryStore is the built-in store implementation for RateLimiter type RateLimiterMemoryStore struct {
RateLimiterMemoryStore struct { visitors map[string]*Visitor
visitors map[string]*Visitor mutex sync.Mutex
mutex sync.Mutex rate rate.Limit // for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
rate rate.Limit // for more info check out Limiter docs - https://pkg.go.dev/golang.org/x/time/rate#Limit.
burst int burst int
expiresIn time.Duration expiresIn time.Duration
lastCleanup time.Time lastCleanup time.Time
}
// Visitor signifies a unique user's limiter details timeNow func() time.Time
Visitor struct { }
*rate.Limiter
lastSeen time.Time // Visitor signifies a unique user's limiter details
} type Visitor struct {
) *rate.Limiter
lastSeen time.Time
}
/* /*
NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with NewRateLimiterMemoryStore returns an instance of RateLimiterMemoryStore with
@ -219,7 +218,8 @@ func NewRateLimiterMemoryStoreWithConfig(config RateLimiterMemoryStoreConfig) (s
store.burst = int(config.Rate) store.burst = int(config.Rate)
} }
store.visitors = make(map[string]*Visitor) store.visitors = make(map[string]*Visitor)
store.lastCleanup = now() store.timeNow = time.Now
store.lastCleanup = store.timeNow()
return return
} }
@ -244,12 +244,13 @@ func (store *RateLimiterMemoryStore) Allow(identifier string) (bool, error) {
limiter.Limiter = rate.NewLimiter(store.rate, store.burst) limiter.Limiter = rate.NewLimiter(store.rate, store.burst)
store.visitors[identifier] = limiter store.visitors[identifier] = limiter
} }
limiter.lastSeen = now() now := store.timeNow()
if now().Sub(store.lastCleanup) > store.expiresIn { limiter.lastSeen = now
if now.Sub(store.lastCleanup) > store.expiresIn {
store.cleanupStaleVisitors() store.cleanupStaleVisitors()
} }
store.mutex.Unlock() store.mutex.Unlock()
return limiter.AllowN(now(), 1), nil return limiter.AllowN(store.timeNow(), 1), nil
} }
/* /*
@ -258,14 +259,9 @@ of users who haven't visited again after the configured expiry time has elapsed
*/ */
func (store *RateLimiterMemoryStore) cleanupStaleVisitors() { func (store *RateLimiterMemoryStore) cleanupStaleVisitors() {
for id, visitor := range store.visitors { for id, visitor := range store.visitors {
if now().Sub(visitor.lastSeen) > store.expiresIn { if store.timeNow().Sub(visitor.lastSeen) > store.expiresIn {
delete(store.visitors, id) delete(store.visitors, id)
} }
} }
store.lastCleanup = now() store.lastCleanup = store.timeNow()
} }
/*
actual time method which is mocked in test file
*/
var now = time.Now

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -9,49 +12,52 @@ import (
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
) )
type ( // LogErrorFunc defines a function for custom logging in the middleware.
type LogErrorFunc func(c echo.Context, err error, stack []byte) error
// RecoverConfig defines the config for Recover middleware.
type RecoverConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Size of the stack to be printed.
// Optional. Default value 4KB.
StackSize int `yaml:"stack_size"`
// DisableStackAll disables formatting stack traces of all other goroutines
// into buffer after the trace for the current goroutine.
// Optional. Default value false.
DisableStackAll bool `yaml:"disable_stack_all"`
// DisablePrintStack disables printing stack trace.
// Optional. Default value as false.
DisablePrintStack bool `yaml:"disable_print_stack"`
// LogLevel is log level to printing stack trace.
// Optional. Default value 0 (Print).
LogLevel log.Lvl
// LogErrorFunc defines a function for custom logging in the middleware. // LogErrorFunc defines a function for custom logging in the middleware.
LogErrorFunc func(c echo.Context, err error, stack []byte) error // If it's set you don't need to provide LogLevel for config.
// If this function returns nil, the centralized HTTPErrorHandler will not be called.
LogErrorFunc LogErrorFunc
// RecoverConfig defines the config for Recover middleware. // DisableErrorHandler disables the call to centralized HTTPErrorHandler.
RecoverConfig struct { // The recovered error is then passed back to upstream middleware, instead of swallowing the error.
// Skipper defines a function to skip middleware. // Optional. Default value false.
Skipper Skipper DisableErrorHandler bool `yaml:"disable_error_handler"`
}
// Size of the stack to be printed. // DefaultRecoverConfig is the default Recover middleware config.
// Optional. Default value 4KB. var DefaultRecoverConfig = RecoverConfig{
StackSize int `yaml:"stack_size"` Skipper: DefaultSkipper,
StackSize: 4 << 10, // 4 KB
// DisableStackAll disables formatting stack traces of all other goroutines DisableStackAll: false,
// into buffer after the trace for the current goroutine. DisablePrintStack: false,
// Optional. Default value false. LogLevel: 0,
DisableStackAll bool `yaml:"disable_stack_all"` LogErrorFunc: nil,
DisableErrorHandler: false,
// DisablePrintStack disables printing stack trace. }
// Optional. Default value as false.
DisablePrintStack bool `yaml:"disable_print_stack"`
// LogLevel is log level to printing stack trace.
// Optional. Default value 0 (Print).
LogLevel log.Lvl
// LogErrorFunc defines a function for custom logging in the middleware.
// If it's set you don't need to provide LogLevel for config.
LogErrorFunc LogErrorFunc
}
)
var (
// DefaultRecoverConfig is the default Recover middleware config.
DefaultRecoverConfig = RecoverConfig{
Skipper: DefaultSkipper,
StackSize: 4 << 10, // 4 KB
DisableStackAll: false,
DisablePrintStack: false,
LogLevel: 0,
LogErrorFunc: nil,
}
)
// Recover returns a middleware which recovers from panics anywhere in the chain // Recover returns a middleware which recovers from panics anywhere in the chain
// and handles the control to the centralized HTTPErrorHandler. // and handles the control to the centralized HTTPErrorHandler.
@ -71,7 +77,7 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
} }
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) (returnErr error) {
if config.Skipper(c) { if config.Skipper(c) {
return next(c) return next(c)
} }
@ -113,7 +119,12 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
c.Logger().Print(msg) c.Logger().Print(msg)
} }
} }
c.Error(err)
if err != nil && !config.DisableErrorHandler {
c.Error(err)
} else {
returnErr = err
}
} }
}() }()
return next(c) return next(c)

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (

View File

@ -1,36 +1,34 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/gommon/random"
) )
type ( // RequestIDConfig defines the config for RequestID middleware.
// RequestIDConfig defines the config for RequestID middleware. type RequestIDConfig struct {
RequestIDConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Generator defines a function to generate an ID. // Generator defines a function to generate an ID.
// Optional. Default value random.String(32). // Optional. Defaults to generator for random string of length 32.
Generator func() string Generator func() string
// RequestIDHandler defines a function which is executed for a request id. // RequestIDHandler defines a function which is executed for a request id.
RequestIDHandler func(echo.Context, string) RequestIDHandler func(echo.Context, string)
// TargetHeader defines what header to look for to populate the id // TargetHeader defines what header to look for to populate the id
TargetHeader string TargetHeader string
} }
)
var ( // DefaultRequestIDConfig is the default RequestID middleware config.
// DefaultRequestIDConfig is the default RequestID middleware config. var DefaultRequestIDConfig = RequestIDConfig{
DefaultRequestIDConfig = RequestIDConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Generator: generator,
Generator: generator, TargetHeader: echo.HeaderXRequestID,
TargetHeader: echo.HeaderXRequestID, }
}
)
// RequestID returns a X-Request-ID middleware. // RequestID returns a X-Request-ID middleware.
func RequestID() echo.MiddlewareFunc { func RequestID() echo.MiddlewareFunc {
@ -73,5 +71,5 @@ func RequestIDWithConfig(config RequestIDConfig) echo.MiddlewareFunc {
} }
func generator() string { func generator() string {
return random.String(32) return randomString(32)
} }

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -8,6 +11,30 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// Example for `slog` https://pkg.go.dev/log/slog
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
// LogStatus: true,
// LogURI: true,
// LogError: true,
// HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code
// LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
// if v.Error == nil {
// logger.LogAttrs(context.Background(), slog.LevelInfo, "REQUEST",
// slog.String("uri", v.URI),
// slog.Int("status", v.Status),
// )
// } else {
// logger.LogAttrs(context.Background(), slog.LevelError, "REQUEST_ERROR",
// slog.String("uri", v.URI),
// slog.Int("status", v.Status),
// slog.String("err", v.Error.Error()),
// )
// }
// return nil
// },
// }))
//
// Example for `fmt.Printf` // Example for `fmt.Printf`
// e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ // e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
// LogStatus: true, // LogStatus: true,
@ -225,7 +252,7 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultSkipper config.Skipper = DefaultSkipper
} }
now = time.Now now := time.Now
if config.timeNow != nil { if config.timeNow != nil {
now = config.timeNow now = config.timeNow
} }
@ -257,7 +284,7 @@ func (config RequestLoggerConfig) ToMiddleware() (echo.MiddlewareFunc, error) {
config.BeforeNextFunc(c) config.BeforeNextFunc(c)
} }
err := next(c) err := next(c)
if config.HandleError { if err != nil && config.HandleError {
c.Error(err) c.Error(err)
} }

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build !go1.20
package middleware
import (
"bufio"
"fmt"
"net"
"net/http"
)
// TODO: remove when Go 1.23 is released and we do not support 1.19 anymore
func responseControllerFlush(rw http.ResponseWriter) error {
for {
switch t := rw.(type) {
case interface{ FlushError() error }:
return t.FlushError()
case http.Flusher:
t.Flush()
return nil
case interface{ Unwrap() http.ResponseWriter }:
rw = t.Unwrap()
default:
return fmt.Errorf("%w", http.ErrNotSupported)
}
}
}
// TODO: remove when Go 1.23 is released and we do not support 1.19 anymore
func responseControllerHijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) {
for {
switch t := rw.(type) {
case http.Hijacker:
return t.Hijack()
case interface{ Unwrap() http.ResponseWriter }:
rw = t.Unwrap()
default:
return nil, nil, fmt.Errorf("%w", http.ErrNotSupported)
}
}
}

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build go1.20
package middleware
import (
"bufio"
"net"
"net/http"
)
func responseControllerFlush(rw http.ResponseWriter) error {
return http.NewResponseController(rw).Flush()
}
func responseControllerHijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) {
return http.NewResponseController(rw).Hijack()
}

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -6,37 +9,33 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // RewriteConfig defines the config for Rewrite middleware.
// RewriteConfig defines the config for Rewrite middleware. type RewriteConfig struct {
RewriteConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Rules defines the URL path rewrite rules. The values captured in asterisk can be // Rules defines the URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on. // retrieved by index e.g. $1, $2 and so on.
// Example: // Example:
// "/old": "/new", // "/old": "/new",
// "/api/*": "/$1", // "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1", // "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2", // "/users/*/orders/*": "/user/$1/order/$2",
// Required. // Required.
Rules map[string]string `yaml:"rules"` Rules map[string]string `yaml:"rules"`
// RegexRules defines the URL path rewrite rules using regexp.Rexexp with captures // RegexRules defines the URL path rewrite rules using regexp.Rexexp with captures
// Every capture group in the values can be retrieved by index e.g. $1, $2 and so on. // Every capture group in the values can be retrieved by index e.g. $1, $2 and so on.
// Example: // Example:
// "^/old/[0.9]+/": "/new", // "^/old/[0.9]+/": "/new",
// "^/api/.+?/(.*)": "/v2/$1", // "^/api/.+?/(.*)": "/v2/$1",
RegexRules map[*regexp.Regexp]string `yaml:"regex_rules"` RegexRules map[*regexp.Regexp]string `yaml:"-"`
} }
)
var ( // DefaultRewriteConfig is the default Rewrite middleware config.
// DefaultRewriteConfig is the default Rewrite middleware config. var DefaultRewriteConfig = RewriteConfig{
DefaultRewriteConfig = RewriteConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, }
}
)
// Rewrite returns a Rewrite middleware. // Rewrite returns a Rewrite middleware.
// //

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -6,84 +9,80 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // SecureConfig defines the config for Secure middleware.
// SecureConfig defines the config for Secure middleware. type SecureConfig struct {
SecureConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// XSSProtection provides protection against cross-site scripting attack (XSS) // XSSProtection provides protection against cross-site scripting attack (XSS)
// by setting the `X-XSS-Protection` header. // by setting the `X-XSS-Protection` header.
// Optional. Default value "1; mode=block". // Optional. Default value "1; mode=block".
XSSProtection string `yaml:"xss_protection"` XSSProtection string `yaml:"xss_protection"`
// ContentTypeNosniff provides protection against overriding Content-Type // ContentTypeNosniff provides protection against overriding Content-Type
// header by setting the `X-Content-Type-Options` header. // header by setting the `X-Content-Type-Options` header.
// Optional. Default value "nosniff". // Optional. Default value "nosniff".
ContentTypeNosniff string `yaml:"content_type_nosniff"` ContentTypeNosniff string `yaml:"content_type_nosniff"`
// XFrameOptions can be used to indicate whether or not a browser should // XFrameOptions can be used to indicate whether or not a browser should
// be allowed to render a page in a <frame>, <iframe> or <object> . // be allowed to render a page in a <frame>, <iframe> or <object> .
// Sites can use this to avoid clickjacking attacks, by ensuring that their // Sites can use this to avoid clickjacking attacks, by ensuring that their
// content is not embedded into other sites.provides protection against // content is not embedded into other sites.provides protection against
// clickjacking. // clickjacking.
// Optional. Default value "SAMEORIGIN". // Optional. Default value "SAMEORIGIN".
// Possible values: // Possible values:
// - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself. // - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself.
// - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so. // - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so.
// - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin. // - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin.
XFrameOptions string `yaml:"x_frame_options"` XFrameOptions string `yaml:"x_frame_options"`
// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how // HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how
// long (in seconds) browsers should remember that this site is only to // long (in seconds) browsers should remember that this site is only to
// be accessed using HTTPS. This reduces your exposure to some SSL-stripping // be accessed using HTTPS. This reduces your exposure to some SSL-stripping
// man-in-the-middle (MITM) attacks. // man-in-the-middle (MITM) attacks.
// Optional. Default value 0. // Optional. Default value 0.
HSTSMaxAge int `yaml:"hsts_max_age"` HSTSMaxAge int `yaml:"hsts_max_age"`
// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security` // HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`
// header, excluding all subdomains from security policy. It has no effect // header, excluding all subdomains from security policy. It has no effect
// unless HSTSMaxAge is set to a non-zero value. // unless HSTSMaxAge is set to a non-zero value.
// Optional. Default value false. // Optional. Default value false.
HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"` HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"`
// ContentSecurityPolicy sets the `Content-Security-Policy` header providing // ContentSecurityPolicy sets the `Content-Security-Policy` header providing
// security against cross-site scripting (XSS), clickjacking and other code // security against cross-site scripting (XSS), clickjacking and other code
// injection attacks resulting from execution of malicious content in the // injection attacks resulting from execution of malicious content in the
// trusted web page context. // trusted web page context.
// Optional. Default value "". // Optional. Default value "".
ContentSecurityPolicy string `yaml:"content_security_policy"` ContentSecurityPolicy string `yaml:"content_security_policy"`
// CSPReportOnly would use the `Content-Security-Policy-Report-Only` header instead // CSPReportOnly would use the `Content-Security-Policy-Report-Only` header instead
// of the `Content-Security-Policy` header. This allows iterative updates of the // of the `Content-Security-Policy` header. This allows iterative updates of the
// content security policy by only reporting the violations that would // content security policy by only reporting the violations that would
// have occurred instead of blocking the resource. // have occurred instead of blocking the resource.
// Optional. Default value false. // Optional. Default value false.
CSPReportOnly bool `yaml:"csp_report_only"` CSPReportOnly bool `yaml:"csp_report_only"`
// HSTSPreloadEnabled will add the preload tag in the `Strict Transport Security` // HSTSPreloadEnabled will add the preload tag in the `Strict Transport Security`
// header, which enables the domain to be included in the HSTS preload list // header, which enables the domain to be included in the HSTS preload list
// maintained by Chrome (and used by Firefox and Safari): https://hstspreload.org/ // maintained by Chrome (and used by Firefox and Safari): https://hstspreload.org/
// Optional. Default value false. // Optional. Default value false.
HSTSPreloadEnabled bool `yaml:"hsts_preload_enabled"` HSTSPreloadEnabled bool `yaml:"hsts_preload_enabled"`
// ReferrerPolicy sets the `Referrer-Policy` header providing security against // ReferrerPolicy sets the `Referrer-Policy` header providing security against
// leaking potentially sensitive request paths to third parties. // leaking potentially sensitive request paths to third parties.
// Optional. Default value "". // Optional. Default value "".
ReferrerPolicy string `yaml:"referrer_policy"` ReferrerPolicy string `yaml:"referrer_policy"`
} }
)
var ( // DefaultSecureConfig is the default Secure middleware config.
// DefaultSecureConfig is the default Secure middleware config. var DefaultSecureConfig = SecureConfig{
DefaultSecureConfig = SecureConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, XSSProtection: "1; mode=block",
XSSProtection: "1; mode=block", ContentTypeNosniff: "nosniff",
ContentTypeNosniff: "nosniff", XFrameOptions: "SAMEORIGIN",
XFrameOptions: "SAMEORIGIN", HSTSPreloadEnabled: false,
HSTSPreloadEnabled: false, }
}
)
// Secure returns a Secure middleware. // Secure returns a Secure middleware.
// Secure middleware provides protection against cross-site scripting (XSS) attack, // Secure middleware provides protection against cross-site scripting (XSS) attack,

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -6,24 +9,20 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ( // TrailingSlashConfig defines the config for TrailingSlash middleware.
// TrailingSlashConfig defines the config for TrailingSlash middleware. type TrailingSlashConfig struct {
TrailingSlashConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional, but when provided the request is redirected using this code. // Optional, but when provided the request is redirected using this code.
RedirectCode int `yaml:"redirect_code"` RedirectCode int `yaml:"redirect_code"`
} }
)
var ( // DefaultTrailingSlashConfig is the default TrailingSlash middleware config.
// DefaultTrailingSlashConfig is the default TrailingSlash middleware config. var DefaultTrailingSlashConfig = TrailingSlashConfig{
DefaultTrailingSlashConfig = TrailingSlashConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, }
}
)
// AddTrailingSlash returns a root level (before router) middleware which adds a // AddTrailingSlash returns a root level (before router) middleware which adds a
// trailing slash to the request `URL#Path`. // trailing slash to the request `URL#Path`.

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -14,40 +17,38 @@ import (
"github.com/labstack/gommon/bytes" "github.com/labstack/gommon/bytes"
) )
type ( // StaticConfig defines the config for Static middleware.
// StaticConfig defines the config for Static middleware. type StaticConfig struct {
StaticConfig struct { // Skipper defines a function to skip middleware.
// Skipper defines a function to skip middleware. Skipper Skipper
Skipper Skipper
// Root directory from where the static content is served. // Root directory from where the static content is served.
// Required. // Required.
Root string `yaml:"root"` Root string `yaml:"root"`
// Index file for serving a directory. // Index file for serving a directory.
// Optional. Default value "index.html". // Optional. Default value "index.html".
Index string `yaml:"index"` Index string `yaml:"index"`
// Enable HTML5 mode by forwarding all not-found requests to root so that // Enable HTML5 mode by forwarding all not-found requests to root so that
// SPA (single-page application) can handle the routing. // SPA (single-page application) can handle the routing.
// Optional. Default value false. // Optional. Default value false.
HTML5 bool `yaml:"html5"` HTML5 bool `yaml:"html5"`
// Enable directory browsing. // Enable directory browsing.
// Optional. Default value false. // Optional. Default value false.
Browse bool `yaml:"browse"` Browse bool `yaml:"browse"`
// Enable ignoring of the base of the URL path. // Enable ignoring of the base of the URL path.
// Example: when assigning a static middleware to a non root path group, // Example: when assigning a static middleware to a non root path group,
// the filesystem path is not doubled // the filesystem path is not doubled
// Optional. Default value false. // Optional. Default value false.
IgnoreBase bool `yaml:"ignoreBase"` IgnoreBase bool `yaml:"ignoreBase"`
// Filesystem provides access to the static content. // Filesystem provides access to the static content.
// Optional. Defaults to http.Dir(config.Root) // Optional. Defaults to http.Dir(config.Root)
Filesystem http.FileSystem `yaml:"-"` Filesystem http.FileSystem `yaml:"-"`
} }
)
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
@ -121,13 +122,11 @@ const html = `
</html> </html>
` `
var ( // DefaultStaticConfig is the default Static middleware config.
// DefaultStaticConfig is the default Static middleware config. var DefaultStaticConfig = StaticConfig{
DefaultStaticConfig = StaticConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Index: "index.html",
Index: "index.html", }
}
)
// Static returns a Static middleware to serves static content from the provided // Static returns a Static middleware to serves static content from the provided
// root directory. // root directory.

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build !windows //go:build !windows
package middleware package middleware

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
@ -77,14 +80,12 @@ type TimeoutConfig struct {
Timeout time.Duration Timeout time.Duration
} }
var ( // DefaultTimeoutConfig is the default Timeout middleware config.
// DefaultTimeoutConfig is the default Timeout middleware config. var DefaultTimeoutConfig = TimeoutConfig{
DefaultTimeoutConfig = TimeoutConfig{ Skipper: DefaultSkipper,
Skipper: DefaultSkipper, Timeout: 0,
Timeout: 0, ErrorMessage: "",
ErrorMessage: "", }
}
)
// Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler // Timeout returns a middleware which returns error (503 Service Unavailable error) to client immediately when handler
// call runs for longer than its time limit. NB: timeout does not stop handler execution. // call runs for longer than its time limit. NB: timeout does not stop handler execution.

View File

@ -1,7 +1,14 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package middleware package middleware
import ( import (
"bufio"
"crypto/rand"
"io"
"strings" "strings"
"sync"
) )
func matchScheme(domain, pattern string) bool { func matchScheme(domain, pattern string) bool {
@ -52,3 +59,45 @@ func matchSubdomain(domain, pattern string) bool {
} }
return false return false
} }
// https://tip.golang.org/doc/go1.19#:~:text=Read%20no%20longer%20buffers%20random%20data%20obtained%20from%20the%20operating%20system%20between%20calls
var randomReaderPool = sync.Pool{New: func() interface{} {
return bufio.NewReader(rand.Reader)
}}
const randomStringCharset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
const randomStringCharsetLen = 52 // len(randomStringCharset)
const randomStringMaxByte = 255 - (256 % randomStringCharsetLen)
func randomString(length uint8) string {
reader := randomReaderPool.Get().(*bufio.Reader)
defer randomReaderPool.Put(reader)
b := make([]byte, length)
r := make([]byte, length+(length/4)) // perf: avoid read from rand.Reader many times
var i uint8 = 0
// security note:
// we can't just simply do b[i]=randomStringCharset[rb%len(randomStringCharset)],
// len(len(randomStringCharset)) is 52, and rb is [0, 255], 256 = 52 * 4 + 48.
// make the first 48 characters more possibly to be generated then others.
// So we have to skip bytes when rb > randomStringMaxByte
for {
_, err := io.ReadFull(reader, r)
if err != nil {
panic("unexpected error happened when reading from bufio.NewReader(crypto/rand.Reader)")
}
for _, rb := range r {
if rb > randomStringMaxByte {
// Skip this number to avoid bias.
continue
}
b[i] = randomStringCharset[rb%randomStringCharsetLen]
i++
if i == length {
return string(b)
}
}
}
}

View File

@ -1,25 +1,27 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
"bufio" "bufio"
"errors"
"net" "net"
"net/http" "net/http"
) )
type ( // Response wraps an http.ResponseWriter and implements its interface to be used
// Response wraps an http.ResponseWriter and implements its interface to be used // by an HTTP handler to construct an HTTP response.
// by an HTTP handler to construct an HTTP response. // See: https://golang.org/pkg/net/http/#ResponseWriter
// See: https://golang.org/pkg/net/http/#ResponseWriter type Response struct {
Response struct { echo *Echo
echo *Echo beforeFuncs []func()
beforeFuncs []func() afterFuncs []func()
afterFuncs []func() Writer http.ResponseWriter
Writer http.ResponseWriter Status int
Status int Size int64
Size int64 Committed bool
Committed bool }
}
)
// NewResponse creates a new instance of Response. // NewResponse creates a new instance of Response.
func NewResponse(w http.ResponseWriter, e *Echo) (r *Response) { func NewResponse(w http.ResponseWriter, e *Echo) (r *Response) {
@ -84,14 +86,24 @@ func (r *Response) Write(b []byte) (n int, err error) {
// buffered data to the client. // buffered data to the client.
// See [http.Flusher](https://golang.org/pkg/net/http/#Flusher) // See [http.Flusher](https://golang.org/pkg/net/http/#Flusher)
func (r *Response) Flush() { func (r *Response) Flush() {
r.Writer.(http.Flusher).Flush() err := responseControllerFlush(r.Writer)
if err != nil && errors.Is(err, http.ErrNotSupported) {
panic(errors.New("response writer flushing is not supported"))
}
} }
// Hijack implements the http.Hijacker interface to allow an HTTP handler to // Hijack implements the http.Hijacker interface to allow an HTTP handler to
// take over the connection. // take over the connection.
// See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker) // See [http.Hijacker](https://golang.org/pkg/net/http/#Hijacker)
func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) { func (r *Response) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return r.Writer.(http.Hijacker).Hijack() return responseControllerHijack(r.Writer)
}
// Unwrap returns the original http.ResponseWriter.
// ResponseController can be used to access the original http.ResponseWriter.
// See [https://go.dev/blog/go1.20]
func (r *Response) Unwrap() http.ResponseWriter {
return r.Writer
} }
func (r *Response) reset(w http.ResponseWriter) { func (r *Response) reset(w http.ResponseWriter) {

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build !go1.20
package echo
import (
"bufio"
"fmt"
"net"
"net/http"
)
// TODO: remove when Go 1.23 is released and we do not support 1.19 anymore
func responseControllerFlush(rw http.ResponseWriter) error {
for {
switch t := rw.(type) {
case interface{ FlushError() error }:
return t.FlushError()
case http.Flusher:
t.Flush()
return nil
case interface{ Unwrap() http.ResponseWriter }:
rw = t.Unwrap()
default:
return fmt.Errorf("%w", http.ErrNotSupported)
}
}
}
// TODO: remove when Go 1.23 is released and we do not support 1.19 anymore
func responseControllerHijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) {
for {
switch t := rw.(type) {
case http.Hijacker:
return t.Hijack()
case interface{ Unwrap() http.ResponseWriter }:
rw = t.Unwrap()
default:
return nil, nil, fmt.Errorf("%w", http.ErrNotSupported)
}
}
}

View File

@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
//go:build go1.20
package echo
import (
"bufio"
"net"
"net/http"
)
func responseControllerFlush(rw http.ResponseWriter) error {
return http.NewResponseController(rw).Flush()
}
func responseControllerHijack(rw http.ResponseWriter) (net.Conn, *bufio.ReadWriter, error) {
return http.NewResponseController(rw).Hijack()
}

View File

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors
package echo package echo
import ( import (
@ -6,56 +9,58 @@ import (
"net/http" "net/http"
) )
type ( // Router is the registry of all registered routes for an `Echo` instance for
// Router is the registry of all registered routes for an `Echo` instance for // request matching and URL path parameter parsing.
// request matching and URL path parameter parsing. type Router struct {
Router struct { tree *node
tree *node routes map[string]*Route
routes map[string]*Route echo *Echo
echo *Echo }
}
node struct {
kind kind
label byte
prefix string
parent *node
staticChildren children
originalPath string
methods *routeMethods
paramChild *node
anyChild *node
paramsCount int
// isLeaf indicates that node does not have child routes
isLeaf bool
// isHandler indicates that node has at least one handler registered to it
isHandler bool
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases type node struct {
notFoundHandler *routeMethod kind kind
} label byte
kind uint8 prefix string
children []*node parent *node
routeMethod struct { staticChildren children
ppath string originalPath string
pnames []string methods *routeMethods
handler HandlerFunc paramChild *node
} anyChild *node
routeMethods struct { paramsCount int
connect *routeMethod // isLeaf indicates that node does not have child routes
delete *routeMethod isLeaf bool
get *routeMethod // isHandler indicates that node has at least one handler registered to it
head *routeMethod isHandler bool
options *routeMethod
patch *routeMethod // notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
post *routeMethod notFoundHandler *routeMethod
propfind *routeMethod }
put *routeMethod
trace *routeMethod type kind uint8
report *routeMethod type children []*node
anyOther map[string]*routeMethod
allowHeader string type routeMethod struct {
} ppath string
) pnames []string
handler HandlerFunc
}
type routeMethods struct {
connect *routeMethod
delete *routeMethod
get *routeMethod
head *routeMethod
options *routeMethod
patch *routeMethod
post *routeMethod
propfind *routeMethod
put *routeMethod
trace *routeMethod
report *routeMethod
anyOther map[string]*routeMethod
allowHeader string
}
const ( const (
staticKind kind = iota staticKind kind = iota
@ -151,7 +156,7 @@ func (r *Router) Routes() []*Route {
return routes return routes
} }
// Reverse generates an URL from route name and provided parameters. // Reverse generates a URL from route name and provided parameters.
func (r *Router) Reverse(name string, params ...interface{}) string { func (r *Router) Reverse(name string, params ...interface{}) string {
uri := new(bytes.Buffer) uri := new(bytes.Buffer)
ln := len(params) ln := len(params)
@ -159,7 +164,12 @@ func (r *Router) Reverse(name string, params ...interface{}) string {
for _, route := range r.routes { for _, route := range r.routes {
if route.Name == name { if route.Name == name {
for i, l := 0, len(route.Path); i < l; i++ { for i, l := 0, len(route.Path); i < l; i++ {
if (route.Path[i] == ':' || route.Path[i] == '*') && n < ln { hasBackslash := route.Path[i] == '\\'
if hasBackslash && i+1 < l && route.Path[i+1] == ':' {
i++ // backslash before colon escapes that colon. in that case skip backslash
}
if n < ln && (route.Path[i] == '*' || (!hasBackslash && route.Path[i] == ':')) {
// in case of `*` wildcard or `:` (unescaped colon) param we replace everything till next slash or end of path
for ; i < l && route.Path[i] != '/'; i++ { for ; i < l && route.Path[i] != '/'; i++ {
} }
uri.WriteString(fmt.Sprintf("%v", params[n])) uri.WriteString(fmt.Sprintf("%v", params[n]))
@ -175,8 +185,18 @@ func (r *Router) Reverse(name string, params ...interface{}) string {
return uri.String() return uri.String()
} }
func normalizePathSlash(path string) string {
if path == "" {
path = "/"
} else if path[0] != '/' {
path = "/" + path
}
return path
}
func (r *Router) add(method, path, name string, h HandlerFunc) *Route { func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
r.Add(method, path, h) path = normalizePathSlash(path)
r.insert(method, path, h)
route := &Route{ route := &Route{
Method: method, Method: method,
@ -189,13 +209,11 @@ func (r *Router) add(method, path, name string, h HandlerFunc) *Route {
// Add registers a new route for method and path with matching handler. // Add registers a new route for method and path with matching handler.
func (r *Router) Add(method, path string, h HandlerFunc) { func (r *Router) Add(method, path string, h HandlerFunc) {
// Validate path r.insert(method, normalizePathSlash(path), h)
if path == "" { }
path = "/"
} func (r *Router) insert(method, path string, h HandlerFunc) {
if path[0] != '/' { path = normalizePathSlash(path)
path = "/" + path
}
pnames := []string{} // Param names pnames := []string{} // Param names
ppath := path // Pristine path ppath := path // Pristine path
@ -214,7 +232,7 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
} }
j := i + 1 j := i + 1
r.insert(method, path[:i], staticKind, routeMethod{}) r.insertNode(method, path[:i], staticKind, routeMethod{})
for ; i < lcpIndex && path[i] != '/'; i++ { for ; i < lcpIndex && path[i] != '/'; i++ {
} }
@ -224,21 +242,21 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
if i == lcpIndex { if i == lcpIndex {
// path node is last fragment of route path. ie. `/users/:id` // path node is last fragment of route path. ie. `/users/:id`
r.insert(method, path[:i], paramKind, routeMethod{ppath, pnames, h}) r.insertNode(method, path[:i], paramKind, routeMethod{ppath, pnames, h})
} else { } else {
r.insert(method, path[:i], paramKind, routeMethod{}) r.insertNode(method, path[:i], paramKind, routeMethod{})
} }
} else if path[i] == '*' { } else if path[i] == '*' {
r.insert(method, path[:i], staticKind, routeMethod{}) r.insertNode(method, path[:i], staticKind, routeMethod{})
pnames = append(pnames, "*") pnames = append(pnames, "*")
r.insert(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h}) r.insertNode(method, path[:i+1], anyKind, routeMethod{ppath, pnames, h})
} }
} }
r.insert(method, path, staticKind, routeMethod{ppath, pnames, h}) r.insertNode(method, path, staticKind, routeMethod{ppath, pnames, h})
} }
func (r *Router) insert(method, path string, t kind, rm routeMethod) { func (r *Router) insertNode(method, path string, t kind, rm routeMethod) {
// Adjust max param // Adjust max param
paramLen := len(rm.pnames) paramLen := len(rm.pnames)
if *r.echo.maxParam < paramLen { if *r.echo.maxParam < paramLen {

View File

@ -1,48 +0,0 @@
package random
import (
"math/rand"
"strings"
"time"
)
type (
Random struct {
}
)
// Charsets
const (
Uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Lowercase = "abcdefghijklmnopqrstuvwxyz"
Alphabetic = Uppercase + Lowercase
Numeric = "0123456789"
Alphanumeric = Alphabetic + Numeric
Symbols = "`" + `~!@#$%^&*()-_+={}[]|\;:"<>,./?`
Hex = Numeric + "abcdef"
)
var (
global = New()
)
func New() *Random {
rand.Seed(time.Now().UnixNano())
return new(Random)
}
func (r *Random) String(length uint8, charsets ...string) string {
charset := strings.Join(charsets, "")
if charset == "" {
charset = Alphanumeric
}
b := make([]byte, length)
for i := range b {
b[i] = charset[rand.Int63()%int64(len(charset))]
}
return string(b)
}
func String(length uint8, charsets ...string) string {
return global.String(length, charsets...)
}

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2012-2023 Li Kexian Copyright 2012-2024 Li Kexian
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -200,6 +200,6 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
APPENDIX: Copyright 2012-2023 Li Kexian APPENDIX: Copyright 2012-2024 Li Kexian
https://www.likexian.com/ https://www.likexian.com/

View File

@ -85,7 +85,7 @@ b := assert.If(a == 1, true, false)
## License ## License
Copyright 2012-2023 [Li Kexian](https://www.likexian.com/) Copyright 2012-2024 [Li Kexian](https://www.likexian.com/)
Licensed under the Apache License 2.0 Licensed under the Apache License 2.0

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 Li Kexian * Copyright 2012-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 Li Kexian * Copyright 2012-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -43,7 +43,7 @@ if err != nil {
## License ## License
Copyright 2012-2023 [Li Kexian](https://www.likexian.com/) Copyright 2012-2024 [Li Kexian](https://www.likexian.com/)
Licensed under the Apache License 2.0 Licensed under the Apache License 2.0

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 Li Kexian * Copyright 2012-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -34,7 +34,7 @@ fmt.Println("new array:", array)
## License ## License
Copyright 2012-2023 [Li Kexian](https://www.likexian.com/) Copyright 2012-2024 [Li Kexian](https://www.likexian.com/)
Licensed under the Apache License 2.0 Licensed under the Apache License 2.0

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2023 Li Kexian * Copyright 2012-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2014-2023 Li Kexian Copyright 2014-2024 Li Kexian
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -200,6 +200,6 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
APPENDIX: Copyright 2014-2023 Li Kexian APPENDIX: Copyright 2014-2024 Li Kexian
https://www.likexian.com/ https://www.likexian.com/

View File

@ -71,7 +71,7 @@ Please refer to [whois](https://github.com/likexian/whois)
## License ## License
Copyright 2014-2023 [Li Kexian](https://www.likexian.com/) Copyright 2014-2024 [Li Kexian](https://www.likexian.com/)
Licensed under the Apache License 2.0 Licensed under the Apache License 2.0

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -66,6 +66,7 @@ func isNotFoundDomain(data string) bool {
"no match", "no match",
"not found", "not found",
"not match", "not match",
"not available",
"no data found", "no data found",
"nothing found", "nothing found",
"no entries found", "no entries found",

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -30,7 +30,7 @@ import (
// Version returns package version // Version returns package version
func Version() string { func Version() string {
return "1.24.8" return "1.24.18"
} }
// Author returns package author // Author returns package author
@ -153,6 +153,8 @@ func Parse(text string) (whoisInfo WhoisInfo, err error) { //nolint:cyclop
if !strings.Contains(name, " ") { if !strings.Contains(name, " ") {
if name == "registrar" { if name == "registrar" {
name += " name" name += " name"
} else if domain.Extension == "dk" {
name = "registrant " + name
} else { } else {
name += " organization" name += " organization"
} }
@ -244,9 +246,9 @@ func parseContact(contact *Contact, name, value string) {
} }
var searchDomainRx1 = regexp.MustCompile(`(?i)\[?domain\:?(\s*\_?name)?\]?[\s\.]*\:?` + var searchDomainRx1 = regexp.MustCompile(`(?i)\[?domain\:?(\s*\_?name)?\]?[\s\.]*\:?` +
`\s*([^\s\,\;\(\)]+)\.([^\s\,\;\(\)\.]{2,})`) `\s*([^\s\,\;\@\(\)]+)\.([^\s\,\;\(\)\.]{2,})`)
var searchDomainRx2 = regexp.MustCompile(`(?i)\[?domain\:?(\s*\_?name)?\]?[\s\.]*\:?` + var searchDomainRx2 = regexp.MustCompile(`(?i)\[?domain\:?(\s*\_?name)?\]?[\s\.]*\:?` +
`\s*([^\s\,\;\(\)\.]{2,})\n`) `\s*([^\s\,\;\@\(\)\.]{2,})\n`)
// searchDomain finds domain name and extension from whois information // searchDomain finds domain name and extension from whois information
func searchDomain(text string) (name, extension string) { func searchDomain(text string) (name, extension string) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -91,6 +91,8 @@ func Prepare(text, ext string) (string, bool) { //nolint:cyclop
return prepareBY(text), true return prepareBY(text), true
case "ua": case "ua":
return prepareUA(text), true return prepareUA(text), true
case "at":
return prepareAT(text), true
default: default:
return text, false return text, false
} }
@ -685,7 +687,7 @@ func prepareRU(text string) string {
return result return result
} }
var prepareJPreplacerRx = regexp.MustCompile(`\n\[(.+?)\][\ ]*(.+?)?`) var prepareJPreplacerRx = regexp.MustCompile(`\n(?:\w+\.\s)?\[(.+?)\][\ ]*(.+?)?`)
// prepareJP do prepare the .jp domain // prepareJP do prepare the .jp domain
func prepareJP(text string) string { func prepareJP(text string) string {
@ -712,6 +714,7 @@ func prepareJP(text string) string {
if strings.ToLower(token) == "registrant" { if strings.ToLower(token) == "registrant" {
v = fmt.Sprintf("registrant name: %s", vs[1]) v = fmt.Sprintf("registrant name: %s", vs[1])
} }
v = prepareSecondLevelJP(v, token, vs[1])
} else { } else {
if token == addressToken { if token == addressToken {
result += ", " + v result += ", " + v
@ -724,6 +727,29 @@ func prepareJP(text string) string {
return result return result
} }
// prepareJP prepares specific mappings for second level .jp domains
// examples include:
// - co.jp
// - ac.jp
// - go.jp
// - or.jp
// - ad.jp
// - ne.jp
// - gr.jp
// - ed.jp
func prepareSecondLevelJP(original string, token string, value string) string {
if strings.ToLower(token) == "administrative contact" {
return fmt.Sprintf("Administrative Contact ID: %s", strings.TrimSpace(value))
}
if strings.ToLower(token) == "technical contact" {
return fmt.Sprintf("Technical Contact ID: %s", strings.TrimSpace(value))
}
if strings.ToLower(token) == "organization" || strings.ToLower(token) == "network service name" {
return fmt.Sprintf("Registrant Organization: %s", strings.TrimSpace(value))
}
return original
}
// prepareUK do prepare the .uk domain // prepareUK do prepare the .uk domain
func prepareUK(text string) string { func prepareUK(text string) string {
tokens := map[string]string{ tokens := map[string]string{
@ -1335,3 +1361,78 @@ func prepareUA(text string) string {
return result return result
} }
// prepareAT prepares the .at domain
func prepareAT(text string) string {
result := ""
registrantID := ""
techID := ""
tokens := map[string]string{
"street address": "address",
"postal code": "address",
"city": "address",
"country": "address",
"e-mail": "email",
"nic-hdl": "id",
"personname": "name",
}
formatLine := func(line, token string) string {
before, after, _ := strings.Cut(line, ":")
key := strings.TrimSpace(before)
if t, ok := tokens[key]; ok {
key = t
}
val := strings.TrimSpace(after)
return fmt.Sprintf("%s %s: %s", token, key, val)
}
for _, v := range strings.Split(text, "\n\n") {
v = strings.TrimSpace(v)
if strings.HasPrefix(v, "%") {
continue
}
if strings.Contains(v, ":") {
b := strings.Split(v, "\n")
if strings.HasPrefix(b[0], "domain") {
for _, l := range b {
w := ""
if before, after, ok := strings.Cut(l, ":"); ok {
key := strings.TrimSpace(before)
val := strings.TrimSpace(after)
switch key {
case "domain":
w = fmt.Sprintf("%s: %s", "domain name", val)
case "registrant":
registrantID = val
case "tech-c":
techID = val
case "changed":
w = fmt.Sprintf("%s: %s", "updated_date", val)
case "nserver":
w = fmt.Sprintf("%s: %s", "name_servers", val)
default:
w = fmt.Sprintf("domain %s: %s", key, val)
}
if w != "" {
result += w + "\n"
}
}
}
} else if strings.HasPrefix(b[0], "personname") {
token := ""
if strings.Contains(v, registrantID) {
token = "registrant"
} else if strings.Contains(v, techID) {
token = "technical contact"
}
for _, l := range strings.Split(v, "\n") {
result += formatLine(l, token) + "\n"
}
}
}
}
return result
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -152,7 +152,9 @@ var (
"registrant contact state province": "registrant_state_province", "registrant contact state province": "registrant_state_province",
"registrant zipcode": "registrant_postal_code", "registrant zipcode": "registrant_postal_code",
"registrant zip code": "registrant_postal_code", "registrant zip code": "registrant_postal_code",
"registrant postalcode": "registrant_postal_code",
"registrant postal code": "registrant_postal_code", "registrant postal code": "registrant_postal_code",
"registrant contact postalcode": "registrant_postal_code",
"registrant contact postal code": "registrant_postal_code", "registrant contact postal code": "registrant_postal_code",
"registrant country": "registrant_country", "registrant country": "registrant_country",
"registrant country economy": "registrant_country", "registrant country economy": "registrant_country",

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -73,6 +73,9 @@ func fixDomainStatus(status []string) []string {
for k, v := range status { for k, v := range status {
names := strings.Split(strings.TrimSpace(v), " ") names := strings.Split(strings.TrimSpace(v), " ")
status[k] = strings.ToLower(names[0]) status[k] = strings.ToLower(names[0])
if status[k] == "not" && len(names) > 1 && strings.ToLower(names[1]) == "delegated" {
status[k] = "not delegated"
}
} }
return status return status
@ -115,16 +118,20 @@ func keys(m map[string]string) []string {
// parseDateString attempts to parse a given date using a collection of common // parseDateString attempts to parse a given date using a collection of common
// format strings. Date formats containing time components are tried first // format strings. Date formats containing time components are tried first
// before attempts are made using date-only formats. // before attempts are made using date-only formats.
func parseDateString(dateString string) (time.Time, error) { func parseDateString(datetime string) (time.Time, error) {
formats := [...]string{ datetime = strings.Trim(datetime, ".")
datetime = strings.ReplaceAll(datetime, ". ", "-")
formats := [...]string{
// Date & time formats // Date & time formats
"2006-01-02 15:04:05", "2006-01-02 15:04:05",
"2006.01.02 15:04:05",
"02/01/2006 15:04:05", "02/01/2006 15:04:05",
"02.01.2006 15:04:05", "02.01.2006 15:04:05",
"02.1.2006 15:04:05", "02.1.2006 15:04:05",
"2.1.2006 15:04:05", "2.1.2006 15:04:05",
"02-Jan-2006 15:04:05", "02-Jan-2006 15:04:05",
"20060102 15:04:05",
time.ANSIC, time.ANSIC,
time.Stamp, time.Stamp,
time.StampMilli, time.StampMilli,
@ -148,24 +155,25 @@ func parseDateString(dateString string) (time.Time, error) {
// Date only formats // Date only formats
"2006-01-02", "2006-01-02",
"2006. 01. 02.",
"02-Jan-2006", "02-Jan-2006",
"02.01.2006", "02.01.2006",
"02-01-2006", "02-01-2006",
"January _2 2006", "January _2 2006",
"Mon Jan _2 2006",
"02/01/2006", "02/01/2006",
"01/02/2006", "01/02/2006",
"2006/01/02",
"2006-Jan-02", "2006-Jan-02",
"2006-Jan-02.", "before Jan-2006",
} }
for _, format := range formats { for _, format := range formats {
result, err := time.Parse(format, dateString) result, err := time.Parse(format, datetime)
if err != nil { if err != nil {
continue continue
} }
return result, nil return result, nil
} }
return time.Now(), fmt.Errorf("could not parse %s as a date", dateString) return time.Now(), fmt.Errorf("could not parse %s as a date", datetime)
} }

View File

@ -1 +1,2 @@
.DS_Store .DS_Store
.idea

View File

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier same "printed page" as the copyright notice for easier
identification within third-party archives. identification within third-party archives.
Copyright 2014-2023 Li Kexian Copyright 2014-2024 Li Kexian
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -200,6 +200,6 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
APPENDIX: Copyright 2014-2023 Li Kexian APPENDIX: Copyright 2014-2024 Li Kexian
https://www.likexian.com/ https://www.likexian.com/

View File

@ -79,7 +79,7 @@ Please refer to [whois-parser](https://github.com/likexian/whois-parser)
## License ## License
Copyright 2014-2023 [Li Kexian](https://www.likexian.com/) Copyright 2014-2024 [Li Kexian](https://www.likexian.com/)
Licensed under the Apache License 2.0 Licensed under the Apache License 2.0

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2014-2023 Li Kexian * Copyright 2014-2024 Li Kexian
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -46,15 +46,16 @@ var DefaultClient = NewClient()
// Client is whois client // Client is whois client
type Client struct { type Client struct {
dialer proxy.Dialer dialer proxy.Dialer
timeout time.Duration timeout time.Duration
elapsed time.Duration elapsed time.Duration
disableStats bool disableStats bool
disableReferral bool
} }
// Version returns package version // Version returns package version
func Version() string { func Version() string {
return "1.15.0" return "1.15.4"
} }
// Author returns package author // Author returns package author
@ -100,6 +101,12 @@ func (c *Client) SetDisableStats(disabled bool) *Client {
return c return c
} }
// SetDisableReferral if set to true, will not query the referral server.
func (c *Client) SetDisableReferral(disabled bool) *Client {
c.disableReferral = disabled
return c
}
// Whois do the whois query and returns whois information // Whois do the whois query and returns whois information
func (c *Client) Whois(domain string, servers ...string) (result string, err error) { func (c *Client) Whois(domain string, servers ...string) (result string, err error) {
start := time.Now() start := time.Now()
@ -149,6 +156,10 @@ func (c *Client) Whois(domain string, servers ...string) (result string, err err
return return
} }
if c.disableReferral {
return
}
refServer, refPort := getServer(result) refServer, refPort := getServer(result)
if refServer == "" || refServer == server { if refServer == "" || refServer == server {
return return

View File

@ -1,6 +1,7 @@
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine //go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo
// +build darwin freebsd openbsd netbsd dragonfly hurd // +build darwin freebsd openbsd netbsd dragonfly hurd
// +build !appengine // +build !appengine
// +build !tinygo
package isatty package isatty

View File

@ -1,5 +1,6 @@
//go:build appengine || js || nacl || wasm //go:build (appengine || js || nacl || tinygo || wasm) && !windows
// +build appengine js nacl wasm // +build appengine js nacl tinygo wasm
// +build !windows
package isatty package isatty

View File

@ -1,6 +1,7 @@
//go:build (linux || aix || zos) && !appengine //go:build (linux || aix || zos) && !appengine && !tinygo
// +build linux aix zos // +build linux aix zos
// +build !appengine // +build !appengine
// +build !tinygo
package isatty package isatty

View File

@ -1,452 +1,192 @@
[run] [run]
deadline = "10m" # This is needed for precious, which may run multiple instances
tests = true # in parallel
allow-parallel-runners = true
go = "1.21"
tests = true
timeout = "10m"
[linters] [linters]
disable-all = true enable-all = true
enable = [ disable = [
"asasalint", "cyclop",
"asciicheck",
"bidichk",
"bodyclose",
"containedctx",
"contextcheck",
"depguard", "depguard",
"dupword", "err113",
"durationcheck", "execinquery",
"errcheck", "exhaustive",
"errchkjson", "exhaustruct",
"errname", "forcetypeassert",
"errorlint", "funlen",
# "exhaustive", "gochecknoglobals",
"exportloopref", "godox",
"forbidigo", "gomnd",
"goconst", "inamedparam",
"gocyclo", "interfacebloat",
"gocritic", "mnd",
"godot", "nlreturn",
"gofumpt", "nonamedreturns",
"gomodguard", "paralleltest",
"gosec", "thelper",
"gosimple", "testpackage",
"govet",
"grouper",
"ineffassign",
"lll",
"makezero",
"maintidx",
"misspell",
"nakedret",
"nilerr",
"noctx",
"nolintlint",
"nosprintfhostport",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"stylecheck",
"tenv",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"usestdlibvars",
"vetshadow",
"wastedassign",
]
[[linters-settings.depguard.rules.main.deny]] "varnamelen",
pkg = "io/ioutil" "wrapcheck",
desc = "Deprecated. Functions have been moved elsewhere." "wsl",
[linters-settings.errcheck] # Require Go 1.22
check-blank = true "copyloopvar",
# Ignoring Close so that we don't have to have a bunch of "intrange",
# `defer func() { _ = r.Close() }()` constructs when we ]
# don't actually care about the error.
ignore = "Close,fmt:.*"
[linters-settings.errorlint] [linters-settings.errorlint]
errorf = true errorf = true
asserts = true asserts = true
comparison = true comparison = true
[linters-settings.exhaustive] [linters-settings.exhaustive]
default-signifies-exhaustive = true default-signifies-exhaustive = true
[linters-settings.forbidigo] [linters-settings.forbidigo]
# Forbid the following identifiers # Forbid the following identifiers
forbid = [ forbid = [
"Geoip", # use "GeoIP" { p = "Geoip", msg = "you should use `GeoIP`" },
"^geoIP", # use "geoip" { p = "geoIP", msg = "you should use `geoip`" },
"Maxmind", # use "MaxMind" { p = "Maxmind", msg = "you should use `MaxMind`" },
"^maxMind", # use "maxmind" { p = "^maxMind", msg = "you should use `maxmind`" },
] { p = "Minfraud", msg = "you should use `MinFraud`" },
{ p = "^minFraud", msg = "you should use `minfraud`" },
{ p = "^math.Max$", msg = "you should use the max built-in instead." },
{ p = "^math.Min$", msg = "you should use the min built-in instead." },
{ p = "^os.IsNotExist", msg = "As per their docs, new code should use errors.Is(err, fs.ErrNotExist)." },
{ p = "^os.IsExist", msg = "As per their docs, new code should use errors.Is(err, fs.ErrExist)" },
]
[linters-settings.gocritic] [linters-settings.gci]
enabled-checks = [ sections = ["standard", "default", "prefix(github.com/oschwald/maxminddb-golang)"]
"appendAssign",
"appendCombine",
"argOrder",
"assignOp",
"badCall",
"badCond",
"badLock",
"badRegexp",
"badSorting",
"boolExprSimplify",
"builtinShadow",
"builtinShadowDecl",
"captLocal",
"caseOrder",
"codegenComment",
"commentedOutCode",
"commentedOutImport",
"commentFormatting",
"defaultCaseOrder",
"deferInLoop",
"deferUnlambda",
"deprecatedComment",
"docStub",
"dupArg",
"dupBranchBody",
"dupCase",
"dupImport",
"dupSubExpr",
"dynamicFmtString",
"elseif",
"emptyDecl",
"emptyFallthrough",
"emptyStringTest",
"equalFold",
"evalOrder",
"exitAfterDefer",
"exposedSyncMutex",
"externalErrorReassign",
"filepathJoin",
"flagDeref",
"flagName",
"hexLiteral",
"httpNoBody",
"hugeParam",
"ifElseChain",
"importShadow",
"indexAlloc",
"initClause",
"mapKey",
"methodExprCall",
"nestingReduce",
"newDeref",
"nilValReturn",
"octalLiteral",
"offBy1",
"paramTypeCombine",
"preferDecodeRune",
"preferFilepathJoin",
"preferFprint",
"preferStringWriter",
"preferWriteByte",
"ptrToRefParam",
"rangeExprCopy",
"rangeValCopy",
"redundantSprint",
"regexpMust",
"regexpPattern",
"regexpSimplify",
"returnAfterHttpError",
"ruleguard",
"singleCaseSwitch",
"sliceClear",
"sloppyLen",
"sloppyReassign",
"sloppyTestFuncName",
"sloppyTypeAssert",
"sortSlice",
"sprintfQuotedString",
"sqlQuery",
"stringsCompare",
"stringConcatSimplify",
"stringXbytes",
"switchTrue",
"syncMapLoadAndDelete",
"timeExprSimplify",
"todoCommentWithoutDetail",
"tooManyResultsChecker",
"truncateCmp",
"typeAssertChain",
"typeDefFirst",
"typeSwitchVar",
"typeUnparen",
"underef",
"unlabelStmt",
"unlambda",
# "unnamedResult",
"unnecessaryBlock",
"unnecessaryDefer",
"unslice",
"valSwap",
"weakCond",
# Covered by nolintlint
# "whyNoLint"
"wrapperFunc",
"yodaStyleExpr",
]
[linters-settings.gofumpt] [linters-settings.gofumpt]
extra-rules = true extra-rules = true
lang-version = "1.19"
[linters-settings.gosec]
excludes = [
# G104 - "Audit errors not checked." We use errcheck for this.
"G104",
# G304 - "Potential file inclusion via variable"
"G304",
# G306 - "Expect WriteFile permissions to be 0600 or less".
"G306",
# Prohibits defer (*os.File).Close, which we allow when reading from file.
"G307",
]
[linters-settings.govet] [linters-settings.govet]
"enable-all" = true enable-all = true
disable = ["shadow"] disable = "shadow"
[linters-settings.lll] [linters-settings.lll]
line-length = 120 line-length = 120
tab-width = 4 tab-width = 4
[linters-settings.misspell]
locale = "US"
[[linters-settings.misspell.extra-words]]
typo = "marshall"
correction = "marshal"
[[linters-settings.misspell.extra-words]]
typo = "marshalling"
correction = "marshaling"
[[linters-settings.misspell.extra-words]]
typo = "marshalls"
correction = "marshals"
[[linters-settings.misspell.extra-words]]
typo = "unmarshall"
correction = "unmarshal"
[[linters-settings.misspell.extra-words]]
typo = "unmarshalling"
correction = "unmarshaling"
[[linters-settings.misspell.extra-words]]
typo = "unmarshalls"
correction = "unmarshals"
[linters-settings.nolintlint] [linters-settings.nolintlint]
allow-leading-space = false allow-unused = false
allow-unused = false allow-no-explanation = ["lll", "misspell"]
allow-no-explanation = ["lll", "misspell"] require-explanation = true
require-explanation = true require-specific = true
require-specific = true
[linters-settings.revive] [linters-settings.revive]
ignore-generated-header = true enable-all-rules = true
severity = "warning" ignore-generated-header = true
severity = "warning"
# [[linters-settings.revive.rules]]
# name = "add-constant" [[linters-settings.revive.rules]]
name = "add-constant"
# [[linters-settings.revive.rules]] disabled = true
# name = "argument-limit"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "cognitive-complexity"
name = "atomic" disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "bare-return" name = "confusing-naming"
disabled = true
[[linters-settings.revive.rules]]
name = "blank-imports" [[linters-settings.revive.rules]]
name = "confusing-results"
[[linters-settings.revive.rules]] disabled = true
name = "bool-literal-in-expr"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "cyclomatic"
name = "call-to-gc" disabled = true
# [[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
# name = "cognitive-complexity" name = "deep-exit"
disabled = true
[[linters-settings.revive.rules]]
name = "comment-spacings" [[linters-settings.revive.rules]]
arguments = ["easyjson", "nolint"] name = "flag-parameter"
disabled = true
# [[linters-settings.revive.rules]]
# name = "confusing-naming" [[linters-settings.revive.rules]]
name = "function-length"
# [[linters-settings.revive.rules]] disabled = true
# name = "confusing-results"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "function-result-limit"
name = "constant-logical-expr" disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "context-as-argument" name = "line-length-limit"
disabled = true
[[linters-settings.revive.rules]]
name = "context-keys-type" [[linters-settings.revive.rules]]
name = "max-public-structs"
# [[linters-settings.revive.rules]] disabled = true
# name = "cyclomatic"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "nested-structs"
name = "datarace" disabled = true
# [[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
# name = "deep-exit" name = "unchecked-type-assertion"
disabled = true
[[linters-settings.revive.rules]]
name = "defer" [[linters-settings.revive.rules]]
name = "unhandled-error"
[[linters-settings.revive.rules]] disabled = true
name = "dot-imports"
[linters-settings.tagliatelle.case.rules]
[[linters-settings.revive.rules]] avro = "snake"
name = "duplicated-imports" bson = "snake"
env = "upperSnake"
[[linters-settings.revive.rules]] envconfig = "upperSnake"
name = "early-return" json = "snake"
mapstructure = "snake"
[[linters-settings.revive.rules]] xml = "snake"
name = "empty-block" yaml = "snake"
[[linters-settings.revive.rules]]
name = "empty-lines"
[[linters-settings.revive.rules]]
name = "errorf"
[[linters-settings.revive.rules]]
name = "error-naming"
[[linters-settings.revive.rules]]
name = "error-return"
[[linters-settings.revive.rules]]
name = "error-strings"
[[linters-settings.revive.rules]]
name = "exported"
# [[linters-settings.revive.rules]]
# name = "file-header"
# [[linters-settings.revive.rules]]
# name = "flag-parameter"
# [[linters-settings.revive.rules]]
# name = "function-result-limit"
[[linters-settings.revive.rules]]
name = "get-return"
[[linters-settings.revive.rules]]
name = "identical-branches"
[[linters-settings.revive.rules]]
name = "if-return"
[[linters-settings.revive.rules]]
name = "imports-blacklist"
[[linters-settings.revive.rules]]
name = "import-shadowing"
[[linters-settings.revive.rules]]
name = "increment-decrement"
[[linters-settings.revive.rules]]
name = "indent-error-flow"
# [[linters-settings.revive.rules]]
# name = "line-length-limit"
# [[linters-settings.revive.rules]]
# name = "max-public-structs"
[[linters-settings.revive.rules]]
name = "modifies-parameter"
[[linters-settings.revive.rules]]
name = "modifies-value-receiver"
# [[linters-settings.revive.rules]]
# name = "nested-structs"
[[linters-settings.revive.rules]]
name = "optimize-operands-order"
[[linters-settings.revive.rules]]
name = "package-comments"
[[linters-settings.revive.rules]]
name = "range"
[[linters-settings.revive.rules]]
name = "range-val-address"
[[linters-settings.revive.rules]]
name = "range-val-in-closure"
[[linters-settings.revive.rules]]
name = "receiver-naming"
[[linters-settings.revive.rules]]
name = "redefines-builtin-id"
[[linters-settings.revive.rules]]
name = "string-of-int"
[[linters-settings.revive.rules]]
name = "struct-tag"
[[linters-settings.revive.rules]]
name = "superfluous-else"
[[linters-settings.revive.rules]]
name = "time-equal"
[[linters-settings.revive.rules]]
name = "time-naming"
[[linters-settings.revive.rules]]
name = "unconditional-recursion"
[[linters-settings.revive.rules]]
name = "unexported-naming"
[[linters-settings.revive.rules]]
name = "unexported-return"
# [[linters-settings.revive.rules]]
# name = "unhandled-error"
[[linters-settings.revive.rules]]
name = "unnecessary-stmt"
[[linters-settings.revive.rules]]
name = "unreachable-code"
[[linters-settings.revive.rules]]
name = "unused-parameter"
[[linters-settings.revive.rules]]
name = "unused-receiver"
[[linters-settings.revive.rules]]
name = "use-any"
[[linters-settings.revive.rules]]
name = "useless-break"
[[linters-settings.revive.rules]]
name = "var-declaration"
[[linters-settings.revive.rules]]
name = "var-naming"
[[linters-settings.revive.rules]]
name = "waitgroup-by-value"
[linters-settings.unparam] [linters-settings.unparam]
check-exported = true check-exported = true
[issues]
exclude-use-default = false
[[issues.exclude-rules]] [[issues.exclude-rules]]
linters = [ linters = [
"govet" "govet",
] "revive",
path = "_test.go" ]
text = "^fieldalignment" path = "_test.go"
text = "fieldalignment:"

View File

@ -70,6 +70,7 @@ type Enterprise struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"` AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
StaticIPScore float64 `maxminddb:"static_ip_score"` StaticIPScore float64 `maxminddb:"static_ip_score"`
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"` IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
IsAnycast bool `maxminddb:"is_anycast"`
IsLegitimateProxy bool `maxminddb:"is_legitimate_proxy"` IsLegitimateProxy bool `maxminddb:"is_legitimate_proxy"`
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"` IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
} `maxminddb:"traits"` } `maxminddb:"traits"`
@ -130,6 +131,7 @@ type City struct {
} `maxminddb:"location"` } `maxminddb:"location"`
Traits struct { Traits struct {
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"` IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
IsAnycast bool `maxminddb:"is_anycast"`
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"` IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
} `maxminddb:"traits"` } `maxminddb:"traits"`
} }
@ -163,6 +165,7 @@ type Country struct {
} `maxminddb:"represented_country"` } `maxminddb:"represented_country"`
Traits struct { Traits struct {
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"` IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
IsAnycast bool `maxminddb:"is_anycast"`
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"` IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
} `maxminddb:"traits"` } `maxminddb:"traits"`
} }

View File

@ -1,452 +1,192 @@
[run] [run]
deadline = "10m" # This is needed for precious, which may run multiple instances
tests = true # in parallel
allow-parallel-runners = true
go = "1.21"
tests = true
timeout = "10m"
[linters] [linters]
disable-all = true enable-all = true
enable = [ disable = [
"asasalint", "cyclop",
"asciicheck",
"bidichk",
"bodyclose",
"containedctx",
"contextcheck",
"depguard", "depguard",
"dupword", "err113",
"durationcheck", "execinquery",
"errcheck", "exhaustive",
"errchkjson", "exhaustruct",
"errname", "forcetypeassert",
"errorlint", "funlen",
# "exhaustive", "gochecknoglobals",
"exportloopref", "godox",
"forbidigo", "gomnd",
"goconst", "inamedparam",
"gocyclo", "interfacebloat",
"gocritic", "mnd",
"godot", "nlreturn",
"gofumpt", "nonamedreturns",
"gomodguard", "paralleltest",
"gosec", "thelper",
"gosimple", "testpackage",
"govet",
"grouper",
"ineffassign",
"lll",
"makezero",
"maintidx",
"misspell",
"nakedret",
"nilerr",
"noctx",
"nolintlint",
"nosprintfhostport",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"stylecheck",
"tenv",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"usestdlibvars",
"vetshadow",
"wastedassign",
]
[[linters-settings.depguard.rules.main.deny]] "varnamelen",
pkg = "io/ioutil" "wrapcheck",
desc = "Deprecated. Functions have been moved elsewhere." "wsl",
[linters-settings.errcheck] # Require Go 1.22
check-blank = true "copyloopvar",
# Ignoring Close so that we don't have to have a bunch of "intrange",
# `defer func() { _ = r.Close() }()` constructs when we ]
# don't actually care about the error.
ignore = "Close,fmt:.*"
[linters-settings.errorlint] [linters-settings.errorlint]
errorf = true errorf = true
asserts = true asserts = true
comparison = true comparison = true
[linters-settings.exhaustive] [linters-settings.exhaustive]
default-signifies-exhaustive = true default-signifies-exhaustive = true
[linters-settings.forbidigo] [linters-settings.forbidigo]
# Forbid the following identifiers # Forbid the following identifiers
forbid = [ forbid = [
"Geoip", # use "GeoIP" { p = "Geoip", msg = "you should use `GeoIP`" },
"^geoIP", # use "geoip" { p = "geoIP", msg = "you should use `geoip`" },
"Maxmind", # use "MaxMind" { p = "Maxmind", msg = "you should use `MaxMind`" },
"^maxMind", # use "maxmind" { p = "^maxMind", msg = "you should use `maxmind`" },
] { p = "Minfraud", msg = "you should use `MinFraud`" },
{ p = "^minFraud", msg = "you should use `minfraud`" },
{ p = "^math.Max$", msg = "you should use the max built-in instead." },
{ p = "^math.Min$", msg = "you should use the min built-in instead." },
{ p = "^os.IsNotExist", msg = "As per their docs, new code should use errors.Is(err, fs.ErrNotExist)." },
{ p = "^os.IsExist", msg = "As per their docs, new code should use errors.Is(err, fs.ErrExist)" },
]
[linters-settings.gocritic] [linters-settings.gci]
enabled-checks = [ sections = ["standard", "default", "prefix(github.com/oschwald/maxminddb-golang)"]
"appendAssign",
"appendCombine",
"argOrder",
"assignOp",
"badCall",
"badCond",
"badLock",
"badRegexp",
"badSorting",
"boolExprSimplify",
"builtinShadow",
"builtinShadowDecl",
"captLocal",
"caseOrder",
"codegenComment",
"commentedOutCode",
"commentedOutImport",
"commentFormatting",
"defaultCaseOrder",
"deferInLoop",
"deferUnlambda",
"deprecatedComment",
"docStub",
"dupArg",
"dupBranchBody",
"dupCase",
"dupImport",
"dupSubExpr",
"dynamicFmtString",
"elseif",
"emptyDecl",
"emptyFallthrough",
"emptyStringTest",
"equalFold",
"evalOrder",
"exitAfterDefer",
"exposedSyncMutex",
"externalErrorReassign",
"filepathJoin",
"flagDeref",
"flagName",
"hexLiteral",
"httpNoBody",
"hugeParam",
"ifElseChain",
"importShadow",
"indexAlloc",
"initClause",
"mapKey",
"methodExprCall",
"nestingReduce",
"newDeref",
"nilValReturn",
"octalLiteral",
"offBy1",
"paramTypeCombine",
"preferDecodeRune",
"preferFilepathJoin",
"preferFprint",
"preferStringWriter",
"preferWriteByte",
"ptrToRefParam",
"rangeExprCopy",
"rangeValCopy",
"redundantSprint",
"regexpMust",
"regexpPattern",
"regexpSimplify",
"returnAfterHttpError",
"ruleguard",
"singleCaseSwitch",
"sliceClear",
"sloppyLen",
"sloppyReassign",
"sloppyTestFuncName",
"sloppyTypeAssert",
"sortSlice",
"sprintfQuotedString",
"sqlQuery",
"stringsCompare",
"stringConcatSimplify",
"stringXbytes",
"switchTrue",
"syncMapLoadAndDelete",
"timeExprSimplify",
"todoCommentWithoutDetail",
"tooManyResultsChecker",
"truncateCmp",
"typeAssertChain",
"typeDefFirst",
"typeSwitchVar",
"typeUnparen",
"underef",
"unlabelStmt",
"unlambda",
# "unnamedResult",
"unnecessaryBlock",
"unnecessaryDefer",
"unslice",
"valSwap",
"weakCond",
# Covered by nolintlint
# "whyNoLint"
"wrapperFunc",
"yodaStyleExpr",
]
[linters-settings.gofumpt] [linters-settings.gofumpt]
extra-rules = true extra-rules = true
lang-version = "1.19"
[linters-settings.gosec]
excludes = [
# G104 - "Audit errors not checked." We use errcheck for this.
"G104",
# G304 - "Potential file inclusion via variable"
"G304",
# G306 - "Expect WriteFile permissions to be 0600 or less".
"G306",
# Prohibits defer (*os.File).Close, which we allow when reading from file.
"G307",
]
[linters-settings.govet] [linters-settings.govet]
"enable-all" = true enable-all = true
disable = ["shadow"] disable = "shadow"
[linters-settings.lll] [linters-settings.lll]
line-length = 120 line-length = 120
tab-width = 4 tab-width = 4
[linters-settings.misspell]
locale = "US"
[[linters-settings.misspell.extra-words]]
typo = "marshall"
correction = "marshal"
[[linters-settings.misspell.extra-words]]
typo = "marshalling"
correction = "marshaling"
[[linters-settings.misspell.extra-words]]
typo = "marshalls"
correction = "marshals"
[[linters-settings.misspell.extra-words]]
typo = "unmarshall"
correction = "unmarshal"
[[linters-settings.misspell.extra-words]]
typo = "unmarshalling"
correction = "unmarshaling"
[[linters-settings.misspell.extra-words]]
typo = "unmarshalls"
correction = "unmarshals"
[linters-settings.nolintlint] [linters-settings.nolintlint]
allow-leading-space = false allow-unused = false
allow-unused = false allow-no-explanation = ["lll", "misspell"]
allow-no-explanation = ["lll", "misspell"] require-explanation = true
require-explanation = true require-specific = true
require-specific = true
[linters-settings.revive] [linters-settings.revive]
ignore-generated-header = true enable-all-rules = true
severity = "warning" ignore-generated-header = true
severity = "warning"
# [[linters-settings.revive.rules]]
# name = "add-constant" [[linters-settings.revive.rules]]
name = "add-constant"
# [[linters-settings.revive.rules]] disabled = true
# name = "argument-limit"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "cognitive-complexity"
name = "atomic" disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "bare-return" name = "confusing-naming"
disabled = true
[[linters-settings.revive.rules]]
name = "blank-imports" [[linters-settings.revive.rules]]
name = "confusing-results"
[[linters-settings.revive.rules]] disabled = true
name = "bool-literal-in-expr"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "cyclomatic"
name = "call-to-gc" disabled = true
# [[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
# name = "cognitive-complexity" name = "deep-exit"
disabled = true
[[linters-settings.revive.rules]]
name = "comment-spacings" [[linters-settings.revive.rules]]
arguments = ["easyjson", "nolint"] name = "flag-parameter"
disabled = true
# [[linters-settings.revive.rules]]
# name = "confusing-naming" [[linters-settings.revive.rules]]
name = "function-length"
# [[linters-settings.revive.rules]] disabled = true
# name = "confusing-results"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "function-result-limit"
name = "constant-logical-expr" disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "context-as-argument" name = "line-length-limit"
disabled = true
[[linters-settings.revive.rules]]
name = "context-keys-type" [[linters-settings.revive.rules]]
name = "max-public-structs"
# [[linters-settings.revive.rules]] disabled = true
# name = "cyclomatic"
[[linters-settings.revive.rules]]
[[linters-settings.revive.rules]] name = "nested-structs"
name = "datarace" disabled = true
# [[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
# name = "deep-exit" name = "unchecked-type-assertion"
disabled = true
[[linters-settings.revive.rules]]
name = "defer" [[linters-settings.revive.rules]]
name = "unhandled-error"
[[linters-settings.revive.rules]] disabled = true
name = "dot-imports"
[linters-settings.tagliatelle.case.rules]
[[linters-settings.revive.rules]] avro = "snake"
name = "duplicated-imports" bson = "snake"
env = "upperSnake"
[[linters-settings.revive.rules]] envconfig = "upperSnake"
name = "early-return" json = "snake"
mapstructure = "snake"
[[linters-settings.revive.rules]] xml = "snake"
name = "empty-block" yaml = "snake"
[[linters-settings.revive.rules]]
name = "empty-lines"
[[linters-settings.revive.rules]]
name = "errorf"
[[linters-settings.revive.rules]]
name = "error-naming"
[[linters-settings.revive.rules]]
name = "error-return"
[[linters-settings.revive.rules]]
name = "error-strings"
[[linters-settings.revive.rules]]
name = "exported"
# [[linters-settings.revive.rules]]
# name = "file-header"
# [[linters-settings.revive.rules]]
# name = "flag-parameter"
# [[linters-settings.revive.rules]]
# name = "function-result-limit"
[[linters-settings.revive.rules]]
name = "get-return"
[[linters-settings.revive.rules]]
name = "identical-branches"
[[linters-settings.revive.rules]]
name = "if-return"
[[linters-settings.revive.rules]]
name = "imports-blacklist"
[[linters-settings.revive.rules]]
name = "import-shadowing"
[[linters-settings.revive.rules]]
name = "increment-decrement"
[[linters-settings.revive.rules]]
name = "indent-error-flow"
# [[linters-settings.revive.rules]]
# name = "line-length-limit"
# [[linters-settings.revive.rules]]
# name = "max-public-structs"
[[linters-settings.revive.rules]]
name = "modifies-parameter"
[[linters-settings.revive.rules]]
name = "modifies-value-receiver"
# [[linters-settings.revive.rules]]
# name = "nested-structs"
[[linters-settings.revive.rules]]
name = "optimize-operands-order"
[[linters-settings.revive.rules]]
name = "package-comments"
[[linters-settings.revive.rules]]
name = "range"
[[linters-settings.revive.rules]]
name = "range-val-address"
[[linters-settings.revive.rules]]
name = "range-val-in-closure"
[[linters-settings.revive.rules]]
name = "receiver-naming"
[[linters-settings.revive.rules]]
name = "redefines-builtin-id"
[[linters-settings.revive.rules]]
name = "string-of-int"
[[linters-settings.revive.rules]]
name = "struct-tag"
[[linters-settings.revive.rules]]
name = "superfluous-else"
[[linters-settings.revive.rules]]
name = "time-equal"
[[linters-settings.revive.rules]]
name = "time-naming"
[[linters-settings.revive.rules]]
name = "unconditional-recursion"
[[linters-settings.revive.rules]]
name = "unexported-naming"
[[linters-settings.revive.rules]]
name = "unexported-return"
# [[linters-settings.revive.rules]]
# name = "unhandled-error"
[[linters-settings.revive.rules]]
name = "unnecessary-stmt"
[[linters-settings.revive.rules]]
name = "unreachable-code"
[[linters-settings.revive.rules]]
name = "unused-parameter"
[[linters-settings.revive.rules]]
name = "unused-receiver"
[[linters-settings.revive.rules]]
name = "use-any"
[[linters-settings.revive.rules]]
name = "useless-break"
[[linters-settings.revive.rules]]
name = "var-declaration"
[[linters-settings.revive.rules]]
name = "var-naming"
[[linters-settings.revive.rules]]
name = "waitgroup-by-value"
[linters-settings.unparam] [linters-settings.unparam]
check-exported = true check-exported = true
[issues]
exclude-use-default = false
[[issues.exclude-rules]] [[issues.exclude-rules]]
linters = [ linters = [
"govet" "govet",
] "revive",
path = "_test.go" ]
text = "^fieldalignment" path = "_test.go"
text = "fieldalignment:"

View File

@ -2,6 +2,7 @@ package maxminddb
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"math" "math"
"math/big" "math/big"
"reflect" "reflect"
@ -418,7 +419,7 @@ func (d *decoder) unmarshalMap(
result = indirect(result) result = indirect(result)
switch result.Kind() { switch result.Kind() {
default: default:
return 0, newUnmarshalTypeError("map", result.Type()) return 0, newUnmarshalTypeStrError("map", result.Type())
case reflect.Struct: case reflect.Struct:
return d.decodeStruct(size, offset, result, depth) return d.decodeStruct(size, offset, result, depth)
case reflect.Map: case reflect.Map:
@ -430,7 +431,7 @@ func (d *decoder) unmarshalMap(
result.Set(rv) result.Set(rv)
return newOffset, err return newOffset, err
} }
return 0, newUnmarshalTypeError("map", result.Type()) return 0, newUnmarshalTypeStrError("map", result.Type())
} }
} }
@ -465,7 +466,7 @@ func (d *decoder) unmarshalSlice(
return newOffset, err return newOffset, err
} }
} }
return 0, newUnmarshalTypeError("array", result.Type()) return 0, newUnmarshalTypeStrError("array", result.Type())
} }
func (d *decoder) unmarshalString(size, offset uint, result reflect.Value) (uint, error) { func (d *decoder) unmarshalString(size, offset uint, result reflect.Value) (uint, error) {
@ -596,24 +597,26 @@ func (d *decoder) decodeMap(
mapType := result.Type() mapType := result.Type()
keyValue := reflect.New(mapType.Key()).Elem() keyValue := reflect.New(mapType.Key()).Elem()
elemType := mapType.Elem() elemType := mapType.Elem()
elemKind := elemType.Kind()
var elemValue reflect.Value var elemValue reflect.Value
for i := uint(0); i < size; i++ { for i := uint(0); i < size; i++ {
var key []byte var key []byte
var err error var err error
key, offset, err = d.decodeKey(offset) key, offset, err = d.decodeKey(offset)
if err != nil { if err != nil {
return 0, err return 0, err
} }
if !elemValue.IsValid() || elemKind == reflect.Interface { if elemValue.IsValid() {
// After 1.20 is the minimum supported version, this can just be
// elemValue.SetZero()
reflectSetZero(elemValue)
} else {
elemValue = reflect.New(elemType).Elem() elemValue = reflect.New(elemType).Elem()
} }
offset, err = d.decode(offset, elemValue, depth) offset, err = d.decode(offset, elemValue, depth)
if err != nil { if err != nil {
return 0, err return 0, fmt.Errorf("decoding value for %s: %w", key, err)
} }
keyValue.SetString(string(key)) keyValue.SetString(string(key))
@ -770,7 +773,7 @@ func (d *decoder) decodeStruct(
offset, err = d.decode(offset, result.Field(j), depth) offset, err = d.decode(offset, result.Field(j), depth)
if err != nil { if err != nil {
return 0, err return 0, fmt.Errorf("decoding value for %s: %w", key, err)
} }
} }
return offset, nil return offset, nil

View File

@ -30,13 +30,17 @@ type UnmarshalTypeError struct {
Value string Value string
} }
func newUnmarshalTypeError(value any, rType reflect.Type) UnmarshalTypeError { func newUnmarshalTypeStrError(value string, rType reflect.Type) UnmarshalTypeError {
return UnmarshalTypeError{ return UnmarshalTypeError{
Value: fmt.Sprintf("%v", value),
Type: rType, Type: rType,
Value: value,
} }
} }
func (e UnmarshalTypeError) Error() string { func newUnmarshalTypeError(value any, rType reflect.Type) UnmarshalTypeError {
return fmt.Sprintf("maxminddb: cannot unmarshal %s into type %s", e.Value, e.Type.String()) return newUnmarshalTypeStrError(fmt.Sprintf("%v (%T)", value, value), rType)
}
func (e UnmarshalTypeError) Error() string {
return fmt.Sprintf("maxminddb: cannot unmarshal %s into type %s", e.Value, e.Type)
} }

View File

@ -1,5 +1,5 @@
//go:build !windows && !appengine && !plan9 && !js && !wasip1 //go:build !windows && !appengine && !plan9 && !js && !wasip1 && !wasi
// +build !windows,!appengine,!plan9,!js,!wasip1 // +build !windows,!appengine,!plan9,!js,!wasip1,!wasi
package maxminddb package maxminddb

View File

@ -65,8 +65,8 @@ func FromBytes(buffer []byte) (*Reader, error) {
var metadata Metadata var metadata Metadata
rvMetdata := reflect.ValueOf(&metadata) rvMetadata := reflect.ValueOf(&metadata)
_, err := metadataDecoder.decode(0, rvMetdata, 0) _, err := metadataDecoder.decode(0, rvMetadata, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,5 +1,5 @@
//go:build appengine || plan9 || js || wasip1 //go:build appengine || plan9 || js || wasip1 || wasi
// +build appengine plan9 js wasip1 // +build appengine plan9 js wasip1 wasi
package maxminddb package maxminddb

View File

@ -1,5 +1,5 @@
//go:build !appengine && !plan9 && !js && !wasip1 //go:build !appengine && !plan9 && !js && !wasip1 && !wasi
// +build !appengine,!plan9,!js,!wasip1 // +build !appengine,!plan9,!js,!wasip1,!wasi
package maxminddb package maxminddb

View File

@ -0,0 +1,10 @@
//go:build go1.20
// +build go1.20
package maxminddb
import "reflect"
func reflectSetZero(v reflect.Value) {
v.SetZero()
}

Some files were not shown because too many files have changed in this diff Show More