Compare commits

..

20 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
f79bc57d83 fixed bug in js stuff
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-07-17 21:12:44 +02:00
9b2e68ae0a fix: country in js file, docker build script
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-15 16:56:11 +02:00
9e2662d1dd updated dependencies
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-07-13 22:34:10 +02:00
2daa8927d1 removed axios / vuejs stuff
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-13 22:25:21 +02:00
6c77212eba updated README.md
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-12 22:25:34 +02:00
eb7ea55016 updated Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2023-07-08 09:48:04 +02:00
5ef001a877 updated dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2023-05-17 13:27:04 +02:00
489 changed files with 34465 additions and 12224 deletions

View File

@ -1,4 +1,4 @@
FROM debian:bullseye-slim FROM debian:bookworm-slim
ADD dip /dip ADD dip /dip

View File

@ -1,7 +1,7 @@
# dip # dip
[![Build Status](https://drone.paulbsd.com/api/badges/paulbsd/dip/status.svg)](https://drone.paulbsd.com/paulbsd/dip) [![Build Status](https://drone.paulbsd.com/api/badges/paulbsd/dip/status.svg)](https://drone.paulbsd.com/paulbsd/dip)
dip is a small webservice designed to return public ip address dip is a small webservice retrieving the IP addressing information
## Howto ## Howto
@ -30,7 +30,7 @@ curl -H "Accept: text/html" http://localhost:8080/
## License ## License
```text ```text
Copyright (c) 2019, 2020, 2021, 2022 PaulBSD Copyright (c) 2020, 2021, 2022, 2023 PaulBSD
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without

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.7.0 // indirect github.com/likexian/whois v1.15.4
golang.org/x/sys v0.6.0 // indirect github.com/likexian/whois-parser v1.24.19
golang.org/x/text v0.8.0 // indirect github.com/oschwald/geoip2-golang v1.11.0
)
require (
github.com/likexian/whois v1.14.6
github.com/likexian/whois-parser v1.24.7
github.com/oschwald/geoip2-golang v1.8.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.11 // indirect github.com/likexian/gokit v0.25.15 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/oschwald/maxminddb-golang v1.10.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.8.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
) )

111
go.sum
View File

@ -1,82 +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/whois v1.14.4 h1:yNQVugAHfeoylzyhitcs0vaPksDduvzW46HLjqXhpCg= github.com/likexian/whois-parser v1.24.18 h1:Xolieo/uwjNwhmQN/oDDNlwFajHipdHedyPBgzG44kw=
github.com/likexian/whois v1.14.4/go.mod h1:ijMqjPbtafr5TB49U3Hs3BsJx/93ixhRi/8YnCSK0iY= github.com/likexian/whois-parser v1.24.18/go.mod h1:k5zmKRZ7xPg1TLv3BGT4g/LOPRIMhvdNMeB0F53V/jk=
github.com/likexian/whois v1.14.6 h1:chNF6xkUShMQHq+5tiovVOQFadb9hqrWcaqbtlvbe3o= github.com/likexian/whois-parser v1.24.19 h1:vT8lWhnV8ogkdaYLyef6IvE5VTHVCwlUDG5BUXCx06k=
github.com/likexian/whois v1.14.6/go.mod h1:upfX0X3o78fvHyFY2zHv46zooPoydUV1msyovvLZTMU= github.com/likexian/whois-parser v1.24.19/go.mod h1:rAtaofg2luol09H+ogDzGIfcG8ig1NtM5R16uQADDz4=
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/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/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs= github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw= github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
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/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/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
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/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/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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

@ -4,3 +4,4 @@
docker build -t dip:latest . docker build -t dip:latest .
docker tag dip:latest registry.paulbsd.com/images/dip:latest docker tag dip:latest registry.paulbsd.com/images/dip:latest
docker push registry.paulbsd.com/images/dip:latest docker push registry.paulbsd.com/images/dip:latest
kubectl rollout restart deployment dip

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
var mc *memcache.Client
if MemcacheConn != "" {
mc = memcache.New(MemcacheConn)
mc.Timeout, err = time.ParseDuration("1s")
mcenabled = true
defer mc.Close()
}
ip.IP = c.Param("ip") ip.IP = c.Param("ip")
} else { 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,7 +21,10 @@ 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")
return c.String(http.StatusOK, string(content)) return c.String(http.StatusOK, string(content))
} }

View File

@ -15,6 +15,7 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Con
if strings.HasSuffix(name, ".html") { if strings.HasSuffix(name, ".html") {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) c.Response().Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
} }
c.Response().Header().Add(echo.HeaderCacheControl, "max-age=172800")
return t.templates.ExecuteTemplate(w, name, data) return t.templates.ExecuteTemplate(w, name, data)
} }
@ -38,29 +39,6 @@ func BuildTemplates(templatefiles *embed.FS) (builttemplates *Template, err erro
return return
} }
/*
// BuildTemplatesDir converts packr packages to html/template
func BuildTemplatesDir(dir string) (builttemplates *Template, err error) {
tmpl := template.New("templates")
err = pkger.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
freader, _ := pkger.Open(path)
tmplContent, err := ioutil.ReadAll(freader)
tmpl.New(info.Name()).Parse(string(tmplContent))
fmt.Println(info.Name(), tmplContent)
return err
})
builttemplates = &Template{
templates: tmpl,
}
return
}*/
// Template is a template struct // Template is a template struct
type Template struct { type Template struct {
templates *template.Template templates *template.Template

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,29 +1,39 @@
var vue = new Vue({ const dip_ip = document.getElementById("dip_ip");
el: '#dip_main_div', 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");
data () { function setData(res) {
return { dip_ip.innerHTML = res["ip"];
title: "Public IP Address service", dip_hostname.innerHTML = res["hostname"];
dip: {"ip": null, dip_city.innerHTML = res["city"];
"hostname": null, dip_country.innerHTML = res["country"];
"city": null, dip_as_number.innerHTML = res["as"]["number"];
"country": null}, dip_as_org.innerHTML = res["as"]["org"];
} }
},
mounted () { function updateIP() {
var ip = ""; let ip = "";
if (window.location.pathname.length > 4) { if (window.location.pathname.length > 4) {
ip = window.location.pathname.split("/")[1]; ip = window.location.pathname.split("/")[1];
console.log(ip);
} }
axios.get(`/json/${ip}`)
.then(response => { const localdata = localStorage.getItem("data");
this.dip = response.data if (localdata) {
}) const data = JSON.parse(localdata);
.catch(err => { setData(data);
// Manage the state of the application if the request }
// has failed
fetch(`/json/${ip}`).then((response) => {
response.json().then((data) => {
setData(data);
localStorage.setItem("data",JSON.stringify(data));
});
}); });
} }
});
updateIP()
setInterval(updateIP,1000);

File diff suppressed because one or more lines are too long

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,12 +4,11 @@
<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" />
<script src="static/js/uikit.min.js"></script> <script src="static/js/uikit.min.js"></script>
<script src="static/js/uikit-icons.min.js"></script> <script src="static/js/uikit-icons.min.js"></script>
<script src="static/js/axios.min.js"></script>
<script src="static/js/vue.min.js"></script>
</head> </head>

View File

@ -8,29 +8,27 @@
<caption>IP informations</caption> <caption>IP informations</caption>
<tr> <tr>
<td>IP</td> <td>IP</td>
<td>{{ "{{" }} dip.ip {{ "}}" }}</td> <td id="dip_ip"></td>
</tr> </tr>
<tr> <tr>
<td>Reverse DNS</td> <td>Reverse DNS</td>
<td>{{ "{{" }} dip.hostname {{ "}}" }}</td> <td id="dip_hostname"></td>
</tr> </tr>
<tr> <tr>
<td>City</td> <td>City</td>
<td>{{ "{{" }} dip.city {{ "}}" }}</td> <td id="dip_city"></td>
</tr> </tr>
<tr> <tr>
<td>Country</td> <td>Country</td>
<td>{{ "{{" }} dip.country {{ "}}" }}</td> <td id="dip_country"></td>
</tr> </tr>
<tr> <tr>
<td>AS number</td> <td>AS number</td>
<td>{{ "{{" }} dip.as.number {{ "}}" }}</td> <td id="dip_as_number"></td>
</tr> </tr>
<tr> <tr>
<td>AS name</td> <td>AS name</td>
<td> <td id="dip_as_org"></td>
{{ "{{" }} dip.as.org {{ "}}" }}
</td>
</tr> </tr>
</div> </div>
{{ template "footer_js.html" .}} {{ template "footer_js.html" .}}

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.
Binder interface { type 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 {
@ -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 {
if isElemString {
val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) 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()
} }
func unmarshalFieldNonPtr(value string, field reflect.Value) (bool, error) {
fieldIValue := field.Addr().Interface() fieldIValue := field.Addr().Interface()
if unmarshaler, ok := fieldIValue.(BindUnmarshaler); ok { unmarshaler, ok := fieldIValue.(bindMultipleUnmarshaler)
return true, unmarshaler.UnmarshalParam(value) if !ok {
return false, nil
} }
if unmarshaler, ok := fieldIValue.(encoding.TextUnmarshaler); ok { return true, unmarshaler.UnmarshalParams(values)
return true, unmarshaler.UnmarshalText([]byte(value)) }
func unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Value) (bool, error) {
if valueKind == reflect.Ptr {
if field.IsNil() {
field.Set(reflect.New(field.Type().Elem()))
}
field = field.Elem()
}
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,10 +16,9 @@ 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.
Context interface { type Context interface {
// Request returns `*http.Request`. // Request returns `*http.Request`.
Request() *http.Request Request() *http.Request
@ -100,8 +102,8 @@ type (
// 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()`.
@ -197,20 +199,33 @@ type (
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
pnames []string
pvalues []string
query url.Values query url.Values
handler HandlerFunc
store Map
echo *Echo echo *Echo
logger Logger logger Logger
store Map
lock sync.RWMutex lock sync.RWMutex
// following fields are set by Router
// 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,17 +63,16 @@ 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!
Echo struct { type 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 binded) without having data races. // listener address info (on which interface/port was listener bound) without having data races.
startupMutex sync.RWMutex startupMutex sync.RWMutex
colorer *color.Color colorer *color.Color
@ -106,50 +109,49 @@ type (
} }
// 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 JavaScript Object Notation (JSON) https://www.rfc-editor.org/rfc/rfc8259
MIMEApplicationJSON = "application/json" 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,8 +280,7 @@ ____________________________________O/_______
` `
) )
var ( var methods = [...]string{
methods = [...]string{
http.MethodConnect, http.MethodConnect,
http.MethodDelete, http.MethodDelete,
http.MethodGet, http.MethodGet,
@ -287,7 +293,6 @@ var (
http.MethodTrace, http.MethodTrace,
REPORT, REPORT,
} }
)
// Errors // Errors
var ( var (
@ -340,13 +345,15 @@ 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
// another matching routes for that requested URL. Returns an error that results HTTP 405 Method Not Allowed status code.
var MethodNotAllowedHandler = func(c Context) error {
// See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed) // See RFC 7231 section 7.4.1: An origin server MUST generate an Allow field in a 405 (Method Not Allowed)
// response and MAY do so in any other response. For disabled resources an empty Allow header may be returned // response and MAY do so in any other response. For disabled resources an empty Allow header may be returned
routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string) routerAllowMethods, ok := c.Get(ContextKeyHeaderAllow).(string)
@ -355,7 +362,6 @@ var (
} }
return ErrMethodNotAllowed 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.
Group struct { type 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,14 +1,15 @@
// 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.
Logger interface { type Logger interface {
Output() io.Writer Output() io.Writer
SetOutput(w io.Writer) SetOutput(w io.Writer)
Prefix() string Prefix() string
@ -38,4 +39,3 @@ type (
Panicj(j log.JSON) Panicj(j log.JSON)
Panicf(format string, args ...interface{}) Panicf(format string, args ...interface{})
} }
)

View File

@ -1,17 +1,19 @@
// 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.
BasicAuthConfig struct { type BasicAuthConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -25,21 +27,20 @@ type (
} }
// 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.
DefaultBasicAuthConfig = BasicAuthConfig{ var 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,9 +14,8 @@ 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.
BodyDumpConfig struct { type BodyDumpConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -22,20 +25,17 @@ type (
} }
// 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.
DefaultBodyDumpConfig = BodyDumpConfig{ var 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,9 +12,8 @@ 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.
BodyLimitConfig struct { type BodyLimitConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -21,20 +23,16 @@ type (
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.
DefaultBodyLimitConfig = BodyLimitConfig{ var 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.
GzipConfig struct { type 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"`
// Length threshold before gzip compression is applied.
// Optional. Default value 0.
//
// 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
} }
gzipResponseWriter struct { type gzipResponseWriter struct {
io.Writer io.Writer
http.ResponseWriter http.ResponseWriter
wroteHeader bool
wroteBody 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.
DefaultGzipConfig = GzipConfig{ var 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,9 +12,8 @@ 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.
CORSConfig struct { type CORSConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -39,7 +41,7 @@ type (
// 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
@ -99,22 +101,20 @@ type (
// 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 // See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
MaxAge int `yaml:"max_age"` MaxAge int `yaml:"max_age"`
} }
)
var (
// DefaultCORSConfig is the default CORS middleware config. // DefaultCORSConfig is the default CORS middleware config.
DefaultCORSConfig = CORSConfig{ var 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,12 +9,10 @@ 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.
CSRFConfig struct { type CSRFConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -67,15 +68,13 @@ type (
} }
// 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.
DefaultCSRFConfig = CSRFConfig{ var DefaultCSRFConfig = CSRFConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
TokenLength: 32, TokenLength: 32,
TokenLookup: "header:" + echo.HeaderXCSRFToken, TokenLookup: "header:" + echo.HeaderXCSRFToken,
@ -84,7 +83,6 @@ var (
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,16 +12,14 @@ 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.
DecompressConfig struct { type 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"
@ -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
DefaultDecompressConfig = DecompressConfig{ var 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 {

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,9 +15,8 @@ import (
"reflect" "reflect"
) )
type (
// JWTConfig defines the config for JWT middleware. // JWTConfig defines the config for JWT middleware.
JWTConfig struct { type JWTConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -112,29 +114,27 @@ type (
} }
// 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") // ErrJWTInvalid is error that is returned when middleware could not parse JWT correctly.
) var ErrJWTInvalid = echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired jwt")
var (
// DefaultJWTConfig is the default JWT auth middleware config. // DefaultJWTConfig is the default JWT auth middleware config.
DefaultJWTConfig = JWTConfig{ var DefaultJWTConfig = JWTConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
SigningMethod: AlgorithmHS256, SigningMethod: AlgorithmHS256,
ContextKey: "user", ContextKey: "user",
@ -144,7 +144,6 @@ var (
Claims: jwt.MapClaims{}, Claims: jwt.MapClaims{},
KeyFunc: nil, 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,9 +9,8 @@ import (
"net/http" "net/http"
) )
type (
// KeyAuthConfig defines the config for KeyAuth middleware. // KeyAuthConfig defines the config for KeyAuth middleware.
KeyAuthConfig struct { type KeyAuthConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -49,26 +51,23 @@ type (
} }
// 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,9 +17,8 @@ import (
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
) )
type (
// LoggerConfig defines the config for Logger middleware. // LoggerConfig defines the config for Logger middleware.
LoggerConfig struct { type LoggerConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -71,11 +73,9 @@ type (
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.
DefaultLoggerConfig = LoggerConfig{ var 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}",` +
@ -84,7 +84,6 @@ var (
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,9 +9,8 @@ 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.
MethodOverrideConfig struct { type MethodOverrideConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -18,16 +20,13 @@ type (
} }
// 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.
DefaultMethodOverrideConfig = MethodOverrideConfig{ var 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.
Skipper func(c echo.Context) bool type 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,9 +22,8 @@ import (
// TODO: Handle TLS proxy // TODO: Handle TLS proxy
type (
// ProxyConfig defines the config for Proxy middleware. // ProxyConfig defines the config for Proxy middleware.
ProxyConfig struct { type ProxyConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -30,6 +31,33 @@ type (
// Required. // Required.
Balancer ProxyBalancer Balancer ProxyBalancer
// RetryCount defines the number of times a failed proxied request should be retried
// using the next available ProxyTarget. Defaults to 0, meaning requests are never retried.
RetryCount int
// RetryFilter defines a function used to determine if a failed request to a
// ProxyTarget should be retried. The RetryFilter will only be called when the number
// of previous retries is less than RetryCount. If the function returns true, the
// request will be retried. The provided error indicates the reason for the request
// failure. When the ProxyTarget is unavailable, the error will be an instance of
// 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
// ErrorHandler defines a function which can be used to return custom errors from
// the Proxy middleware. ErrorHandler is only invoked when there has been
// 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
// Rewrite defines URL path rewrite rules. The values captured in asterisk can be // Rewrite defines 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.
// Examples: // Examples:
@ -59,62 +87,61 @@ type (
} }
// ProxyTarget defines the upstream target. // ProxyTarget defines the upstream target.
ProxyTarget struct { type ProxyTarget struct {
Name string Name string
URL *url.URL URL *url.URL
Meta echo.Map Meta echo.Map
} }
// ProxyBalancer defines an interface to implement a load balancing technique. // ProxyBalancer defines an interface to implement a load balancing technique.
ProxyBalancer interface { type ProxyBalancer interface {
AddTarget(*ProxyTarget) bool AddTarget(*ProxyTarget) bool
RemoveTarget(string) bool RemoveTarget(string) bool
Next(echo.Context) *ProxyTarget Next(echo.Context) *ProxyTarget
} }
// TargetProvider defines an interface that gives the opportunity for balancer to return custom errors when selecting target. // TargetProvider defines an interface that gives the opportunity for balancer
TargetProvider interface { // to return custom errors when selecting target.
type TargetProvider interface {
NextTarget(echo.Context) (*ProxyTarget, error) NextTarget(echo.Context) (*ProxyTarget, error)
} }
commonBalancer struct { type commonBalancer struct {
targets []*ProxyTarget targets []*ProxyTarget
mutex sync.RWMutex mutex sync.Mutex
} }
// RandomBalancer implements a random load balancing technique. // RandomBalancer implements a random load balancing technique.
randomBalancer struct { type randomBalancer struct {
*commonBalancer commonBalancer
random *rand.Rand random *rand.Rand
} }
// RoundRobinBalancer implements a round-robin load balancing technique. // RoundRobinBalancer implements a round-robin load balancing technique.
roundRobinBalancer struct { type roundRobinBalancer struct {
*commonBalancer commonBalancer
i uint32 // tracking the index on `targets` slice for the next `*ProxyTarget` to be used
i int
} }
)
var (
// DefaultProxyConfig is the default Proxy middleware config. // DefaultProxyConfig is the default Proxy middleware config.
DefaultProxyConfig = ProxyConfig{ var DefaultProxyConfig = ProxyConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
ContextKey: "target", 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())
} }
retries := config.RetryCount
for {
var tgt *ProxyTarget
var err error
if isTargetProvider {
tgt, err = provider.NextTarget(c)
if err != nil {
return config.ErrorHandler(c, err)
}
} else {
tgt = config.Balancer.Next(c)
}
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 // Proxy
switch { switch {
case c.IsWebSocket(): case c.IsWebSocket():
proxyRaw(tgt, c).ServeHTTP(res, req) proxyRaw(tgt, c).ServeHTTP(res, req)
case req.Header.Get(echo.HeaderAccept) == "text/event-stream": default: // even SSE requests
default:
proxyHTTP(tgt, c, config).ServeHTTP(res, req) proxyHTTP(tgt, c, config).ServeHTTP(res, req)
} }
if e, ok := c.Get("_error").(error); ok {
err = e err, hasError := c.Get("_error").(error)
if !hasError {
return nil
} }
return 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,17 +12,14 @@ 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.
RateLimiterStore interface { type 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
RateLimiterConfig struct { type 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
@ -31,17 +31,15 @@ type (
// 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 denotes an error raised when rate limit is exceeded
ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded") var ErrRateLimitExceeded = echo.NewHTTPError(http.StatusTooManyRequests, "rate limit exceeded")
// ErrExtractorError denotes an error raised when extractor function is unsuccessful // ErrExtractorError denotes an error raised when extractor function is unsuccessful
ErrExtractorError = echo.NewHTTPError(http.StatusForbidden, "error while extracting identifier") 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,9 +148,8 @@ 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
RateLimiterMemoryStore struct { type 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.
@ -160,13 +157,15 @@ type (
burst int burst int
expiresIn time.Duration expiresIn time.Duration
lastCleanup time.Time lastCleanup time.Time
timeNow func() time.Time
} }
// Visitor signifies a unique user's limiter details // Visitor signifies a unique user's limiter details
Visitor struct { type Visitor struct {
*rate.Limiter *rate.Limiter
lastSeen time.Time 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,12 +12,11 @@ import (
"github.com/labstack/gommon/log" "github.com/labstack/gommon/log"
) )
type (
// 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 type LogErrorFunc func(c echo.Context, err error, stack []byte) error
// RecoverConfig defines the config for Recover middleware. // RecoverConfig defines the config for Recover middleware.
RecoverConfig struct { type RecoverConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -37,21 +39,25 @@ type (
// LogErrorFunc defines a function for custom logging in the middleware. // LogErrorFunc defines a function for custom logging in the middleware.
// If it's set you don't need to provide LogLevel for config. // 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 LogErrorFunc LogErrorFunc
}
)
var ( // DisableErrorHandler disables the call to centralized HTTPErrorHandler.
// The recovered error is then passed back to upstream middleware, instead of swallowing the error.
// Optional. Default value false.
DisableErrorHandler bool `yaml:"disable_error_handler"`
}
// DefaultRecoverConfig is the default Recover middleware config. // DefaultRecoverConfig is the default Recover middleware config.
DefaultRecoverConfig = RecoverConfig{ var DefaultRecoverConfig = RecoverConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
StackSize: 4 << 10, // 4 KB StackSize: 4 << 10, // 4 KB
DisableStackAll: false, DisableStackAll: false,
DisablePrintStack: false, DisablePrintStack: false,
LogLevel: 0, LogLevel: 0,
LogErrorFunc: nil, LogErrorFunc: nil,
DisableErrorHandler: false,
} }
)
// 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)
} }
} }
if err != nil && !config.DisableErrorHandler {
c.Error(err) 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,18 +1,19 @@
// 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.
RequestIDConfig struct { type 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.
@ -21,16 +22,13 @@ type (
// 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.
DefaultRequestIDConfig = RequestIDConfig{ var 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,9 +9,8 @@ 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.
RewriteConfig struct { type RewriteConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -27,16 +29,13 @@ type (
// 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.
DefaultRewriteConfig = RewriteConfig{ var 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,9 +9,8 @@ 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.
SecureConfig struct { type SecureConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -72,18 +74,15 @@ type (
// 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.
DefaultSecureConfig = SecureConfig{ var 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,9 +9,8 @@ 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.
TrailingSlashConfig struct { type TrailingSlashConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -16,14 +18,11 @@ type (
// 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.
DefaultTrailingSlashConfig = TrailingSlashConfig{ var 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,9 +17,8 @@ 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.
StaticConfig struct { type StaticConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
@ -47,7 +49,6 @@ type (
// 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.
DefaultStaticConfig = StaticConfig{ var 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.
DefaultTimeoutConfig = TimeoutConfig{ var 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,16 +1,19 @@
// 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
Response struct { type Response struct {
echo *Echo echo *Echo
beforeFuncs []func() beforeFuncs []func()
afterFuncs []func() afterFuncs []func()
@ -19,7 +22,6 @@ type (
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,15 +9,15 @@ 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.
Router struct { type Router struct {
tree *node tree *node
routes map[string]*Route routes map[string]*Route
echo *Echo echo *Echo
} }
node struct {
type node struct {
kind kind kind kind
label byte label byte
prefix string prefix string
@ -33,14 +36,17 @@ type (
// notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases // notFoundHandler is handler registered with RouteNotFound method and is executed for 404 cases
notFoundHandler *routeMethod notFoundHandler *routeMethod
} }
kind uint8
children []*node type kind uint8
routeMethod struct { type children []*node
type routeMethod struct {
ppath string ppath string
pnames []string pnames []string
handler HandlerFunc handler HandlerFunc
} }
routeMethods struct {
type routeMethods struct {
connect *routeMethod connect *routeMethod
delete *routeMethod delete *routeMethod
get *routeMethod get *routeMethod
@ -55,7 +61,6 @@ type (
anyOther map[string]*routeMethod anyOther map[string]*routeMethod
allowHeader string 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 = "/"
}
if path[0] != '/' {
path = "/" + path
} }
func (r *Router) insert(method, path string, h HandlerFunc) {
path = normalizePathSlash(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.

54
vendor/github.com/likexian/gokit/xrand/README.md generated vendored Normal file
View File

@ -0,0 +1,54 @@
# GoKit - xrand
Rand kits for Golang development.
## Installation
go get -u github.com/likexian/gokit
## Importing
import (
"github.com/likexian/gokit/xrand"
)
## Documentation
Visit the docs on [GoDoc](https://godoc.org/github.com/likexian/gokit/xrand)
## Example
### Rand int between 0 and 10000
```go
n := xrand.Int(10000)
fmt.Println("rand int between 0 and 10000 is:", n)
```
### Rand int between 1000 and 10000
```go
n := xrand.IntRange(1000, 10000)
fmt.Println("rand int between 1000 and 10000 is:", n)
```
### Rand bytes with length of 10
```go
b, err := xrand.Bytes(10)
if err != nil {
fmt.Println("rand bytes:", b)
}
```
## License
Copyright 2012-2024 [Li Kexian](https://www.likexian.com/)
Licensed under the Apache License 2.0
## Donation
If this project is helpful, please share it with friends.
If you want to thank me, you can [give me a cup of coffee](https://www.likexian.com/donate/).

115
vendor/github.com/likexian/gokit/xrand/xrand.go generated vendored Normal file
View File

@ -0,0 +1,115 @@
/*
* Copyright 2012-2024 Li Kexian
*
* 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.
*
* A toolkit for Golang development
* https://www.likexian.com/
*/
package xrand
import (
crand "crypto/rand"
"encoding/base64"
"encoding/hex"
"math/rand"
"time"
)
// Version returns package version
func Version() string {
return "0.2.0"
}
// Author returns package author
func Author() string {
return "[Li Kexian](https://www.likexian.com/)"
}
// License returns package license
func License() string {
return "Licensed under the Apache License 2.0"
}
// Int returns random int in [0, max)
func Int(max int) int {
if max <= 0 {
return 0
}
seed := rand.NewSource(time.Now().UnixNano())
return rand.New(seed).Intn(max)
}
// IntRange returns random int in [min, max)
func IntRange(min, max int) int {
if min > max {
min, max = max, min
}
return Int(max-min) + min
}
// String returns n random string from 0-9,a-z,A-Z
func String(n int) string {
sources := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
return StringRange(n, sources)
}
// StringRange returns n random string base on source
func StringRange(n int, source string) string {
if source == "" {
return ""
}
ss := []rune(source)
bs := make([]rune, n)
for i := range bs {
bs[i] = ss[Int(len(ss))]
}
return string(bs)
}
// Bytes returns n random bytes
func Bytes(n int) (bs []byte, err error) {
bs = make([]byte, n)
_, err = crand.Read(bs)
return
}
// Hex returns hex string of n random bytes
func Hex(n int) (ss string, err error) {
bs, err := Bytes(n)
if err != nil {
return
}
ss = hex.EncodeToString(bs)
return
}
// Base64 returns base64 string of n random bytes
func Base64(n int) (ss string, err error) {
bs, err := Bytes(n)
if err != nil {
return
}
ss = base64.StdEncoding.EncodeToString(bs)
return
}

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.
@ -22,13 +22,14 @@ package xslice
import ( import (
"fmt" "fmt"
"math" "math"
"math/rand"
"reflect" "reflect"
"github.com/likexian/gokit/xrand"
) )
// Version returns package version // Version returns package version
func Version() string { func Version() string {
return "0.22.0" return "0.23.0"
} }
// Author returns package author // Author returns package author
@ -209,7 +210,7 @@ func Shuffle(v interface{}) {
swap := reflect.Swapper(v) swap := reflect.Swapper(v)
for i := vv.Len() - 1; i >= 1; i-- { for i := vv.Len() - 1; i >= 1; i-- {
j := rand.Intn(i + 1) j := xrand.Int(i + 1)
swap(i, j) swap(i, j)
} }
} }

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.1" 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{
@ -929,7 +955,7 @@ func prepareEU(text string) string {
if _, ok := tokens[v]; ok { if _, ok := tokens[v]; ok {
token = tokens[v] token = tokens[v]
continue continue
} else { }
if token != "" { if token != "" {
if strings.Contains(v, ":") { if strings.Contains(v, ":") {
v = fmt.Sprintf("%s %s", token, v) v = fmt.Sprintf("%s %s", token, v)
@ -940,7 +966,6 @@ func prepareEU(text string) string {
v = fmt.Sprintf("%s: %s", token, v) v = fmt.Sprintf("%s: %s", token, v)
} }
} }
}
result += "\n" + v result += "\n" + v
} }
@ -1155,9 +1180,8 @@ func prepareEE(text string) string {
if t, ok := tokens[v]; ok { if t, ok := tokens[v]; ok {
token = t token = t
continue continue
} else {
v = fmt.Sprintf("%s %s", token, v)
} }
v = fmt.Sprintf("%s %s", token, v)
result += "\n" + strings.TrimSpace(v) result += "\n" + strings.TrimSpace(v)
} }
@ -1194,9 +1218,8 @@ func preparePL(text string) string {
ns := strings.SplitN(v, "[", 2) ns := strings.SplitN(v, "[", 2)
result += fmt.Sprintf("\nnameservers: %s", strings.TrimSpace(ns[0])) result += fmt.Sprintf("\nnameservers: %s", strings.TrimSpace(ns[0]))
continue continue
} else {
special = ""
} }
special = ""
} else if special == "REGISTRAR" { } else if special == "REGISTRAR" {
if strings.TrimSpace(v) == "" { if strings.TrimSpace(v) == "" {
special = "" special = ""
@ -1338,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.
@ -68,9 +68,9 @@ var (
"registration time": "created_date", "registration time": "created_date",
"first registration date": "created_date", "first registration date": "created_date",
"domain record activated": "created_date", "domain record activated": "created_date",
"record created": "created_date",
"record created on": "created_date", "record created on": "created_date",
"domain registered": "created_date", "domain registered": "created_date",
"record created": "created_date",
"modified": "updated_date", "modified": "updated_date",
"changed": "updated_date", "changed": "updated_date",
"update date": "updated_date", "update date": "updated_date",
@ -153,6 +153,8 @@ var (
"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 postalcode": "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.
@ -49,11 +49,13 @@ type Client struct {
dialer proxy.Dialer dialer proxy.Dialer
timeout time.Duration timeout time.Duration
elapsed time.Duration elapsed time.Duration
disableStats bool
disableReferral bool
} }
// Version returns package version // Version returns package version
func Version() string { func Version() string {
return "1.14.5" return "1.15.4"
} }
// Author returns package author // Author returns package author
@ -82,24 +84,39 @@ func NewClient() *Client {
} }
// SetDialer set query net dialer // SetDialer set query net dialer
func (c *Client) SetDialer(dialer proxy.Dialer) { func (c *Client) SetDialer(dialer proxy.Dialer) *Client {
c.dialer = dialer c.dialer = dialer
return c
} }
// SetTimeout set query timeout // SetTimeout set query timeout
func (c *Client) SetTimeout(timeout time.Duration) { func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.timeout = timeout c.timeout = timeout
return c
}
// SetDisableStats set disable stats
func (c *Client) SetDisableStats(disabled bool) *Client {
c.disableStats = disabled
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()
defer func() { defer func() {
result = fmt.Sprintf("%s\n\n;; Query time: %d msec\n;; WHEN: %s\n", result = strings.TrimSpace(result)
strings.TrimRight(result, "\n"), if result != "" && !c.disableStats {
time.Since(start).Milliseconds(), result = fmt.Sprintf("%s\n\n%% Query time: %d msec\n%% WHEN: %s\n",
start.Format("Mon Jan 02 15:04:05 MST 2006"), result, time.Since(start).Milliseconds(), start.Format("Mon Jan 02 15:04:05 MST 2006"),
) )
}
}() }()
domain = strings.Trim(strings.TrimSpace(domain), ".") domain = strings.Trim(strings.TrimSpace(domain), ".")
@ -139,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
@ -170,6 +191,7 @@ func (c *Client) rawQuery(domain, server, port string) (string, error) {
server = "whois.godaddy.com" server = "whois.godaddy.com"
} }
// See: https://github.com/likexian/whois/pull/30
if server == "porkbun.com/whois" { if server == "porkbun.com/whois" {
server = "whois.porkbun.com" server = "whois.porkbun.com"
} }

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,98 +1,43 @@
[run] [run]
deadline = "10m" # This is needed for precious, which may run multiple instances
# in parallel
allow-parallel-runners = true
go = "1.21"
tests = true tests = true
timeout = "10m"
[linters] [linters]
disable-all = true enable-all = true
enable = [ disable = [
"asciicheck", "cyclop",
"bidichk",
"bodyclose",
"containedctx",
"contextcheck",
"deadcode",
"depguard", "depguard",
"durationcheck", "err113",
"errcheck", "execinquery",
"errchkjson", "exhaustive",
"errname", "exhaustruct",
"errorlint", "forcetypeassert",
"exportloopref", "funlen",
"forbidigo", "gochecknoglobals",
#"forcetypeassert", "godox",
"goconst", "gomnd",
"gocyclo", "inamedparam",
"gocritic", "interfacebloat",
"godot", "mnd",
"gofumpt", "nlreturn",
"gomodguard", "nonamedreturns",
"gosec", "paralleltest",
"gosimple", "thelper",
"govet", "testpackage",
"grouper",
"ineffassign", "varnamelen",
"lll", "wrapcheck",
"makezero", "wsl",
"maintidx",
"misspell", # Require Go 1.22
"nakedret", "copyloopvar",
"nilerr", "intrange",
"noctx",
"nolintlint",
"nosprintfhostport",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"structcheck",
"stylecheck",
"tenv",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"varcheck",
"vetshadow",
"wastedassign",
] ]
# Please note that we only use depguard for stdlib as gomodguard only
# supports modules currently. See https://github.com/ryancurrah/gomodguard/issues/12
[linters-settings.depguard]
list-type = "blacklist"
include-go-root = true
packages = [
# ioutil is deprecated. The functions have been moved elsewhere:
# https://golang.org/doc/go1.16#ioutil
"io/ioutil",
]
[linters-settings.errcheck]
# Don't allow setting of error to the blank identifier. If there is a legtimate
# reason, there should be a nolint with an explanation.
check-blank = true
exclude-functions = [
# If we are rolling back a transaction, we are often already in an error
# state.
'(*database/sql.Tx).Rollback',
# It is reasonable to ignore errors if Cleanup fails in most cases.
'(*github.com/google/renameio/v2.PendingFile).Cleanup',
# We often don't care if removing a file failed (e.g., it doesn't exist)
'os.Remove',
'os.RemoveAll',
]
# Ignoring Close so that we don't have to have a bunch of
# `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
@ -104,369 +49,144 @@
[linters-settings.forbidigo] [linters-settings.forbidigo]
# Forbid the following identifiers # Forbid the following identifiers
forbid = [ forbid = [
"^minFraud*", { p = "Geoip", msg = "you should use `GeoIP`" },
"^maxMind*", { p = "geoIP", msg = "you should use `geoip`" },
{ p = "Maxmind", msg = "you should 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",
# Revive's defer rule already captures this. This caught no extra cases.
# "deferInLoop",
"deferUnlambda",
"deprecatedComment",
"docStub",
"dupArg",
"dupBranchBody",
"dupCase",
"dupImport",
"dupSubExpr",
"dynamicFmtString",
"elseif",
"emptyDecl",
"emptyFallthrough",
"emptyStringTest",
"equalFold",
"evalOrder",
"exitAfterDefer",
"exposedSyncMutex",
"externalErrorReassign",
# Given that all of our code runs on Linux and the / separate should
# work fine, this seems less important.
# "filepathJoin",
"flagDeref",
"flagName",
"hexLiteral",
"ifElseChain",
"importShadow",
"indexAlloc",
"initClause",
"ioutilDeprecated",
"mapKey",
"methodExprCall",
"nestingReduce",
"newDeref",
"nilValReturn",
"octalLiteral",
"offBy1",
"paramTypeCombine",
"preferDecodeRune",
"preferFilepathJoin",
"preferFprint",
"preferStringWriter",
"preferWriteByte",
"ptrToRefParam",
"rangeExprCopy",
"rangeValCopy",
"redundantSprint",
"regexpMust",
"regexpPattern",
# This might be good, but I don't think we want to encourage
# significant changes to regexes as we port stuff from Perl.
# "regexpSimplify",
"ruleguard",
"singleCaseSwitch",
"sliceClear",
"sloppyLen",
# This seems like it might also be good, but a lot of existing code
# fails.
# "sloppyReassign",
"returnAfterHttpError",
"sloppyTypeAssert",
"sortSlice",
"sprintfQuotedString",
"sqlQuery",
"stringsCompare",
"stringXbytes",
"switchTrue",
"syncMapLoadAndDelete",
"timeExprSimplify",
"todoCommentWithoutDetail",
"tooManyResultsChecker",
"truncateCmp",
"typeAssertChain",
"typeDefFirst",
"typeSwitchVar",
"typeUnparen",
"underef",
"unlabelStmt",
"unlambda",
# I am not sure we would want this linter and a lot of existing
# code fails.
# "unnamedResult",
"unnecessaryBlock",
"unnecessaryDefer",
"unslice",
"valSwap",
"weakCond",
"wrapperFunc",
"yodaStyleExpr",
# This requires explanations for "nolint" directives. This would be
# nice for gosec ones, but I am not sure we want it generally unless
# we can get the false positive rate lower.
# "whyNoLint"
]
[linters-settings.gofumpt] [linters-settings.gofumpt]
extra-rules = true extra-rules = true
lang-version = "1.18"
[linters-settings.govet] [linters-settings.govet]
"enable-all" = true enable-all = true
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]
enable-all-rules = true
ignore-generated-header = true ignore-generated-header = true
severity = "warning" severity = "warning"
# This might be nice but it is so common that it is hard [[linters-settings.revive.rules]]
# to enable. name = "add-constant"
# [[linters-settings.revive.rules]] disabled = true
# name = "add-constant"
# [[linters-settings.revive.rules]]
# name = "argument-limit"
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "atomic" name = "cognitive-complexity"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "bare-return" name = "confusing-naming"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "blank-imports" name = "confusing-results"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "bool-literal-in-expr" name = "cyclomatic"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "call-to-gc" name = "deep-exit"
disabled = true
# [[linters-settings.revive.rules]]
# name = "cognitive-complexity"
# Probably a good rule, but we have a lot of names that
# only have case differences.
# [[linters-settings.revive.rules]]
# name = "confusing-naming"
# [[linters-settings.revive.rules]]
# name = "confusing-results"
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "constant-logical-expr" name = "flag-parameter"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "context-as-argument" name = "function-length"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "context-keys-type" name = "function-result-limit"
disabled = true
# [[linters-settings.revive.rules]]
# name = "cyclomatic"
# [[linters-settings.revive.rules]]
# name = "deep-exit"
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "defer" name = "line-length-limit"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "dot-imports" name = "max-public-structs"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "duplicated-imports" name = "nested-structs"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "early-return" name = "unchecked-type-assertion"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "empty-block" name = "unhandled-error"
disabled = true
[[linters-settings.revive.rules]] [linters-settings.tagliatelle.case.rules]
name = "empty-lines" avro = "snake"
bson = "snake"
[[linters-settings.revive.rules]] env = "upperSnake"
name = "errorf" envconfig = "upperSnake"
json = "snake"
[[linters-settings.revive.rules]] mapstructure = "snake"
name = "error-naming" xml = "snake"
yaml = "snake"
[[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"
# We have a lot of flag parameters. This linter probably makes
# a good point, but we would need some cleanup or a lot of nolints.
# [[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"
# We frequently use nested structs, particularly in tests.
# [[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-naming"
[[linters-settings.revive.rules]]
name = "unconditional-recursion"
[[linters-settings.revive.rules]]
name = "unexported-naming"
[[linters-settings.revive.rules]]
name = "unexported-return"
# This is covered elsewhere and we want to ignore some
# functions such as fmt.Fprintf.
# [[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"
# We generally have unused receivers in tests for meeting the
# requirements of an interface.
# [[linters-settings.revive.rules]]
# name = "unused-receiver"
# This probably makes sense after we upgrade to 1.18
# [[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-rules]] [[issues.exclude-rules]]
linters = [ linters = [
"govet" "govet",
"revive",
] ]
# we want to enable almost all govet rules. It is easier to just filter out path = "_test.go"
# the ones we don't want: text = "fieldalignment:"
#
# * fieldalignment - way too noisy. Although it is very useful in particular
# cases where we are trying to use as little memory as possible, having
# it go off on every struct isn't helpful.
# * shadow - although often useful, it complains about _many_ err
# shadowing assignments and some others where shadowing is clear.
text = "^(fieldalignment|shadow)"

View File

@ -17,119 +17,121 @@ import (
// The Enterprise struct corresponds to the data in the GeoIP2 Enterprise // The Enterprise struct corresponds to the data in the GeoIP2 Enterprise
// database. // database.
type Enterprise struct { type Enterprise struct {
City struct {
Confidence uint8 `maxminddb:"confidence"`
GeoNameID uint `maxminddb:"geoname_id"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"city"`
Continent struct { Continent struct {
Names map[string]string `maxminddb:"names"`
Code string `maxminddb:"code"` Code string `maxminddb:"code"`
GeoNameID uint `maxminddb:"geoname_id"` GeoNameID uint `maxminddb:"geoname_id"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"continent"` } `maxminddb:"continent"`
Country struct { City struct {
GeoNameID uint `maxminddb:"geoname_id"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"` Names map[string]string `maxminddb:"names"`
GeoNameID uint `maxminddb:"geoname_id"`
Confidence uint8 `maxminddb:"confidence"` Confidence uint8 `maxminddb:"confidence"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` } `maxminddb:"city"`
} `maxminddb:"country"`
Location struct {
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
Latitude float64 `maxminddb:"latitude"`
Longitude float64 `maxminddb:"longitude"`
MetroCode uint `maxminddb:"metro_code"`
TimeZone string `maxminddb:"time_zone"`
} `maxminddb:"location"`
Postal struct { Postal struct {
Code string `maxminddb:"code"` Code string `maxminddb:"code"`
Confidence uint8 `maxminddb:"confidence"` Confidence uint8 `maxminddb:"confidence"`
} `maxminddb:"postal"` } `maxminddb:"postal"`
RegisteredCountry struct { Subdivisions []struct {
GeoNameID uint `maxminddb:"geoname_id"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"` Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"`
Confidence uint8 `maxminddb:"confidence"`
} `maxminddb:"subdivisions"`
RepresentedCountry struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
Type string `maxminddb:"type"`
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
} `maxminddb:"represented_country"`
Country struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"`
Confidence uint8 `maxminddb:"confidence"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
} `maxminddb:"country"`
RegisteredCountry struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"`
Confidence uint8 `maxminddb:"confidence"` Confidence uint8 `maxminddb:"confidence"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
} `maxminddb:"registered_country"` } `maxminddb:"registered_country"`
RepresentedCountry struct {
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
Type string `maxminddb:"type"`
} `maxminddb:"represented_country"`
Subdivisions []struct {
Confidence uint8 `maxminddb:"confidence"`
GeoNameID uint `maxminddb:"geoname_id"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"subdivisions"`
Traits struct { Traits struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
ConnectionType string `maxminddb:"connection_type"` ConnectionType string `maxminddb:"connection_type"`
Domain string `maxminddb:"domain"` Domain string `maxminddb:"domain"`
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
IsLegitimateProxy bool `maxminddb:"is_legitimate_proxy"`
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
ISP string `maxminddb:"isp"` ISP string `maxminddb:"isp"`
MobileCountryCode string `maxminddb:"mobile_country_code"` MobileCountryCode string `maxminddb:"mobile_country_code"`
MobileNetworkCode string `maxminddb:"mobile_network_code"` MobileNetworkCode string `maxminddb:"mobile_network_code"`
Organization string `maxminddb:"organization"` Organization string `maxminddb:"organization"`
StaticIPScore float64 `maxminddb:"static_ip_score"`
UserType string `maxminddb:"user_type"` UserType string `maxminddb:"user_type"`
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
StaticIPScore float64 `maxminddb:"static_ip_score"`
IsAnonymousProxy bool `maxminddb:"is_anonymous_proxy"`
IsAnycast bool `maxminddb:"is_anycast"`
IsLegitimateProxy bool `maxminddb:"is_legitimate_proxy"`
IsSatelliteProvider bool `maxminddb:"is_satellite_provider"`
} `maxminddb:"traits"` } `maxminddb:"traits"`
Location struct {
TimeZone string `maxminddb:"time_zone"`
Latitude float64 `maxminddb:"latitude"`
Longitude float64 `maxminddb:"longitude"`
MetroCode uint `maxminddb:"metro_code"`
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
} `maxminddb:"location"`
} }
// The City struct corresponds to the data in the GeoIP2/GeoLite2 City // The City struct corresponds to the data in the GeoIP2/GeoLite2 City
// databases. // databases.
type City struct { type City struct {
City struct { City struct {
GeoNameID uint `maxminddb:"geoname_id"`
Names map[string]string `maxminddb:"names"` Names map[string]string `maxminddb:"names"`
GeoNameID uint `maxminddb:"geoname_id"`
} `maxminddb:"city"` } `maxminddb:"city"`
Continent struct {
Code string `maxminddb:"code"`
GeoNameID uint `maxminddb:"geoname_id"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"continent"`
Country struct {
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"country"`
Location struct {
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
Latitude float64 `maxminddb:"latitude"`
Longitude float64 `maxminddb:"longitude"`
MetroCode uint `maxminddb:"metro_code"`
TimeZone string `maxminddb:"time_zone"`
} `maxminddb:"location"`
Postal struct { Postal struct {
Code string `maxminddb:"code"` Code string `maxminddb:"code"`
} `maxminddb:"postal"` } `maxminddb:"postal"`
RegisteredCountry struct { Continent struct {
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"` Names map[string]string `maxminddb:"names"`
} `maxminddb:"registered_country"` Code string `maxminddb:"code"`
RepresentedCountry struct {
GeoNameID uint `maxminddb:"geoname_id"` GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` } `maxminddb:"continent"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
Type string `maxminddb:"type"`
} `maxminddb:"represented_country"`
Subdivisions []struct { Subdivisions []struct {
GeoNameID uint `maxminddb:"geoname_id"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"` Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"`
} `maxminddb:"subdivisions"` } `maxminddb:"subdivisions"`
RepresentedCountry struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
Type string `maxminddb:"type"`
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
} `maxminddb:"represented_country"`
Country struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
} `maxminddb:"country"`
RegisteredCountry struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
} `maxminddb:"registered_country"`
Location struct {
TimeZone string `maxminddb:"time_zone"`
Latitude float64 `maxminddb:"latitude"`
Longitude float64 `maxminddb:"longitude"`
MetroCode uint `maxminddb:"metro_code"`
AccuracyRadius uint16 `maxminddb:"accuracy_radius"`
} `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"`
} }
@ -138,31 +140,32 @@ type City struct {
// Country databases. // Country databases.
type Country struct { type Country struct {
Continent struct { Continent struct {
Names map[string]string `maxminddb:"names"`
Code string `maxminddb:"code"` Code string `maxminddb:"code"`
GeoNameID uint `maxminddb:"geoname_id"` GeoNameID uint `maxminddb:"geoname_id"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"continent"` } `maxminddb:"continent"`
Country struct { Country struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"` GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"country"` } `maxminddb:"country"`
RegisteredCountry struct { RegisteredCountry struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
GeoNameID uint `maxminddb:"geoname_id"` GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
} `maxminddb:"registered_country"` } `maxminddb:"registered_country"`
RepresentedCountry struct { RepresentedCountry struct {
Names map[string]string `maxminddb:"names"`
IsoCode string `maxminddb:"iso_code"`
Type string `maxminddb:"type"`
GeoNameID uint `maxminddb:"geoname_id"` GeoNameID uint `maxminddb:"geoname_id"`
IsInEuropeanUnion bool `maxminddb:"is_in_european_union"` IsInEuropeanUnion bool `maxminddb:"is_in_european_union"`
IsoCode string `maxminddb:"iso_code"`
Names map[string]string `maxminddb:"names"`
Type string `maxminddb:"type"`
} `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"`
} }
@ -180,8 +183,8 @@ type AnonymousIP struct {
// The ASN struct corresponds to the data in the GeoLite2 ASN database. // The ASN struct corresponds to the data in the GeoLite2 ASN database.
type ASN struct { type ASN struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
} }
// The ConnectionType struct corresponds to the data in the GeoIP2 // The ConnectionType struct corresponds to the data in the GeoIP2
@ -197,12 +200,12 @@ type Domain struct {
// The ISP struct corresponds to the data in the GeoIP2 ISP database. // The ISP struct corresponds to the data in the GeoIP2 ISP database.
type ISP struct { type ISP struct {
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"` AutonomousSystemOrganization string `maxminddb:"autonomous_system_organization"`
ISP string `maxminddb:"isp"` ISP string `maxminddb:"isp"`
MobileCountryCode string `maxminddb:"mobile_country_code"` MobileCountryCode string `maxminddb:"mobile_country_code"`
MobileNetworkCode string `maxminddb:"mobile_network_code"` MobileNetworkCode string `maxminddb:"mobile_network_code"`
Organization string `maxminddb:"organization"` Organization string `maxminddb:"organization"`
AutonomousSystemNumber uint `maxminddb:"autonomous_system_number"`
} }
type databaseType int type databaseType int
@ -305,8 +308,7 @@ func getDBType(reader *maxminddb.Reader) (databaseType, error) {
"DBIP-Location-ISP (compat=Enterprise)", "DBIP-Location-ISP (compat=Enterprise)",
"GeoIP2-Enterprise": "GeoIP2-Enterprise":
return isEnterprise | isCity | isCountry, nil return isEnterprise | isCity | isCountry, nil
case "GeoIP2-ISP", case "GeoIP2-ISP", "GeoIP2-Precision-ISP":
"GeoIP2-Precision-ISP":
return isISP | isASN, nil return isISP | isASN, nil
default: default:
return 0, UnknownDatabaseTypeError{reader.Metadata.DatabaseType} return 0, UnknownDatabaseTypeError{reader.Metadata.DatabaseType}

View File

@ -1,98 +1,43 @@
[run] [run]
deadline = "10m" # This is needed for precious, which may run multiple instances
# in parallel
allow-parallel-runners = true
go = "1.21"
tests = true tests = true
timeout = "10m"
[linters] [linters]
disable-all = true enable-all = true
enable = [ disable = [
"asciicheck", "cyclop",
"bidichk",
"bodyclose",
"containedctx",
"contextcheck",
"deadcode",
"depguard", "depguard",
"durationcheck", "err113",
"errcheck", "execinquery",
"errchkjson", "exhaustive",
"errname", "exhaustruct",
"errorlint", "forcetypeassert",
"exportloopref", "funlen",
"forbidigo", "gochecknoglobals",
#"forcetypeassert", "godox",
"goconst", "gomnd",
"gocyclo", "inamedparam",
"gocritic", "interfacebloat",
"godot", "mnd",
"gofumpt", "nlreturn",
"gomodguard", "nonamedreturns",
"gosec", "paralleltest",
"gosimple", "thelper",
"govet", "testpackage",
"grouper",
"ineffassign", "varnamelen",
"lll", "wrapcheck",
"makezero", "wsl",
"maintidx",
"misspell", # Require Go 1.22
"nakedret", "copyloopvar",
"nilerr", "intrange",
"noctx",
"nolintlint",
"nosprintfhostport",
"predeclared",
"revive",
"rowserrcheck",
"sqlclosecheck",
"staticcheck",
"structcheck",
"stylecheck",
"tenv",
"tparallel",
"typecheck",
"unconvert",
"unparam",
"unused",
"varcheck",
"vetshadow",
"wastedassign",
] ]
# Please note that we only use depguard for stdlib as gomodguard only
# supports modules currently. See https://github.com/ryancurrah/gomodguard/issues/12
[linters-settings.depguard]
list-type = "blacklist"
include-go-root = true
packages = [
# ioutil is deprecated. The functions have been moved elsewhere:
# https://golang.org/doc/go1.16#ioutil
"io/ioutil",
]
[linters-settings.errcheck]
# Don't allow setting of error to the blank identifier. If there is a legtimate
# reason, there should be a nolint with an explanation.
check-blank = true
exclude-functions = [
# If we are rolling back a transaction, we are often already in an error
# state.
'(*database/sql.Tx).Rollback',
# It is reasonable to ignore errors if Cleanup fails in most cases.
'(*github.com/google/renameio/v2.PendingFile).Cleanup',
# We often don't care if removing a file failed (e.g., it doesn't exist)
'os.Remove',
'os.RemoveAll',
]
# Ignoring Close so that we don't have to have a bunch of
# `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
@ -104,369 +49,144 @@
[linters-settings.forbidigo] [linters-settings.forbidigo]
# Forbid the following identifiers # Forbid the following identifiers
forbid = [ forbid = [
"^minFraud*", { p = "Geoip", msg = "you should use `GeoIP`" },
"^maxMind*", { p = "geoIP", msg = "you should use `geoip`" },
{ p = "Maxmind", msg = "you should 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",
# Revive's defer rule already captures this. This caught no extra cases.
# "deferInLoop",
"deferUnlambda",
"deprecatedComment",
"docStub",
"dupArg",
"dupBranchBody",
"dupCase",
"dupImport",
"dupSubExpr",
"dynamicFmtString",
"elseif",
"emptyDecl",
"emptyFallthrough",
"emptyStringTest",
"equalFold",
"evalOrder",
"exitAfterDefer",
"exposedSyncMutex",
"externalErrorReassign",
# Given that all of our code runs on Linux and the / separate should
# work fine, this seems less important.
# "filepathJoin",
"flagDeref",
"flagName",
"hexLiteral",
"ifElseChain",
"importShadow",
"indexAlloc",
"initClause",
"ioutilDeprecated",
"mapKey",
"methodExprCall",
"nestingReduce",
"newDeref",
"nilValReturn",
"octalLiteral",
"offBy1",
"paramTypeCombine",
"preferDecodeRune",
"preferFilepathJoin",
"preferFprint",
"preferStringWriter",
"preferWriteByte",
"ptrToRefParam",
"rangeExprCopy",
"rangeValCopy",
"redundantSprint",
"regexpMust",
"regexpPattern",
# This might be good, but I don't think we want to encourage
# significant changes to regexes as we port stuff from Perl.
# "regexpSimplify",
"ruleguard",
"singleCaseSwitch",
"sliceClear",
"sloppyLen",
# This seems like it might also be good, but a lot of existing code
# fails.
# "sloppyReassign",
"returnAfterHttpError",
"sloppyTypeAssert",
"sortSlice",
"sprintfQuotedString",
"sqlQuery",
"stringsCompare",
"stringXbytes",
"switchTrue",
"syncMapLoadAndDelete",
"timeExprSimplify",
"todoCommentWithoutDetail",
"tooManyResultsChecker",
"truncateCmp",
"typeAssertChain",
"typeDefFirst",
"typeSwitchVar",
"typeUnparen",
"underef",
"unlabelStmt",
"unlambda",
# I am not sure we would want this linter and a lot of existing
# code fails.
# "unnamedResult",
"unnecessaryBlock",
"unnecessaryDefer",
"unslice",
"valSwap",
"weakCond",
"wrapperFunc",
"yodaStyleExpr",
# This requires explanations for "nolint" directives. This would be
# nice for gosec ones, but I am not sure we want it generally unless
# we can get the false positive rate lower.
# "whyNoLint"
]
[linters-settings.gofumpt] [linters-settings.gofumpt]
extra-rules = true extra-rules = true
lang-version = "1.18"
[linters-settings.govet] [linters-settings.govet]
"enable-all" = true enable-all = true
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]
enable-all-rules = true
ignore-generated-header = true ignore-generated-header = true
severity = "warning" severity = "warning"
# This might be nice but it is so common that it is hard [[linters-settings.revive.rules]]
# to enable. name = "add-constant"
# [[linters-settings.revive.rules]] disabled = true
# name = "add-constant"
# [[linters-settings.revive.rules]]
# name = "argument-limit"
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "atomic" name = "cognitive-complexity"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "bare-return" name = "confusing-naming"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "blank-imports" name = "confusing-results"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "bool-literal-in-expr" name = "cyclomatic"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "call-to-gc" name = "deep-exit"
disabled = true
# [[linters-settings.revive.rules]]
# name = "cognitive-complexity"
# Probably a good rule, but we have a lot of names that
# only have case differences.
# [[linters-settings.revive.rules]]
# name = "confusing-naming"
# [[linters-settings.revive.rules]]
# name = "confusing-results"
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "constant-logical-expr" name = "flag-parameter"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "context-as-argument" name = "function-length"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "context-keys-type" name = "function-result-limit"
disabled = true
# [[linters-settings.revive.rules]]
# name = "cyclomatic"
# [[linters-settings.revive.rules]]
# name = "deep-exit"
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "defer" name = "line-length-limit"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "dot-imports" name = "max-public-structs"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "duplicated-imports" name = "nested-structs"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "early-return" name = "unchecked-type-assertion"
disabled = true
[[linters-settings.revive.rules]] [[linters-settings.revive.rules]]
name = "empty-block" name = "unhandled-error"
disabled = true
[[linters-settings.revive.rules]] [linters-settings.tagliatelle.case.rules]
name = "empty-lines" avro = "snake"
bson = "snake"
[[linters-settings.revive.rules]] env = "upperSnake"
name = "errorf" envconfig = "upperSnake"
json = "snake"
[[linters-settings.revive.rules]] mapstructure = "snake"
name = "error-naming" xml = "snake"
yaml = "snake"
[[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"
# We have a lot of flag parameters. This linter probably makes
# a good point, but we would need some cleanup or a lot of nolints.
# [[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"
# We frequently use nested structs, particularly in tests.
# [[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-naming"
[[linters-settings.revive.rules]]
name = "unconditional-recursion"
[[linters-settings.revive.rules]]
name = "unexported-naming"
[[linters-settings.revive.rules]]
name = "unexported-return"
# This is covered elsewhere and we want to ignore some
# functions such as fmt.Fprintf.
# [[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"
# We generally have unused receivers in tests for meeting the
# requirements of an interface.
# [[linters-settings.revive.rules]]
# name = "unused-receiver"
# This probably makes sense after we upgrade to 1.18
# [[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-rules]] [[issues.exclude-rules]]
linters = [ linters = [
"govet" "govet",
"revive",
] ]
# we want to enable almost all govet rules. It is easier to just filter out path = "_test.go"
# the ones we don't want: text = "fieldalignment:"
#
# * fieldalignment - way too noisy. Although it is very useful in particular
# cases where we are trying to use as little memory as possible, having
# it go off on every struct isn't helpful.
# * shadow - although often useful, it complains about _many_ err
# shadowing assignments and some others where shadowing is clear.
text = "^(fieldalignment|shadow)"

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