Initialize repository

This commit is contained in:
eikendev 2020-07-26 00:28:38 +02:00
commit 1d758fcfd0
No known key found for this signature in database
GPG key ID: A1BDB1B28C8EF694
28 changed files with 1107 additions and 0 deletions

18
.editorconfig Normal file
View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_style = space
[Makefile]
indent_style = tab

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
*.db
*.yml
debugging/
pushbits
### Go
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/

7
LICENSE Normal file
View file

@ -0,0 +1,7 @@
ISC License (ISC)
Copyright 2020 eikendev
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

15
Makefile Normal file
View file

@ -0,0 +1,15 @@
.PHONY: test
test:
stdout=$$(gofmt -l . 2>&1); \
if [ "$$stdout" ]; then \
exit 1; \
fi
go test -v -cover ./...
stdout=$$(golint ./... 2>&1); \
if [ "$$stdout" ]; then \
exit 1; \
fi
.PHONY: dependencies
dependencies:
go get -u golang.org/x/lint/golint

30
README.md Normal file
View file

@ -0,0 +1,30 @@
## About
PushBits is a relay server for push notifications.
It enables your services to send notifications via a simple web API, and delivers them to you through various messaging services.
The vision is to have compatibility with [Gotify](https://gotify.net/) on the sending side, while on the receiving side established services are used.
This has the advantages that
- plugins written for Gotify and
- clients written for all major platforms can be reused.
For now, only the [Matrix protocol](https://matrix.org/) is supported, but support for different services like [Telegram](https://telegram.org/) could be added in the future.
I am myself experimenting with Matrix currently because I like the idea of a federated, synchronized but still end-to-end encrypted protocol.
The idea for this software and most parts of the initial source are heavily inspired by [Gotify](https://gotify.net/).
Many thanks to [jmattheis](https://jmattheis.de/) for his well-structured code.
## Usage
PushBits is meant to be self-hosted.
You are advised to install PushBits behind a reverse proxy and enable TLS.
At the moment, there is no front-end implemented.
New users and applications need to be created via the API.
Details will be made available once the interface is more stable.
## Development
PushBits is currently in alpha stage.
The API is neither stable, nor is provided functionality guaranteed to work.
Stay tuned! 😉

63
api/application.go Normal file
View file

@ -0,0 +1,63 @@
package api
import (
"log"
"net/http"
"github.com/eikendev/pushbits/authentication"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
// The ApplicationDatabase interface for encapsulating database access.
type ApplicationDatabase interface {
CreateApplication(application *model.Application) error
GetApplicationByToken(token string) (*model.Application, error)
}
// The ApplicationDispatcher interface for relaying notifications.
type ApplicationDispatcher interface {
RegisterApplication(name, user string) (string, error)
}
// ApplicationHandler holds information for processing requests about applications.
type ApplicationHandler struct {
DB ApplicationDatabase
Dispatcher ApplicationDispatcher
}
func (h *ApplicationHandler) applicationExists(token string) bool {
application, _ := h.DB.GetApplicationByToken(token)
return application != nil
}
// CreateApplication is used to create a new user.
func (h *ApplicationHandler) CreateApplication(ctx *gin.Context) {
application := model.Application{}
if success := successOrAbort(ctx, http.StatusBadRequest, ctx.Bind(&application)); !success {
return
}
user := authentication.GetUser(ctx)
application.Token = authentication.GenerateNotExistingToken(authentication.GenerateApplicationToken, h.applicationExists)
application.UserID = user.ID
log.Printf("User %s will receive notifications for application %s.\n", user.Name, application.Name)
matrixid, err := h.Dispatcher.RegisterApplication(application.Name, user.MatrixID)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}
application.MatrixID = matrixid
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DB.CreateApplication(&application)); !success {
return
}
ctx.JSON(http.StatusOK, &application)
}

53
api/notification.go Normal file
View file

@ -0,0 +1,53 @@
package api
import (
"log"
"net/http"
"strings"
"time"
"github.com/eikendev/pushbits/authentication"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
// The NotificationDatabase interface for encapsulating database access.
type NotificationDatabase interface {
}
// The NotificationDispatcher interface for relaying notifications.
type NotificationDispatcher interface {
SendNotification(a *model.Application, n *model.Notification) error
}
// NotificationHandler holds information for processing requests about notifications.
type NotificationHandler struct {
DB NotificationDatabase
Dispatcher NotificationDispatcher
}
// CreateNotification is used to create a new notification for a user.
func (h *NotificationHandler) CreateNotification(ctx *gin.Context) {
notification := model.Notification{}
if success := successOrAbort(ctx, http.StatusBadRequest, ctx.Bind(&notification)); !success {
return
}
application := authentication.GetApplication(ctx)
log.Printf("Sending notification for application %s.\n", application.Name)
notification.ID = 0
notification.ApplicationID = application.ID
if strings.TrimSpace(notification.Title) == "" {
notification.Title = application.Name
}
notification.Date = time.Now()
if success := successOrAbort(ctx, http.StatusInternalServerError, h.Dispatcher.SendNotification(application, &notification)); !success {
return
}
ctx.JSON(http.StatusOK, &notification)
}

48
api/user.go Normal file
View file

@ -0,0 +1,48 @@
package api
import (
"errors"
"net/http"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
// The UserDatabase interface for encapsulating database access.
type UserDatabase interface {
CreateUser(user *model.User) error
GetUserByName(name string) (*model.User, error)
}
// UserHandler holds information for processing requests about users.
type UserHandler struct {
DB UserDatabase
}
func (h *UserHandler) userExists(name string) bool {
user, _ := h.DB.GetUserByName(name)
return user != nil
}
// CreateUser creates a new user.
func (h *UserHandler) CreateUser(ctx *gin.Context) {
externalUser := model.ExternalUserWithCredentials{}
if success := successOrAbort(ctx, http.StatusBadRequest, ctx.Bind(&externalUser)); !success {
return
}
user := externalUser.IntoInternalUser()
if h.userExists(user.Name) {
ctx.AbortWithError(http.StatusBadRequest, errors.New("username already exists"))
return
}
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DB.CreateUser(user)); !success {
return
}
ctx.JSON(http.StatusOK, user.IntoExternalUser())
}

13
api/util.go Normal file
View file

@ -0,0 +1,13 @@
package api
import (
"github.com/gin-gonic/gin"
)
func successOrAbort(ctx *gin.Context, code int, err error) bool {
if err != nil {
ctx.AbortWithError(code, err)
}
return err == nil
}

53
app.go Normal file
View file

@ -0,0 +1,53 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/eikendev/pushbits/configuration"
"github.com/eikendev/pushbits/database"
"github.com/eikendev/pushbits/dispatcher"
"github.com/eikendev/pushbits/router"
"github.com/eikendev/pushbits/runner"
)
func setupCleanup(db *database.Database, dp *dispatcher.Dispatcher) {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
dp.Close()
db.Close()
os.Exit(1)
}()
}
func main() {
log.Println("Starting PushBits.")
c := configuration.Get()
db, err := database.Create(c.Database.Dialect, c.Database.Connection)
if err != nil {
panic(err)
}
defer db.Close()
if err := db.Populate(c.Admin.Name, c.Admin.Password, c.Admin.MatrixID); err != nil {
panic(err)
}
dp, err := dispatcher.Create(db, c.Matrix.Homeserver, c.Matrix.Username, c.Matrix.Password)
if err != nil {
panic(err)
}
defer dp.Close()
setupCleanup(db, dp)
engine := router.Create(db, dp)
runner.Run(engine)
}

View file

@ -0,0 +1,106 @@
package authentication
import (
"errors"
"net/http"
"github.com/eikendev/pushbits/authentication/credentials"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
const (
headerName = "X-Gotify-Key"
)
// The Database interface for encapsulating database access.
type Database interface {
GetApplicationByToken(token string) (*model.Application, error)
GetUserByName(name string) (*model.User, error)
}
// Authenticator is the provider for authentication middleware.
type Authenticator struct {
DB Database
}
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")
}
}
return nil, errors.New("no credentials were supplied")
}
func (a *Authenticator) requireUserProperty(has hasUserProperty) gin.HandlerFunc {
return func(ctx *gin.Context) {
user, err := a.userFromBasicAuth(ctx)
if err != nil {
ctx.AbortWithError(http.StatusForbidden, err)
return
}
if !has(user) {
ctx.AbortWithError(http.StatusForbidden, errors.New("authentication failed"))
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
})
}
// 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 {
return user.IsAdmin
})
}
func (a *Authenticator) tokenFromQueryOrHeader(ctx *gin.Context) string {
if token := a.tokenFromQuery(ctx); token != "" {
return token
} else if token := a.tokenFromHeader(ctx); token != "" {
return token
}
return ""
}
func (a *Authenticator) tokenFromQuery(ctx *gin.Context) string {
return ctx.Request.URL.Query().Get("token")
}
func (a *Authenticator) tokenFromHeader(ctx *gin.Context) string {
return ctx.Request.Header.Get(headerName)
}
// RequireApplicationToken returns a Gin middleware which requires an application token to be supplied with the request.
func (a *Authenticator) RequireApplicationToken() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := a.tokenFromQueryOrHeader(ctx)
app, err := a.DB.GetApplicationByToken(token)
if err != nil {
ctx.AbortWithError(http.StatusForbidden, err)
return
}
ctx.Set("app", app)
}
}

30
authentication/context.go Normal file
View file

@ -0,0 +1,30 @@
package authentication
import (
"errors"
"net/http"
"github.com/eikendev/pushbits/model"
"github.com/gin-gonic/gin"
)
// GetApplication returns the application which was previously registered by the authentication middleware.
func GetApplication(ctx *gin.Context) *model.Application {
app, ok := ctx.MustGet("app").(*model.Application)
if app == nil || !ok {
ctx.AbortWithError(http.StatusInternalServerError, errors.New("an error occured while retrieving application from context"))
}
return app
}
// GetUser returns the user which was previously registered by the authentication middleware.
func GetUser(ctx *gin.Context) *model.User {
user, ok := ctx.MustGet("user").(*model.User)
if user == nil || !ok {
ctx.AbortWithError(http.StatusInternalServerError, errors.New("an error occured while retrieving user from context"))
}
return user
}

View file

@ -0,0 +1,21 @@
package credentials
import "golang.org/x/crypto/bcrypt"
// CreatePassword returns a hashed version of the given password.
// TODO: Make strength configurable.
func CreatePassword(pw string) []byte {
strength := 12
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(pw), strength)
if err != nil {
panic(err)
}
return hashedPassword
}
// ComparePassword compares a hashed password with its possible plaintext equivalent.
func ComparePassword(hashedPassword, password []byte) bool {
return bcrypt.CompareHashAndPassword(hashedPassword, password) == nil
}

54
authentication/token.go Normal file
View file

@ -0,0 +1,54 @@
package authentication
import (
"crypto/rand"
"math/big"
)
var (
tokenCharacters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
randomTokenLength = 64
applicationPrefix = "A"
)
func randIntn(n int) int {
max := big.NewInt(int64(n))
res, err := rand.Int(rand.Reader, max)
if err != nil {
panic("random source is not available")
}
return int(res.Int64())
}
// GenerateNotExistingToken receives a token generation function and a function to check whether the token exists, returns a unique token.
func GenerateNotExistingToken(generateToken func() string, tokenExists func(token string) bool) string {
for {
token := generateToken()
if !tokenExists(token) {
return token
}
}
}
func generateRandomString(length int) string {
res := make([]byte, length)
for i := range res {
index := randIntn(len(tokenCharacters))
res[i] = tokenCharacters[index]
}
return string(res)
}
func generateRandomToken(prefix string) string {
return prefix + generateRandomString(randomTokenLength)
}
// GenerateApplicationToken generates a token for an application.
func GenerateApplicationToken() string {
return generateRandomToken(applicationPrefix)
}

View file

@ -0,0 +1,43 @@
package configuration
import (
"github.com/jinzhu/configor"
)
// Configuration holds values that can be configured by the user.
type Configuration struct {
Database struct {
Dialect string `default:"sqlite3"`
Connection string `default:"pushbits.db"`
}
Admin struct {
Name string `default:"admin"`
Password string `default:"admin"`
MatrixID string `required:"true"`
}
Matrix struct {
Homeserver string `default:"https://matrix.org"`
Username string `required:"true"`
Password string `required:"true"`
}
}
func configFiles() []string {
return []string{"config.yml"}
}
// Get returns the configuration extracted from env variables or config file.
func Get() *Configuration {
config := new(Configuration)
err := configor.New(&configor.Config{
Environment: "production",
ENVPrefix: "PUSHBITS",
ErrorOnUnmatchedKeys: true,
}).Load(config, configFiles()...)
if err != nil {
panic(err)
}
return config
}

31
database/application.go Normal file
View file

@ -0,0 +1,31 @@
package database
import (
"errors"
"github.com/eikendev/pushbits/model"
"gorm.io/gorm"
)
// CreateApplication creates an application.
func (d *Database) CreateApplication(application *model.Application) error {
return d.gormdb.Create(application).Error
}
// UpdateApplication updates an application.
func (d *Database) UpdateApplication(app *model.Application) error {
return d.gormdb.Save(app).Error
}
// GetApplicationByToken returns the application for the given token or nil.
func (d *Database) GetApplicationByToken(token string) (*model.Application, error) {
app := new(model.Application)
err := d.gormdb.Where("token = ?", token).First(app).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
return app, err
}

101
database/database.go Normal file
View file

@ -0,0 +1,101 @@
package database
import (
"database/sql"
"errors"
"log"
"os"
"path/filepath"
"time"
"github.com/eikendev/pushbits/authentication/credentials"
"github.com/eikendev/pushbits/model"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Database holds information for the database connection.
type Database struct {
gormdb *gorm.DB
sqldb *sql.DB
}
func createFileDir(file string) {
if _, err := os.Stat(filepath.Dir(file)); os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil {
panic(err)
}
}
}
// Create instanciates a database connection.
func Create(dialect, connection string) (*Database, error) {
log.Println("Setting up database connection.")
maxOpenConns := 5
var db *gorm.DB
var err error
switch dialect {
case "sqlite3":
createFileDir(connection)
maxOpenConns = 1
db, err = gorm.Open(sqlite.Open(connection), &gorm.Config{})
case "mysql":
db, err = gorm.Open(mysql.Open(connection), &gorm.Config{})
default:
message := "Database dialect is not supported"
return nil, errors.New(message)
}
if err != nil {
return nil, err
}
sql, err := db.DB()
if err != nil {
return nil, err
}
sql.SetMaxOpenConns(maxOpenConns)
if dialect == "mysql" {
sql.SetConnMaxLifetime(9 * time.Minute)
}
db.AutoMigrate(&model.User{}, &model.Application{})
return &Database{gormdb: db, sqldb: sql}, nil
}
// Close closes the database connection.
func (d *Database) Close() {
d.sqldb.Close()
}
// Populate fills the database with initial information like the admin user.
func (d *Database) Populate(name, password, matrixID string) error {
user := new(model.User)
query := d.gormdb.Where("name = ?", name).First(user)
if errors.Is(query.Error, gorm.ErrRecordNotFound) {
user := model.NewUser(name, password, true, matrixID)
if err := d.gormdb.Create(&user).Error; err != nil {
return errors.New("user cannot be created")
}
} else {
log.Printf("Admin user %s already exists.\n", name)
user.PasswordHash = credentials.CreatePassword(password)
user.IsAdmin = true
user.MatrixID = matrixID
d.gormdb.Save(&user)
}
return nil
}

26
database/user.go Normal file
View file

@ -0,0 +1,26 @@
package database
import (
"errors"
"github.com/eikendev/pushbits/model"
"gorm.io/gorm"
)
// CreateUser creates a user.
func (d *Database) CreateUser(user *model.User) error {
return d.gormdb.Create(user).Error
}
// GetUserByName returns the user by the given name or nil.
func (d *Database) GetUserByName(name string) (*model.User, error) {
user := new(model.User)
err := d.gormdb.Where("name = ?", name).First(user).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
return user, err
}

29
dispatcher/application.go Normal file
View file

@ -0,0 +1,29 @@
package dispatcher
import (
"log"
"github.com/matrix-org/gomatrix"
)
// RegisterApplication creates a new channel for the application.
func (d *Dispatcher) RegisterApplication(name, user string) (string, error) {
log.Printf("Registering application %s, notifications will be relayed to user %s.\n", name, user)
response, err := d.client.CreateRoom(&gomatrix.ReqCreateRoom{
Visibility: "private",
Name: name,
Invite: []string{user},
Preset: "private_chat",
IsDirect: true,
})
if err != nil {
log.Fatal(err)
return "", err
}
log.Printf("Application %s is now relayed to room with ID %s.\n", name, response.RoomID)
return response.RoomID, err
}

58
dispatcher/dispatcher.go Normal file
View file

@ -0,0 +1,58 @@
package dispatcher
import (
"log"
"github.com/eikendev/pushbits/model"
"github.com/matrix-org/gomatrix"
)
var (
loginType = "m.login.password"
)
// The Database interface for encapsulating database access.
type Database interface {
UpdateApplication(application *model.Application) error
}
// Dispatcher holds information for sending notifications to clients.
type Dispatcher struct {
db Database
client *gomatrix.Client
}
// Create instanciates a dispatcher connection.
// TODO: Call JoinedRooms() to validate room mappings on startup.
// TODO: ForgetRoom() for unused rooms.
// TODO: InviteUser() if the user is no longer in the room.
func Create(db Database, homeserver, username, password string) (*Dispatcher, error) {
log.Println("Setting up dispatcher.")
client, err := gomatrix.NewClient(homeserver, "", "")
if err != nil {
return nil, err
}
response, err := client.Login(&gomatrix.ReqLogin{
Type: loginType,
User: username,
Password: password,
})
if err != nil {
return nil, err
}
client.SetCredentials(response.UserID, response.AccessToken)
return &Dispatcher{client: client}, nil
}
// Close closes the dispatcher connection.
func (d *Dispatcher) Close() {
log.Printf("Logging out.\n")
d.client.Logout()
d.client.ClearCredentials()
}

View file

@ -0,0 +1,19 @@
package dispatcher
import (
"fmt"
"log"
"github.com/eikendev/pushbits/model"
)
// SendNotification sends a notification to the specified user.
func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) error {
log.Printf("Sending notification to room %s.\n", a.MatrixID)
text := fmt.Sprintf("%s\n\n%s", n.Title, n.Message)
_, err := d.client.SendText(a.MatrixID, text)
return err
}

16
go.mod Normal file
View file

@ -0,0 +1,16 @@
module github.com/eikendev/pushbits
go 1.14
require (
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/gin v1.6.3
github.com/jinzhu/configor v1.2.0
github.com/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/tools v0.0.0-20200724172932-b5fc9d354d99 // indirect
gorm.io/driver/mysql v0.3.1
gorm.io/driver/sqlite v1.0.8
gorm.io/gorm v0.2.24
)

100
go.sum Normal file
View file

@ -0,0 +1,100 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
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/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=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
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=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
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.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/jinzhu/configor v1.2.0 h1:u78Jsrxw2+3sGbGMgpY64ObKU4xWCNmNRJIjGVqxYQA=
github.com/jinzhu/configor v1.2.0/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/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
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/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752 h1:xYU2vdY5uh7QX4RZqeuKpmv0ffBEGYHhgu4Mmm3RVnc=
github.com/matrix-org/gomatrix v0.0.0-20200501121722-e5578b12c752/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s=
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.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7 h1:EBZoQjiKKPaLbPrbpssUfuHtwM6KV/vb4U85g/cigFY=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200724172932-b5fc9d354d99 h1:OHn441rq5CeM5r1xJ0OmY7lfdTvnedi6k+vQiI7G9b8=
golang.org/x/tools v0.0.0-20200724172932-b5fc9d354d99/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gorm.io/driver/mysql v0.3.1 h1:yvUT7Q0I3B9EHJ67NSp6cHbVwcdDHhVUsDAUiFFxRk0=
gorm.io/driver/mysql v0.3.1/go.mod h1:A7H1JD9dKdcjeUTpTuWKEC+E1a74qzW7/zaXqKaTbfM=
gorm.io/driver/sqlite v1.0.8 h1:omllgSb7/eh9D6lGvLZOdU1ZElxdXuO3dn3Rk+dQxUE=
gorm.io/driver/sqlite v1.0.8/go.mod h1:xkm8/CEmA3yc4zRd0pdCqm43BjO8Hm6avfTpxWb/7c4=
gorm.io/gorm v0.2.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v0.2.19/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v0.2.24 h1:NKjfopoXim5xAHYkt/uN0n7ZNmyoXZ9FEZUX4ijzXlI=
gorm.io/gorm v0.2.24/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=

10
model/application.go Normal file
View file

@ -0,0 +1,10 @@
package model
// Application holds information like the name, the token, and the associated user of an application.
type Application struct {
ID uint `gorm:"AUTO_INCREMENT;primary_key" json:"id"`
Token string `gorm:"type:string;size:64;unique" json:"token"`
UserID uint `json:"-"`
Name string `gorm:"type:string" form:"name" query:"name" json:"name" binding:"required"`
MatrixID string `gorm:"type:string"`
}

16
model/notification.go Normal file
View file

@ -0,0 +1,16 @@
package model
import (
"time"
)
// Notification holds information like the message, the title, and the priority of a notification.
type Notification struct {
ID uint `json:"id"`
ApplicationID uint `json:"appid"`
Message string `json:"message" form:"message" query:"message" binding:"required"`
Title string `json:"title" form:"title" query:"title"`
Priority int `json:"priority" form:"priority" query:"priority"`
Extras map[string]interface{} `json:"extras,omitempty" form:"-" query:"-"`
Date time.Time `json:"date"`
}

70
model/user.go Normal file
View file

@ -0,0 +1,70 @@
package model
import (
"log"
"github.com/eikendev/pushbits/authentication/credentials"
)
// User holds information like the name, the secret, and the applications of a user.
type User struct {
ID uint `gorm:"AUTO_INCREMENT;primary_key"`
Name string `gorm:"type:string;size:128;unique"`
PasswordHash []byte
IsAdmin bool
MatrixID string `gorm:"type:string"`
Applications []Application
}
// ExternalUser represents a user for external purposes.
type ExternalUser struct {
ID uint `json:"id"`
Name string `json:"name" form:"name" query:"name" binding:"required"`
IsAdmin bool `json:"is_admin" form:"is_admin" query:"is_admin"`
MatrixID string `json:"matrix_id" form:"matrix_id" query:"matrix_id" binding:"required"`
}
// UserCredentials holds information for authenticating a user.
type UserCredentials struct {
Password string `json:"password,omitempty" form:"password" query:"password" binding:"required"`
}
// ExternalUserWithCredentials represents a user for external purposes and includes the user's credentials in plaintext.
type ExternalUserWithCredentials struct {
ExternalUser
UserCredentials
}
// NewUser creates a new user.
func NewUser(name, password string, isAdmin bool, matrixID string) *User {
log.Printf("Creating user %s.\n", name)
user := User{
Name: name,
PasswordHash: credentials.CreatePassword(password),
IsAdmin: isAdmin,
MatrixID: matrixID,
}
return &user
}
// IntoInternalUser converts a ExternalUserWithCredentials into a User.
func (u *ExternalUserWithCredentials) IntoInternalUser() *User {
return &User{
Name: u.Name,
PasswordHash: credentials.CreatePassword(u.Password),
IsAdmin: u.IsAdmin,
MatrixID: u.MatrixID,
}
}
// IntoExternalUser converts a User into a ExternalUser.
func (u *User) IntoExternalUser() *ExternalUser {
return &ExternalUser{
ID: u.ID,
Name: u.Name,
IsAdmin: u.IsAdmin,
MatrixID: u.MatrixID,
}
}

45
router/router.go Normal file
View file

@ -0,0 +1,45 @@
package router
import (
"log"
"github.com/eikendev/pushbits/api"
"github.com/eikendev/pushbits/authentication"
"github.com/eikendev/pushbits/database"
"github.com/eikendev/pushbits/dispatcher"
"github.com/gin-contrib/location"
"github.com/gin-gonic/gin"
)
// Create a Gin engine and setup all routes.
func Create(db *database.Database, dp *dispatcher.Dispatcher) *gin.Engine {
log.Println("Setting up HTTP routes.")
auth := authentication.Authenticator{DB: db}
applicationHandler := api.ApplicationHandler{DB: db, Dispatcher: dp}
notificationHandler := api.NotificationHandler{DB: db, Dispatcher: dp}
userHandler := api.UserHandler{DB: db}
r := gin.Default()
r.Use(location.Default())
applicationGroup := r.Group("/application")
applicationGroup.Use(auth.RequireUser())
{
applicationGroup.POST("", applicationHandler.CreateApplication)
//applicationGroup.DELETE("/:id", applicationHandler.DeleteApplication)
}
r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification)
userGroup := r.Group("/user")
userGroup.Use(auth.RequireAdmin())
{
userGroup.POST("", userHandler.CreateUser)
//userGroup.DELETE("/:id", userHandler.DeleteUser)
}
return r
}

10
runner/runner.go Normal file
View file

@ -0,0 +1,10 @@
package runner
import (
"github.com/gin-gonic/gin"
)
// Run starts the Gin engine.
func Run(engine *gin.Engine) {
engine.Run(":8080")
}