CalDAV support (#15)

This commit is contained in:
konrad 2018-11-03 15:05:45 +00:00 committed by Gitea
parent 31a4a1dd00
commit d03fca801b
59 changed files with 2192 additions and 998 deletions

View file

@ -236,6 +236,11 @@ Teams sind global, d.h. Ein Team kann mehrere Namespaces verwalten.
* [x] Email-Verifizierung beim Registrieren * [x] Email-Verifizierung beim Registrieren
* [x] Password Reset -> Link via email oder so * [x] Password Reset -> Link via email oder so
* [ ] Settings * [ ] Settings
* [ ] CalDAV
* [x] Basics
* [ ] Reminders
* [ ] Discovery, stichwort PROPFIND
### Later/Nice to have ### Later/Nice to have

View file

@ -105,3 +105,9 @@ GET http://localhost:8080/api/v1/tasks
Authorization: Bearer {{auth_token}} Authorization: Bearer {{auth_token}}
### ###
# Get all pending tasks in caldav
GET http://localhost:8080/api/v1/tasks/caldav
#Authorization: Bearer {{auth_token}}
###

11
go.mod
View file

@ -5,7 +5,7 @@ require (
github.com/BurntSushi/toml v0.3.1 // indirect github.com/BurntSushi/toml v0.3.1 // indirect
github.com/client9/misspell v0.3.4 github.com/client9/misspell v0.3.4
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835 github.com/fzipp/gocyclo v0.0.0-20150627053110-6acd4345c835
github.com/garyburd/redigo v1.6.0 // indirect github.com/garyburd/redigo v1.6.0 // indirect
github.com/go-openapi/analysis v0.17.2 // indirect github.com/go-openapi/analysis v0.17.2 // indirect
@ -32,11 +32,9 @@ require (
github.com/joho/godotenv v1.3.0 // indirect github.com/joho/godotenv v1.3.0 // indirect
github.com/karalabe/xgo v0.0.0-20181007145344-72da7d1d3970 github.com/karalabe/xgo v0.0.0-20181007145344-72da7d1d3970
github.com/kr/pretty v0.1.0 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/labstack/echo v3.1.0+incompatible github.com/labstack/echo v0.0.0-20180911044237-1abaa3049251
github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0 github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec
github.com/lib/pq v1.0.0 // indirect github.com/lib/pq v1.0.0 // indirect
github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd // indirect
github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee // indirect
github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5 // indirect github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5 // indirect
github.com/mattn/go-sqlite3 v1.9.0 github.com/mattn/go-sqlite3 v1.9.0
github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473
@ -44,9 +42,8 @@ require (
github.com/stretchr/testify v1.2.2 github.com/stretchr/testify v1.2.2
github.com/toqueteos/webbrowser v1.1.0 // indirect github.com/toqueteos/webbrowser v1.1.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
github.com/ziutek/mymysql v1.5.4 // indirect github.com/ziutek/mymysql v1.5.4 // indirect
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 golang.org/x/crypto v0.0.0-20180312195533-182114d58262
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 // indirect golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

18
go.sum
View file

@ -10,12 +10,15 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf h1:eg0MeVzs
github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI= github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 h1:BZGp1dbKFjqlGmxEpwkDpCWNxVwEYnUPoncIzLiHlPo= github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 h1:BZGp1dbKFjqlGmxEpwkDpCWNxVwEYnUPoncIzLiHlPo=
github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc=
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a h1:nmYyGtn9AO7FCeZ2tHr1ZWjJAHi6SfGB3o80F8o7EbA= github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a h1:nmYyGtn9AO7FCeZ2tHr1ZWjJAHi6SfGB3o80F8o7EbA=
github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -92,10 +95,16 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo v0.0.0-20180911044237-1abaa3049251 h1:4q++nZ4OEtmbHazhA/7i3T9B+CBWtnHpuMMcW55ZjRk=
github.com/labstack/echo v0.0.0-20180911044237-1abaa3049251/go.mod h1:rWD2DNQgFb1IY9lVYZVLWn2Ko4dyHZ/LpHORyBLP3hI=
github.com/labstack/echo v3.1.0+incompatible h1:kb0CCZ0boaiGsZOqR9E9+GDpQEoIaKClVqqo0+/hzbM= github.com/labstack/echo v3.1.0+incompatible h1:kb0CCZ0boaiGsZOqR9E9+GDpQEoIaKClVqqo0+/hzbM=
github.com/labstack/echo v3.1.0+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s= github.com/labstack/echo v3.1.0+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo v3.2.1+incompatible h1:J2M7YArHx4gi8p/3fDw8tX19SXhBCoRpviyAZSN3I88=
github.com/labstack/echo v3.2.1+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0 h1:kcJPx2Ug9owxOsVfuXPCludLaIudyI57YQd6ocyrO4o= github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0 h1:kcJPx2Ug9owxOsVfuXPCludLaIudyI57YQd6ocyrO4o=
github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4= github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec h1:aYKwS4iCpqxskMuvI8+Byq0CxnnWHO/xuLk2pZJ96tY=
github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
@ -104,8 +113,12 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd h1:eYiiP5pgdf+n78BU5JFWt7yI2bpxW31L/R5Rrk8vLgs= github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd h1:eYiiP5pgdf+n78BU5JFWt7yI2bpxW31L/R5Rrk8vLgs=
github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee h1:tUyoJR5V1TdXnTh9v8c1YAHvDdut2+zkuyUX3gAY/wI= github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee h1:tUyoJR5V1TdXnTh9v8c1YAHvDdut2+zkuyUX3gAY/wI=
github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5 h1:+IPgoz43mdEYG5lrqNcjr3DQpAE38SqHtyx1IsqqQGM= github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5 h1:+IPgoz43mdEYG5lrqNcjr3DQpAE38SqHtyx1IsqqQGM=
github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8= github.com/mattn/go-oci8 v0.0.0-20181011085415-1a014d1384b5/go.mod h1:/M9VLO+lUPmxvoOK2PfWRZ8mTtB4q1Hy9lEGijv9Nr8=
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
@ -131,10 +144,12 @@ github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc= github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc=
github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI= github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/toqueteos/webbrowser v1.1.0 h1:Prj1okiysRgHPoe3B1bOIVxcv+UuSt525BDQmR5W0x0= github.com/toqueteos/webbrowser v1.1.0 h1:Prj1okiysRgHPoe3B1bOIVxcv+UuSt525BDQmR5W0x0=
github.com/toqueteos/webbrowser v1.1.0/go.mod h1:Hqqqmzj8AHn+VlZyVjaRWY20i25hoOZGAABCcg2el4A= github.com/toqueteos/webbrowser v1.1.0/go.mod h1:Hqqqmzj8AHn+VlZyVjaRWY20i25hoOZGAABCcg2el4A=
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
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 v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8= github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
@ -143,10 +158,13 @@ github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 h1:9lP3x0pW80sDI6t1UMSLA4to18W7R7imwAI/sWS9S8Q=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180312195533-182114d58262 h1:1NLVUmR8SQ7cNNA5Vo7ronpXbR+5A+9IwIC/bLE7D8Y=
golang.org/x/crypto v0.0.0-20180312195533-182114d58262/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3 h1:x/bBzNauLQAlE3fLku/xy92Y8QwKX5HZymrMz2IiKFc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE= golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20180312081825-c28acc882ebc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=

95
pkg/caldav/caldav.go Normal file
View file

@ -0,0 +1,95 @@
package caldav
import (
"code.vikunja.io/api/pkg/utils"
"strconv"
"time"
)
// Event holds a single caldav event
type Event struct {
Summary string
Description string
UID string
Alarms []Alarm
TimestampUnix int64
StartUnix int64
EndUnix int64
}
// Alarm holds infos about an alarm from a caldav event
type Alarm struct {
TimeUnix int64
Description string
}
// Config is the caldav calendar config
type Config struct {
Name string
ProdID string
}
// ParseEvents parses an array of caldav events and gives them back as string
func ParseEvents(config *Config, events []*Event) (caldavevents string) {
caldavevents += `BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
X-PUBLISHED-TTL:PT4H
X-WR-CALNAME:` + config.Name + `
PRODID:-//` + config.ProdID + `//EN`
for _, e := range events {
if e.UID == "" {
e.UID = makeCalDavTimeFromUnixTime(e.TimestampUnix) + utils.Sha256(e.Summary)
}
caldavevents += `
BEGIN:VEVENT
UID:` + e.UID + `
SUMMARY:` + e.Summary + `
DESCRIPTION:` + e.Description + `
DTSTAMP:` + makeCalDavTimeFromUnixTime(e.TimestampUnix) + `
DTSTART:` + makeCalDavTimeFromUnixTime(e.StartUnix) + `
DTEND:` + makeCalDavTimeFromUnixTime(e.EndUnix)
for _, a := range e.Alarms {
if a.Description == "" {
a.Description = e.Summary
}
caldavevents += `
BEGIN:VALARM
TRIGGER:` + calcAlarmDateFromReminder(e.StartUnix, a.TimeUnix) + `
ACTION:DISPLAY
DESCRIPTION:` + a.Description + `
END:VALARM`
}
caldavevents += `
END:VEVENT`
}
caldavevents += `
END:VCALENDAR` // Need a line break
return
}
func makeCalDavTimeFromUnixTime(unixtime int64) (caldavtime string) {
tm := time.Unix(unixtime, 0)
return tm.Format("20060102T150405")
}
func calcAlarmDateFromReminder(eventStartUnix, reminderUnix int64) (alarmTime string) {
if eventStartUnix > reminderUnix {
alarmTime += `-`
}
alarmTime += `PT`
diff := eventStartUnix - reminderUnix
if diff < 0 { // Make it positive
diff = diff * -1
}
alarmTime += strconv.Itoa(int(diff/60)) + "M"
return
}

View file

@ -161,7 +161,11 @@ type ListTasksDummy struct {
// ReadAll gets all tasks for a user // ReadAll gets all tasks for a user
func (lt *ListTasksDummy) ReadAll(u *User) (interface{}, error) { func (lt *ListTasksDummy) ReadAll(u *User) (interface{}, error) {
return GetTasksByUser(u)
}
//GetTasksByUser returns all tasks for a user
func GetTasksByUser(u *User) (tasks []*ListTask, err error) {
// Get all lists // Get all lists
lists, err := getRawListsForUser(u) lists, err := getRawListsForUser(u)
if err != nil { if err != nil {
@ -175,7 +179,6 @@ func (lt *ListTasksDummy) ReadAll(u *User) (interface{}, error) {
} }
// Then return all tasks for that lists // Then return all tasks for that lists
var tasks []*ListTask
if err := x.In("list_id", listIDs).Find(&tasks); err != nil { if err := x.In("list_id", listIDs).Find(&tasks); err != nil {
return nil, err return nil, err
} }

View file

@ -0,0 +1,74 @@
package v1
import (
"code.vikunja.io/api/pkg/caldav"
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/api/pkg/routes/crud"
"github.com/labstack/echo"
"net/http"
"time"
)
// Caldav returns a caldav-readable format with all tasks
func Caldav(c echo.Context) error {
// swagger:operation GET /tasks/caldav list caldavTasks
// ---
// summary: Get all tasks as caldav
// responses:
// "200":
// "$ref": "#/responses/Message"
// "400":
// "$ref": "#/responses/Message"
// "500":
// "$ref": "#/responses/Message"
// Request basic auth
user, pass, ok := c.Request().BasicAuth()
// Check credentials
creds := &models.UserLogin{
Username: user,
Password: pass,
}
u, err := models.CheckUserCredentials(creds)
if !ok || err != nil {
c.Response().Header().Set("WWW-Authenticate", `Basic realm="Vikunja cal"`)
return c.String(http.StatusUnauthorized, "Unauthorized.")
}
// Get all tasks for that user
tasks, err := models.GetTasksByUser(&u)
if err != nil {
return crud.HandleHTTPError(err)
}
hour := int64(time.Hour.Seconds())
var caldavTasks []*caldav.Event
for _, t := range tasks {
if t.DueDateUnix != 0 {
event := &caldav.Event{
Summary: t.Text,
Description: t.Description,
UID: "",
TimestampUnix: t.Updated,
StartUnix: t.DueDateUnix,
EndUnix: t.DueDateUnix + hour,
}
if t.ReminderUnix != 0 {
event.Alarms = append(event.Alarms, caldav.Alarm{TimeUnix: t.ReminderUnix})
}
caldavTasks = append(caldavTasks, event)
}
}
caldavConfig := &caldav.Config{
Name: "Vikunja Calendar for " + u.Username,
ProdID: "Vikunja Todo App",
}
return c.String(http.StatusOK, caldav.ParseEvents(caldavConfig, caldavTasks))
}

View file

@ -41,6 +41,8 @@ import (
func NewEcho() *echo.Echo { func NewEcho() *echo.Echo {
e := echo.New() e := echo.New()
e.HideBanner = true
// Logger // Logger
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
Format: "${time_rfc3339_nano}: ${remote_ip} ${method} ${status} ${uri} ${latency_human} - ${user_agent}\n", Format: "${time_rfc3339_nano}: ${remote_ip} ${method} ${status} ${uri} ${latency_human} - ${user_agent}\n",
@ -69,6 +71,9 @@ func RegisterRoutes(e *echo.Echo) {
a.POST("/user/password/reset", apiv1.UserResetPassword) a.POST("/user/password/reset", apiv1.UserResetPassword)
a.POST("/user/confirm", apiv1.UserConfirmEmail) a.POST("/user/confirm", apiv1.UserConfirmEmail)
// Caldav, with auth
a.GET("/tasks/caldav", apiv1.Caldav)
// ===== Routes with Authetification ===== // ===== Routes with Authetification =====
// Authetification // Authetification
a.Use(middleware.JWT([]byte(viper.GetString("service.JWTSecret")))) a.Use(middleware.JWT([]byte(viper.GetString("service.JWTSecret"))))

11
pkg/utils/sha256.go Normal file
View file

@ -0,0 +1,11 @@
package utils
import (
"crypto/sha256"
"fmt"
)
// Sha256 calculates a sha256 hash from a string
func Sha256(cleartext string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(cleartext)))[:45]
}

View file

@ -1,11 +1,15 @@
A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html) # jwt-go
[![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go) [![Build Status](https://travis-ci.org/dgrijalva/jwt-go.svg?branch=master)](https://travis-ci.org/dgrijalva/jwt-go)
[![GoDoc](https://godoc.org/github.com/dgrijalva/jwt-go?status.svg)](https://godoc.org/github.com/dgrijalva/jwt-go)
**BREAKING CHANGES:*** Version 3.0.0 is here. It includes _a lot_ of changes including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code. A [go](http://www.golang.org) (or 'golang' for search engine friendliness) implementation of [JSON Web Tokens](http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html)
**NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided. **NEW VERSION COMING:** There have been a lot of improvements suggested since the version 3.0.0 released in 2016. I'm working now on cutting two different releases: 3.2.0 will contain any non-breaking changes or enhancements. 4.0.0 will follow shortly which will include breaking changes. See the 4.0.0 milestone to get an idea of what's coming. If you have other ideas, or would like to participate in 4.0.0, now's the time. If you depend on this library and don't want to be interrupted, I recommend you use your dependency mangement tool to pin to version 3.
**SECURITY NOTICE:** Some older versions of Go have a security issue in the cryotp/elliptic. Recommendation is to upgrade to at least 1.8.3. See issue #216 for more detail.
**SECURITY NOTICE:** It's important that you [validate the `alg` presented is what you expect](https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/). This library attempts to make it easy to do the right thing by requiring key types match the expected alg, but you should take the extra step to verify it in your usage. See the examples provided.
## What the heck is a JWT? ## What the heck is a JWT?
@ -47,7 +51,10 @@ This library is considered production ready. Feedback and feature requests are
This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases). This project uses [Semantic Versioning 2.0.0](http://semver.org). Accepted pull requests will land on `master`. Periodically, versions will be tagged from `master`. You can find all the releases on [the project releases page](https://github.com/dgrijalva/jwt-go/releases).
While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v2`. It will do the right thing WRT semantic versioning. While we try to make it obvious when we make breaking changes, there isn't a great mechanism for pushing announcements out to users. You may want to use this alternative package include: `gopkg.in/dgrijalva/jwt-go.v3`. It will do the right thing WRT semantic versioning.
**BREAKING CHANGES:***
* Version 3.0.0 includes _a lot_ of changes from the 2.x line, including a few that break the API. We've tried to break as few things as possible, so there should just be a few type signature changes. A full list of breaking changes is available in `VERSION_HISTORY.md`. See `MIGRATION_GUIDE.md` for more information on updating your code.
## Usage Tips ## Usage Tips
@ -68,6 +75,14 @@ Symmetric signing methods, such as HSA, use only a single secret. This is probab
Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification. Asymmetric signing methods, such as RSA, use different keys for signing and verifying tokens. This makes it possible to produce tokens with a private key, and allow any consumer to access the public key for verification.
### Signing Methods and Key Types
Each signing method expects a different object type for its signing keys. See the package documentation for details. Here are the most common ones:
* The [HMAC signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodHMAC) (`HS256`,`HS384`,`HS512`) expect `[]byte` values for signing and validation
* The [RSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodRSA) (`RS256`,`RS384`,`RS512`) expect `*rsa.PrivateKey` for signing and `*rsa.PublicKey` for validation
* The [ECDSA signing method](https://godoc.org/github.com/dgrijalva/jwt-go#SigningMethodECDSA) (`ES256`,`ES384`,`ES512`) expect `*ecdsa.PrivateKey` for signing and `*ecdsa.PublicKey` for validation
### JWT and OAuth ### JWT and OAuth
It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication. It's worth mentioning that OAuth and JWT are not the same thing. A JWT token is simply a signed JSON object. It can be used anywhere such a thing is useful. There is some confusion, though, as JWT is the most common type of bearer token used in OAuth2 authentication.

View file

@ -1,5 +1,18 @@
## `jwt-go` Version History ## `jwt-go` Version History
#### 3.2.0
* Added method `ParseUnverified` to allow users to split up the tasks of parsing and validation
* HMAC signing method returns `ErrInvalidKeyType` instead of `ErrInvalidKey` where appropriate
* Added options to `request.ParseFromRequest`, which allows for an arbitrary list of modifiers to parsing behavior. Initial set include `WithClaims` and `WithParser`. Existing usage of this function will continue to work as before.
* Deprecated `ParseFromRequestWithClaims` to simplify API in the future.
#### 3.1.0
* Improvements to `jwt` command line tool
* Added `SkipClaimsValidation` option to `Parser`
* Documentation updates
#### 3.0.0 #### 3.0.0
* **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code * **Compatibility Breaking Changes**: See MIGRATION_GUIDE.md for tips on updating your code

View file

@ -14,6 +14,7 @@ var (
) )
// Implements the ECDSA family of signing methods signing methods // Implements the ECDSA family of signing methods signing methods
// Expects *ecdsa.PrivateKey for signing and *ecdsa.PublicKey for verification
type SigningMethodECDSA struct { type SigningMethodECDSA struct {
Name string Name string
Hash crypto.Hash Hash crypto.Hash

View file

@ -7,6 +7,7 @@ import (
) )
// Implements the HMAC-SHA family of signing methods signing methods // Implements the HMAC-SHA family of signing methods signing methods
// Expects key type of []byte for both signing and validation
type SigningMethodHMAC struct { type SigningMethodHMAC struct {
Name string Name string
Hash crypto.Hash Hash crypto.Hash
@ -90,5 +91,5 @@ func (m *SigningMethodHMAC) Sign(signingString string, key interface{}) (string,
return EncodeSegment(hasher.Sum(nil)), nil return EncodeSegment(hasher.Sum(nil)), nil
} }
return "", ErrInvalidKey return "", ErrInvalidKeyType
} }

View file

@ -21,55 +21,9 @@ func (p *Parser) Parse(tokenString string, keyFunc Keyfunc) (*Token, error) {
} }
func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) { func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {
parts := strings.Split(tokenString, ".") token, parts, err := p.ParseUnverified(tokenString, claims)
if len(parts) != 3 {
return nil, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
}
var err error
token := &Token{Raw: tokenString}
// parse Header
var headerBytes []byte
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
return token, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
}
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// parse Claims
var claimBytes []byte
token.Claims = claims
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
if p.UseJSONNumber {
dec.UseNumber()
}
// JSON Decode. Special case for map type to avoid weird pointer behavior
if c, ok := token.Claims.(MapClaims); ok {
err = dec.Decode(&c)
} else {
err = dec.Decode(&claims)
}
// Handle decode error
if err != nil { if err != nil {
return token, &ValidationError{Inner: err, Errors: ValidationErrorMalformed} return token, err
}
// Lookup signature method
if method, ok := token.Header["alg"].(string); ok {
if token.Method = GetSigningMethod(method); token.Method == nil {
return token, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
}
} else {
return token, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
} }
// Verify signing method is in the required set // Verify signing method is in the required set
@ -96,6 +50,9 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
} }
if key, err = keyFunc(token); err != nil { if key, err = keyFunc(token); err != nil {
// keyFunc returned an error // keyFunc returned an error
if ve, ok := err.(*ValidationError); ok {
return token, ve
}
return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable} return token, &ValidationError{Inner: err, Errors: ValidationErrorUnverifiable}
} }
@ -129,3 +86,63 @@ func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyf
return token, vErr return token, vErr
} }
// WARNING: Don't use this method unless you know what you're doing
//
// This method parses the token but doesn't validate the signature. It's only
// ever useful in cases where you know the signature is valid (because it has
// been checked previously in the stack) and you want to extract values from
// it.
func (p *Parser) ParseUnverified(tokenString string, claims Claims) (token *Token, parts []string, err error) {
parts = strings.Split(tokenString, ".")
if len(parts) != 3 {
return nil, parts, NewValidationError("token contains an invalid number of segments", ValidationErrorMalformed)
}
token = &Token{Raw: tokenString}
// parse Header
var headerBytes []byte
if headerBytes, err = DecodeSegment(parts[0]); err != nil {
if strings.HasPrefix(strings.ToLower(tokenString), "bearer ") {
return token, parts, NewValidationError("tokenstring should not contain 'bearer '", ValidationErrorMalformed)
}
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
if err = json.Unmarshal(headerBytes, &token.Header); err != nil {
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// parse Claims
var claimBytes []byte
token.Claims = claims
if claimBytes, err = DecodeSegment(parts[1]); err != nil {
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
dec := json.NewDecoder(bytes.NewBuffer(claimBytes))
if p.UseJSONNumber {
dec.UseNumber()
}
// JSON Decode. Special case for map type to avoid weird pointer behavior
if c, ok := token.Claims.(MapClaims); ok {
err = dec.Decode(&c)
} else {
err = dec.Decode(&claims)
}
// Handle decode error
if err != nil {
return token, parts, &ValidationError{Inner: err, Errors: ValidationErrorMalformed}
}
// Lookup signature method
if method, ok := token.Header["alg"].(string); ok {
if token.Method = GetSigningMethod(method); token.Method == nil {
return token, parts, NewValidationError("signing method (alg) is unavailable.", ValidationErrorUnverifiable)
}
} else {
return token, parts, NewValidationError("signing method (alg) is unspecified.", ValidationErrorUnverifiable)
}
return token, parts, nil
}

View file

@ -7,6 +7,7 @@ import (
) )
// Implements the RSA family of signing methods signing methods // Implements the RSA family of signing methods signing methods
// Expects *rsa.PrivateKey for signing and *rsa.PublicKey for validation
type SigningMethodRSA struct { type SigningMethodRSA struct {
Name string Name string
Hash crypto.Hash Hash crypto.Hash
@ -44,7 +45,7 @@ func (m *SigningMethodRSA) Alg() string {
} }
// Implements the Verify method from SigningMethod // Implements the Verify method from SigningMethod
// For this signing method, must be an rsa.PublicKey structure. // For this signing method, must be an *rsa.PublicKey structure.
func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error { func (m *SigningMethodRSA) Verify(signingString, signature string, key interface{}) error {
var err error var err error
@ -73,7 +74,7 @@ func (m *SigningMethodRSA) Verify(signingString, signature string, key interface
} }
// Implements the Sign method from SigningMethod // Implements the Sign method from SigningMethod
// For this signing method, must be an rsa.PrivateKey structure. // For this signing method, must be an *rsa.PrivateKey structure.
func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) { func (m *SigningMethodRSA) Sign(signingString string, key interface{}) (string, error) {
var rsaKey *rsa.PrivateKey var rsaKey *rsa.PrivateKey
var ok bool var ok bool

View file

@ -39,6 +39,38 @@ func ParseRSAPrivateKeyFromPEM(key []byte) (*rsa.PrivateKey, error) {
return pkey, nil return pkey, nil
} }
// Parse PEM encoded PKCS1 or PKCS8 private key protected with password
func ParseRSAPrivateKeyFromPEMWithPassword(key []byte, password string) (*rsa.PrivateKey, error) {
var err error
// Parse PEM block
var block *pem.Block
if block, _ = pem.Decode(key); block == nil {
return nil, ErrKeyMustBePEMEncoded
}
var parsedKey interface{}
var blockDecrypted []byte
if blockDecrypted, err = x509.DecryptPEMBlock(block, []byte(password)); err != nil {
return nil, err
}
if parsedKey, err = x509.ParsePKCS1PrivateKey(blockDecrypted); err != nil {
if parsedKey, err = x509.ParsePKCS8PrivateKey(blockDecrypted); err != nil {
return nil, err
}
}
var pkey *rsa.PrivateKey
var ok bool
if pkey, ok = parsedKey.(*rsa.PrivateKey); !ok {
return nil, ErrNotRSAPrivateKey
}
return pkey, nil
}
// Parse PEM encoded PKCS1 or PKCS8 public key // Parse PEM encoded PKCS1 or PKCS8 public key
func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) { func ParseRSAPublicKeyFromPEM(key []byte) (*rsa.PublicKey, error) {
var err error var err error

View file

@ -1,8 +1,7 @@
# Website
website/public
# Glide
vendor
.DS_Store .DS_Store
coverage.txt
_test _test
vendor
.idea
*.iml
*.out

View file

@ -1,19 +1,15 @@
language: go language: go
go: go:
- 1.7 - 1.9.x
- 1.8 - 1.10.x
- 1.11.x
- tip - tip
install: install:
- go get golang.org/x/tools/cmd/cover - make dependency
- go get github.com/Masterminds/glide
- go get github.com/mattn/goveralls
- go get github.com/modocache/gover
- glide install
script: script:
- go test -coverprofile=echo.coverprofile - make test
- go test -coverprofile=middleware.coverprofile ./middleware after_success:
- gover - bash <(curl -s https://codecov.io/bash)
- goveralls -coverprofile=gover.coverprofile -service=travis-ci
matrix: matrix:
allow_failures: allow_failures:
- go: tip - go: tip

114
vendor/github.com/labstack/echo/Gopkg.lock generated vendored Normal file
View file

@ -0,0 +1,114 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec"
name = "github.com/davecgh/go-spew"
packages = ["spew"]
pruneopts = "UT"
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
version = "v1.1.1"
[[projects]]
digest = "1:76dc72490af7174349349838f2fe118996381b31ea83243812a97e5a0fd5ed55"
name = "github.com/dgrijalva/jwt-go"
packages = ["."]
pruneopts = "UT"
revision = "06ea1031745cb8b3dab3f6a236daf2b0aa468b7e"
version = "v3.2.0"
[[projects]]
digest = "1:568171fc14a3d819b112c3e219d351ea7b05e8dad7935c4168c6b3373244a686"
name = "github.com/labstack/gommon"
packages = [
"bytes",
"color",
"log",
"random",
]
pruneopts = "UT"
revision = "2a618302b929cc20862dda3aa6f02f64dbe740dd"
version = "v0.2.7"
[[projects]]
digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67"
name = "github.com/mattn/go-colorable"
packages = ["."]
pruneopts = "UT"
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
version = "v0.0.9"
[[projects]]
digest = "1:0981502f9816113c9c8c4ac301583841855c8cf4da8c72f696b3ebedf6d0e4e5"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
version = "v0.0.4"
[[projects]]
digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe"
name = "github.com/pmezard/go-difflib"
packages = ["difflib"]
pruneopts = "UT"
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
version = "v1.0.0"
[[projects]]
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
name = "github.com/stretchr/testify"
packages = ["assert"]
pruneopts = "UT"
revision = "f35b8ab0b5a2cef36673838d662e249dd9c94686"
version = "v1.2.2"
[[projects]]
digest = "1:c468422f334a6b46a19448ad59aaffdfc0a36b08fdcc1c749a0b29b6453d7e59"
name = "github.com/valyala/bytebufferpool"
packages = ["."]
pruneopts = "UT"
revision = "e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7"
version = "v1.0.0"
[[projects]]
branch = "master"
digest = "1:268b8bce0064e8c057d7b913605459f9a26dcab864c0886a56d196540fbf003f"
name = "github.com/valyala/fasttemplate"
packages = ["."]
pruneopts = "UT"
revision = "dcecefd839c4193db0d35b88ec65b4c12d360ab0"
[[projects]]
branch = "master"
digest = "1:dedf20eb0d3e8d6aa8a4d3d2fae248222b688ed528201995e152cc497899123c"
name = "golang.org/x/crypto"
packages = [
"acme",
"acme/autocert",
]
pruneopts = "UT"
revision = "0e37d006457bf46f9e6692014ba72ef82c33022c"
[[projects]]
branch = "master"
digest = "1:6eb2645d74b43d9c87b51947df39f7c668a4f422cd512053f7f6f75bfaad0197"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "d0be0721c37eeb5299f245a996a483160fc36940"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = [
"github.com/dgrijalva/jwt-go",
"github.com/labstack/gommon/bytes",
"github.com/labstack/gommon/color",
"github.com/labstack/gommon/log",
"github.com/labstack/gommon/random",
"github.com/stretchr/testify/assert",
"github.com/valyala/fasttemplate",
"golang.org/x/crypto/acme/autocert",
]
solver-name = "gps-cdcl"
solver-version = 1

50
vendor/github.com/labstack/echo/Gopkg.toml generated vendored Normal file
View file

@ -0,0 +1,50 @@
# Gopkg.toml example
#
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[[constraint]]
name = "github.com/dgrijalva/jwt-go"
version = "3.2.0"
[[constraint]]
name = "github.com/labstack/gommon"
version = "0.2.7"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.2.2"
[[constraint]]
branch = "master"
name = "github.com/valyala/fasttemplate"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"
[prune]
go-tests = true
unused-packages = true

17
vendor/github.com/labstack/echo/Makefile generated vendored Normal file
View file

@ -0,0 +1,17 @@
DEP_VERSION=0.4.1
dependency:
curl -fsSL -o ${GOPATH}/bin/dep https://github.com/golang/dep/releases/download/v${DEP_VERSION}/dep-linux-amd64
chmod +x ${GOPATH}/bin/dep
dep ensure
test:
echo "" > coverage.txt
for d in $(shell go list ./... | grep -v vendor); do \
go test -race -coverprofile=profile.out -covermode=atomic $$d || exit 1; \
[ -f profile.out ] && cat profile.out >> coverage.txt && rm profile.out; \
done
tag:
@git tag `grep -P '^\tversion = ' echo.go|cut -f2 -d'"'`
@git tag|grep -v ^v

View file

@ -1,4 +1,14 @@
# [Echo](https://echo.labstack.com) [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/echo) [![License](http://img.shields.io/badge/license-mit-blue.svg?style=flat-square)](https://raw.githubusercontent.com/labstack/echo/master/LICENSE) [![Build Status](http://img.shields.io/travis/labstack/echo.svg?style=flat-square)](https://travis-ci.org/labstack/echo) [![Coverage Status](http://img.shields.io/coveralls/labstack/echo.svg?style=flat-square)](https://coveralls.io/r/labstack/echo) [![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo) [![Twitter](https://img.shields.io/badge/twitter-@labstack-55acee.svg?style=flat-square)](https://twitter.com/labstack) <a href="https://echo.labstack.com"><img height="80" src="https://cdn.labstack.com/images/echo-logo.svg"></a>
[![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)](http://godoc.org/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)
[![Codecov](https://img.shields.io/codecov/c/github/labstack/echo.svg?style=flat-square)](https://codecov.io/gh/labstack/echo)
[![Join the chat at https://gitter.im/labstack/echo](https://img.shields.io/badge/gitter-join%20chat-brightgreen.svg?style=flat-square)](https://gitter.im/labstack/echo)
[![Forum](https://img.shields.io/badge/community-forum-00afd1.svg?style=flat-square)](https://forum.labstack.com)
[![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)
## Feature Overview ## Feature Overview
@ -16,18 +26,53 @@
- Automatic TLS via Lets Encrypt - Automatic TLS via Lets Encrypt
- HTTP/2 support - HTTP/2 support
## Performance ## Benchmarks
![Performance](https://i.imgur.com/F2V7TfO.png) Date: 2018/03/15<br>
Source: https://github.com/vishr/web-framework-benchmark<br>
Lower is better!
## [Get Started](https://echo.labstack.com/guide) <img src="https://i.imgur.com/I32VdMJ.png">
## Support Us ## [Guide](https://echo.labstack.com/guide)
- :star: the project ### Example
- [Donate](https://echo.labstack.com/support-echo)
- :earth_americas: spread the word ```go
- [Contribute](#contribute) to the project package main
import (
"net/http"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", hello)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}
// Handler
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
```
## Help
- [Forum](https://forum.labstack.com)
- [Chat](https://gitter.im/labstack/echo)
## Contribute ## Contribute

View file

@ -44,12 +44,11 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
case strings.HasPrefix(ctype, MIMEApplicationJSON): case strings.HasPrefix(ctype, MIMEApplicationJSON):
if err = json.NewDecoder(req.Body).Decode(i); err != nil { if err = json.NewDecoder(req.Body).Decode(i); err != nil {
if ute, ok := err.(*json.UnmarshalTypeError); ok { if ute, ok := err.(*json.UnmarshalTypeError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, offset=%v", ute.Type, ute.Value, ute.Offset)) return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unmarshal type error: expected=%v, got=%v, field=%v, offset=%v", ute.Type, ute.Value, ute.Field, ute.Offset))
} else if se, ok := err.(*json.SyntaxError); ok { } else if se, ok := err.(*json.SyntaxError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error())) return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: offset=%v, error=%v", se.Offset, se.Error()))
} else {
return NewHTTPError(http.StatusBadRequest, err.Error())
} }
return NewHTTPError(http.StatusBadRequest, err.Error())
} }
case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML): case strings.HasPrefix(ctype, MIMEApplicationXML), strings.HasPrefix(ctype, MIMETextXML):
if err = xml.NewDecoder(req.Body).Decode(i); err != nil { if err = xml.NewDecoder(req.Body).Decode(i); err != nil {
@ -57,9 +56,8 @@ func (b *DefaultBinder) Bind(i interface{}, c Context) (err error) {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())) return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error()))
} else if se, ok := err.(*xml.SyntaxError); ok { } else if se, ok := err.(*xml.SyntaxError); ok {
return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: line=%v, error=%v", se.Line, se.Error())) return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Syntax error: line=%v, error=%v", se.Line, se.Error()))
} else {
return NewHTTPError(http.StatusBadRequest, err.Error())
} }
return NewHTTPError(http.StatusBadRequest, err.Error())
} }
case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm): case strings.HasPrefix(ctype, MIMEApplicationForm), strings.HasPrefix(ctype, MIMEMultipartForm):
params, err := c.FormParams() params, err := c.FormParams()
@ -80,7 +78,7 @@ func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag
val := reflect.ValueOf(ptr).Elem() val := reflect.ValueOf(ptr).Elem()
if typ.Kind() != reflect.Struct { if typ.Kind() != reflect.Struct {
return errors.New("Binding element must be a struct") return errors.New("binding element must be a struct")
} }
for i := 0; i < typ.NumField(); i++ { for i := 0; i < typ.NumField(); i++ {
@ -96,14 +94,29 @@ func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag
inputFieldName = typeField.Name inputFieldName = typeField.Name
// If tag is nil, we inspect if the field is a struct. // If tag is nil, we inspect if the field is a struct.
if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct { if _, ok := bindUnmarshaler(structField); !ok && structFieldKind == reflect.Struct {
err := b.bindData(structField.Addr().Interface(), data, tag) if err := b.bindData(structField.Addr().Interface(), data, tag); err != nil {
if err != nil {
return err return err
} }
continue continue
} }
} }
inputValue, exists := data[inputFieldName] inputValue, exists := data[inputFieldName]
if !exists {
// Go json.Unmarshal supports case insensitive binding. However the
// url params are bound case sensitive which is inconsistent. To
// fix this we must check all of the map values in a
// case-insensitive search.
inputFieldName = strings.ToLower(inputFieldName)
for k, v := range data {
if strings.ToLower(k) == inputFieldName {
inputValue = v
exists = true
break
}
}
}
if !exists { if !exists {
continue continue
} }
@ -126,10 +139,9 @@ func (b *DefaultBinder) bindData(ptr interface{}, data map[string][]string, tag
} }
} }
val.Field(i).Set(slice) val.Field(i).Set(slice)
} else { } else if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
return err return err
}
} }
} }
return nil return nil
@ -142,6 +154,8 @@ func setWithProperType(valueKind reflect.Kind, val string, structField reflect.V
} }
switch valueKind { switch valueKind {
case reflect.Ptr:
return setWithProperType(structField.Elem().Kind(), val, structField.Elem())
case reflect.Int: case reflect.Int:
return setIntField(val, 0, structField) return setIntField(val, 0, structField)
case reflect.Int8: case reflect.Int8:

View file

@ -31,6 +31,9 @@ type (
// IsTLS returns true if HTTP connection is TLS otherwise false. // IsTLS returns true if HTTP connection is TLS otherwise false.
IsTLS() bool IsTLS() bool
// IsWebSocket returns true if HTTP connection is WebSocket otherwise false.
IsWebSocket() bool
// Scheme returns the HTTP protocol scheme, `http` or `https`. // Scheme returns the HTTP protocol scheme, `http` or `https`.
Scheme() string Scheme() string
@ -203,6 +206,13 @@ const (
indexPage = "index.html" indexPage = "index.html"
) )
func (c *context) writeContentType(value string) {
header := c.Response().Header()
if header.Get(HeaderContentType) == "" {
header.Set(HeaderContentType, value)
}
}
func (c *context) Request() *http.Request { func (c *context) Request() *http.Request {
return c.request return c.request
} }
@ -219,12 +229,29 @@ func (c *context) IsTLS() bool {
return c.request.TLS != nil return c.request.TLS != nil
} }
func (c *context) IsWebSocket() bool {
upgrade := c.request.Header.Get(HeaderUpgrade)
return upgrade == "websocket" || upgrade == "Websocket"
}
func (c *context) Scheme() string { func (c *context) Scheme() string {
// Can't use `r.Request.URL.Scheme` // Can't use `r.Request.URL.Scheme`
// See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0 // See: https://groups.google.com/forum/#!topic/golang-nuts/pMUkBlQBDF0
if c.IsTLS() { if c.IsTLS() {
return "https" return "https"
} }
if scheme := c.request.Header.Get(HeaderXForwardedProto); scheme != "" {
return scheme
}
if scheme := c.request.Header.Get(HeaderXForwardedProtocol); scheme != "" {
return scheme
}
if ssl := c.request.Header.Get(HeaderXForwardedSsl); ssl == "on" {
return "https"
}
if scheme := c.request.Header.Get(HeaderXUrlScheme); scheme != "" {
return scheme
}
return "http" return "http"
} }
@ -254,13 +281,6 @@ func (c *context) Param(name string) string {
if n == name { if n == name {
return c.pvalues[i] return c.pvalues[i]
} }
// Param name with aliases
for _, p := range strings.Split(n, ",") {
if p == name {
return c.pvalues[i]
}
}
} }
} }
return "" return ""
@ -385,7 +405,8 @@ func (c *context) String(code int, s string) (err error) {
} }
func (c *context) JSON(code int, i interface{}) (err error) { func (c *context) JSON(code int, i interface{}) (err error) {
if c.echo.Debug { _, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
return c.JSONPretty(code, i, " ") return c.JSONPretty(code, i, " ")
} }
b, err := json.Marshal(i) b, err := json.Marshal(i)
@ -416,7 +437,7 @@ func (c *context) JSONP(code int, callback string, i interface{}) (err error) {
} }
func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) { func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) {
c.response.Header().Set(HeaderContentType, MIMEApplicationJavaScriptCharsetUTF8) c.writeContentType(MIMEApplicationJavaScriptCharsetUTF8)
c.response.WriteHeader(code) c.response.WriteHeader(code)
if _, err = c.response.Write([]byte(callback + "(")); err != nil { if _, err = c.response.Write([]byte(callback + "(")); err != nil {
return return
@ -429,7 +450,8 @@ func (c *context) JSONPBlob(code int, callback string, b []byte) (err error) {
} }
func (c *context) XML(code int, i interface{}) (err error) { func (c *context) XML(code int, i interface{}) (err error) {
if c.echo.Debug { _, pretty := c.QueryParams()["pretty"]
if c.echo.Debug || pretty {
return c.XMLPretty(code, i, " ") return c.XMLPretty(code, i, " ")
} }
b, err := xml.Marshal(i) b, err := xml.Marshal(i)
@ -448,7 +470,7 @@ func (c *context) XMLPretty(code int, i interface{}, indent string) (err error)
} }
func (c *context) XMLBlob(code int, b []byte) (err error) { func (c *context) XMLBlob(code int, b []byte) (err error) {
c.response.Header().Set(HeaderContentType, MIMEApplicationXMLCharsetUTF8) c.writeContentType(MIMEApplicationXMLCharsetUTF8)
c.response.WriteHeader(code) c.response.WriteHeader(code)
if _, err = c.response.Write([]byte(xml.Header)); err != nil { if _, err = c.response.Write([]byte(xml.Header)); err != nil {
return return
@ -458,28 +480,23 @@ func (c *context) XMLBlob(code int, b []byte) (err error) {
} }
func (c *context) Blob(code int, contentType string, b []byte) (err error) { func (c *context) Blob(code int, contentType string, b []byte) (err error) {
c.response.Header().Set(HeaderContentType, contentType) c.writeContentType(contentType)
c.response.WriteHeader(code) c.response.WriteHeader(code)
_, err = c.response.Write(b) _, err = c.response.Write(b)
return return
} }
func (c *context) Stream(code int, contentType string, r io.Reader) (err error) { func (c *context) Stream(code int, contentType string, r io.Reader) (err error) {
c.response.Header().Set(HeaderContentType, contentType) c.writeContentType(contentType)
c.response.WriteHeader(code) c.response.WriteHeader(code)
_, err = io.Copy(c.response, r) _, err = io.Copy(c.response, r)
return return
} }
func (c *context) File(file string) (err error) { func (c *context) File(file string) (err error) {
file, err = url.QueryUnescape(file) // Issue #839
if err != nil {
return
}
f, err := os.Open(file) f, err := os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return NotFoundHandler(c)
} }
defer f.Close() defer f.Close()
@ -488,7 +505,7 @@ func (c *context) File(file string) (err error) {
file = filepath.Join(file, indexPage) file = filepath.Join(file, indexPage)
f, err = os.Open(file) f, err = os.Open(file)
if err != nil { if err != nil {
return ErrNotFound return NotFoundHandler(c)
} }
defer f.Close() defer f.Close()
if fi, err = f.Stat(); err != nil { if fi, err = f.Stat(); err != nil {
@ -499,18 +516,17 @@ func (c *context) File(file string) (err error) {
return return
} }
func (c *context) Attachment(file, name string) (err error) { func (c *context) Attachment(file, name string) error {
return c.contentDisposition(file, name, "attachment") return c.contentDisposition(file, name, "attachment")
} }
func (c *context) Inline(file, name string) (err error) { func (c *context) Inline(file, name string) error {
return c.contentDisposition(file, name, "inline") return c.contentDisposition(file, name, "inline")
} }
func (c *context) contentDisposition(file, name, dispositionType string) (err error) { func (c *context) contentDisposition(file, name, dispositionType string) error {
c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%s", dispositionType, name)) c.response.Header().Set(HeaderContentDisposition, fmt.Sprintf("%s; filename=%q", dispositionType, name))
c.File(file) return c.File(file)
return
} }
func (c *context) NoContent(code int) error { func (c *context) NoContent(code int) error {

View file

@ -38,6 +38,7 @@ package echo
import ( import (
"bytes" "bytes"
stdContext "context"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
@ -45,6 +46,7 @@ import (
stdLog "log" stdLog "log"
"net" "net"
"net/http" "net/http"
"net/url"
"path" "path"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -72,34 +74,36 @@ type (
TLSServer *http.Server TLSServer *http.Server
Listener net.Listener Listener net.Listener
TLSListener net.Listener TLSListener net.Listener
AutoTLSManager autocert.Manager
DisableHTTP2 bool DisableHTTP2 bool
Debug bool Debug bool
HideBanner bool
HidePort bool
HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler HTTPErrorHandler
Binder Binder Binder Binder
Validator Validator Validator Validator
Renderer Renderer Renderer Renderer
AutoTLSManager autocert.Manager
Mutex sync.RWMutex
Logger Logger Logger Logger
} }
// Route contains a handler and information for matching against requests. // Route contains a handler and information for matching against requests.
Route struct { Route struct {
Method string Method string `json:"method"`
Path string Path string `json:"path"`
Handler string 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 { HTTPError struct {
Code int Code int
Message interface{} Message interface{}
Internal error // Stores the error returned by an external dependency
} }
// MiddlewareFunc defines a function to process middleware. // MiddlewareFunc defines a function to process middleware.
MiddlewareFunc func(HandlerFunc) HandlerFunc MiddlewareFunc func(HandlerFunc) HandlerFunc
// HandlerFunc defines a function to server HTTP requests. // HandlerFunc defines a function to serve HTTP requests.
HandlerFunc func(Context) error HandlerFunc func(Context) error
// HTTPErrorHandler is a centralized HTTP error handler. // HTTPErrorHandler is a centralized HTTP error handler.
@ -120,7 +124,7 @@ type (
// i is the interface for Echo and Group. // i is the interface for Echo and Group.
i interface { i interface {
GET(string, HandlerFunc, ...MiddlewareFunc) GET(string, HandlerFunc, ...MiddlewareFunc) *Route
} }
) )
@ -133,6 +137,7 @@ const (
OPTIONS = "OPTIONS" OPTIONS = "OPTIONS"
PATCH = "PATCH" PATCH = "PATCH"
POST = "POST" POST = "POST"
PROPFIND = "PROPFIND"
PUT = "PUT" PUT = "PUT"
TRACE = "TRACE" TRACE = "TRACE"
) )
@ -164,6 +169,7 @@ const (
// Headers // Headers
const ( const (
HeaderAccept = "Accept"
HeaderAcceptEncoding = "Accept-Encoding" HeaderAcceptEncoding = "Accept-Encoding"
HeaderAllow = "Allow" HeaderAllow = "Allow"
HeaderAuthorization = "Authorization" HeaderAuthorization = "Authorization"
@ -179,13 +185,19 @@ const (
HeaderUpgrade = "Upgrade" HeaderUpgrade = "Upgrade"
HeaderVary = "Vary" HeaderVary = "Vary"
HeaderWWWAuthenticate = "WWW-Authenticate" HeaderWWWAuthenticate = "WWW-Authenticate"
HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
HeaderXForwardedFor = "X-Forwarded-For" HeaderXForwardedFor = "X-Forwarded-For"
HeaderXForwardedProto = "X-Forwarded-Proto"
HeaderXForwardedProtocol = "X-Forwarded-Protocol"
HeaderXForwardedSsl = "X-Forwarded-Ssl"
HeaderXUrlScheme = "X-Url-Scheme"
HeaderXHTTPMethodOverride = "X-HTTP-Method-Override"
HeaderXRealIP = "X-Real-IP" HeaderXRealIP = "X-Real-IP"
HeaderXRequestID = "X-Request-ID" HeaderXRequestID = "X-Request-ID"
HeaderXRequestedWith = "X-Requested-With"
HeaderServer = "Server" HeaderServer = "Server"
HeaderOrigin = "Origin" HeaderOrigin = "Origin"
// Access control
HeaderAccessControlRequestMethod = "Access-Control-Request-Method" HeaderAccessControlRequestMethod = "Access-Control-Request-Method"
HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers" HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers"
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin" HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
@ -204,6 +216,22 @@ const (
HeaderXCSRFToken = "X-CSRF-Token" HeaderXCSRFToken = "X-CSRF-Token"
) )
const (
Version = "3.3.6"
website = "https://echo.labstack.com"
// http://patorjk.com/software/taag/#p=display&f=Small%20Slant&t=Echo
banner = `
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ %s
High performance, minimalist Go web framework
%s
____________________________________O/_______
O\
`
)
var ( var (
methods = [...]string{ methods = [...]string{
CONNECT, CONNECT,
@ -213,6 +241,7 @@ var (
OPTIONS, OPTIONS,
PATCH, PATCH,
POST, POST,
PROPFIND,
PUT, PUT,
TRACE, TRACE,
} }
@ -226,10 +255,10 @@ var (
ErrForbidden = NewHTTPError(http.StatusForbidden) ErrForbidden = NewHTTPError(http.StatusForbidden)
ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed) ErrMethodNotAllowed = NewHTTPError(http.StatusMethodNotAllowed)
ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge) ErrStatusRequestEntityTooLarge = NewHTTPError(http.StatusRequestEntityTooLarge)
ErrValidatorNotRegistered = errors.New("Validator not registered") ErrValidatorNotRegistered = errors.New("validator not registered")
ErrRendererNotRegistered = errors.New("Renderer not registered") ErrRendererNotRegistered = errors.New("renderer not registered")
ErrInvalidRedirectCode = errors.New("Invalid redirect status code") ErrInvalidRedirectCode = errors.New("invalid redirect status code")
ErrCookieNotFound = errors.New("Cookie not found") ErrCookieNotFound = errors.New("cookie not found")
) )
// Error handlers // Error handlers
@ -259,7 +288,7 @@ func New() (e *Echo) {
e.TLSServer.Handler = e e.TLSServer.Handler = e
e.HTTPErrorHandler = e.DefaultHTTPErrorHandler e.HTTPErrorHandler = e.DefaultHTTPErrorHandler
e.Binder = &DefaultBinder{} e.Binder = &DefaultBinder{}
e.Logger.SetLevel(log.OFF) e.Logger.SetLevel(log.ERROR)
e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0) e.stdLogger = stdLog.New(e.Logger.Output(), e.Logger.Prefix()+": ", 0)
e.pool.New = func() interface{} { e.pool.New = func() interface{} {
return e.NewContext(nil, nil) return e.NewContext(nil, nil)
@ -296,6 +325,9 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
if he, ok := err.(*HTTPError); ok { if he, ok := err.(*HTTPError); ok {
code = he.Code code = he.Code
msg = he.Message msg = he.Message
if he.Internal != nil {
err = fmt.Errorf("%v, %v", err, he.Internal)
}
} else if e.Debug { } else if e.Debug {
msg = err.Error() msg = err.Error()
} else { } else {
@ -305,20 +337,18 @@ func (e *Echo) DefaultHTTPErrorHandler(err error, c Context) {
msg = Map{"message": msg} msg = Map{"message": msg}
} }
// Send response
if !c.Response().Committed { if !c.Response().Committed {
if c.Request().Method == HEAD { // Issue #608 if c.Request().Method == HEAD { // Issue #608
if err := c.NoContent(code); err != nil { err = c.NoContent(code)
goto ERROR
}
} else { } else {
if err := c.JSON(code, msg); err != nil { err = c.JSON(code, msg)
goto ERROR
} }
} if err != nil {
}
ERROR:
e.Logger.Error(err) e.Logger.Error(err)
} }
}
}
// Pre adds middleware to the chain which is run before router. // Pre adds middleware to the chain which is run before router.
func (e *Echo) Pre(middleware ...MiddlewareFunc) { func (e *Echo) Pre(middleware ...MiddlewareFunc) {
@ -332,104 +362,114 @@ func (e *Echo) Use(middleware ...MiddlewareFunc) {
// CONNECT registers a new CONNECT route for a path with matching handler in the // CONNECT registers a new CONNECT route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(CONNECT, path, h, m...) return e.Add(CONNECT, path, h, m...)
} }
// DELETE registers a new DELETE route for a path with matching handler in the router // DELETE registers a new DELETE route for a path with matching handler in the router
// with optional route-level middleware. // with optional route-level middleware.
func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(DELETE, path, h, m...) return e.Add(DELETE, path, h, m...)
} }
// GET registers a new GET route for a path with matching handler in the router // GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware. // with optional route-level middleware.
func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(GET, path, h, m...) return e.Add(GET, path, h, m...)
} }
// HEAD registers a new HEAD route for a path with matching handler in the // HEAD registers a new HEAD route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(HEAD, path, h, m...) return e.Add(HEAD, path, h, m...)
} }
// OPTIONS registers a new OPTIONS route for a path with matching handler in the // OPTIONS registers a new OPTIONS route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(OPTIONS, path, h, m...) return e.Add(OPTIONS, path, h, m...)
} }
// PATCH registers a new PATCH route for a path with matching handler in the // PATCH registers a new PATCH route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(PATCH, path, h, m...) return e.Add(PATCH, path, h, m...)
} }
// POST registers a new POST route for a path with matching handler in the // POST registers a new POST route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(POST, path, h, m...) return e.Add(POST, path, h, m...)
} }
// PUT registers a new PUT route for a path with matching handler in the // PUT registers a new PUT route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(PUT, path, h, m...) return e.Add(PUT, path, h, m...)
} }
// TRACE registers a new TRACE route for a path with matching handler in the // TRACE registers a new TRACE route for a path with matching handler in the
// router with optional route-level middleware. // router with optional route-level middleware.
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
e.add(TRACE, path, h, m...) return e.Add(TRACE, path, h, m...)
} }
// Any registers a new route for all HTTP methods and path with matching handler // Any registers a new route for all HTTP methods and path with matching handler
// in the router with optional route-level middleware. // in the router with optional route-level middleware.
func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (e *Echo) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
e.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = e.Add(m, path, handler, middleware...)
} }
return routes
} }
// Match registers a new route for multiple HTTP methods and path with matching // Match registers a new route for multiple HTTP methods and path with matching
// handler in the router with optional route-level middleware. // handler in the router with optional route-level middleware.
func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (e *Echo) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
e.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = e.Add(m, path, handler, middleware...)
} }
return routes
} }
// Static registers a new route with path prefix to serve static files from the // Static registers a new route with path prefix to serve static files from the
// provided root directory. // provided root directory.
func (e *Echo) Static(prefix, root string) { func (e *Echo) Static(prefix, root string) *Route {
if root == "" { if root == "" {
root = "." // For security we want to restrict to CWD. root = "." // For security we want to restrict to CWD.
} }
static(e, prefix, root) return static(e, prefix, root)
} }
func static(i i, prefix, root string) { func static(i i, prefix, root string) *Route {
h := func(c Context) error { h := func(c Context) error {
name := filepath.Join(root, path.Clean("/"+c.Param("*"))) // "/"+ for security p, err := url.PathUnescape(c.Param("*"))
if err != nil {
return err
}
name := filepath.Join(root, path.Clean("/"+p)) // "/"+ for security
return c.File(name) return c.File(name)
} }
i.GET(prefix, h) i.GET(prefix, h)
if prefix == "/" { if prefix == "/" {
i.GET(prefix+"*", h) return i.GET(prefix+"*", h)
} else {
i.GET(prefix+"/*", h)
}
} }
// File registers a new route with path to serve a static file. return i.GET(prefix+"/*", h)
func (e *Echo) File(path, file string) { }
e.GET(path, func(c Context) error {
// File registers a new route with path to serve a static file with optional route-level middleware.
func (e *Echo) File(path, file string, m ...MiddlewareFunc) *Route {
return e.GET(path, func(c Context) error {
return c.File(file) return c.File(file)
}) }, m...)
} }
func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { // Add registers a new route for an HTTP method and path with matching handler
// in the router with optional route-level middleware.
func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
name := handlerName(handler) name := handlerName(handler)
e.router.Add(method, path, func(c Context) error { e.router.Add(method, path, func(c Context) error {
h := handler h := handler
@ -439,12 +479,13 @@ func (e *Echo) add(method, path string, handler HandlerFunc, middleware ...Middl
} }
return h(c) return h(c)
}) })
r := Route{ r := &Route{
Method: method, Method: method,
Path: path, Path: path,
Handler: name, Name: name,
} }
e.router.routes[method+path] = r e.router.routes[method+path] = r
return r
} }
// Group creates a new router group with prefix and optional group-level middleware. // Group creates a new router group with prefix and optional group-level middleware.
@ -456,12 +497,22 @@ func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) {
// URI generates a URI from handler. // URI generates a URI from handler.
func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string { func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
name := handlerName(handler)
return e.Reverse(name, params...)
}
// URL is an alias for `URI` function.
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...)
}
// Reverse generates an URL from route name and provided parameters.
func (e *Echo) Reverse(name string, params ...interface{}) string {
uri := new(bytes.Buffer) uri := new(bytes.Buffer)
ln := len(params) ln := len(params)
n := 0 n := 0
name := handlerName(handler)
for _, r := range e.router.routes { for _, r := range e.router.routes {
if r.Handler == name { if r.Name == name {
for i, l := 0, len(r.Path); i < l; i++ { for i, l := 0, len(r.Path); i < l; i++ {
if r.Path[i] == ':' && n < ln { if r.Path[i] == ':' && n < ln {
for ; i < l && r.Path[i] != '/'; i++ { for ; i < l && r.Path[i] != '/'; i++ {
@ -479,14 +530,9 @@ func (e *Echo) URI(handler HandlerFunc, params ...interface{}) string {
return uri.String() return uri.String()
} }
// URL is an alias for `URI` function.
func (e *Echo) URL(h HandlerFunc, params ...interface{}) string {
return e.URI(h, params...)
}
// Routes returns the registered routes. // Routes returns the registered routes.
func (e *Echo) Routes() []Route { func (e *Echo) Routes() []*Route {
routes := []Route{} routes := make([]*Route, 0, len(e.router.routes))
for _, v := range e.router.routes { for _, v := range e.router.routes {
routes = append(routes, v) routes = append(routes, v)
} }
@ -507,39 +553,39 @@ func (e *Echo) ReleaseContext(c Context) {
// ServeHTTP implements `http.Handler` interface, which serves HTTP requests. // ServeHTTP implements `http.Handler` interface, which serves HTTP requests.
func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Acquire lock
e.Mutex.RLock()
defer e.Mutex.RUnlock()
// Acquire context // Acquire context
c := e.pool.Get().(*context) c := e.pool.Get().(*context)
defer e.pool.Put(c)
c.Reset(r, w) c.Reset(r, w)
// Middleware h := NotFoundHandler
h := func(c Context) error {
method := r.Method if e.premiddleware == nil {
path := r.URL.RawPath e.router.Find(r.Method, getPath(r), c)
if path == "" { h = c.Handler()
path = r.URL.Path for i := len(e.middleware) - 1; i >= 0; i-- {
h = e.middleware[i](h)
} }
e.router.Find(method, path, c) } else {
h = func(c Context) error {
e.router.Find(r.Method, getPath(r), c)
h := c.Handler() h := c.Handler()
for i := len(e.middleware) - 1; i >= 0; i-- { for i := len(e.middleware) - 1; i >= 0; i-- {
h = e.middleware[i](h) h = e.middleware[i](h)
} }
return h(c) return h(c)
} }
// Premiddleware
for i := len(e.premiddleware) - 1; i >= 0; i-- { for i := len(e.premiddleware) - 1; i >= 0; i-- {
h = e.premiddleware[i](h) h = e.premiddleware[i](h)
} }
}
// Execute chain // Execute chain
if err := h(c); err != nil { if err := h(c); err != nil {
e.HTTPErrorHandler(err, c) e.HTTPErrorHandler(err, c)
} }
// Release context
e.pool.Put(c)
} }
// Start starts an HTTP server. // Start starts an HTTP server.
@ -565,6 +611,10 @@ func (e *Echo) StartTLS(address string, certFile, keyFile string) (err error) {
// StartAutoTLS starts an HTTPS server using certificates automatically installed from https://letsencrypt.org. // StartAutoTLS starts an HTTPS server using certificates automatically installed from https://letsencrypt.org.
func (e *Echo) StartAutoTLS(address string) error { func (e *Echo) StartAutoTLS(address string) error {
if e.Listener == nil {
go http.ListenAndServe(":http", e.AutoTLSManager.HTTPHandler(nil))
}
s := e.TLSServer s := e.TLSServer
s.TLSConfig = new(tls.Config) s.TLSConfig = new(tls.Config)
s.TLSConfig.GetCertificate = e.AutoTLSManager.GetCertificate s.TLSConfig.GetCertificate = e.AutoTLSManager.GetCertificate
@ -584,8 +634,15 @@ func (e *Echo) startTLS(address string) error {
func (e *Echo) StartServer(s *http.Server) (err error) { func (e *Echo) StartServer(s *http.Server) (err error) {
// Setup // Setup
e.colorer.SetOutput(e.Logger.Output()) e.colorer.SetOutput(e.Logger.Output())
s.Handler = e
s.ErrorLog = e.stdLogger s.ErrorLog = e.stdLogger
s.Handler = e
if e.Debug {
e.Logger.SetLevel(log.DEBUG)
}
if !e.HideBanner {
e.colorer.Printf(banner, e.colorer.Red("v"+Version), e.colorer.Blue(website))
}
if s.TLSConfig == nil { if s.TLSConfig == nil {
if e.Listener == nil { if e.Listener == nil {
@ -594,7 +651,9 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
return err return err
} }
} }
e.colorer.Printf("⇛ http server started on %s\n", e.colorer.Green(e.Listener.Addr())) if !e.HidePort {
e.colorer.Printf("⇨ http server started on %s\n", e.colorer.Green(e.Listener.Addr()))
}
return s.Serve(e.Listener) return s.Serve(e.Listener)
} }
if e.TLSListener == nil { if e.TLSListener == nil {
@ -604,10 +663,30 @@ func (e *Echo) StartServer(s *http.Server) (err error) {
} }
e.TLSListener = tls.NewListener(l, s.TLSConfig) e.TLSListener = tls.NewListener(l, s.TLSConfig)
} }
e.colorer.Printf("⇛ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr())) if !e.HidePort {
e.colorer.Printf("⇨ https server started on %s\n", e.colorer.Green(e.TLSListener.Addr()))
}
return s.Serve(e.TLSListener) return s.Serve(e.TLSListener)
} }
// Close immediately stops the server.
// It internally calls `http.Server#Close()`.
func (e *Echo) Close() error {
if err := e.TLSServer.Close(); err != nil {
return err
}
return e.Server.Close()
}
// Shutdown stops server the gracefully.
// It internally calls `http.Server#Shutdown()`.
func (e *Echo) Shutdown(ctx stdContext.Context) error {
if err := e.TLSServer.Shutdown(ctx); err != nil {
return err
}
return e.Server.Shutdown(ctx)
}
// NewHTTPError creates a new HTTPError instance. // NewHTTPError creates a new HTTPError instance.
func NewHTTPError(code int, message ...interface{}) *HTTPError { func NewHTTPError(code int, message ...interface{}) *HTTPError {
he := &HTTPError{Code: code, Message: http.StatusText(code)} he := &HTTPError{Code: code, Message: http.StatusText(code)}
@ -619,7 +698,7 @@ func NewHTTPError(code int, message ...interface{}) *HTTPError {
// Error makes it compatible with `error` interface. // Error makes it compatible with `error` interface.
func (he *HTTPError) Error() string { func (he *HTTPError) Error() string {
return fmt.Sprintf("code=%d, message=%s", he.Code, he.Message) return fmt.Sprintf("code=%d, message=%v", he.Code, he.Message)
} }
// WrapHandler wraps `http.Handler` into `echo.HandlerFunc`. // WrapHandler wraps `http.Handler` into `echo.HandlerFunc`.
@ -643,6 +722,14 @@ func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc {
} }
} }
func getPath(r *http.Request) string {
path := r.URL.RawPath
if path == "" {
path = r.URL.Path
}
return path
}
func handlerName(h HandlerFunc) string { func handlerName(h HandlerFunc) string {
t := reflect.ValueOf(h).Type() t := reflect.ValueOf(h).Type()
if t.Kind() == reflect.Func { if t.Kind() == reflect.Func {
@ -651,6 +738,11 @@ func handlerName(h HandlerFunc) string {
return t.String() return t.String()
} }
// // PathUnescape is wraps `url.PathUnescape`
// func PathUnescape(s string) (string, error) {
// return url.PathUnescape(s)
// }
// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted
// connections. It's used by ListenAndServe and ListenAndServeTLS so // connections. It's used by ListenAndServe and ListenAndServeTLS so
// dead TCP connections (e.g. closing laptop mid-download) eventually // dead TCP connections (e.g. closing laptop mid-download) eventually

View file

@ -1,25 +0,0 @@
// +build go1.8
package echo
import (
stdContext "context"
)
// Close immediately stops the server.
// It internally calls `http.Server#Close()`.
func (e *Echo) Close() error {
if err := e.TLSServer.Close(); err != nil {
return err
}
return e.Server.Close()
}
// Shutdown stops server the gracefully.
// It internally calls `http.Server#Shutdown()`.
func (e *Echo) Shutdown(ctx stdContext.Context) error {
if err := e.TLSServer.Shutdown(ctx); err != nil {
return err
}
return e.Server.Shutdown(ctx)
}

View file

@ -1,92 +0,0 @@
hash: 3de2a96bbdc145cce325de2a482111b0524cc330f60a4fbc781a08ed3b879e58
updated: 2017-01-28T10:22:00.230111692-08:00
imports:
- name: github.com/daaku/go.zipexe
version: a5fe2436ffcb3236e175e5149162b41cd28bd27d
- name: github.com/dgrijalva/jwt-go
version: a601269ab70c205d26370c16f7c81e9017c14e04
- name: github.com/facebookgo/clock
version: 600d898af40aa09a7a93ecb9265d87b0504b6f03
- name: github.com/facebookgo/grace
version: 5729e484473f52048578af1b80d0008c7024089b
subpackages:
- gracehttp
- gracenet
- name: github.com/facebookgo/httpdown
version: a3b1354551a26449fbe05f5d855937f6e7acbd71
- name: github.com/facebookgo/stats
version: 1b76add642e42c6ffba7211ad7b3939ce654526e
- name: github.com/GeertJohan/go.rice
version: 4bbccbfa39e784796e483270451217d3369ecfbe
subpackages:
- embedded
- name: github.com/golang/protobuf
version: 8ee79997227bf9b34611aee7946ae64735e6fd93
subpackages:
- proto
- name: github.com/gorilla/websocket
version: c36f2fe5c330f0ac404b616b96c438b8616b1aaf
- name: github.com/kardianos/osext
version: c2c54e542fb797ad986b31721e1baedf214ca413
- name: github.com/labstack/gommon
version: f72d3c883f8ea180da8f085dd320804c41332ad1
subpackages:
- bytes
- color
- log
- random
- name: github.com/mattn/go-colorable
version: d228849504861217f796da67fae4f6e347643f15
- name: github.com/mattn/go-isatty
version: 30a891c33c7cde7b02a981314b4228ec99380cca
- name: github.com/tylerb/graceful
version: 0e9129e9c6d47da90dc0c188b26bd7bb1dab53cd
- name: github.com/valyala/bytebufferpool
version: e746df99fe4a3986f4d4f79e13c1e0117ce9c2f7
- name: github.com/valyala/fasttemplate
version: d090d65668a286d9a180d43a19dfdc5dcad8fe88
- name: golang.org/x/crypto
version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd
subpackages:
- acme
- acme/autocert
- name: golang.org/x/net
version: f2499483f923065a842d38eb4c7f1927e6fc6e6d
subpackages:
- context
- context/ctxhttp
- websocket
- name: golang.org/x/sys
version: d75a52659825e75fff6158388dddc6a5b04f9ba5
subpackages:
- unix
- name: google.golang.org/appengine
version: a2c54d2174c17540446e0ced57d9d459af61bc1c
subpackages:
- internal
- internal/app_identity
- internal/base
- internal/datastore
- internal/log
- internal/modules
- internal/remote_api
- name: gopkg.in/mgo.v2
version: 3f83fa5005286a7fe593b055f0d7771a7dce4655
subpackages:
- bson
- internal/json
- internal/sasl
- internal/scram
testImports:
- name: github.com/davecgh/go-spew
version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9
subpackages:
- spew
- name: github.com/pmezard/go-difflib
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
subpackages:
- difflib
- name: github.com/stretchr/testify
version: 2402e8e7a02fc811447d11f881aa9746cdc57983
subpackages:
- assert

View file

@ -1,30 +0,0 @@
package: github.com/labstack/echo
import:
- package: github.com/GeertJohan/go.rice
- package: github.com/dgrijalva/jwt-go
- package: github.com/facebookgo/grace
subpackages:
- gracehttp
- package: github.com/gorilla/websocket
- package: github.com/labstack/gommon
subpackages:
- bytes
- color
- log
- random
- package: github.com/tylerb/graceful
- package: github.com/valyala/fasttemplate
- package: golang.org/x/crypto
subpackages:
- acme/autocert
- package: golang.org/x/net
subpackages:
- websocket
- package: google.golang.org/appengine
- package: gopkg.in/mgo.v2
subpackages:
- bson
testImport:
- package: github.com/stretchr/testify
subpackages:
- assert

15
vendor/github.com/labstack/echo/go.mod generated vendored Normal file
View file

@ -0,0 +1,15 @@
module github.com/labstack/echo
require (
github.com/davecgh/go-spew v1.1.0 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-isatty v0.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.2.1
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a // indirect
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4
golang.org/x/crypto v0.0.0-20180312195533-182114d58262
golang.org/x/sys v0.0.0-20180312081825-c28acc882ebc // indirect
)

22
vendor/github.com/labstack/echo/go.sum generated vendored Normal file
View file

@ -0,0 +1,22 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec h1:aYKwS4iCpqxskMuvI8+Byq0CxnnWHO/xuLk2pZJ96tY=
github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
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/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U=
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a h1:AOcehBWpFhYPYw0ioDTppQzgI8pAAahVCiMSKTp9rbo=
github.com/valyala/bytebufferpool v0.0.0-20160817181652-e746df99fe4a/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
golang.org/x/crypto v0.0.0-20180312195533-182114d58262 h1:1NLVUmR8SQ7cNNA5Vo7ronpXbR+5A+9IwIC/bLE7D8Y=
golang.org/x/crypto v0.0.0-20180312195533-182114d58262/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/sys v0.0.0-20180312081825-c28acc882ebc h1:eCzBKjjvhDDXMoH5l7eA+YK1PEtyJ2QLj3f4IArr+zg=
golang.org/x/sys v0.0.0-20180312081825-c28acc882ebc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

View file

@ -20,73 +20,79 @@ func (g *Group) Use(middleware ...MiddlewareFunc) {
g.middleware = append(g.middleware, middleware...) g.middleware = append(g.middleware, middleware...)
// Allow all requests to reach the group as they might get dropped if router // Allow all requests to reach the group as they might get dropped if router
// doesn't find a match, making none of the group middleware process. // doesn't find a match, making none of the group middleware process.
g.echo.Any(path.Clean(g.prefix+"/*"), func(c Context) error { for _, p := range []string{"", "/*"} {
return ErrNotFound g.echo.Any(path.Clean(g.prefix+p), func(c Context) error {
return NotFoundHandler(c)
}, g.middleware...) }, g.middleware...)
} }
}
// CONNECT implements `Echo#CONNECT()` for sub-routes within the Group. // CONNECT implements `Echo#CONNECT()` for sub-routes within the Group.
func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) CONNECT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(CONNECT, path, h, m...) return g.Add(CONNECT, path, h, m...)
} }
// DELETE implements `Echo#DELETE()` for sub-routes within the Group. // DELETE implements `Echo#DELETE()` for sub-routes within the Group.
func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(DELETE, path, h, m...) return g.Add(DELETE, path, h, m...)
} }
// GET implements `Echo#GET()` for sub-routes within the Group. // GET implements `Echo#GET()` for sub-routes within the Group.
func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(GET, path, h, m...) return g.Add(GET, path, h, m...)
} }
// HEAD implements `Echo#HEAD()` for sub-routes within the Group. // HEAD implements `Echo#HEAD()` for sub-routes within the Group.
func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) HEAD(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(HEAD, path, h, m...) return g.Add(HEAD, path, h, m...)
} }
// OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group. // OPTIONS implements `Echo#OPTIONS()` for sub-routes within the Group.
func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) OPTIONS(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(OPTIONS, path, h, m...) return g.Add(OPTIONS, path, h, m...)
} }
// PATCH implements `Echo#PATCH()` for sub-routes within the Group. // PATCH implements `Echo#PATCH()` for sub-routes within the Group.
func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) PATCH(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(PATCH, path, h, m...) return g.Add(PATCH, path, h, m...)
} }
// POST implements `Echo#POST()` for sub-routes within the Group. // POST implements `Echo#POST()` for sub-routes within the Group.
func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) POST(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(POST, path, h, m...) return g.Add(POST, path, h, m...)
} }
// PUT implements `Echo#PUT()` for sub-routes within the Group. // PUT implements `Echo#PUT()` for sub-routes within the Group.
func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(PUT, path, h, m...) return g.Add(PUT, path, h, m...)
} }
// TRACE implements `Echo#TRACE()` for sub-routes within the Group. // TRACE implements `Echo#TRACE()` for sub-routes within the Group.
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) { func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) *Route {
g.add(TRACE, path, h, m...) return g.Add(TRACE, path, h, m...)
} }
// Any implements `Echo#Any()` for sub-routes within the Group. // Any implements `Echo#Any()` for sub-routes within the Group.
func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (g *Group) Any(path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
g.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...)
} }
return routes
} }
// Match implements `Echo#Match()` for sub-routes within the Group. // Match implements `Echo#Match()` for sub-routes within the Group.
func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { func (g *Group) Match(methods []string, path string, handler HandlerFunc, middleware ...MiddlewareFunc) []*Route {
for _, m := range methods { routes := make([]*Route, len(methods))
g.add(m, path, handler, middleware...) for i, m := range methods {
routes[i] = g.Add(m, path, handler, middleware...)
} }
return routes
} }
// Group creates a new sub-group with prefix and optional sub-group-level middleware. // Group creates a new sub-group with prefix and optional sub-group-level middleware.
func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group { func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) *Group {
m := []MiddlewareFunc{} m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))
m = append(m, g.middleware...) m = append(m, g.middleware...)
m = append(m, middleware...) m = append(m, middleware...)
return g.echo.Group(g.prefix+prefix, m...) return g.echo.Group(g.prefix+prefix, m...)
@ -102,12 +108,13 @@ func (g *Group) File(path, file string) {
g.echo.File(g.prefix+path, file) g.echo.File(g.prefix+path, file)
} }
func (g *Group) add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) { // Add implements `Echo#Add()` for sub-routes within the Group.
func (g *Group) Add(method, path string, handler HandlerFunc, middleware ...MiddlewareFunc) *Route {
// Combine into a new slice to avoid accidentally passing the same slice for // Combine into a new slice to avoid accidentally passing the same slice for
// multiple routes, which would lead to later add() calls overwriting the // multiple routes, which would lead to later add() calls overwriting the
// middleware from earlier calls. // middleware from earlier calls.
m := []MiddlewareFunc{} m := make([]MiddlewareFunc, 0, len(g.middleware)+len(middleware))
m = append(m, g.middleware...) m = append(m, g.middleware...)
m = append(m, middleware...) m = append(m, middleware...)
g.echo.add(method, g.prefix+path, handler, m...) return g.echo.Add(method, g.prefix+path, handler, m...)
} }

View file

@ -3,6 +3,7 @@ package middleware
import ( import (
"encoding/base64" "encoding/base64"
"strconv" "strconv"
"strings"
"github.com/labstack/echo" "github.com/labstack/echo"
) )
@ -23,11 +24,11 @@ type (
} }
// BasicAuthValidator defines a function to validate BasicAuth credentials. // BasicAuthValidator defines a function to validate BasicAuth credentials.
BasicAuthValidator func(string, string, echo.Context) (error, bool) BasicAuthValidator func(string, string, echo.Context) (bool, error)
) )
const ( const (
basic = "Basic" basic = "basic"
defaultRealm = "Restricted" defaultRealm = "Restricted"
) )
@ -54,7 +55,7 @@ func BasicAuth(fn BasicAuthValidator) echo.MiddlewareFunc {
func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc { func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
// Defaults // Defaults
if config.Validator == nil { if config.Validator == nil {
panic("basic-auth middleware requires a validator function") panic("echo: basic-auth middleware requires a validator function")
} }
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultBasicAuthConfig.Skipper config.Skipper = DefaultBasicAuthConfig.Skipper
@ -72,7 +73,7 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
auth := c.Request().Header.Get(echo.HeaderAuthorization) auth := c.Request().Header.Get(echo.HeaderAuthorization)
l := len(basic) l := len(basic)
if len(auth) > l+1 && auth[:l] == basic { if len(auth) > l+1 && strings.ToLower(auth[:l]) == basic {
b, err := base64.StdEncoding.DecodeString(auth[l+1:]) b, err := base64.StdEncoding.DecodeString(auth[l+1:])
if err != nil { if err != nil {
return err return err
@ -81,20 +82,19 @@ func BasicAuthWithConfig(config BasicAuthConfig) echo.MiddlewareFunc {
for i := 0; i < len(cred); i++ { for i := 0; i < len(cred); i++ {
if cred[i] == ':' { if cred[i] == ':' {
// Verify credentials // Verify credentials
err, valid := config.Validator(cred[:i], cred[i+1:], c) valid, err := config.Validator(cred[:i], cred[i+1:], c)
if err != nil { if err != nil {
return err return err
} else if valid { } else if valid {
return next(c) return next(c)
} }
break
} }
} }
} }
realm := "" realm := defaultRealm
if config.Realm == defaultRealm { if config.Realm != defaultRealm {
realm = defaultRealm
} else {
realm = strconv.Quote(config.Realm) realm = strconv.Quote(config.Realm)
} }

111
vendor/github.com/labstack/echo/middleware/body_dump.go generated vendored Normal file
View file

@ -0,0 +1,111 @@
package middleware
import (
"bufio"
"bytes"
"io"
"io/ioutil"
"net"
"net/http"
"github.com/labstack/echo"
)
type (
// BodyDumpConfig defines the config for BodyDump middleware.
BodyDumpConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Handler receives request and response payload.
// Required.
Handler BodyDumpHandler
}
// BodyDumpHandler receives the request and response payload.
BodyDumpHandler func(echo.Context, []byte, []byte)
bodyDumpResponseWriter struct {
io.Writer
http.ResponseWriter
}
)
var (
// DefaultBodyDumpConfig is the default BodyDump middleware config.
DefaultBodyDumpConfig = BodyDumpConfig{
Skipper: DefaultSkipper,
}
)
// BodyDump returns a BodyDump middleware.
//
// BodyLimit middleware captures the request and response payload and calls the
// registered handler.
func BodyDump(handler BodyDumpHandler) echo.MiddlewareFunc {
c := DefaultBodyDumpConfig
c.Handler = handler
return BodyDumpWithConfig(c)
}
// BodyDumpWithConfig returns a BodyDump middleware with config.
// See: `BodyDump()`.
func BodyDumpWithConfig(config BodyDumpConfig) echo.MiddlewareFunc {
// Defaults
if config.Handler == nil {
panic("echo: body-dump middleware requires a handler function")
}
if config.Skipper == nil {
config.Skipper = DefaultBodyDumpConfig.Skipper
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
// Request
reqBody := []byte{}
if c.Request().Body != nil { // Read
reqBody, _ = ioutil.ReadAll(c.Request().Body)
}
c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) // Reset
// Response
resBody := new(bytes.Buffer)
mw := io.MultiWriter(c.Response().Writer, resBody)
writer := &bodyDumpResponseWriter{Writer: mw, ResponseWriter: c.Response().Writer}
c.Response().Writer = writer
if err = next(c); err != nil {
c.Error(err)
}
// Callback
config.Handler(c, reqBody, resBody.Bytes())
return
}
}
}
func (w *bodyDumpResponseWriter) WriteHeader(code int) {
w.ResponseWriter.WriteHeader(code)
}
func (w *bodyDumpResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}
func (w *bodyDumpResponseWriter) Flush() {
w.ResponseWriter.(http.Flusher).Flush()
}
func (w *bodyDumpResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return w.ResponseWriter.(http.Hijacker).Hijack()
}
func (w *bodyDumpResponseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
}

View file

@ -17,7 +17,7 @@ type (
// Maximum allowed size for a request body, it can be specified // Maximum allowed size for a request body, it can be specified
// as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P. // as `4x` or `4xB`, where x is one of the multiple from K, M, G, T or P.
Limit string `json:"limit"` Limit string `yaml:"limit"`
limit int64 limit int64
} }
@ -30,7 +30,7 @@ type (
) )
var ( var (
// DefaultBodyLimitConfig is the default Gzip middleware config. // DefaultBodyLimitConfig is the default BodyLimit middleware config.
DefaultBodyLimitConfig = BodyLimitConfig{ DefaultBodyLimitConfig = BodyLimitConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
} }
@ -60,7 +60,7 @@ func BodyLimitWithConfig(config BodyLimitConfig) echo.MiddlewareFunc {
limit, err := bytes.Parse(config.Limit) limit, err := bytes.Parse(config.Limit)
if err != nil { if err != nil {
panic(fmt.Errorf("invalid body-limit=%s", config.Limit)) panic(fmt.Errorf("echo: invalid body-limit=%s", config.Limit))
} }
config.limit = limit config.limit = limit
pool := limitedReaderPool(config) pool := limitedReaderPool(config)
@ -105,6 +105,7 @@ func (r *limitedReader) Close() error {
func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) { func (r *limitedReader) Reset(reader io.ReadCloser, context echo.Context) {
r.reader = reader r.reader = reader
r.context = context r.context = context
r.read = 0
} }
func limitedReaderPool(c BodyLimitConfig) sync.Pool { func limitedReaderPool(c BodyLimitConfig) sync.Pool {

View file

@ -20,7 +20,7 @@ type (
// Gzip compression level. // Gzip compression level.
// Optional. Default value -1. // Optional. Default value -1.
Level int `json:"level"` Level int `yaml:"level"`
} }
gzipResponseWriter struct { gzipResponseWriter struct {
@ -67,7 +67,7 @@ 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().Add(echo.HeaderContentEncoding, gzipScheme) // Issue #806 res.Header().Set(echo.HeaderContentEncoding, gzipScheme) // Issue #806
rw := res.Writer rw := res.Writer
w, err := gzip.NewWriterLevel(rw, config.Level) w, err := gzip.NewWriterLevel(rw, config.Level)
if err != nil { if err != nil {
@ -98,6 +98,7 @@ func (w *gzipResponseWriter) WriteHeader(code int) {
if code == http.StatusNoContent { // Issue #489 if code == http.StatusNoContent { // Issue #489
w.ResponseWriter.Header().Del(echo.HeaderContentEncoding) w.ResponseWriter.Header().Del(echo.HeaderContentEncoding)
} }
w.Header().Del(echo.HeaderContentLength) // Issue #444
w.ResponseWriter.WriteHeader(code) w.ResponseWriter.WriteHeader(code)
} }

View file

@ -16,34 +16,34 @@ type (
// AllowOrigin defines a list of origins that may access the resource. // AllowOrigin defines a list of origins that may access the resource.
// Optional. Default value []string{"*"}. // Optional. Default value []string{"*"}.
AllowOrigins []string `json:"allow_origins"` AllowOrigins []string `yaml:"allow_origins"`
// AllowMethods defines a list methods allowed when accessing the resource. // AllowMethods defines a list methods allowed when accessing the resource.
// This is used in response to a preflight request. // This is used in response to a preflight request.
// Optional. Default value DefaultCORSConfig.AllowMethods. // Optional. Default value DefaultCORSConfig.AllowMethods.
AllowMethods []string `json:"allow_methods"` AllowMethods []string `yaml:"allow_methods"`
// AllowHeaders defines a list of request headers that can be used when // AllowHeaders defines a list of request headers that can be used when
// making the actual request. This in response to a preflight request. // making the actual request. This in response to a preflight request.
// Optional. Default value []string{}. // Optional. Default value []string{}.
AllowHeaders []string `json:"allow_headers"` AllowHeaders []string `yaml:"allow_headers"`
// AllowCredentials indicates whether or not the response to the request // AllowCredentials indicates whether or not the response to the request
// can be exposed when the credentials flag is true. When used as part of // can be exposed when the credentials flag is true. When used as part of
// a response to a preflight request, this indicates whether or not the // a response to a preflight request, this indicates whether or not the
// actual request can be made using credentials. // actual request can be made using credentials.
// Optional. Default value false. // Optional. Default value false.
AllowCredentials bool `json:"allow_credentials"` AllowCredentials bool `yaml:"allow_credentials"`
// ExposeHeaders defines a whitelist headers that clients are allowed to // ExposeHeaders defines a whitelist headers that clients are allowed to
// access. // access.
// Optional. Default value []string{}. // Optional. Default value []string{}.
ExposeHeaders []string `json:"expose_headers"` ExposeHeaders []string `yaml:"expose_headers"`
// MaxAge indicates how long (in seconds) the results of a preflight request // MaxAge indicates how long (in seconds) the results of a preflight request
// can be cached. // can be cached.
// Optional. Default value 0. // Optional. Default value 0.
MaxAge int `json:"max_age"` MaxAge int `yaml:"max_age"`
} }
) )
@ -94,6 +94,10 @@ func CORSWithConfig(config CORSConfig) echo.MiddlewareFunc {
// Check allowed origins // Check allowed origins
for _, o := range config.AllowOrigins { for _, o := range config.AllowOrigins {
if o == "*" && config.AllowCredentials {
allowOrigin = origin
break
}
if o == "*" || o == origin { if o == "*" || o == origin {
allowOrigin = o allowOrigin = o
break break

View file

@ -18,7 +18,7 @@ type (
Skipper Skipper Skipper Skipper
// TokenLength is the length of the generated token. // TokenLength is the length of the generated token.
TokenLength uint8 `json:"token_length"` TokenLength uint8 `yaml:"token_length"`
// Optional. Default value 32. // Optional. Default value 32.
// TokenLookup is a string in the form of "<source>:<key>" that is used // TokenLookup is a string in the form of "<source>:<key>" that is used
@ -28,35 +28,35 @@ type (
// - "header:<name>" // - "header:<name>"
// - "form:<name>" // - "form:<name>"
// - "query:<name>" // - "query:<name>"
TokenLookup string `json:"token_lookup"` TokenLookup string `yaml:"token_lookup"`
// Context key to store generated CSRF token into context. // Context key to store generated CSRF token into context.
// Optional. Default value "csrf". // Optional. Default value "csrf".
ContextKey string `json:"context_key"` ContextKey string `yaml:"context_key"`
// Name of the CSRF cookie. This cookie will store CSRF token. // Name of the CSRF cookie. This cookie will store CSRF token.
// Optional. Default value "csrf". // Optional. Default value "csrf".
CookieName string `json:"cookie_name"` CookieName string `yaml:"cookie_name"`
// Domain of the CSRF cookie. // Domain of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookieDomain string `json:"cookie_domain"` CookieDomain string `yaml:"cookie_domain"`
// Path of the CSRF cookie. // Path of the CSRF cookie.
// Optional. Default value none. // Optional. Default value none.
CookiePath string `json:"cookie_path"` CookiePath string `yaml:"cookie_path"`
// Max age (in seconds) of the CSRF cookie. // Max age (in seconds) of the CSRF cookie.
// Optional. Default value 86400 (24hr). // Optional. Default value 86400 (24hr).
CookieMaxAge int `json:"cookie_max_age"` CookieMaxAge int `yaml:"cookie_max_age"`
// Indicates if CSRF cookie is secure. // Indicates if CSRF cookie is secure.
// Optional. Default value false. // Optional. Default value false.
CookieSecure bool `json:"cookie_secure"` CookieSecure bool `yaml:"cookie_secure"`
// Indicates if CSRF cookie is HTTP only. // Indicates if CSRF cookie is HTTP only.
// Optional. Default value false. // Optional. Default value false.
CookieHTTPOnly bool `json:"cookie_http_only"` CookieHTTPOnly bool `yaml:"cookie_http_only"`
} }
// csrfTokenExtractor defines a function that takes `echo.Context` and returns // csrfTokenExtractor defines a function that takes `echo.Context` and returns
@ -126,8 +126,8 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
k, err := c.Cookie(config.CookieName) k, err := c.Cookie(config.CookieName)
token := "" token := ""
if err != nil {
// Generate token // Generate token
if err != nil {
token = random.String(config.TokenLength) token = random.String(config.TokenLength)
} else { } else {
// Reuse token // Reuse token
@ -143,7 +143,7 @@ func CSRFWithConfig(config CSRFConfig) echo.MiddlewareFunc {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} }
if !validateCSRFToken(token, clientToken) { if !validateCSRFToken(token, clientToken) {
return echo.NewHTTPError(http.StatusForbidden, "Invalid csrf token") return echo.NewHTTPError(http.StatusForbidden, "invalid csrf token")
} }
} }
@ -187,7 +187,7 @@ func csrfTokenFromForm(param string) csrfTokenExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
token := c.FormValue(param) token := c.FormValue(param)
if token == "" { if token == "" {
return "", errors.New("Missing csrf token in the form parameter") return "", errors.New("missing csrf token in the form parameter")
} }
return token, nil return token, nil
} }
@ -199,7 +199,7 @@ func csrfTokenFromQuery(param string) csrfTokenExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
token := c.QueryParam(param) token := c.QueryParam(param)
if token == "" { if token == "" {
return "", errors.New("Missing csrf token in the query string") return "", errors.New("missing csrf token in the query string")
} }
return token, nil return token, nil
} }

View file

@ -1,7 +1,6 @@
package middleware package middleware
import ( import (
"errors"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@ -17,6 +16,16 @@ type (
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper Skipper
// BeforeFunc defines a function which is executed just before the middleware.
BeforeFunc BeforeFunc
// SuccessHandler defines a function which is executed for a valid token.
SuccessHandler JWTSuccessHandler
// ErrorHandler defines a function which is executed for an invalid token.
// It may be used to define a custom JWT error.
ErrorHandler JWTErrorHandler
// Signing key to validate token. // Signing key to validate token.
// Required. // Required.
SigningKey interface{} SigningKey interface{}
@ -49,6 +58,12 @@ type (
keyFunc jwt.Keyfunc keyFunc jwt.Keyfunc
} }
// JWTSuccessHandler defines a function which is executed for a valid token.
JWTSuccessHandler func(echo.Context)
// JWTErrorHandler defines a function which is executed for an invalid token.
JWTErrorHandler func(error) error
jwtExtractor func(echo.Context) (string, error) jwtExtractor func(echo.Context) (string, error)
) )
@ -57,6 +72,11 @@ const (
AlgorithmHS256 = "HS256" AlgorithmHS256 = "HS256"
) )
// Errors
var (
ErrJWTMissing = echo.NewHTTPError(http.StatusBadRequest, "missing or malformed jwt")
)
var ( var (
// DefaultJWTConfig is the default JWT auth middleware config. // DefaultJWTConfig is the default JWT auth middleware config.
DefaultJWTConfig = JWTConfig{ DefaultJWTConfig = JWTConfig{
@ -77,7 +97,7 @@ var (
// //
// See: https://jwt.io/introduction // See: https://jwt.io/introduction
// See `JWTConfig.TokenLookup` // See `JWTConfig.TokenLookup`
func JWT(key []byte) echo.MiddlewareFunc { func JWT(key interface{}) echo.MiddlewareFunc {
c := DefaultJWTConfig c := DefaultJWTConfig
c.SigningKey = key c.SigningKey = key
return JWTWithConfig(c) return JWTWithConfig(c)
@ -91,7 +111,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
config.Skipper = DefaultJWTConfig.Skipper config.Skipper = DefaultJWTConfig.Skipper
} }
if config.SigningKey == nil { if config.SigningKey == nil {
panic("jwt middleware requires signing key") panic("echo: jwt middleware requires signing key")
} }
if config.SigningMethod == "" { if config.SigningMethod == "" {
config.SigningMethod = DefaultJWTConfig.SigningMethod config.SigningMethod = DefaultJWTConfig.SigningMethod
@ -111,7 +131,7 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
config.keyFunc = func(t *jwt.Token) (interface{}, error) { config.keyFunc = func(t *jwt.Token) (interface{}, error) {
// Check the signing method // Check the signing method
if t.Method.Alg() != config.SigningMethod { if t.Method.Alg() != config.SigningMethod {
return nil, fmt.Errorf("Unexpected jwt signing method=%v", t.Header["alg"]) return nil, fmt.Errorf("unexpected jwt signing method=%v", t.Header["alg"])
} }
return config.SigningKey, nil return config.SigningKey, nil
} }
@ -132,24 +152,42 @@ func JWTWithConfig(config JWTConfig) echo.MiddlewareFunc {
return next(c) return next(c)
} }
if config.BeforeFunc != nil {
config.BeforeFunc(c)
}
auth, err := extractor(c) auth, err := extractor(c)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) if config.ErrorHandler != nil {
return config.ErrorHandler(err)
}
return err
} }
token := new(jwt.Token) token := new(jwt.Token)
// Issue #647, #656 // Issue #647, #656
if _, ok := config.Claims.(jwt.MapClaims); ok { if _, ok := config.Claims.(jwt.MapClaims); ok {
token, err = jwt.Parse(auth, config.keyFunc) token, err = jwt.Parse(auth, config.keyFunc)
} else { } else {
claims := reflect.ValueOf(config.Claims).Interface().(jwt.Claims) t := reflect.ValueOf(config.Claims).Type().Elem()
claims := reflect.New(t).Interface().(jwt.Claims)
token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc) token, err = jwt.ParseWithClaims(auth, claims, config.keyFunc)
} }
if err == nil && token.Valid { if err == nil && token.Valid {
// Store user information from token into context. // Store user information from token into context.
c.Set(config.ContextKey, token) c.Set(config.ContextKey, token)
if config.SuccessHandler != nil {
config.SuccessHandler(c)
}
return next(c) return next(c)
} }
return echo.ErrUnauthorized if config.ErrorHandler != nil {
return config.ErrorHandler(err)
}
return &echo.HTTPError{
Code: http.StatusUnauthorized,
Message: "invalid or expired jwt",
Internal: err,
}
} }
} }
} }
@ -162,7 +200,7 @@ func jwtFromHeader(header string, authScheme string) jwtExtractor {
if len(auth) > l+1 && auth[:l] == authScheme { if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil return auth[l+1:], nil
} }
return "", errors.New("Missing or invalid jwt in the request header") return "", ErrJWTMissing
} }
} }
@ -171,7 +209,7 @@ func jwtFromQuery(param string) jwtExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
token := c.QueryParam(param) token := c.QueryParam(param)
if token == "" { if token == "" {
return "", errors.New("Missing jwt in the query string") return "", ErrJWTMissing
} }
return token, nil return token, nil
} }
@ -182,7 +220,7 @@ func jwtFromCookie(name string) jwtExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
cookie, err := c.Cookie(name) cookie, err := c.Cookie(name)
if err != nil { if err != nil {
return "", errors.New("Missing jwt in the cookie") return "", ErrJWTMissing
} }
return cookie.Value, nil return cookie.Value, nil
} }

View file

@ -20,7 +20,8 @@ type (
// Possible values: // Possible values:
// - "header:<name>" // - "header:<name>"
// - "query:<name>" // - "query:<name>"
KeyLookup string `json:"key_lookup"` // - "form:<name>"
KeyLookup string `yaml:"key_lookup"`
// AuthScheme to be used in the Authorization header. // AuthScheme to be used in the Authorization header.
// Optional. Default value "Bearer". // Optional. Default value "Bearer".
@ -32,7 +33,7 @@ type (
} }
// KeyAuthValidator defines a function to validate KeyAuth credentials. // KeyAuthValidator defines a function to validate KeyAuth credentials.
KeyAuthValidator func(string, echo.Context) (error, bool) KeyAuthValidator func(string, echo.Context) (bool, error)
keyExtractor func(echo.Context) (string, error) keyExtractor func(echo.Context) (string, error)
) )
@ -72,7 +73,7 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
config.KeyLookup = DefaultKeyAuthConfig.KeyLookup config.KeyLookup = DefaultKeyAuthConfig.KeyLookup
} }
if config.Validator == nil { if config.Validator == nil {
panic("key-auth middleware requires a validator function") panic("echo: key-auth middleware requires a validator function")
} }
// Initialize // Initialize
@ -81,6 +82,8 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
switch parts[0] { switch parts[0] {
case "query": case "query":
extractor = keyFromQuery(parts[1]) extractor = keyFromQuery(parts[1])
case "form":
extractor = keyFromForm(parts[1])
} }
return func(next echo.HandlerFunc) echo.HandlerFunc { return func(next echo.HandlerFunc) echo.HandlerFunc {
@ -94,7 +97,7 @@ func KeyAuthWithConfig(config KeyAuthConfig) echo.MiddlewareFunc {
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error()) return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} }
err, valid := config.Validator(key, c) valid, err := config.Validator(key, c)
if err != nil { if err != nil {
return err return err
} else if valid { } else if valid {
@ -111,14 +114,14 @@ func keyFromHeader(header string, authScheme string) keyExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
auth := c.Request().Header.Get(header) auth := c.Request().Header.Get(header)
if auth == "" { if auth == "" {
return "", errors.New("Missing key in request header") return "", errors.New("missing key in request header")
} }
if header == echo.HeaderAuthorization { if header == echo.HeaderAuthorization {
l := len(authScheme) l := len(authScheme)
if len(auth) > l+1 && auth[:l] == authScheme { if len(auth) > l+1 && auth[:l] == authScheme {
return auth[l+1:], nil return auth[l+1:], nil
} }
return "", errors.New("Invalid key in the request header") return "", errors.New("invalid key in the request header")
} }
return auth, nil return auth, nil
} }
@ -129,7 +132,18 @@ func keyFromQuery(param string) keyExtractor {
return func(c echo.Context) (string, error) { return func(c echo.Context) (string, error) {
key := c.QueryParam(param) key := c.QueryParam(param)
if key == "" { if key == "" {
return "", errors.New("Missing key in the query string") return "", errors.New("missing key in the query string")
}
return key, nil
}
}
// keyFromForm returns a `keyExtractor` that extracts key from the form.
func keyFromForm(param string) keyExtractor {
return func(c echo.Context) (string, error) {
key := c.FormValue(param)
if key == "" {
return "", errors.New("missing key in the form")
} }
return key, nil return key, nil
} }

View file

@ -26,15 +26,18 @@ type (
// - time_unix_nano // - time_unix_nano
// - time_rfc3339 // - time_rfc3339
// - time_rfc3339_nano // - time_rfc3339_nano
// - time_custom
// - id (Request ID) // - id (Request ID)
// - remote_ip // - remote_ip
// - uri // - uri
// - host // - host
// - method // - method
// - path // - path
// - protocol
// - referer // - referer
// - user_agent // - user_agent
// - status // - status
// - error
// - latency (In nanoseconds) // - latency (In nanoseconds)
// - latency_human (Human readable) // - latency_human (Human readable)
// - bytes_in (Bytes received) // - bytes_in (Bytes received)
@ -46,7 +49,10 @@ type (
// Example "${remote_ip} ${status}" // Example "${remote_ip} ${status}"
// //
// Optional. Default value DefaultLoggerConfig.Format. // Optional. Default value DefaultLoggerConfig.Format.
Format string `json:"format"` Format string `yaml:"format"`
// Optional. Default value DefaultLoggerConfig.CustomTimeFormat.
CustomTimeFormat string `yaml:"custom_time_format"`
// Output is a writer where logs in JSON format are written. // Output is a writer where logs in JSON format are written.
// Optional. Default value os.Stdout. // Optional. Default value os.Stdout.
@ -63,9 +69,10 @@ var (
DefaultLoggerConfig = LoggerConfig{ DefaultLoggerConfig = LoggerConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}",` + Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}","host":"${host}",` +
`"method":"${method}","uri":"${uri}","status":${status}, "latency":${latency},` + `"method":"${method}","uri":"${uri}","status":${status},"error":"${error}","latency":${latency},` +
`"latency_human":"${latency_human}","bytes_in":${bytes_in},` + `"latency_human":"${latency_human}","bytes_in":${bytes_in},` +
`"bytes_out":${bytes_out}}` + "\n", `"bytes_out":${bytes_out}}` + "\n",
CustomTimeFormat: "2006-01-02 15:04:05.00000",
Output: os.Stdout, Output: os.Stdout,
colorer: color.New(), colorer: color.New(),
} }
@ -126,6 +133,8 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.WriteString(time.Now().Format(time.RFC3339)) return buf.WriteString(time.Now().Format(time.RFC3339))
case "time_rfc3339_nano": case "time_rfc3339_nano":
return buf.WriteString(time.Now().Format(time.RFC3339Nano)) return buf.WriteString(time.Now().Format(time.RFC3339Nano))
case "time_custom":
return buf.WriteString(time.Now().Format(config.CustomTimeFormat))
case "id": case "id":
id := req.Header.Get(echo.HeaderXRequestID) id := req.Header.Get(echo.HeaderXRequestID)
if id == "" { if id == "" {
@ -146,6 +155,8 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
p = "/" p = "/"
} }
return buf.WriteString(p) return buf.WriteString(p)
case "protocol":
return buf.WriteString(req.Proto)
case "referer": case "referer":
return buf.WriteString(req.Referer()) return buf.WriteString(req.Referer())
case "user_agent": case "user_agent":
@ -162,6 +173,10 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
s = config.colorer.Cyan(n) s = config.colorer.Cyan(n)
} }
return buf.WriteString(s) return buf.WriteString(s)
case "error":
if err != nil {
return buf.WriteString(err.Error())
}
case "latency": case "latency":
l := stop.Sub(start) l := stop.Sub(start)
return buf.WriteString(strconv.FormatInt(int64(l), 10)) return buf.WriteString(strconv.FormatInt(int64(l), 10))
@ -183,6 +198,11 @@ func LoggerWithConfig(config LoggerConfig) echo.MiddlewareFunc {
return buf.Write([]byte(c.QueryParam(tag[6:]))) return buf.Write([]byte(c.QueryParam(tag[6:])))
case strings.HasPrefix(tag, "form:"): case strings.HasPrefix(tag, "form:"):
return buf.Write([]byte(c.FormValue(tag[5:]))) return buf.Write([]byte(c.FormValue(tag[5:])))
case strings.HasPrefix(tag, "cookie:"):
cookie, err := c.Cookie(tag[7:])
if err == nil {
return buf.Write([]byte(cookie.Value))
}
} }
} }
return 0, nil return 0, nil

View file

@ -1,13 +1,37 @@
package middleware package middleware
import "github.com/labstack/echo" import (
"regexp"
"strconv"
"strings"
"github.com/labstack/echo"
)
type ( 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 Skipper func(echo.Context) bool
// BeforeFunc defines a function which is executed just before the middleware.
BeforeFunc func(echo.Context)
) )
func captureTokens(pattern *regexp.Regexp, input string) *strings.Replacer {
groups := pattern.FindAllStringSubmatch(input, -1)
if groups == nil {
return nil
}
values := groups[0][1:]
replace := make([]string, 2*len(values))
for i, v := range values {
j := 2 * i
replace[j] = "$" + strconv.Itoa(i+1)
replace[j+1] = v
}
return strings.NewReplacer(replace...)
}
// DefaultSkipper returns false which processes the middleware. // DefaultSkipper returns false which processes the middleware.
func DefaultSkipper(echo.Context) bool { func DefaultSkipper(echo.Context) bool {
return false return false

252
vendor/github.com/labstack/echo/middleware/proxy.go generated vendored Normal file
View file

@ -0,0 +1,252 @@
package middleware
import (
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/http/httputil"
"net/url"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/labstack/echo"
)
// TODO: Handle TLS proxy
type (
// ProxyConfig defines the config for Proxy middleware.
ProxyConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Balancer defines a load balancing technique.
// Required.
Balancer ProxyBalancer
// Rewrite defines URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on.
// Examples:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
Rewrite map[string]string
rewriteRegex map[*regexp.Regexp]string
}
// ProxyTarget defines the upstream target.
ProxyTarget struct {
Name string
URL *url.URL
}
// ProxyBalancer defines an interface to implement a load balancing technique.
ProxyBalancer interface {
AddTarget(*ProxyTarget) bool
RemoveTarget(string) bool
Next() *ProxyTarget
}
commonBalancer struct {
targets []*ProxyTarget
mutex sync.RWMutex
}
// RandomBalancer implements a random load balancing technique.
randomBalancer struct {
*commonBalancer
random *rand.Rand
}
// RoundRobinBalancer implements a round-robin load balancing technique.
roundRobinBalancer struct {
*commonBalancer
i uint32
}
)
var (
// DefaultProxyConfig is the default Proxy middleware config.
DefaultProxyConfig = ProxyConfig{
Skipper: DefaultSkipper,
}
)
func proxyHTTP(t *ProxyTarget) http.Handler {
return httputil.NewSingleHostReverseProxy(t.URL)
}
func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
in, _, err := c.Response().Hijack()
if err != nil {
c.Error(fmt.Errorf("proxy raw, hijack error=%v, url=%s", t.URL, err))
return
}
defer in.Close()
out, err := net.Dial("tcp", t.URL.Host)
if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, dial error=%v, url=%s", t.URL, err))
c.Error(he)
return
}
defer out.Close()
// Write header
err = r.Write(out)
if err != nil {
he := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("proxy raw, request header copy error=%v, url=%s", t.URL, err))
c.Error(he)
return
}
errCh := make(chan error, 2)
cp := func(dst io.Writer, src io.Reader) {
_, err = io.Copy(dst, src)
errCh <- err
}
go cp(out, in)
go cp(in, out)
err = <-errCh
if err != nil && err != io.EOF {
c.Logger().Errorf("proxy raw, copy body error=%v, url=%s", t.URL, err)
}
})
}
// NewRandomBalancer returns a random proxy balancer.
func NewRandomBalancer(targets []*ProxyTarget) ProxyBalancer {
b := &randomBalancer{commonBalancer: new(commonBalancer)}
b.targets = targets
return b
}
// NewRoundRobinBalancer returns a round-robin proxy balancer.
func NewRoundRobinBalancer(targets []*ProxyTarget) ProxyBalancer {
b := &roundRobinBalancer{commonBalancer: new(commonBalancer)}
b.targets = targets
return b
}
// AddTarget adds an upstream target to the list.
func (b *commonBalancer) AddTarget(target *ProxyTarget) bool {
for _, t := range b.targets {
if t.Name == target.Name {
return false
}
}
b.mutex.Lock()
defer b.mutex.Unlock()
b.targets = append(b.targets, target)
return true
}
// RemoveTarget removes an upstream target from the list.
func (b *commonBalancer) RemoveTarget(name string) bool {
b.mutex.Lock()
defer b.mutex.Unlock()
for i, t := range b.targets {
if t.Name == name {
b.targets = append(b.targets[:i], b.targets[i+1:]...)
return true
}
}
return false
}
// Next randomly returns an upstream target.
func (b *randomBalancer) Next() *ProxyTarget {
if b.random == nil {
b.random = rand.New(rand.NewSource(int64(time.Now().Nanosecond())))
}
b.mutex.RLock()
defer b.mutex.RUnlock()
return b.targets[b.random.Intn(len(b.targets))]
}
// Next returns an upstream target using round-robin technique.
func (b *roundRobinBalancer) Next() *ProxyTarget {
b.i = b.i % uint32(len(b.targets))
t := b.targets[b.i]
atomic.AddUint32(&b.i, 1)
return t
}
// Proxy returns a Proxy middleware.
//
// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
c := DefaultProxyConfig
c.Balancer = balancer
return ProxyWithConfig(c)
}
// ProxyWithConfig returns a Proxy middleware with config.
// See: `Proxy()`
func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
// Defaults
if config.Skipper == nil {
config.Skipper = DefaultLoggerConfig.Skipper
}
if config.Balancer == nil {
panic("echo: proxy middleware requires balancer")
}
config.rewriteRegex = map[*regexp.Regexp]string{}
// Initialize
for k, v := range config.Rewrite {
k = strings.Replace(k, "*", "(\\S*)", -1)
config.rewriteRegex[regexp.MustCompile(k)] = v
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
res := c.Response()
tgt := config.Balancer.Next()
// Rewrite
for k, v := range config.rewriteRegex {
replacer := captureTokens(k, req.URL.Path)
if replacer != nil {
req.URL.Path = replacer.Replace(v)
}
}
// Fix header
if req.Header.Get(echo.HeaderXRealIP) == "" {
req.Header.Set(echo.HeaderXRealIP, c.RealIP())
}
if req.Header.Get(echo.HeaderXForwardedProto) == "" {
req.Header.Set(echo.HeaderXForwardedProto, c.Scheme())
}
if c.IsWebSocket() && req.Header.Get(echo.HeaderXForwardedFor) == "" { // For HTTP, it is automatically set by Go HTTP reverse proxy.
req.Header.Set(echo.HeaderXForwardedFor, c.RealIP())
}
// Proxy
switch {
case c.IsWebSocket():
proxyRaw(tgt, c).ServeHTTP(res, req)
case req.Header.Get(echo.HeaderAccept) == "text/event-stream":
default:
proxyHTTP(tgt).ServeHTTP(res, req)
}
return
}
}
}

View file

@ -5,7 +5,6 @@ import (
"runtime" "runtime"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/gommon/color"
) )
type ( type (
@ -16,16 +15,16 @@ type (
// Size of the stack to be printed. // Size of the stack to be printed.
// Optional. Default value 4KB. // Optional. Default value 4KB.
StackSize int `json:"stack_size"` StackSize int `yaml:"stack_size"`
// DisableStackAll disables formatting stack traces of all other goroutines // DisableStackAll disables formatting stack traces of all other goroutines
// into buffer after the trace for the current goroutine. // into buffer after the trace for the current goroutine.
// Optional. Default value false. // Optional. Default value false.
DisableStackAll bool `json:"disable_stack_all"` DisableStackAll bool `yaml:"disable_stack_all"`
// DisablePrintStack disables printing stack trace. // DisablePrintStack disables printing stack trace.
// Optional. Default value as false. // Optional. Default value as false.
DisablePrintStack bool `json:"disable_print_stack"` DisablePrintStack bool `yaml:"disable_print_stack"`
} }
) )
@ -64,17 +63,14 @@ func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
var err error err, ok := r.(error)
switch r := r.(type) { if !ok {
case error:
err = r
default:
err = fmt.Errorf("%v", r) err = fmt.Errorf("%v", r)
} }
stack := make([]byte, config.StackSize) stack := make([]byte, config.StackSize)
length := runtime.Stack(stack, !config.DisableStackAll) length := runtime.Stack(stack, !config.DisableStackAll)
if !config.DisablePrintStack { if !config.DisablePrintStack {
c.Logger().Printf("[%s] %s %s\n", color.Red("PANIC RECOVER"), err, stack[:length]) c.Logger().Printf("[PANIC RECOVER] %v %s\n", err, stack[:length])
} }
c.Error(err) c.Error(err)
} }

View file

@ -6,29 +6,28 @@ import (
"github.com/labstack/echo" "github.com/labstack/echo"
) )
type (
// RedirectConfig defines the config for Redirect middleware. // RedirectConfig defines the config for Redirect middleware.
RedirectConfig struct { type RedirectConfig struct {
// Skipper defines a function to skip middleware. // Skipper defines a function to skip middleware.
Skipper Skipper Skipper
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional. Default value http.StatusMovedPermanently. // Optional. Default value http.StatusMovedPermanently.
Code int `json:"code"` Code int `yaml:"code"`
} }
)
const ( // redirectLogic represents a function that given a scheme, host and uri
www = "www" // can both: 1) determine if redirect is needed (will set ok accordingly) and
) // 2) return the appropriate redirect url.
type redirectLogic func(scheme, host, uri string) (ok bool, url string)
const www = "www"
var (
// DefaultRedirectConfig is the default Redirect middleware config. // DefaultRedirectConfig is the default Redirect middleware config.
DefaultRedirectConfig = RedirectConfig{ var DefaultRedirectConfig = RedirectConfig{
Skipper: DefaultSkipper, Skipper: DefaultSkipper,
Code: http.StatusMovedPermanently, Code: http.StatusMovedPermanently,
} }
)
// HTTPSRedirect redirects http requests to https. // HTTPSRedirect redirects http requests to https.
// For example, http://labstack.com will be redirect to https://labstack.com. // For example, http://labstack.com will be redirect to https://labstack.com.
@ -41,29 +40,12 @@ func HTTPSRedirect() echo.MiddlewareFunc {
// HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSRedirect()`. // See `HTTPSRedirect()`.
func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https"; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = "https://" + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() {
return c.Redirect(config.Code, "https://"+host+uri)
}
return next(c)
}
} }
return
})
} }
// HTTPSWWWRedirect redirects http requests to https www. // HTTPSWWWRedirect redirects http requests to https www.
@ -77,29 +59,12 @@ func HTTPSWWWRedirect() echo.MiddlewareFunc {
// HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSWWWRedirect()`. // See `HTTPSWWWRedirect()`.
func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https" && host[:3] != www; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = "https://www." + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() && host[:3] != www {
return c.Redirect(config.Code, "https://www."+host+uri)
}
return next(c)
}
} }
return
})
} }
// HTTPSNonWWWRedirect redirects http requests to https non www. // HTTPSNonWWWRedirect redirects http requests to https non www.
@ -113,32 +78,15 @@ func HTTPSNonWWWRedirect() echo.MiddlewareFunc {
// HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // HTTPSNonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `HTTPSNonWWWRedirect()`. // See `HTTPSNonWWWRedirect()`.
func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func HTTPSNonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = scheme != "https"; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
host := req.Host
uri := req.RequestURI
if !c.IsTLS() {
if host[:3] == www { if host[:3] == www {
return c.Redirect(config.Code, "https://"+host[4:]+uri) host = host[4:]
}
return c.Redirect(config.Code, "https://"+host+uri)
}
return next(c)
} }
url = "https://" + host + uri
} }
return
})
} }
// WWWRedirect redirects non www requests to www. // WWWRedirect redirects non www requests to www.
@ -152,30 +100,12 @@ func WWWRedirect() echo.MiddlewareFunc {
// WWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // WWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `WWWRedirect()`. // See `WWWRedirect()`.
func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func WWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
// Defaults return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if config.Skipper == nil { if ok = host[:3] != www; ok {
config.Skipper = DefaultTrailingSlashConfig.Skipper url = scheme + "://www." + host + uri
}
if config.Code == 0 {
config.Code = DefaultRedirectConfig.Code
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
scheme := c.Scheme()
host := req.Host
if host[:3] != www {
uri := req.RequestURI
return c.Redirect(config.Code, scheme+"://www."+host+uri)
}
return next(c)
}
} }
return
})
} }
// NonWWWRedirect redirects www requests to non www. // NonWWWRedirect redirects www requests to non www.
@ -189,6 +119,15 @@ func NonWWWRedirect() echo.MiddlewareFunc {
// NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config. // NonWWWRedirectWithConfig returns an HTTPSRedirect middleware with config.
// See `NonWWWRedirect()`. // See `NonWWWRedirect()`.
func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc { func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
return redirect(config, func(scheme, host, uri string) (ok bool, url string) {
if ok = host[:3] == www; ok {
url = scheme + "://" + host[4:] + uri
}
return
})
}
func redirect(config RedirectConfig, cb redirectLogic) echo.MiddlewareFunc {
if config.Skipper == nil { if config.Skipper == nil {
config.Skipper = DefaultTrailingSlashConfig.Skipper config.Skipper = DefaultTrailingSlashConfig.Skipper
} }
@ -202,13 +141,12 @@ func NonWWWRedirectWithConfig(config RedirectConfig) echo.MiddlewareFunc {
return next(c) return next(c)
} }
req := c.Request() req, scheme := c.Request(), c.Scheme()
scheme := c.Scheme()
host := req.Host host := req.Host
if host[:3] == www { if ok, url := cb(scheme, host, req.RequestURI); ok {
uri := req.RequestURI return c.Redirect(config.Code, url)
return c.Redirect(config.Code, scheme+"://"+host[4:]+uri)
} }
return next(c) return next(c)
} }
} }

84
vendor/github.com/labstack/echo/middleware/rewrite.go generated vendored Normal file
View file

@ -0,0 +1,84 @@
package middleware
import (
"regexp"
"strings"
"github.com/labstack/echo"
)
type (
// RewriteConfig defines the config for Rewrite middleware.
RewriteConfig struct {
// Skipper defines a function to skip middleware.
Skipper Skipper
// Rules defines the URL path rewrite rules. The values captured in asterisk can be
// retrieved by index e.g. $1, $2 and so on.
// Example:
// "/old": "/new",
// "/api/*": "/$1",
// "/js/*": "/public/javascripts/$1",
// "/users/*/orders/*": "/user/$1/order/$2",
// Required.
Rules map[string]string `yaml:"rules"`
rulesRegex map[*regexp.Regexp]string
}
)
var (
// DefaultRewriteConfig is the default Rewrite middleware config.
DefaultRewriteConfig = RewriteConfig{
Skipper: DefaultSkipper,
}
)
// Rewrite returns a Rewrite middleware.
//
// Rewrite middleware rewrites the URL path based on the provided rules.
func Rewrite(rules map[string]string) echo.MiddlewareFunc {
c := DefaultRewriteConfig
c.Rules = rules
return RewriteWithConfig(c)
}
// RewriteWithConfig returns a Rewrite middleware with config.
// See: `Rewrite()`.
func RewriteWithConfig(config RewriteConfig) echo.MiddlewareFunc {
// Defaults
if config.Rules == nil {
panic("echo: rewrite middleware requires url path rewrite rules")
}
if config.Skipper == nil {
config.Skipper = DefaultBodyDumpConfig.Skipper
}
config.rulesRegex = map[*regexp.Regexp]string{}
// Initialize
for k, v := range config.Rules {
k = strings.Replace(k, "*", "(.*)", -1)
k = k + "$"
config.rulesRegex[regexp.MustCompile(k)] = v
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (err error) {
if config.Skipper(c) {
return next(c)
}
req := c.Request()
// Rewrite
for k, v := range config.rulesRegex {
replacer := captureTokens(k, req.URL.Path)
if replacer != nil {
req.URL.Path = replacer.Replace(v)
break
}
}
return next(c)
}
}
}

View file

@ -15,12 +15,12 @@ type (
// XSSProtection provides protection against cross-site scripting attack (XSS) // XSSProtection provides protection against cross-site scripting attack (XSS)
// by setting the `X-XSS-Protection` header. // by setting the `X-XSS-Protection` header.
// Optional. Default value "1; mode=block". // Optional. Default value "1; mode=block".
XSSProtection string `json:"xss_protection"` XSSProtection string `yaml:"xss_protection"`
// ContentTypeNosniff provides protection against overriding Content-Type // ContentTypeNosniff provides protection against overriding Content-Type
// header by setting the `X-Content-Type-Options` header. // header by setting the `X-Content-Type-Options` header.
// Optional. Default value "nosniff". // Optional. Default value "nosniff".
ContentTypeNosniff string `json:"content_type_nosniff"` ContentTypeNosniff string `yaml:"content_type_nosniff"`
// XFrameOptions can be used to indicate whether or not a browser should // XFrameOptions can be used to indicate whether or not a browser should
// be allowed to render a page in a <frame>, <iframe> or <object> . // be allowed to render a page in a <frame>, <iframe> or <object> .
@ -32,27 +32,27 @@ type (
// - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself. // - "SAMEORIGIN" - The page can only be displayed in a frame on the same origin as the page itself.
// - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so. // - "DENY" - The page cannot be displayed in a frame, regardless of the site attempting to do so.
// - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin. // - "ALLOW-FROM uri" - The page can only be displayed in a frame on the specified origin.
XFrameOptions string `json:"x_frame_options"` XFrameOptions string `yaml:"x_frame_options"`
// HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how // HSTSMaxAge sets the `Strict-Transport-Security` header to indicate how
// long (in seconds) browsers should remember that this site is only to // long (in seconds) browsers should remember that this site is only to
// be accessed using HTTPS. This reduces your exposure to some SSL-stripping // be accessed using HTTPS. This reduces your exposure to some SSL-stripping
// man-in-the-middle (MITM) attacks. // man-in-the-middle (MITM) attacks.
// Optional. Default value 0. // Optional. Default value 0.
HSTSMaxAge int `json:"hsts_max_age"` HSTSMaxAge int `yaml:"hsts_max_age"`
// HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security` // HSTSExcludeSubdomains won't include subdomains tag in the `Strict Transport Security`
// header, excluding all subdomains from security policy. It has no effect // header, excluding all subdomains from security policy. It has no effect
// unless HSTSMaxAge is set to a non-zero value. // unless HSTSMaxAge is set to a non-zero value.
// Optional. Default value false. // Optional. Default value false.
HSTSExcludeSubdomains bool `json:"hsts_exclude_subdomains"` HSTSExcludeSubdomains bool `yaml:"hsts_exclude_subdomains"`
// ContentSecurityPolicy sets the `Content-Security-Policy` header providing // ContentSecurityPolicy sets the `Content-Security-Policy` header providing
// security against cross-site scripting (XSS), clickjacking and other code // security against cross-site scripting (XSS), clickjacking and other code
// injection attacks resulting from execution of malicious content in the // injection attacks resulting from execution of malicious content in the
// trusted web page context. // trusted web page context.
// Optional. Default value "". // Optional. Default value "".
ContentSecurityPolicy string `json:"content_security_policy"` ContentSecurityPolicy string `yaml:"content_security_policy"`
} }
) )

View file

@ -12,7 +12,7 @@ type (
// Status code to be used when redirecting the request. // Status code to be used when redirecting the request.
// Optional, but when provided the request is redirected using this code. // Optional, but when provided the request is redirected using this code.
RedirectCode int `json:"redirect_code"` RedirectCode int `yaml:"redirect_code"`
} }
) )

View file

@ -2,12 +2,16 @@ package middleware
import ( import (
"fmt" "fmt"
"html/template"
"net/http"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/labstack/echo" "github.com/labstack/echo"
"github.com/labstack/gommon/bytes"
) )
type ( type (
@ -18,23 +22,95 @@ type (
// Root directory from where the static content is served. // Root directory from where the static content is served.
// Required. // Required.
Root string `json:"root"` Root string `yaml:"root"`
// Index file for serving a directory. // Index file for serving a directory.
// Optional. Default value "index.html". // Optional. Default value "index.html".
Index string `json:"index"` Index string `yaml:"index"`
// Enable HTML5 mode by forwarding all not-found requests to root so that // Enable HTML5 mode by forwarding all not-found requests to root so that
// SPA (single-page application) can handle the routing. // SPA (single-page application) can handle the routing.
// Optional. Default value false. // Optional. Default value false.
HTML5 bool `json:"html5"` HTML5 bool `yaml:"html5"`
// Enable directory browsing. // Enable directory browsing.
// Optional. Default value false. // Optional. Default value false.
Browse bool `json:"browse"` Browse bool `yaml:"browse"`
} }
) )
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ .Name }}</title>
<style>
body {
font-family: Menlo, Consolas, monospace;
padding: 48px;
}
header {
padding: 4px 16px;
font-size: 24px;
}
ul {
list-style-type: none;
margin: 0;
padding: 20px 0 0 0;
display: flex;
flex-wrap: wrap;
}
li {
width: 300px;
padding: 16px;
}
li a {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-decoration: none;
transition: opacity 0.25s;
}
li span {
color: #707070;
font-size: 12px;
}
li a:hover {
opacity: 0.50;
}
.dir {
color: #E91E63;
}
.file {
color: #673AB7;
}
</style>
</head>
<body>
<header>
{{ .Name }}
</header>
<ul>
{{ range .Files }}
<li>
{{ if .Dir }}
{{ $name := print .Name "/" }}
<a class="dir" href="{{ $name }}">{{ $name }}</a>
{{ else }}
<a class="file" href="{{ .Name }}">{{ .Name }}</a>
<span>{{ .Size }}</span>
{{ end }}
</li>
{{ end }}
</ul>
</body>
</html>
`
var ( var (
// DefaultStaticConfig is the default Static middleware config. // DefaultStaticConfig is the default Static middleware config.
DefaultStaticConfig = StaticConfig{ DefaultStaticConfig = StaticConfig{
@ -65,8 +141,14 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
config.Index = DefaultStaticConfig.Index config.Index = DefaultStaticConfig.Index
} }
// Index template
t, err := template.New("index").Parse(html)
if err != nil {
panic(fmt.Sprintf("echo: %v", err))
}
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) (err error) {
if config.Skipper(c) { if config.Skipper(c) {
return next(c) return next(c)
} }
@ -75,17 +157,25 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`. if strings.HasSuffix(c.Path(), "*") { // When serving from a group, e.g. `/static*`.
p = c.Param("*") p = c.Param("*")
} }
p, err = url.PathUnescape(p)
if err != nil {
return
}
name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security name := filepath.Join(config.Root, path.Clean("/"+p)) // "/"+ for security
fi, err := os.Stat(name) fi, err := os.Stat(name)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
if config.HTML5 && path.Ext(p) == "" { if err = next(c); err != nil {
if he, ok := err.(*echo.HTTPError); ok {
if config.HTML5 && he.Code == http.StatusNotFound {
return c.File(filepath.Join(config.Root, config.Index)) return c.File(filepath.Join(config.Root, config.Index))
} }
return next(c)
} }
return err return
}
}
return
} }
if fi.IsDir() { if fi.IsDir() {
@ -94,12 +184,12 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
if err != nil { if err != nil {
if config.Browse { if config.Browse {
return listDir(name, c.Response()) return listDir(t, name, c.Response())
} }
if os.IsNotExist(err) { if os.IsNotExist(err) {
return next(c) return next(c)
} }
return err return
} }
return c.File(index) return c.File(index)
@ -110,32 +200,30 @@ func StaticWithConfig(config StaticConfig) echo.MiddlewareFunc {
} }
} }
func listDir(name string, res *echo.Response) error { func listDir(t *template.Template, name string, res *echo.Response) (err error) {
dir, err := os.Open(name) file, err := os.Open(name)
if err != nil { if err != nil {
return err return
} }
dirs, err := dir.Readdir(-1) files, err := file.Readdir(-1)
if err != nil { if err != nil {
return err return
} }
// Create a directory index // Create directory index
res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8) res.Header().Set(echo.HeaderContentType, echo.MIMETextHTMLCharsetUTF8)
if _, err = fmt.Fprintf(res, "<pre>\n"); err != nil { data := struct {
return err Name string
Files []interface{}
}{
Name: name,
} }
for _, d := range dirs { for _, f := range files {
name := d.Name() data.Files = append(data.Files, struct {
color := "#212121" Name string
if d.IsDir() { Dir bool
color = "#e91e63" Size string
name += "/" }{f.Name(), f.IsDir(), bytes.Format(f.Size())})
} }
if _, err = fmt.Fprintf(res, "<a href=\"%s\" style=\"color: %s;\">%s</a>\n", name, color, name); err != nil { return t.Execute(res, data)
return err
}
}
_, err = fmt.Fprintf(res, "</pre>\n")
return err
} }

View file

@ -11,11 +11,13 @@ type (
// 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 { Response struct {
echo *Echo
beforeFuncs []func()
afterFuncs []func()
Writer http.ResponseWriter Writer http.ResponseWriter
Status int Status int
Size int64 Size int64
Committed bool Committed bool
echo *Echo
} }
) )
@ -34,6 +36,17 @@ func (r *Response) Header() http.Header {
return r.Writer.Header() return r.Writer.Header()
} }
// Before registers a function which is called just before the response is written.
func (r *Response) Before(fn func()) {
r.beforeFuncs = append(r.beforeFuncs, fn)
}
// After registers a function which is called just after the response is written.
// If the `Content-Length` is unknown, none of the after function is executed.
func (r *Response) After(fn func()) {
r.afterFuncs = append(r.afterFuncs, fn)
}
// WriteHeader sends an HTTP response header with status code. If WriteHeader is // WriteHeader sends an HTTP response header with status code. If WriteHeader is
// not called explicitly, the first call to Write will trigger an implicit // not called explicitly, the first call to Write will trigger an implicit
// WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly // WriteHeader(http.StatusOK). Thus explicit calls to WriteHeader are mainly
@ -43,6 +56,9 @@ func (r *Response) WriteHeader(code int) {
r.echo.Logger.Warn("response already committed") r.echo.Logger.Warn("response already committed")
return return
} }
for _, fn := range r.beforeFuncs {
fn()
}
r.Status = code r.Status = code
r.Writer.WriteHeader(code) r.Writer.WriteHeader(code)
r.Committed = true r.Committed = true
@ -55,6 +71,9 @@ func (r *Response) Write(b []byte) (n int, err error) {
} }
n, err = r.Writer.Write(b) n, err = r.Writer.Write(b)
r.Size += int64(n) r.Size += int64(n)
for _, fn := range r.afterFuncs {
fn()
}
return return
} }
@ -82,6 +101,8 @@ func (r *Response) CloseNotify() <-chan bool {
} }
func (r *Response) reset(w http.ResponseWriter) { func (r *Response) reset(w http.ResponseWriter) {
r.beforeFuncs = nil
r.afterFuncs = nil
r.Writer = w r.Writer = w
r.Size = 0 r.Size = 0
r.Status = http.StatusOK r.Status = http.StatusOK

View file

@ -1,13 +1,11 @@
package echo package echo
import "strings"
type ( 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 { Router struct {
tree *node tree *node
routes map[string]Route routes map[string]*Route
echo *Echo echo *Echo
} }
node struct { node struct {
@ -30,6 +28,7 @@ type (
options HandlerFunc options HandlerFunc
patch HandlerFunc patch HandlerFunc
post HandlerFunc post HandlerFunc
propfind HandlerFunc
put HandlerFunc put HandlerFunc
trace HandlerFunc trace HandlerFunc
} }
@ -47,7 +46,7 @@ func NewRouter(e *Echo) *Router {
tree: &node{ tree: &node{
methodHandler: new(methodHandler), methodHandler: new(methodHandler),
}, },
routes: map[string]Route{}, routes: map[string]*Route{},
echo: e, echo: e,
} }
} }
@ -61,8 +60,8 @@ func (r *Router) Add(method, path string, h HandlerFunc) {
if path[0] != '/' { if path[0] != '/' {
path = "/" + path path = "/" + path
} }
ppath := path // Pristine path
pnames := []string{} // Param names pnames := []string{} // Param names
ppath := path // Pristine path
for i, l := 0, len(path); i < l; i++ { for i, l := 0, len(path); i < l; i++ {
if path[i] == ':' { if path[i] == ':' {
@ -101,7 +100,7 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
cn := r.tree // Current node as root cn := r.tree // Current node as root
if cn == nil { if cn == nil {
panic("echo invalid method") panic("echo: invalid method")
} }
search := path search := path
@ -175,12 +174,6 @@ func (r *Router) insert(method, path string, h HandlerFunc, t kind, ppath string
if len(cn.pnames) == 0 { // Issue #729 if len(cn.pnames) == 0 { // Issue #729
cn.pnames = pnames cn.pnames = pnames
} }
for i, n := range pnames {
// Param name aliases
if i < len(cn.pnames) && !strings.Contains(cn.pnames[i], n) {
cn.pnames[i] += "," + n
}
}
} }
} }
return return
@ -233,22 +226,24 @@ func (n *node) findChildByKind(t kind) *node {
func (n *node) addHandler(method string, h HandlerFunc) { func (n *node) addHandler(method string, h HandlerFunc) {
switch method { switch method {
case GET:
n.methodHandler.get = h
case POST:
n.methodHandler.post = h
case PUT:
n.methodHandler.put = h
case DELETE:
n.methodHandler.delete = h
case PATCH:
n.methodHandler.patch = h
case OPTIONS:
n.methodHandler.options = h
case HEAD:
n.methodHandler.head = h
case CONNECT: case CONNECT:
n.methodHandler.connect = h n.methodHandler.connect = h
case DELETE:
n.methodHandler.delete = h
case GET:
n.methodHandler.get = h
case HEAD:
n.methodHandler.head = h
case OPTIONS:
n.methodHandler.options = h
case PATCH:
n.methodHandler.patch = h
case POST:
n.methodHandler.post = h
case PROPFIND:
n.methodHandler.propfind = h
case PUT:
n.methodHandler.put = h
case TRACE: case TRACE:
n.methodHandler.trace = h n.methodHandler.trace = h
} }
@ -256,22 +251,24 @@ func (n *node) addHandler(method string, h HandlerFunc) {
func (n *node) findHandler(method string) HandlerFunc { func (n *node) findHandler(method string) HandlerFunc {
switch method { switch method {
case GET:
return n.methodHandler.get
case POST:
return n.methodHandler.post
case PUT:
return n.methodHandler.put
case DELETE:
return n.methodHandler.delete
case PATCH:
return n.methodHandler.patch
case OPTIONS:
return n.methodHandler.options
case HEAD:
return n.methodHandler.head
case CONNECT: case CONNECT:
return n.methodHandler.connect return n.methodHandler.connect
case DELETE:
return n.methodHandler.delete
case GET:
return n.methodHandler.get
case HEAD:
return n.methodHandler.head
case OPTIONS:
return n.methodHandler.options
case PATCH:
return n.methodHandler.patch
case POST:
return n.methodHandler.post
case PROPFIND:
return n.methodHandler.propfind
case PUT:
return n.methodHandler.put
case TRACE: case TRACE:
return n.methodHandler.trace return n.methodHandler.trace
default: default:
@ -394,7 +391,7 @@ func (r *Router) Find(method, path string, c Context) {
if cn = cn.findChildByKind(akind); cn == nil { if cn = cn.findChildByKind(akind); cn == nil {
if nn != nil { if nn != nil {
cn = nn cn = nn
nn = nil // Next nn = cn.parent // Next (Issue #954)
search = ns search = ns
if nk == pkind { if nk == pkind {
goto Param goto Param

View file

@ -7,12 +7,12 @@ import (
) )
type ( type (
Bytes struct { // Bytes struct
} Bytes struct{}
) )
const ( const (
B = 1 << (10 * iota) _ = 1.0 << (10 * iota) // ignore first value by assigning to blank identifier
KB KB
MB MB
GB GB
@ -22,7 +22,7 @@ const (
) )
var ( var (
pattern = regexp.MustCompile(`(?i)^(-?\d+)([KMGTP]B?|B)$`) pattern = regexp.MustCompile(`(?i)^(-?\d+(?:\.\d+)?)([KMGTPE]B?|B?)$`)
global = New() global = New()
) )
@ -38,29 +38,31 @@ func (*Bytes) Format(b int64) string {
value := float64(b) value := float64(b)
switch { switch {
case b < KB: case b >= EB:
return strconv.FormatInt(b, 10) + "B" value /= EB
case b < MB: multiple = "EB"
value /= KB case b >= PB:
multiple = "KB"
case b < MB:
value /= KB
multiple = "KB"
case b < GB:
value /= MB
multiple = "MB"
case b < TB:
value /= GB
multiple = "GB"
case b < PB:
value /= TB
multiple = "TB"
case b < EB:
value /= PB value /= PB
multiple = "PB" multiple = "PB"
case b >= TB:
value /= TB
multiple = "TB"
case b >= GB:
value /= GB
multiple = "GB"
case b >= MB:
value /= MB
multiple = "MB"
case b >= KB:
value /= KB
multiple = "KB"
case b == 0:
return "0"
default:
return strconv.FormatInt(b, 10) + "B"
} }
return fmt.Sprintf("%.02f%s", value, multiple) return fmt.Sprintf("%.2f%s", value, multiple)
} }
// Parse parses human readable bytes string to bytes integer. // Parse parses human readable bytes string to bytes integer.
@ -72,27 +74,27 @@ func (*Bytes) Parse(value string) (i int64, err error) {
} }
bytesString := parts[1] bytesString := parts[1]
multiple := parts[2] multiple := parts[2]
bytes, err := strconv.ParseInt(bytesString, 10, 64) bytes, err := strconv.ParseFloat(bytesString, 64)
if err != nil { if err != nil {
return return
} }
switch multiple { switch multiple {
case "B": default:
return bytes * B, nil return int64(bytes), nil
case "K", "KB": case "K", "KB":
return bytes * KB, nil return int64(bytes * KB), nil
case "M", "MB": case "M", "MB":
return bytes * MB, nil return int64(bytes * MB), nil
case "G", "GB": case "G", "GB":
return bytes * GB, nil return int64(bytes * GB), nil
case "T", "TB": case "T", "TB":
return bytes * TB, nil return int64(bytes * TB), nil
case "P", "PB": case "P", "PB":
return bytes * PB, nil return int64(bytes * PB), nil
case "E", "EB":
return int64(bytes * EB), nil
} }
return
} }
// Format wraps global Bytes's Format function. // Format wraps global Bytes's Format function.

View file

@ -8,11 +8,10 @@ import (
"os" "os"
"path" "path"
"runtime" "runtime"
"strconv"
"sync" "sync"
"time" "time"
"strconv"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/valyala/fasttemplate" "github.com/valyala/fasttemplate"
@ -343,7 +342,7 @@ func (l *Logger) log(v Lvl, format string, args ...interface{}) {
buf := l.bufferPool.Get().(*bytes.Buffer) buf := l.bufferPool.Get().(*bytes.Buffer)
buf.Reset() buf.Reset()
defer l.bufferPool.Put(buf) defer l.bufferPool.Put(buf)
_, file, line, _ := runtime.Caller(3) _, file, line, _ := runtime.Caller(2)
if v >= l.level || v == 0 { if v >= l.level || v == 0 {
message := "" message := ""

View file

@ -13,7 +13,7 @@ type (
// Charsets // Charsets
const ( const (
Uppercase string = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" Uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
Lowercase = "abcdefghijklmnopqrstuvwxyz" Lowercase = "abcdefghijklmnopqrstuvwxyz"
Alphabetic = Uppercase + Lowercase Alphabetic = Uppercase + Lowercase
Numeric = "0123456789" Numeric = "0123456789"

View file

@ -29,15 +29,6 @@ const (
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity) backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
) )
const (
genericRead = 0x80000000
genericWrite = 0x40000000
)
const (
consoleTextmodeBuffer = 0x1
)
type wchar uint16 type wchar uint16
type short int16 type short int16
type dword uint32 type dword uint32
@ -78,17 +69,14 @@ var (
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo") procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo") procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW") procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW")
procCreateConsoleScreenBuffer = kernel32.NewProc("CreateConsoleScreenBuffer")
) )
// Writer provide colorable Writer to the console // Writer provide colorable Writer to the console
type Writer struct { type Writer struct {
out io.Writer out io.Writer
handle syscall.Handle handle syscall.Handle
althandle syscall.Handle
oldattr word oldattr word
oldpos coord oldpos coord
rest bytes.Buffer
} }
// NewColorable return new instance of Writer which handle escape sequence from File. // NewColorable return new instance of Writer which handle escape sequence from File.
@ -419,18 +407,7 @@ func (w *Writer) Write(data []byte) (n int, err error) {
var csbi consoleScreenBufferInfo var csbi consoleScreenBufferInfo
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
handle := w.handle er := bytes.NewReader(data)
var er *bytes.Reader
if w.rest.Len() > 0 {
var rest bytes.Buffer
w.rest.WriteTo(&rest)
w.rest.Reset()
rest.Write(data)
er = bytes.NewReader(rest.Bytes())
} else {
er = bytes.NewReader(data)
}
var bw [1]byte var bw [1]byte
loop: loop:
for { for {
@ -449,42 +426,28 @@ loop:
} }
if c2 == ']' { if c2 == ']' {
w.rest.WriteByte(c1) if err := doTitleSequence(er); err != nil {
w.rest.WriteByte(c2)
er.WriteTo(&w.rest)
if bytes.IndexByte(w.rest.Bytes(), 0x07) == -1 {
break loop break loop
} }
er = bytes.NewReader(w.rest.Bytes()[2:])
err := doTitleSequence(er)
if err != nil {
break loop
}
w.rest.Reset()
continue continue
} }
if c2 != 0x5b { if c2 != 0x5b {
continue continue
} }
w.rest.WriteByte(c1)
w.rest.WriteByte(c2)
er.WriteTo(&w.rest)
var buf bytes.Buffer var buf bytes.Buffer
var m byte var m byte
for i, c := range w.rest.Bytes()[2:] { for {
c, err := er.ReadByte()
if err != nil {
break loop
}
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
m = c m = c
er = bytes.NewReader(w.rest.Bytes()[2+i+1:])
w.rest.Reset()
break break
} }
buf.Write([]byte(string(c))) buf.Write([]byte(string(c)))
} }
if m == 0 {
break loop
}
switch m { switch m {
case 'A': case 'A':
@ -492,64 +455,61 @@ loop:
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.y -= short(n) csbi.cursorPosition.y -= short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'B': case 'B':
n, err = strconv.Atoi(buf.String()) n, err = strconv.Atoi(buf.String())
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.y += short(n) csbi.cursorPosition.y += short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'C': case 'C':
n, err = strconv.Atoi(buf.String()) n, err = strconv.Atoi(buf.String())
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x += short(n) csbi.cursorPosition.x += short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'D': case 'D':
n, err = strconv.Atoi(buf.String()) n, err = strconv.Atoi(buf.String())
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x -= short(n) csbi.cursorPosition.x -= short(n)
if csbi.cursorPosition.x < 0 { procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
csbi.cursorPosition.x = 0
}
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'E': case 'E':
n, err = strconv.Atoi(buf.String()) n, err = strconv.Atoi(buf.String())
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x = 0 csbi.cursorPosition.x = 0
csbi.cursorPosition.y += short(n) csbi.cursorPosition.y += short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'F': case 'F':
n, err = strconv.Atoi(buf.String()) n, err = strconv.Atoi(buf.String())
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x = 0 csbi.cursorPosition.x = 0
csbi.cursorPosition.y -= short(n) csbi.cursorPosition.y -= short(n)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'G': case 'G':
n, err = strconv.Atoi(buf.String()) n, err = strconv.Atoi(buf.String())
if err != nil { if err != nil {
continue continue
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
csbi.cursorPosition.x = short(n - 1) csbi.cursorPosition.x = short(n - 1)
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'H', 'f': case 'H', 'f':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
if buf.Len() > 0 { if buf.Len() > 0 {
token := strings.Split(buf.String(), ";") token := strings.Split(buf.String(), ";")
switch len(token) { switch len(token) {
@ -574,7 +534,7 @@ loop:
} else { } else {
csbi.cursorPosition.y = 0 csbi.cursorPosition.y = 0
} }
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
case 'J': case 'J':
n := 0 n := 0
if buf.Len() > 0 { if buf.Len() > 0 {
@ -585,7 +545,7 @@ loop:
} }
var count, written dword var count, written dword
var cursor coord var cursor coord
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
switch n { switch n {
case 0: case 0:
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y}
@ -597,8 +557,8 @@ loop:
cursor = coord{x: csbi.window.left, y: csbi.window.top} cursor = coord{x: csbi.window.left, y: csbi.window.top}
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x) count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
} }
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
case 'K': case 'K':
n := 0 n := 0
if buf.Len() > 0 { if buf.Len() > 0 {
@ -607,13 +567,13 @@ loop:
continue continue
} }
} }
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
var cursor coord var cursor coord
var count, written dword var count, written dword
switch n { switch n {
case 0: case 0:
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y} cursor = coord{x: csbi.cursorPosition.x + 1, y: csbi.cursorPosition.y}
count = dword(csbi.size.x - csbi.cursorPosition.x) count = dword(csbi.size.x - csbi.cursorPosition.x - 1)
case 1: case 1:
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
count = dword(csbi.size.x - csbi.cursorPosition.x) count = dword(csbi.size.x - csbi.cursorPosition.x)
@ -621,14 +581,14 @@ loop:
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y} cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
count = dword(csbi.size.x) count = dword(csbi.size.x)
} }
procFillConsoleOutputCharacter.Call(uintptr(handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
procFillConsoleOutputAttribute.Call(uintptr(handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written))) procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
case 'm': case 'm':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
attr := csbi.attributes attr := csbi.attributes
cs := buf.String() cs := buf.String()
if cs == "" { if cs == "" {
procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(w.oldattr)) procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.oldattr))
continue continue
} }
token := strings.Split(cs, ";") token := strings.Split(cs, ";")
@ -667,21 +627,6 @@ loop:
attr |= n256foreAttr[n256] attr |= n256foreAttr[n256]
i += 2 i += 2
} }
} else if len(token) == 5 && token[i+1] == "2" {
var r, g, b int
r, _ = strconv.Atoi(token[i+2])
g, _ = strconv.Atoi(token[i+3])
b, _ = strconv.Atoi(token[i+4])
i += 4
if r > 127 {
attr |= foregroundRed
}
if g > 127 {
attr |= foregroundGreen
}
if b > 127 {
attr |= foregroundBlue
}
} else { } else {
attr = attr & (w.oldattr & backgroundMask) attr = attr & (w.oldattr & backgroundMask)
} }
@ -709,21 +654,6 @@ loop:
attr |= n256backAttr[n256] attr |= n256backAttr[n256]
i += 2 i += 2
} }
} else if len(token) == 5 && token[i+1] == "2" {
var r, g, b int
r, _ = strconv.Atoi(token[i+2])
g, _ = strconv.Atoi(token[i+3])
b, _ = strconv.Atoi(token[i+4])
i += 4
if r > 127 {
attr |= backgroundRed
}
if g > 127 {
attr |= backgroundGreen
}
if b > 127 {
attr |= backgroundBlue
}
} else { } else {
attr = attr & (w.oldattr & foregroundMask) attr = attr & (w.oldattr & foregroundMask)
} }
@ -755,52 +685,38 @@ loop:
attr |= backgroundBlue attr |= backgroundBlue
} }
} }
procSetConsoleTextAttribute.Call(uintptr(handle), uintptr(attr)) procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
} }
} }
case 'h': case 'h':
var ci consoleCursorInfo var ci consoleCursorInfo
cs := buf.String() cs := buf.String()
if cs == "5>" { if cs == "5>" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 0 ci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?25" { } else if cs == "?25" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 1 ci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?1049" {
if w.althandle == 0 {
h, _, _ := procCreateConsoleScreenBuffer.Call(uintptr(genericRead|genericWrite), 0, 0, uintptr(consoleTextmodeBuffer), 0, 0)
w.althandle = syscall.Handle(h)
if w.althandle != 0 {
handle = w.althandle
}
}
} }
case 'l': case 'l':
var ci consoleCursorInfo var ci consoleCursorInfo
cs := buf.String() cs := buf.String()
if cs == "5>" { if cs == "5>" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 1 ci.visible = 1
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?25" { } else if cs == "?25" {
procGetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
ci.visible = 0 ci.visible = 0
procSetConsoleCursorInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&ci))) procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
} else if cs == "?1049" {
if w.althandle != 0 {
syscall.CloseHandle(w.althandle)
w.althandle = 0
handle = w.handle
}
} }
case 's': case 's':
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi))) procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
w.oldpos = csbi.cursorPosition w.oldpos = csbi.cursorPosition
case 'u': case 'u':
procSetConsoleCursorPosition.Call(uintptr(handle), *(*uintptr)(unsafe.Pointer(&w.oldpos))) procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&w.oldpos)))
} }
} }

View file

@ -142,7 +142,7 @@ func (c *Client) Discover(ctx context.Context) (Directory, error) {
// //
// In the case where CA server does not provide the issued certificate in the response, // In the case where CA server does not provide the issued certificate in the response,
// CreateCert will poll certURL using c.FetchCert, which will result in additional round-trips. // CreateCert will poll certURL using c.FetchCert, which will result in additional round-trips.
// In such scenario the caller can cancel the polling with ctx. // In such a scenario, the caller can cancel the polling with ctx.
// //
// CreateCert returns an error if the CA's response or chain was unreasonably large. // CreateCert returns an error if the CA's response or chain was unreasonably large.
// Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features. // Callers are encouraged to parse the returned value to ensure the certificate is valid and has the expected features.
@ -257,7 +257,7 @@ func (c *Client) RevokeCert(ctx context.Context, key crypto.Signer, cert []byte,
func AcceptTOS(tosURL string) bool { return true } func AcceptTOS(tosURL string) bool { return true }
// Register creates a new account registration by following the "new-reg" flow. // Register creates a new account registration by following the "new-reg" flow.
// It returns registered account. The account is not modified. // It returns the registered account. The account is not modified.
// //
// The registration may require the caller to agree to the CA's Terms of Service (TOS). // The registration may require the caller to agree to the CA's Terms of Service (TOS).
// If so, and the account has not indicated the acceptance of the terms (see Account for details), // If so, and the account has not indicated the acceptance of the terms (see Account for details),
@ -400,7 +400,7 @@ func (c *Client) RevokeAuthorization(ctx context.Context, url string) error {
// WaitAuthorization polls an authorization at the given URL // WaitAuthorization polls an authorization at the given URL
// until it is in one of the final states, StatusValid or StatusInvalid, // until it is in one of the final states, StatusValid or StatusInvalid,
// or the context is done. // the ACME CA responded with a 4xx error code, or the context is done.
// //
// It returns a non-nil Authorization only if its Status is StatusValid. // It returns a non-nil Authorization only if its Status is StatusValid.
// In all other cases WaitAuthorization returns an error. // In all other cases WaitAuthorization returns an error.
@ -412,6 +412,13 @@ func (c *Client) WaitAuthorization(ctx context.Context, url string) (*Authorizat
if err != nil { if err != nil {
return nil, err return nil, err
} }
if res.StatusCode >= 400 && res.StatusCode <= 499 {
// Non-retriable error. For instance, Let's Encrypt may return 404 Not Found
// when requesting an expired authorization.
defer res.Body.Close()
return nil, responseError(res)
}
retry := res.Header.Get("Retry-After") retry := res.Header.Get("Retry-After")
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusAccepted { if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusAccepted {
res.Body.Close() res.Body.Close()
@ -995,6 +1002,7 @@ func keyAuth(pub crypto.PublicKey, token string) (string, error) {
// tlsChallengeCert creates a temporary certificate for TLS-SNI challenges // tlsChallengeCert creates a temporary certificate for TLS-SNI challenges
// with the given SANs and auto-generated public/private key pair. // with the given SANs and auto-generated public/private key pair.
// The Subject Common Name is set to the first SAN to aid debugging.
// To create a cert with a custom key pair, specify WithKey option. // To create a cert with a custom key pair, specify WithKey option.
func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) { func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
var ( var (
@ -1033,6 +1041,9 @@ func tlsChallengeCert(san []string, opt []CertOption) (tls.Certificate, error) {
} }
} }
tmpl.DNSNames = san tmpl.DNSNames = san
if len(san) > 0 {
tmpl.Subject.CommonName = san[0]
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key) der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, key.Public(), key)
if err != nil { if err != nil {

View file

@ -24,8 +24,9 @@ import (
"fmt" "fmt"
"io" "io"
mathrand "math/rand" mathrand "math/rand"
"net"
"net/http" "net/http"
"strconv" "path"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -80,8 +81,9 @@ func defaultHostPolicy(context.Context, string) error {
} }
// Manager is a stateful certificate manager built on top of acme.Client. // Manager is a stateful certificate manager built on top of acme.Client.
// It obtains and refreshes certificates automatically, // It obtains and refreshes certificates automatically using "tls-sni-01",
// as well as providing them to a TLS server via tls.Config. // "tls-sni-02" and "http-01" challenge types, as well as providing them
// to a TLS server via tls.Config.
// //
// You must specify a cache implementation, such as DirCache, // You must specify a cache implementation, such as DirCache,
// to reuse obtained certificates across program restarts. // to reuse obtained certificates across program restarts.
@ -150,15 +152,26 @@ type Manager struct {
stateMu sync.Mutex stateMu sync.Mutex
state map[string]*certState // keyed by domain name state map[string]*certState // keyed by domain name
// tokenCert is keyed by token domain name, which matches server name
// of ClientHello. Keys always have ".acme.invalid" suffix.
tokenCertMu sync.RWMutex
tokenCert map[string]*tls.Certificate
// renewal tracks the set of domains currently running renewal timers. // renewal tracks the set of domains currently running renewal timers.
// It is keyed by domain name. // It is keyed by domain name.
renewalMu sync.Mutex renewalMu sync.Mutex
renewal map[string]*domainRenewal renewal map[string]*domainRenewal
// tokensMu guards the rest of the fields: tryHTTP01, certTokens and httpTokens.
tokensMu sync.RWMutex
// tryHTTP01 indicates whether the Manager should try "http-01" challenge type
// during the authorization flow.
tryHTTP01 bool
// httpTokens contains response body values for http-01 challenges
// and is keyed by the URL path at which a challenge response is expected
// to be provisioned.
// The entries are stored for the duration of the authorization flow.
httpTokens map[string][]byte
// certTokens contains temporary certificates for tls-sni challenges
// and is keyed by token domain name, which matches server name of ClientHello.
// Keys always have ".acme.invalid" suffix.
// The entries are stored for the duration of the authorization flow.
certTokens map[string]*tls.Certificate
} }
// GetCertificate implements the tls.Config.GetCertificate hook. // GetCertificate implements the tls.Config.GetCertificate hook.
@ -185,14 +198,16 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
return nil, errors.New("acme/autocert: server name contains invalid character") return nil, errors.New("acme/autocert: server name contains invalid character")
} }
// In the worst-case scenario, the timeout needs to account for caching, host policy,
// domain ownership verification and certificate issuance.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel() defer cancel()
// check whether this is a token cert requested for TLS-SNI challenge // check whether this is a token cert requested for TLS-SNI challenge
if strings.HasSuffix(name, ".acme.invalid") { if strings.HasSuffix(name, ".acme.invalid") {
m.tokenCertMu.RLock() m.tokensMu.RLock()
defer m.tokenCertMu.RUnlock() defer m.tokensMu.RUnlock()
if cert := m.tokenCert[name]; cert != nil { if cert := m.certTokens[name]; cert != nil {
return cert, nil return cert, nil
} }
if cert, err := m.cacheGet(ctx, name); err == nil { if cert, err := m.cacheGet(ctx, name); err == nil {
@ -224,6 +239,68 @@ func (m *Manager) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
return cert, nil return cert, nil
} }
// HTTPHandler configures the Manager to provision ACME "http-01" challenge responses.
// It returns an http.Handler that responds to the challenges and must be
// running on port 80. If it receives a request that is not an ACME challenge,
// it delegates the request to the optional fallback handler.
//
// If fallback is nil, the returned handler redirects all GET and HEAD requests
// to the default TLS port 443 with 302 Found status code, preserving the original
// request path and query. It responds with 400 Bad Request to all other HTTP methods.
// The fallback is not protected by the optional HostPolicy.
//
// Because the fallback handler is run with unencrypted port 80 requests,
// the fallback should not serve TLS-only requests.
//
// If HTTPHandler is never called, the Manager will only use TLS SNI
// challenges for domain verification.
func (m *Manager) HTTPHandler(fallback http.Handler) http.Handler {
m.tokensMu.Lock()
defer m.tokensMu.Unlock()
m.tryHTTP01 = true
if fallback == nil {
fallback = http.HandlerFunc(handleHTTPRedirect)
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
fallback.ServeHTTP(w, r)
return
}
// A reasonable context timeout for cache and host policy only,
// because we don't wait for a new certificate issuance here.
ctx, cancel := context.WithTimeout(r.Context(), time.Minute)
defer cancel()
if err := m.hostPolicy()(ctx, r.Host); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
data, err := m.httpToken(ctx, r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
w.Write(data)
})
}
func handleHTTPRedirect(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" && r.Method != "HEAD" {
http.Error(w, "Use HTTPS", http.StatusBadRequest)
return
}
target := "https://" + stripPort(r.Host) + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusFound)
}
func stripPort(hostport string) string {
host, _, err := net.SplitHostPort(hostport)
if err != nil {
return hostport
}
return net.JoinHostPort(host, "443")
}
// cert returns an existing certificate either from m.state or cache. // cert returns an existing certificate either from m.state or cache.
// If a certificate is found in cache but not in m.state, the latter will be filled // If a certificate is found in cache but not in m.state, the latter will be filled
// with the cached value. // with the cached value.
@ -371,7 +448,7 @@ func (m *Manager) createCert(ctx context.Context, domain string) (*tls.Certifica
// We are the first; state is locked. // We are the first; state is locked.
// Unblock the readers when domain ownership is verified // Unblock the readers when domain ownership is verified
// and the we got the cert or the process failed. // and we got the cert or the process failed.
defer state.Unlock() defer state.Unlock()
state.locked = false state.locked = false
@ -439,16 +516,17 @@ func (m *Manager) certState(domain string) (*certState, error) {
return state, nil return state, nil
} }
// authorizedCert starts domain ownership verification process and requests a new cert upon success. // authorizedCert starts the domain ownership verification process and requests a new cert upon success.
// The key argument is the certificate private key. // The key argument is the certificate private key.
func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain string) (der [][]byte, leaf *x509.Certificate, err error) { func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain string) (der [][]byte, leaf *x509.Certificate, err error) {
if err := m.verify(ctx, domain); err != nil {
return nil, nil, err
}
client, err := m.acmeClient(ctx) client, err := m.acmeClient(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
if err := m.verify(ctx, client, domain); err != nil {
return nil, nil, err
}
csr, err := certRequest(key, domain) csr, err := certRequest(key, domain)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -464,98 +542,171 @@ func (m *Manager) authorizedCert(ctx context.Context, key crypto.Signer, domain
return der, leaf, nil return der, leaf, nil
} }
// verify starts a new identifier (domain) authorization flow. // verify runs the identifier (domain) authorization flow
// It prepares a challenge response and then blocks until the authorization // using each applicable ACME challenge type.
// is marked as "completed" by the CA (either succeeded or failed). func (m *Manager) verify(ctx context.Context, client *acme.Client, domain string) error {
// // The list of challenge types we'll try to fulfill
// verify returns nil iff the verification was successful. // in this specific order.
func (m *Manager) verify(ctx context.Context, domain string) error { challengeTypes := []string{"tls-sni-02", "tls-sni-01"}
client, err := m.acmeClient(ctx) m.tokensMu.RLock()
if err != nil { if m.tryHTTP01 {
return err challengeTypes = append(challengeTypes, "http-01")
} }
m.tokensMu.RUnlock()
// start domain authorization and get the challenge var nextTyp int // challengeType index of the next challenge type to try
for {
// Start domain authorization and get the challenge.
authz, err := client.Authorize(ctx, domain) authz, err := client.Authorize(ctx, domain)
if err != nil { if err != nil {
return err return err
} }
// maybe don't need to at all // No point in accepting challenges if the authorization status
if authz.Status == acme.StatusValid { // is in a final state.
switch authz.Status {
case acme.StatusValid:
return nil // already authorized
case acme.StatusInvalid:
return fmt.Errorf("acme/autocert: invalid authorization %q", authz.URI)
}
// Pick the next preferred challenge.
var chal *acme.Challenge
for chal == nil && nextTyp < len(challengeTypes) {
chal = pickChallenge(challengeTypes[nextTyp], authz.Challenges)
nextTyp++
}
if chal == nil {
return fmt.Errorf("acme/autocert: unable to authorize %q; tried %q", domain, challengeTypes)
}
cleanup, err := m.fulfill(ctx, client, chal)
if err != nil {
continue
}
defer cleanup()
if _, err := client.Accept(ctx, chal); err != nil {
continue
}
// A challenge is fulfilled and accepted: wait for the CA to validate.
if _, err := client.WaitAuthorization(ctx, authz.URI); err == nil {
return nil
}
}
}
// fulfill provisions a response to the challenge chal.
// The cleanup is non-nil only if provisioning succeeded.
func (m *Manager) fulfill(ctx context.Context, client *acme.Client, chal *acme.Challenge) (cleanup func(), err error) {
switch chal.Type {
case "tls-sni-01":
cert, name, err := client.TLSSNI01ChallengeCert(chal.Token)
if err != nil {
return nil, err
}
m.putCertToken(ctx, name, &cert)
return func() { go m.deleteCertToken(name) }, nil
case "tls-sni-02":
cert, name, err := client.TLSSNI02ChallengeCert(chal.Token)
if err != nil {
return nil, err
}
m.putCertToken(ctx, name, &cert)
return func() { go m.deleteCertToken(name) }, nil
case "http-01":
resp, err := client.HTTP01ChallengeResponse(chal.Token)
if err != nil {
return nil, err
}
p := client.HTTP01ChallengePath(chal.Token)
m.putHTTPToken(ctx, p, resp)
return func() { go m.deleteHTTPToken(p) }, nil
}
return nil, fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type)
}
func pickChallenge(typ string, chal []*acme.Challenge) *acme.Challenge {
for _, c := range chal {
if c.Type == typ {
return c
}
}
return nil return nil
} }
// pick a challenge: prefer tls-sni-02 over tls-sni-01 // putCertToken stores the cert under the named key in both m.certTokens map
// TODO: consider authz.Combinations
var chal *acme.Challenge
for _, c := range authz.Challenges {
if c.Type == "tls-sni-02" {
chal = c
break
}
if c.Type == "tls-sni-01" {
chal = c
}
}
if chal == nil {
return errors.New("acme/autocert: no supported challenge type found")
}
// create a token cert for the challenge response
var (
cert tls.Certificate
name string
)
switch chal.Type {
case "tls-sni-01":
cert, name, err = client.TLSSNI01ChallengeCert(chal.Token)
case "tls-sni-02":
cert, name, err = client.TLSSNI02ChallengeCert(chal.Token)
default:
err = fmt.Errorf("acme/autocert: unknown challenge type %q", chal.Type)
}
if err != nil {
return err
}
m.putTokenCert(ctx, name, &cert)
defer func() {
// verification has ended at this point
// don't need token cert anymore
go m.deleteTokenCert(name)
}()
// ready to fulfill the challenge
if _, err := client.Accept(ctx, chal); err != nil {
return err
}
// wait for the CA to validate
_, err = client.WaitAuthorization(ctx, authz.URI)
return err
}
// putTokenCert stores the cert under the named key in both m.tokenCert map
// and m.Cache. // and m.Cache.
func (m *Manager) putTokenCert(ctx context.Context, name string, cert *tls.Certificate) { func (m *Manager) putCertToken(ctx context.Context, name string, cert *tls.Certificate) {
m.tokenCertMu.Lock() m.tokensMu.Lock()
defer m.tokenCertMu.Unlock() defer m.tokensMu.Unlock()
if m.tokenCert == nil { if m.certTokens == nil {
m.tokenCert = make(map[string]*tls.Certificate) m.certTokens = make(map[string]*tls.Certificate)
} }
m.tokenCert[name] = cert m.certTokens[name] = cert
m.cachePut(ctx, name, cert) m.cachePut(ctx, name, cert)
} }
// deleteTokenCert removes the token certificate for the specified domain name // deleteCertToken removes the token certificate for the specified domain name
// from both m.tokenCert map and m.Cache. // from both m.certTokens map and m.Cache.
func (m *Manager) deleteTokenCert(name string) { func (m *Manager) deleteCertToken(name string) {
m.tokenCertMu.Lock() m.tokensMu.Lock()
defer m.tokenCertMu.Unlock() defer m.tokensMu.Unlock()
delete(m.tokenCert, name) delete(m.certTokens, name)
if m.Cache != nil { if m.Cache != nil {
m.Cache.Delete(context.Background(), name) m.Cache.Delete(context.Background(), name)
} }
} }
// httpToken retrieves an existing http-01 token value from an in-memory map
// or the optional cache.
func (m *Manager) httpToken(ctx context.Context, tokenPath string) ([]byte, error) {
m.tokensMu.RLock()
defer m.tokensMu.RUnlock()
if v, ok := m.httpTokens[tokenPath]; ok {
return v, nil
}
if m.Cache == nil {
return nil, fmt.Errorf("acme/autocert: no token at %q", tokenPath)
}
return m.Cache.Get(ctx, httpTokenCacheKey(tokenPath))
}
// putHTTPToken stores an http-01 token value using tokenPath as key
// in both in-memory map and the optional Cache.
//
// It ignores any error returned from Cache.Put.
func (m *Manager) putHTTPToken(ctx context.Context, tokenPath, val string) {
m.tokensMu.Lock()
defer m.tokensMu.Unlock()
if m.httpTokens == nil {
m.httpTokens = make(map[string][]byte)
}
b := []byte(val)
m.httpTokens[tokenPath] = b
if m.Cache != nil {
m.Cache.Put(ctx, httpTokenCacheKey(tokenPath), b)
}
}
// deleteHTTPToken removes an http-01 token value from both in-memory map
// and the optional Cache, ignoring any error returned from the latter.
//
// If m.Cache is non-nil, it blocks until Cache.Delete returns without a timeout.
func (m *Manager) deleteHTTPToken(tokenPath string) {
m.tokensMu.Lock()
defer m.tokensMu.Unlock()
delete(m.httpTokens, tokenPath)
if m.Cache != nil {
m.Cache.Delete(context.Background(), httpTokenCacheKey(tokenPath))
}
}
// httpTokenCacheKey returns a key at which an http-01 token value may be stored
// in the Manager's optional Cache.
func httpTokenCacheKey(tokenPath string) string {
return "http-01-" + path.Base(tokenPath)
}
// renew starts a cert renewal timer loop, one per domain. // renew starts a cert renewal timer loop, one per domain.
// //
// The loop is scheduled in two cases: // The loop is scheduled in two cases:
@ -790,16 +941,6 @@ func validCert(domain string, der [][]byte, key crypto.Signer) (leaf *x509.Certi
return leaf, nil return leaf, nil
} }
func retryAfter(v string) time.Duration {
if i, err := strconv.Atoi(v); err == nil {
return time.Duration(i) * time.Second
}
if t, err := http.ParseTime(v); err == nil {
return t.Sub(timeNow())
}
return time.Second
}
type lockedMathRand struct { type lockedMathRand struct {
sync.Mutex sync.Mutex
rnd *mathrand.Rand rnd *mathrand.Rand

View file

@ -102,7 +102,9 @@ func (dr *domainRenewal) do(ctx context.Context) (time.Duration, error) {
if err != nil { if err != nil {
return 0, err return 0, err
} }
dr.m.cachePut(ctx, dr.domain, tlscert) if err := dr.m.cachePut(ctx, dr.domain, tlscert); err != nil {
return 0, err
}
dr.m.stateMu.Lock() dr.m.stateMu.Lock()
defer dr.m.stateMu.Unlock() defer dr.m.stateMu.Unlock()
// m.state is guaranteed to be non-nil at this point // m.state is guaranteed to be non-nil at this point

View file

@ -241,11 +241,11 @@ func (p *hashed) Hash() []byte {
n = 3 n = 3
} }
arr[n] = '$' arr[n] = '$'
n += 1 n++
copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost))) copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost)))
n += 2 n += 2
arr[n] = '$' arr[n] = '$'
n += 1 n++
copy(arr[n:], p.salt) copy(arr[n:], p.salt)
n += encodedSaltSize n += encodedSaltSize
copy(arr[n:], p.hash) copy(arr[n:], p.hash)

12
vendor/modules.txt vendored
View file

@ -9,7 +9,7 @@ github.com/client9/misspell/cmd/misspell
github.com/client9/misspell github.com/client9/misspell
# github.com/davecgh/go-spew v1.1.1 # github.com/davecgh/go-spew v1.1.1
github.com/davecgh/go-spew/spew github.com/davecgh/go-spew/spew
# github.com/dgrijalva/jwt-go v0.0.0-20170608005149-a539ee1a749a # github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dgrijalva/jwt-go github.com/dgrijalva/jwt-go
# github.com/fsnotify/fsnotify v1.4.7 # github.com/fsnotify/fsnotify v1.4.7
github.com/fsnotify/fsnotify github.com/fsnotify/fsnotify
@ -93,10 +93,10 @@ github.com/karalabe/xgo
github.com/kr/pretty github.com/kr/pretty
# github.com/kr/text v0.1.0 # github.com/kr/text v0.1.0
github.com/kr/text github.com/kr/text
# github.com/labstack/echo v3.1.0+incompatible # github.com/labstack/echo v0.0.0-20180911044237-1abaa3049251
github.com/labstack/echo github.com/labstack/echo
github.com/labstack/echo/middleware github.com/labstack/echo/middleware
# github.com/labstack/gommon v0.0.0-20170925052817-57409ada9da0 # github.com/labstack/gommon v0.0.0-20180312174116-6fe1405d73ec
github.com/labstack/gommon/log github.com/labstack/gommon/log
github.com/labstack/gommon/color github.com/labstack/gommon/color
github.com/labstack/gommon/bytes github.com/labstack/gommon/bytes
@ -107,9 +107,9 @@ github.com/magiconair/properties
github.com/mailru/easyjson/jlexer github.com/mailru/easyjson/jlexer
github.com/mailru/easyjson/jwriter github.com/mailru/easyjson/jwriter
github.com/mailru/easyjson/buffer github.com/mailru/easyjson/buffer
# github.com/mattn/go-colorable v0.0.0-20170816031813-ad5389df28cd # github.com/mattn/go-colorable v0.0.9
github.com/mattn/go-colorable github.com/mattn/go-colorable
# github.com/mattn/go-isatty v0.0.0-20170925054904-a5cdd64afdee # github.com/mattn/go-isatty v0.0.3
github.com/mattn/go-isatty github.com/mattn/go-isatty
# github.com/mattn/go-sqlite3 v1.9.0 # github.com/mattn/go-sqlite3 v1.9.0
github.com/mattn/go-sqlite3 github.com/mattn/go-sqlite3
@ -140,7 +140,7 @@ github.com/toqueteos/webbrowser
github.com/valyala/bytebufferpool github.com/valyala/bytebufferpool
# github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 # github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4
github.com/valyala/fasttemplate github.com/valyala/fasttemplate
# golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44 # golang.org/x/crypto v0.0.0-20180312195533-182114d58262
golang.org/x/crypto/bcrypt golang.org/x/crypto/bcrypt
golang.org/x/crypto/acme/autocert golang.org/x/crypto/acme/autocert
golang.org/x/crypto/blowfish golang.org/x/crypto/blowfish