diff --git a/README.md b/README.md index 68af19d..dcbd35c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,244 @@ Still, if you haven't tried it yet, we'd encourage you to check it out. - [ ] Two-factor authentication, [issue](https://github.com/pushbits/server/issues/19) - [ ] Bi-directional key verification, [issue](https://github.com/pushbits/server/issues/20) +## 🚀 Installation + +PushBits is meant to be self-hosted. +That means you have to install it on your own server. + +Currently, the only supported way of installing PushBits is via [Docker](https://www.docker.com/) or [Podman](https://podman.io/). +The image is hosted [here on Docker Hub](https://hub.docker.com/r/eikendev/pushbits). + +| :warning: **You are advised to install PushBits behind a reverse proxy and enable TLS.** Otherwise, your credentials will be transmitted unencrypted. | +|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +## âš™ Configuration + +To see what can be configured, have a look at the `config.sample.yml` file inside the root of the repository. + +Settings can optionally be provided via environment variables. +The name of the environment variable is composed of a starting `PUSHBITS_`, followed by the keys of the setting, all +joined with `_`. +As an example, the HTTP port can be provided as an environment variable called `PUSHBITS_HTTP_PORT`. + +To get started, here is a Docker Compose file you can use. +```yaml +version: '2' + +services: + server: + image: eikendev/pushbits:latest + ports: + - 8080:8080 + environment: + PUSHBITS_DATABASE_DIALECT: 'sqlite3' + PUSHBITS_ADMIN_MATRIXID: '@your/matrix/username:matrix.org' # The Matrix account on which the admin will receive their notifications. + PUSHBITS_ADMIN_PASSWORD: 'your/pushbits/password' # The login password of the admin account. Default username is 'admin'. + PUSHBITS_MATRIX_USERNAME: 'your/matrix/username' # The Matrix account from which notifications are sent to all users. + PUSHBITS_MATRIX_PASSWORD: 'your/matrix/password' # The password of the above account. + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - ./data:/data +``` + +In this example, the configuration file would be located at `./data/config.yml` on the host. +The SQLite database would be written to `./data/pushbits.db`. +**Don't forget to adjust the permissions** of the `./data` directory, otherwise PushBits will fail to operate. + +## 📄 Usage + +Now, how can you interact with the server? +We provide [a little CLI tool called pbcli](https://github.com/PushBits/cli) to make basic API requests to the server. +It helps you to create new users and applications. +You will find further instructions in the linked repository. + +At the time of writing, there is no fancy GUI built-in, and we're not sure if this is necessary at all. +Currently, we would like to avoid front end development, so if you want to contribute in this regard we're happy if you reach out! + +After you have created a user and an application, you can use the API to send a push notification to your Matrix account. + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + --data '{"message":"my message","title":"my title"}' \ + "https://pushbits.example.com/message?token=$PB_TOKEN" +``` + +Note that the token is associated with your application and has to be kept secret. +You can retrieve the token using [pbcli](https://github.com/PushBits/cli) by running following command. + +```bash +pbcli application show $PB_APPLICATION --url https://pushbits.example.com --username $PB_USERNAME +``` + +### Authentication + +Pushbits offers you two methods of authenticating against the server: + +* Basic authentication (`basic`) +* [Oauth 2.0](https://oauth.net/2/) (`oauth`) + +You will find the corresponding setting in the security section. + +```yaml +... +security: + ... + # The authentication method to use + authentication: basic +... +``` + +#### Basic authentication + +For [basic authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) you have to provide your username and password in each request to the server. For example in curl you can do this with the `--user` flag: + +```bash +curl -u myusername:totallysecretpassword +``` + +#### Oauth 2.0 + +[Oauth 2.0](https://en.wikipedia.org/wiki/OAuth) is a token based authentication method. Instead of passing your password with each request you request a token from an authorization server. With this token you are then able to authenticate yourself against the PushBits server. + +Make sure to setup the "oauth" section in the config file correctly. + +##### Authenticating + +For authentication use the ``/oauth2/auth` endpoint. E.g.: + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + "https://pushbits.example.com/oauth2/auth" -d "client_id=000000&username=admin&password=1233456&response_type=code&redirect_uri=https://myapp.example.com" +``` + +This will return a HTTP redirect with the status code `302` and an authentication code set as parameter: + +``` +HTTP/2 302 +date: Sun, 23 May 2021 10:33:27 GMT +location: https://myapp.example.com?code=4T1TJXMBPTOS4NNGILBDYW +content-length: 0 +``` + +Your app then needs to use this code to trade it for a access token. + +**Hint for command line users:** you can extract the authentication code from the redirect without the need of a running webserver. + +##### Receiving an access token + +You can get an access token from the `/oauth/token` endpoint. There are several methods, so called "grant types" for receiving a token. PushBits currently supports the following one's: + +* Refresh +* Authentication code + +Oauth 2.0 authentication is based on "clients", thus you need to provide identifiers for a client with your request. These are the `client_id` and the `client_secret`. + +For your first token you will need a authentication code, see the section above. Then use it like this: + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + "https://pushbits.example.com/oauth2/token" -d "grant_type=authorization_code&client_id=000000&client_secret=49gjg4js9&response_type=token&redirect_uri=https://myapp.example.com&code=OP1Q2UJEVL-RPR9GZAUURA" +``` + +This will then return an access token and refresh token for you. + +```json +{ + "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ", + "expires_in": 86400, + "refresh_token": "OP1Q2UJEVL-RPR9GZAUURA", + "token_type": "Bearer" +} +``` + +The access token is short lived, the refresh token is long lived, but can not be used for authentication. If your access token runs out, you can use the refresh token to generate a new access token: + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + "https://pushbits.example.com/oauth2/token" -d "grant_type=refresh_token&client_id=000000&client_secret=49gjg4js9&response_type=token&refresh_token=OP1Q2UJEVL-RPR9GZAUURA" +``` + +##### Getting information about a access token + +With a valid access token you can get information about it from `/oauth/tokeninfo`. This is meant for testing if a token is issued correctly. + +```bash +curl \ + --header "Content-Type: application/json" \ + --request GET \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + "https://pushbits.example.com/oauth2/tokeninfo" +``` + +##### Revoking a token + +Admin users are eligible to revoke tokens. This should not be necessary in normal operation, as tokens are only short lived. But there might be situations where attackers might have gotten knowledge about a token. + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + --data '{"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NDg1MDYsInN1YiI6IjEifQ.cO0_8fqsJDG4KswjC0CSzc_EznntH-FDQejdolPAISo"}' \ + "https://pushbits.example.com/oauth2/revoke" +``` + +##### Requesting a longterm token + +Longterm tokens are tokens that life for multiple years. They can be used for scripts and other software that access PushBits. So the other software does not need knowledge about the actuall password of the user. However be carefull with longterm tokens, if you loose one others might be able to perform actions on your user account. + +```bash +curl \ + --request POST \ + --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIwMDAwMDAiLCJleHAiOjE2MjE4NTU3ODcsInN1YiI6IjEifQ.jMux7CBw6fY15Ohc8exEbcnUiMBVVgCowvq3rMrw7MQ" \ + --data '{"client_id": "000000", "client_secret": "49gjg4js9"}' \ + "https://push.remote.alexanderebhart.de/oauth2/longtermtoken" +``` + +### Message options + +Messages can be specified in three different syntaxes: + +* `text/plain` +* `text/html` +* `text/markdown` + +To set a specific syntax you need to set the `extras` parameter ([inspired by Gotify's message extras](https://gotify.net/docs/msgextras#clientdisplay)): + +```bash +curl \ + --header "Content-Type: application/json" \ + --request POST \ + --data '{"message":"my message with\n\n**Markdown** _support_.","title":"my title","extras":{"client::display":{"contentType": "text/markdown"}}}' \ + "https://pushbits.example.com/message?token=$PB_TOKEN" +``` + +HTML content might not be fully rendered in your Matrix client; see the corresponding [Matrix specs](https://spec.matrix.org/unstable/client-server-api/#mroommessage-msgtypes). +This also holds for Markdown, as it is translated into the corresponding HTML syntax. + +### Deleting a Message + +You can delete a message, this will send a notification in response to the original message informing you that the message is "deleted". + +To delete a message, you need its message ID which is provided as part of the response when you send the message. +The ID might contain characters not valid in URIs. +We hence provide an additional `id_url_encoded` field for messages; you can directly use it when deleting a message without performing encoding yourself. + +```bash +curl \ + --request DELETE \ + "https://pushbits.example.com/message/${MESSAGE_ID}?token=$PB_TOKEN" +``` + ## 👮 Acknowledgments The idea for this software and most parts of the initial source are heavily inspired by [Gotify](https://gotify.net/). diff --git a/cmd/pushbits/main.go b/cmd/pushbits/main.go index 254065e..276c6a4 100644 --- a/cmd/pushbits/main.go +++ b/cmd/pushbits/main.go @@ -52,6 +52,7 @@ func main() { cm := credentials.CreateManager(c.Security.CheckHIBP, c.Crypto) + log.Println(c.Database.Dialect) db, err := database.Create(cm, c.Database.Dialect, c.Database.Connection) if err != nil { log.Fatal(err) @@ -75,7 +76,7 @@ func main() { log.Fatal(err) } - engine := router.Create(c.Debug, cm, db, dp) + engine := router.Create(c.Debug, cm, db, dp, c) runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port) } diff --git a/config.example.yml b/config.example.yml index 34457c5..7e39bbd 100644 --- a/config.example.yml +++ b/config.example.yml @@ -60,3 +60,18 @@ crypto: formatting: # Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red). coloredtitle: false + +authentication: + # The authentication method to use. + method: basic + # Only needed if you choose oauth method. + oauth: + clients: + # Oauth client identifier (random string). + - clientid: "000000" + # Oauth client secret (random string). + clientsecret: "" + # Oauth redirect url after successful auth. Redirects will only be accepted for this url. + clientredirect: "http://localhost" + # Key used for signing (JWT with HS512) the access tokens (random string). + tokenkey: "" diff --git a/go.mod b/go.mod index 51ba480..22fbbd2 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,16 @@ go 1.16 require ( github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect github.com/gin-contrib/location v0.0.2 github.com/gin-gonic/gin v1.7.0 + github.com/go-oauth2/gin-server v1.0.0 github.com/golang/protobuf v1.4.3 // indirect github.com/gomarkdown/markdown v0.0.0-20210408062403-ad838ccf8cdd github.com/google/go-cmp v0.5.0 // indirect + github.com/imrenagi/go-oauth2-mysql v1.1.0 github.com/jinzhu/configor v1.2.1 + github.com/jmoiron/sqlx v1.3.3 github.com/json-iterator/go v1.1.10 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd @@ -20,6 +24,7 @@ require ( github.com/ugorji/go v1.2.4 // indirect golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/oauth2.v3 v3.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 gorm.io/driver/mysql v1.0.4 gorm.io/driver/sqlite v1.1.4 diff --git a/go.sum b/go.sum index df6944d..fc1e857 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,22 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b h1:jEg+fE+POnmUy40B+aSKEPqZDmsdl55hZU0YKXEzz1k= github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b/go.mod h1:Kmn5t2Rb93Q4NTprN4+CCgARGvigKMJyxP0WckpTUp0= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 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/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/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gavv/httpexpect v0.0.0-20180803094507-bdde30871313/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/gavv/monotime v0.0.0-20171021193802-6f8212e8d10d/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA= github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4= github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -12,6 +24,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-oauth2/gin-server v1.0.0 h1:KR4YMbaK6Od/a0ZIeu1UhoIiYCKT7+2xfrHet0g6E9U= +github.com/go-oauth2/gin-server v1.0.0/go.mod h1:f08F3l5/Pbayb4pjnv5PpUdQLFejgGfHrTjA6IZb0eM= github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= @@ -21,8 +35,12 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+ github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -38,23 +56,57 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/imrenagi/go-oauth2-mysql v1.1.0 h1:VcLoIB5RMfYjLg9OoOvawKZ32ErQ47BxgL96T7LDjf4= +github.com/imrenagi/go-oauth2-mysql v1.1.0/go.mod h1:mKCypNaKZroPJoeuMmA2g6q1tO4tIKD3hN7FoMUhwUA= +github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= +github.com/jackc/pgx v3.5.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jmoiron/sqlx v1.3.3 h1:j82X0bf7oQ27XeqxicSZsTU5suPwKElg3oyxNn43iTk= +github.com/jmoiron/sqlx v1.3.3/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd h1:xVrqJK3xHREMNjwjljkAUaadalWc0rRbmVuQatzmgwg= github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= @@ -64,28 +116,92 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k= +github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/buntdb v1.0.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= +github.com/tidwall/buntdb v1.1.0 h1:H6LzK59KiNjf1nHVPFrYj4Qnl8d8YLBsYamdL8N+Bao= +github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE= +github.com/tidwall/gjson v1.1.3/go.mod h1:c/nTNbUr0E0OrXEhq1pwa8iEgc2DOt4ZZqAt1HtCkPA= +github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI= +github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= +github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.2.4 h1:cTciPbZ/VSOzCLKclmssnfQ/jyoVyOcJ3aoJyUV1Urc= github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo4= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8= github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.0.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= +github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/vgarvardt/go-pg-adapter v0.3.0/go.mod h1:+ogRTaGusDQb1lhZGoUxKQAGIbL+Lv43ePl/NUQArPI= +github.com/vgarvardt/pgx-helpers v0.0.0-20190703163610-cbb413594454/go.mod h1:xp2aDvL8NKu92fXxNr9kbH03+OJ+dIVu/dYfPxt3LWs= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181217023233-e147a9138326/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8= @@ -94,9 +210,13 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -106,6 +226,13 @@ google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyz google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/oauth2.v3 v3.10.0/go.mod h1:nTG+m2PRcHR9jzGNrGdxSsUKz7vvwkqSlhFrstgZcRU= +gopkg.in/oauth2.v3 v3.12.0 h1:yOffAPoolH/i2JxwmC+pgtnY3362iPahsDpLXfDFvNg= +gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/internal/authentication/authentication.go b/internal/authentication/authentication.go index 8cd0635..c609396 100644 --- a/internal/authentication/authentication.go +++ b/internal/authentication/authentication.go @@ -2,9 +2,10 @@ package authentication import ( "errors" + "log" "net/http" - "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/model" "github.com/gin-gonic/gin" @@ -14,62 +15,81 @@ const ( headerName = "X-Gotify-Key" ) +type ( + // AuthenticationValidator defines a type for authenticating a user + AuthenticationValidator func() gin.HandlerFunc + // UserSetter defines a type for setting a user object + UserSetter func() gin.HandlerFunc +) + +// AuthHandler defines the minimal interface for an auth handler +type AuthHandler interface { + AuthenticationValidator() gin.HandlerFunc + UserSetter() gin.HandlerFunc +} + // The Database interface for encapsulating database access. type Database interface { GetApplicationByToken(token string) (*model.Application, error) GetUserByName(name string) (*model.User, error) + GetUserByID(id uint) (*model.User, error) } // Authenticator is the provider for authentication middleware. type Authenticator struct { - DB Database + DB Database + Config configuration.Authentication + AuthenticationValidator AuthenticationValidator + UserSetter UserSetter } type hasUserProperty func(user *model.User) bool -func (a *Authenticator) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { - if name, password, ok := ctx.Request.BasicAuth(); ok { - if user, err := a.DB.GetUserByName(name); err != nil { - return nil, err - } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { - return user, nil - } else { - return nil, errors.New("credentials were invalid") +func (a *Authenticator) requireUserProperty(has hasUserProperty, errorMessage string) gin.HandlerFunc { + return func(ctx *gin.Context) { + err := errors.New("user not found") + + u, exists := ctx.Get("user") + + if !exists { + log.Println("no user object in context") + ctx.AbortWithError(http.StatusForbidden, err) + return } - } - return nil, errors.New("no credentials were supplied") -} + user, ok := u.(*model.User) -func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc { - return func(ctx *gin.Context) { - user, err := a.userFromBasicAuth(ctx) - if err != nil { + if !ok { + log.Println("user object from context has wrong format") ctx.AbortWithError(http.StatusForbidden, err) return } if !has(user) { - ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) + ctx.AbortWithError(http.StatusForbidden, errors.New(errorMessage)) return } - - ctx.Set("user", user) } } // RequireUser returns a Gin middleware which requires valid user credentials to be supplied with the request. -func (a *Authenticator) RequireUser() gin.HandlerFunc { - return a.requireUserProperty(func(user *model.User) bool { - return true - }) +func (a *Authenticator) RequireUser() []gin.HandlerFunc { + funcs := make([]gin.HandlerFunc, 0) + funcs = append(funcs, a.RequireValidAuthentication()) + funcs = append(funcs, a.UserSetter()) + return funcs } // RequireAdmin returns a Gin middleware which requires valid admin credentials to be supplied with the request. -func (a *Authenticator) RequireAdmin() gin.HandlerFunc { - return a.requireUserProperty(func(user *model.User) bool { +func (a *Authenticator) RequireAdmin() []gin.HandlerFunc { + funcs := make([]gin.HandlerFunc, 0) + funcs = append(funcs, a.RequireValidAuthentication()) + funcs = append(funcs, a.UserSetter()) + funcs = append(funcs, a.requireUserProperty(func(user *model.User) bool { return user.IsAdmin - }) + }, "user does not have permission: admin")) + + return funcs } func (a *Authenticator) tokenFromQueryOrHeader(ctx *gin.Context) string { @@ -104,3 +124,14 @@ func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc { ctx.Set("app", app) } } + +// RequireValidAuthentication returns a Gin middleware which requires a valid authentication +func (a *Authenticator) RequireValidAuthentication() gin.HandlerFunc { + return a.AuthenticationValidator() +} + +// RegisterHandler registers an authentication handler +func (a *Authenticator) RegisterHandler(handler AuthHandler) { + a.UserSetter = handler.UserSetter + a.AuthenticationValidator = handler.AuthenticationValidator +} diff --git a/internal/authentication/basicauth/authhandler.go b/internal/authentication/basicauth/authhandler.go new file mode 100644 index 0000000..ff2f415 --- /dev/null +++ b/internal/authentication/basicauth/authhandler.go @@ -0,0 +1,80 @@ +package basicauth + +import ( + "errors" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/database" + "github.com/pushbits/server/internal/model" +) + +// The Database interface for encapsulating database access. +type Database interface { + GetApplicationByToken(token string) (*model.Application, error) + GetUserByName(name string) (*model.User, error) +} + +// AuthHandler is the basic auth provider for authentication +type AuthHandler struct { + db Database +} + +// Initialize prepares the AuthHandler +func (a *AuthHandler) Initialize(db *database.Database) error { + a.db = db + return nil +} + +// AuthenticationValidator returns a gin HandlerFunc that takes the basic auth credentials and validates them +func (a AuthHandler) AuthenticationValidator() gin.HandlerFunc { + return func(ctx *gin.Context) { + var user *model.User + user, err := a.userFromBasicAuth(ctx) + + if err != nil { + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + if user == nil { + ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) + return + } + } +} + +// UserSetter returns a gin HandlerFunc that takes the basic auth credentials and sets the corresponding user object +func (a AuthHandler) UserSetter() gin.HandlerFunc { + return func(ctx *gin.Context) { + var user *model.User + user, err := a.userFromBasicAuth(ctx) + + if err != nil { + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + if user == nil { + ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed")) + return + } + + ctx.Set("user", user) + } +} + +func (a *AuthHandler) userFromBasicAuth(ctx *gin.Context) (*model.User, error) { + if name, password, ok := ctx.Request.BasicAuth(); ok { + if user, err := a.db.GetUserByName(name); err != nil { + return nil, err + } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return user, nil + } else { + return nil, errors.New("credentials were invalid") + } + } + + return nil, errors.New("no credentials were supplied") +} diff --git a/internal/authentication/oauth/authhandler.go b/internal/authentication/oauth/authhandler.go new file mode 100644 index 0000000..127d716 --- /dev/null +++ b/internal/authentication/oauth/authhandler.go @@ -0,0 +1,199 @@ +package oauth + +import ( + "errors" + "fmt" + "log" + "net/http" + "time" + + "github.com/dgrijalva/jwt-go" + ginserver "github.com/go-oauth2/gin-server" + mysql "github.com/imrenagi/go-oauth2-mysql" + "github.com/jmoiron/sqlx" + "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" + "github.com/pushbits/server/internal/database" + "github.com/pushbits/server/internal/model" + + "gopkg.in/oauth2.v3" + oauth_error "gopkg.in/oauth2.v3/errors" + "gopkg.in/oauth2.v3/generates" + "gopkg.in/oauth2.v3/manage" + "gopkg.in/oauth2.v3/models" + "gopkg.in/oauth2.v3/server" + "gopkg.in/oauth2.v3/store" +) + +// The Database interface for encapsulating database access. +type Database interface { + GetApplicationByToken(token string) (*model.Application, error) + GetUserByName(name string) (*model.User, error) + GetUserByID(id uint) (*model.User, error) +} + +// AuthHandler is the oauth provider for authentication +type AuthHandler struct { + db Database + manager *manage.Manager + config configuration.Authentication +} + +// Initialize prepares the AuthHandler +func (a *AuthHandler) Initialize(db *database.Database, configAuth configuration.Authentication, configDatabase configuration.Database) error { + a.db = db + a.config = configAuth + + if len(a.config.Oauth.TokenKey) < 5 { + panic("Your Oauth 2.0 token key is empty or not long enough to be secure. Please change it in the configuration file.") + } + + // The manager handles the tokens + a.manager = manage.NewDefaultManager() + a.manager.SetAuthorizeCodeExp(time.Duration(24) * time.Hour) + a.manager.SetAuthorizeCodeTokenCfg(&manage.Config{ + AccessTokenExp: time.Duration(24) * time.Hour, // 1 day + RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days + IsGenerateRefresh: true, + }) + a.manager.SetRefreshTokenCfg(&manage.RefreshingConfig{ + AccessTokenExp: time.Duration(24) * time.Hour, // 1 day + RefreshTokenExp: time.Duration(24) * time.Hour * 30, // 30 days + IsGenerateRefresh: true, + IsResetRefreshTime: true, + IsRemoveAccess: false, + IsRemoveRefreshing: true, + }) + a.manager.MapAccessGenerate(generates.NewJWTAccessGenerate([]byte(a.config.Oauth.TokenKey), jwt.SigningMethodHS512)) // unfortunately only symmetric algorithms seem to be supported + + // Define a storage for the tokens + switch configDatabase.Dialect { + case "mysql": + dbOauth := sqlx.NewDb(db.GetSqldb(), "mysql") + // Workaround to get rid of clients that still exist in the database but no longer in the config file + _, err := dbOauth.Exec("DROP TABLE IF EXISTS oauth_clients") + if err != nil { + panic(err) + } + + a.manager.MustTokenStorage(mysql.NewTokenStore(dbOauth)) + + clientStore, _ := mysql.NewClientStore(dbOauth, mysql.WithClientStoreTableName("oauth_clients")) + a.manager.MapClientStorage(clientStore) + + for _, client := range a.config.Oauth.Clients { + if len(client.ClientSecret) < 5 { + panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") + } + + clientStore.Create(&models.Client{ + ID: client.ClientID, + Secret: client.ClientSecret, + Domain: client.ClientRedirect, + }) + } + + case "sqlite3": + a.manager.MustTokenStorage(store.NewFileTokenStore("pushbits_tokens.db")) + clientStore := store.NewClientStore() // memory store + a.manager.MapClientStorage(clientStore) + + for _, client := range a.config.Oauth.Clients { + if len(client.ClientSecret) < 5 { + panic("Your Oauth 2.0 client secret is empty or not long enough to be secure. Please change it in the configuration file.") + } + + clientStore.Set(client.ClientID, &models.Client{ + ID: client.ClientID, + Secret: client.ClientSecret, + Domain: client.ClientRedirect, + }) + } + + default: + log.Panicln("Unknown (oauth) storage dialect") + } + // Initialize and configure the token server + ginserver.InitServer(a.manager) + ginserver.SetAllowGetAccessRequest(true) + ginserver.SetClientInfoHandler(server.ClientFormHandler) + ginserver.SetUserAuthorizationHandler(a.UserAuthHandler()) + ginserver.SetPasswordAuthorizationHandler(a.passwordAuthorizationHandler()) + ginserver.SetInternalErrorHandler(a.InternalErrorHandler()) + ginserver.SetAllowedGrantType( + oauth2.AuthorizationCode, + //oauth2.PasswordCredentials, + oauth2.Refreshing, + ) + ginserver.SetAllowedResponseType( + oauth2.Code, + ) + ginserver.SetClientScopeHandler(ClientScopeHandler()) + ginserver.SetAccessTokenExpHandler(AccessTokenExpHandler()) + return nil +} + +// PasswordAuthorizationHandler returns a PasswordAuthorizationHandler that handles username and password based authentication for access tokens +func (a *AuthHandler) passwordAuthorizationHandler() server.PasswordAuthorizationHandler { + return func(username string, password string) (string, error) { + log.Println("Received password based authentication request") + + user, err := a.db.GetUserByName(username) + + if err != nil || user == nil { + return "", nil + } + + if !credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return "", nil + } + + return fmt.Sprintf("%d", user.ID), nil + } +} + +// UserAuthHandler extracts user information from an auth request +func (a *AuthHandler) UserAuthHandler() server.UserAuthorizationHandler { + return func(w http.ResponseWriter, r *http.Request) (string, error) { + username := r.FormValue("username") + password := r.FormValue("password") + + if user, err := a.db.GetUserByName(username); err != nil { + return "", err + } else if user != nil && credentials.ComparePassword(user.PasswordHash, []byte(password)) { + return fmt.Sprint(user.ID), nil + } + return "", errors.New("no credentials provided") + } +} + +// ClientScopeHandler returns a ClientScopeHandler that allows or disallows scopes for access tokens +func ClientScopeHandler() server.ClientScopeHandler { + return func(clientID, scope string) (allowed bool, err error) { + if scope == "all" || scope == "" { // For now only allow generic scopes so there is place for future expansion + return true, nil + } + + return false, nil + } +} + +// AccessTokenExpHandler returns an AccessTokenExpHandler that sets the expiration time of access tokens +func AccessTokenExpHandler() server.AccessTokenExpHandler { + return func(w http.ResponseWriter, r *http.Request) (exp time.Duration, err error) { + return time.Duration(24) * time.Hour, nil + } +} + +// InternalErrorHandler handles errors for authentication, it will always return a server_error +func (a *AuthHandler) InternalErrorHandler() server.InternalErrorHandler { + return func(err error) *oauth_error.Response { + var re oauth_error.Response + log.Println(err) + + re.Error = oauth_error.ErrServerError + re.Description = oauth_error.Descriptions[oauth_error.ErrServerError] + re.StatusCode = oauth_error.StatusCodes[oauth_error.ErrServerError] + return &re + } +} diff --git a/internal/authentication/oauth/handler.go b/internal/authentication/oauth/handler.go new file mode 100644 index 0000000..3b53a5c --- /dev/null +++ b/internal/authentication/oauth/handler.go @@ -0,0 +1,112 @@ +package oauth + +import ( + "errors" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + ginserver "github.com/go-oauth2/gin-server" + "gopkg.in/oauth2.v3" +) + +// RevokeAccessRequest holds data required in a revoke request +type RevokeAccessRequest struct { + Access string `json:"access_token"` +} + +type LongtermTokenRequest struct { + ClientID string `json:"client_id"` + ClientSecret string `json:"client_secret"` +} + +// GetTokenInfo answers with information about an access token +func (a *AuthHandler) GetTokenInfo(c *gin.Context) { + ti, err := a.tokenFromContext(c) + if err != nil { + c.AbortWithError(http.StatusNotFound, err) + return + } + + tdi := TokenDisplayInfo{} + tdi.ReadFromTi(ti) + + c.JSON(200, tdi) +} + +// RevokeAccess revokes an access token +func (a *AuthHandler) RevokeAccess(c *gin.Context) { + var request RevokeAccessRequest + + err := c.BindJSON(&request) + if err != nil || request.Access == "" { + log.Println("Error when reading request.") + c.AbortWithError(http.StatusUnprocessableEntity, errors.New("missing access_token")) + return + } + + err = a.manager.RemoveAccessToken(request.Access) + if err != nil { + log.Println("Error when revoking: ", err) + c.AbortWithError(http.StatusNotFound, errors.New("unknown access token")) + return + } + + c.JSON(200, request) +} + +// LongtermToken handles request for longterm access tokens +func (a *AuthHandler) LongtermToken(c *gin.Context) { + var tokenGenerateRequest oauth2.TokenGenerateRequest + var request LongtermTokenRequest + var ltdi LongtermTokenDisplayInfo + + err := c.BindJSON(&request) + if err != nil { + log.Println(err) + c.AbortWithError(http.StatusUnprocessableEntity, errors.New("missing or malformated request")) + return + } + + userTi, err := a.tokenFromContext(c) + + if err != nil { + log.Println(err) + c.AbortWithError(http.StatusNotFound, err) + return + } + + tokenGenerateRequest.UserID = userTi.GetUserID() + tokenGenerateRequest.Scope = userTi.GetScope() + tokenGenerateRequest.ClientID = request.ClientID + tokenGenerateRequest.ClientSecret = request.ClientSecret + tokenGenerateRequest.AccessTokenExp = time.Hour * 24 * 365 * 5 // 5 years + + ti, err := a.manager.GenerateAccessToken(oauth2.Implicit, &tokenGenerateRequest) + if err != nil { + log.Println(err) + c.AbortWithError(http.StatusInternalServerError, err) + return + } + ltdi.ReadFromTi(ti) + + c.JSON(200, ltdi) +} + +func (a *AuthHandler) tokenFromContext(c *gin.Context) (oauth2.TokenInfo, error) { + err := errors.New("token not found") + + data, exists := c.Get(ginserver.DefaultConfig.TokenKey) + if !exists { + log.Println("token does not exist in context.") + return nil, err + } + + ti, ok := data.(oauth2.TokenInfo) + if !ok { + log.Println("token from context has wrong format.") + return nil, err + } + return ti, nil +} diff --git a/internal/authentication/oauth/middleware.go b/internal/authentication/oauth/middleware.go new file mode 100644 index 0000000..5f8500b --- /dev/null +++ b/internal/authentication/oauth/middleware.go @@ -0,0 +1,51 @@ +package oauth + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + ginserver "github.com/go-oauth2/gin-server" + "gopkg.in/oauth2.v3" +) + +// AuthenticationValidator returns a gin middleware for authenticating users based on a oauth access token +func (a AuthHandler) AuthenticationValidator() gin.HandlerFunc { + return ginserver.HandleTokenVerify() +} + +// UserSetter returns a gin HandlerFunc that takes the token from the AuthenticationValidator and sets the corresponding user object +func (a AuthHandler) UserSetter() gin.HandlerFunc { + return func(ctx *gin.Context) { + var err error + ti, exists := ctx.Get(ginserver.DefaultConfig.TokenKey) + if !exists { + err = errors.New("no token available") + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + token, ok := ti.(oauth2.TokenInfo) + if !ok { + err = errors.New("wrong token format") + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + userID, err := strconv.ParseUint(token.GetUserID(), 10, 64) + if err != nil { + err = errors.New("user information of wrong format") + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + user, err := a.db.GetUserByID(uint(userID)) + if err != nil { + ctx.AbortWithError(http.StatusForbidden, err) + return + } + + ctx.Set("user", user) + } +} diff --git a/internal/authentication/oauth/tokendisplayinfo.go b/internal/authentication/oauth/tokendisplayinfo.go new file mode 100644 index 0000000..ff1471a --- /dev/null +++ b/internal/authentication/oauth/tokendisplayinfo.go @@ -0,0 +1,41 @@ +package oauth + +import ( + "time" + + "gopkg.in/oauth2.v3" +) + +// LongtermTokenDisplayInfo holds information about an longterm access token but without sensitive, internal data +type LongtermTokenDisplayInfo struct { + Token string `json:"access_token"` + TokenExpires time.Time `json:"expires_at"` + TokenCreated time.Time `json:"created_at"` + UserID string `json:"user_id"` +} + +// ReadFromTi sets the TokenDisplayInfo with data from the TokenInfo +func (tdi *LongtermTokenDisplayInfo) ReadFromTi(ti oauth2.TokenInfo) { + tdi.Token = ti.GetAccess() + tdi.TokenExpires = ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn()) + tdi.TokenCreated = ti.GetAccessCreateAt() + tdi.UserID = ti.GetUserID() +} + +// TokenDisplayInfo holds information about an access token but without sensitive, internal data +type TokenDisplayInfo struct { + Token string `json:"access_token"` + TokenExpires time.Time `json:"expires_at"` + TokenCreated time.Time `json:"created_at"` + UserID string `json:"user_id"` + RefreshExpires time.Time `json:"refresh_expires_at"` +} + +// ReadFromTi sets the TokenDisplayInfo with data from the TokenInfo +func (tdi *TokenDisplayInfo) ReadFromTi(ti oauth2.TokenInfo) { + tdi.Token = ti.GetAccess() + tdi.TokenExpires = ti.GetAccessCreateAt().Add(ti.GetAccessExpiresIn()) + tdi.TokenCreated = ti.GetAccessCreateAt() + tdi.UserID = ti.GetUserID() + tdi.RefreshExpires = ti.GetRefreshCreateAt().Add(ti.GetRefreshExpiresIn()) +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 5da3653..5f37240 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -26,6 +26,31 @@ type Formatting struct { ColoredTitle bool `default:"false"` } +// Authentication holds the settings for the oauth server +type Authentication struct { + Method string `default:"basic"` + Oauth Oauth +} + +// Oauth holds information about the oauth server +type Oauth struct { + Clients []OauthClient + TokenKey string `default:""` +} + +// OauthClient holds information about an oauth client +type OauthClient struct { + ClientID string `default:"000000"` + ClientSecret string `default:""` + ClientRedirect string `default:"http://localhost"` +} + +// Database holds information about the used database type +type Database struct { + Dialect string `default:"sqlite3"` + Connection string `default:"pushbits.db"` +} + // Matrix holds credentials for a matrix account type Matrix struct { Homeserver string `default:"https://matrix.org"` @@ -40,11 +65,8 @@ type Configuration struct { ListenAddress string `default:""` Port int `default:"8080"` } - Database struct { - Dialect string `default:"sqlite3"` - Connection string `default:"pushbits.db"` - } - Admin struct { + Database Database + Admin struct { Name string `default:"admin"` Password string `default:"admin"` MatrixID string `required:"true"` @@ -53,8 +75,9 @@ type Configuration struct { Security struct { CheckHIBP bool `default:"false"` } - Crypto CryptoConfig - Formatting Formatting + Crypto CryptoConfig + Formatting Formatting + Authentication Authentication } func configFiles() []string { diff --git a/internal/database/database.go b/internal/database/database.go index c7f52ff..9f50035 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -6,6 +6,7 @@ import ( "log" "os" "path/filepath" + "strings" "time" "github.com/pushbits/server/internal/authentication/credentials" @@ -46,6 +47,9 @@ func Create(cm *credentials.Manager, dialect, connection string) (*Database, err maxOpenConns = 1 db, err = gorm.Open(sqlite.Open(connection), &gorm.Config{}) case "mysql": + if !strings.Contains(connection, "parseTime=true") { + connection += "?parseTime=true" + } db, err = gorm.Open(mysql.Open(connection), &gorm.Config{}) default: message := "Database dialect is not supported" @@ -139,3 +143,8 @@ func (d *Database) RepairChannels(dp Dispatcher) error { return nil } + +// GetSqldb returns the databases sql.DB object +func (d *Database) GetSqldb() *sql.DB { + return d.sqldb +} diff --git a/internal/router/router.go b/internal/router/router.go index 68f61ca..060bf1e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -5,35 +5,66 @@ import ( "github.com/pushbits/server/internal/api" "github.com/pushbits/server/internal/authentication" + "github.com/pushbits/server/internal/authentication/basicauth" "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/authentication/oauth" + "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/dispatcher" "github.com/gin-contrib/location" "github.com/gin-gonic/gin" + ginserver "github.com/go-oauth2/gin-server" ) // Create a Gin engine and setup all routes. -func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher) *gin.Engine { +func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, config *configuration.Configuration) *gin.Engine { log.Println("Setting up HTTP routes.") if !debug { gin.SetMode(gin.ReleaseMode) } - auth := authentication.Authenticator{DB: db} + // Initialize gin + r := gin.Default() + r.Use(location.Default()) + + // Set up authentication and handler + auth := authentication.Authenticator{ + DB: db, + Config: config.Authentication, + } + + switch config.Authentication.Method { + case "oauth": + authHandler := oauth.AuthHandler{} + authHandler.Initialize(db, config.Authentication, config.Database) + auth.RegisterHandler(authHandler) + + // Register oauth endpoints + oauthGroup := r.Group("/oauth2") + { + oauthGroup.POST("/token", ginserver.HandleTokenRequest) + oauthGroup.POST("/auth", ginserver.HandleAuthorizeRequest) + oauthGroup.GET("/tokeninfo", auth.RequireValidAuthentication(), authHandler.GetTokenInfo) + oauthGroup.POST("/revoke", append(auth.RequireAdmin(), authHandler.RevokeAccess)...) + oauthGroup.POST("/longtermtoken", auth.RequireValidAuthentication(), authHandler.LongtermToken) + } + case "basic": + authHandler := basicauth.AuthHandler{} + authHandler.Initialize(db) + auth.RegisterHandler(authHandler) + default: + panic("Unknown authentication method set. Please use one of basic, oauth.") + } applicationHandler := api.ApplicationHandler{DB: db, DP: dp} healthHandler := api.HealthHandler{DB: db} notificationHandler := api.NotificationHandler{DB: db, DP: dp} userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp} - r := gin.Default() - - r.Use(location.Default()) - applicationGroup := r.Group("/application") - applicationGroup.Use(auth.RequireUser()) + applicationGroup.Use(auth.RequireUser()...) { applicationGroup.POST("", applicationHandler.CreateApplication) applicationGroup.GET("", applicationHandler.GetApplications) @@ -49,7 +80,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp r.DELETE("/message/:messageid", api.RequireMessageIDInURI(), auth.RequireApplicationToken(), notificationHandler.DeleteNotification) userGroup := r.Group("/user") - userGroup.Use(auth.RequireAdmin()) + userGroup.Use(auth.RequireAdmin()...) { userGroup.POST("", userHandler.CreateUser) userGroup.GET("", userHandler.GetUsers)