Initial commit
This commit is contained in:
commit
6424512612
55
config/pkg.go
Normal file
55
config/pkg.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DatabaseDetails struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
Name string
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Network struct {
|
||||||
|
Address string
|
||||||
|
Port int
|
||||||
|
}
|
||||||
|
|
||||||
|
Websocket struct {
|
||||||
|
Domains []string
|
||||||
|
}
|
||||||
|
|
||||||
|
Database DatabaseDetails
|
||||||
|
|
||||||
|
Session struct {
|
||||||
|
DaysValid int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(filename string) (config Config, err error) {
|
||||||
|
var rawConfigData []byte
|
||||||
|
rawConfigData, err = os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(rawConfigData, &config)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Session.DaysValid == 0 {
|
||||||
|
err = errors.New("Configuration: session.daysvalid needs to be higher than 0.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
244
database/pkg.go
Normal file
244
database/pkg.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"git.gibonuddevalla.se/go/dbschema"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
"git.gibonuddevalla.se/go/webservice/config"
|
||||||
|
"git.gibonuddevalla.se/go/webservice/session"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SqlProvider func(string, int) ([]byte, bool)
|
||||||
|
|
||||||
|
type T struct {
|
||||||
|
cfg config.DatabaseDetails
|
||||||
|
Conn *sqlx.DB
|
||||||
|
logger *slog.Logger
|
||||||
|
|
||||||
|
sqlProvider SqlProvider
|
||||||
|
logProvider func(string, string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.DatabaseDetails) (db *T) { // {{{
|
||||||
|
db = new(T)
|
||||||
|
db.cfg = cfg
|
||||||
|
db.logProvider = db.defaultLogProvider
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (db *T) SetLogger(l *slog.Logger) { // {{{
|
||||||
|
db.logger = l
|
||||||
|
} // }}}
|
||||||
|
func (db *T) SetSQLProvider(fn func(string, int) ([]byte, bool)) { // {{{
|
||||||
|
db.sqlProvider = fn
|
||||||
|
} // }}}
|
||||||
|
func (db *T) SetLogProvider(fn func(string, string)) { // {{{
|
||||||
|
db.logProvider = fn
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (db *T) defaultLogProvider(category, msg string) { // {{{
|
||||||
|
db.logger.Info("database", category, msg)
|
||||||
|
} // }}}
|
||||||
|
func webserviceSQLProvider(dbname string, version int) ([]byte, bool) { // {{{
|
||||||
|
sql := map[int]string{
|
||||||
|
1: `
|
||||||
|
CREATE TABLE _webservice.user (
|
||||||
|
id serial NOT NULL,
|
||||||
|
"name" varchar NOT NULL,
|
||||||
|
"username" varchar NOT NULL,
|
||||||
|
"password" char(96) NOT NULL,
|
||||||
|
last_login timetz NOT NULL DEFAULT '1970-01-01 00:00:00',
|
||||||
|
CONSTRAINT user_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT user_un UNIQUE (username)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "_webservice"."session" (
|
||||||
|
id serial NOT NULL,
|
||||||
|
user_id int4 NULL,
|
||||||
|
"uuid" char(36) NOT NULL,
|
||||||
|
created time with time zone NOT NULL DEFAULT NOW(),
|
||||||
|
CONSTRAINT session_pk PRIMARY KEY (id),
|
||||||
|
CONSTRAINT session_un UNIQUE ("uuid"),
|
||||||
|
CONSTRAINT session_user_fk FOREIGN KEY (user_id) REFERENCES "_webservice"."user"(id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto SCHEMA _webservice;
|
||||||
|
|
||||||
|
CREATE FUNCTION _webservice.password_hash(salt_hex char(32), pass bytea)
|
||||||
|
RETURNS char(96)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS
|
||||||
|
$$
|
||||||
|
BEGIN
|
||||||
|
RETURN (
|
||||||
|
SELECT
|
||||||
|
salt_hex ||
|
||||||
|
encode(
|
||||||
|
sha256(
|
||||||
|
decode(salt_hex, 'hex') || /* salt in binary */
|
||||||
|
pass /* password */
|
||||||
|
),
|
||||||
|
'hex'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
`,
|
||||||
|
2: `
|
||||||
|
ALTER TABLE _webservice.session ADD last_used timetz NOT NULL DEFAULT NOW();
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
|
statement, found := sql[version]
|
||||||
|
return []byte(statement), found
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (db *T) Upgrade() (err error) { // {{{
|
||||||
|
upgrader := dbschema.NewUpgrader("_webservice")
|
||||||
|
upgrader.SetSqlCallback(webserviceSQLProvider)
|
||||||
|
upgrader.SetLogCallback(db.logProvider)
|
||||||
|
if err = upgrader.AddDatabase(
|
||||||
|
db.cfg.Host,
|
||||||
|
db.cfg.Port,
|
||||||
|
db.cfg.Name,
|
||||||
|
db.cfg.Username,
|
||||||
|
db.cfg.Password,
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = upgrader.Run()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
upgrader = dbschema.NewUpgrader("_db")
|
||||||
|
upgrader.SetSqlCallback(db.sqlProvider)
|
||||||
|
upgrader.SetLogCallback(db.logProvider)
|
||||||
|
if err = upgrader.AddDatabase(
|
||||||
|
db.cfg.Host,
|
||||||
|
db.cfg.Port,
|
||||||
|
db.cfg.Name,
|
||||||
|
db.cfg.Username,
|
||||||
|
db.cfg.Password,
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = upgrader.Run()
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (db *T) Connect() (err error) { // {{{
|
||||||
|
db.logger.Info("database", "host", db.cfg.Host, "port", db.cfg.Port, "name", db.cfg.Name)
|
||||||
|
|
||||||
|
dbConn := fmt.Sprintf(
|
||||||
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||||
|
db.cfg.Host,
|
||||||
|
db.cfg.Port,
|
||||||
|
db.cfg.Username,
|
||||||
|
db.cfg.Password,
|
||||||
|
db.cfg.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if db.Conn, err = sqlx.Connect("postgres", dbConn); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (db *T) Authenticate(username, password string) (authenticated bool, userID int, err error) { // {{{
|
||||||
|
var rows *sql.Rows
|
||||||
|
if rows, err = db.Conn.Query(`
|
||||||
|
SELECT id
|
||||||
|
FROM _webservice.user
|
||||||
|
WHERE
|
||||||
|
username = $1 AND
|
||||||
|
password = _webservice.password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
|
||||||
|
`,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if rows.Next() {
|
||||||
|
rows.Scan(&userID)
|
||||||
|
authenticated = userID > 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (db *T) NewSession(uuid string) (err error) { // {{{
|
||||||
|
_, err = db.Conn.Exec("INSERT INTO _webservice.session(uuid) VALUES($1)", uuid)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (db *T) RetrieveSession(uuid string) (sess *session.T, err error) {// {{{
|
||||||
|
var rows *sqlx.Rows
|
||||||
|
rows, err = db.Conn.Queryx(`
|
||||||
|
WITH session_data AS (
|
||||||
|
UPDATE _webservice.session
|
||||||
|
SET
|
||||||
|
last_used=NOW()
|
||||||
|
WHERE
|
||||||
|
uuid=$1
|
||||||
|
RETURNING
|
||||||
|
uuid, created, last_used, user_id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
sd.uuid, sd.created, sd.last_used,
|
||||||
|
COALESCE(u.username, '') AS username,
|
||||||
|
COALESCE(u.name, '') AS name
|
||||||
|
FROM session_data sd
|
||||||
|
LEFT JOIN _webservice.user u ON sd.user_id = u.id
|
||||||
|
`,
|
||||||
|
uuid,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
sess = new(session.T)
|
||||||
|
err = rows.StructScan(sess)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
func (db *T) SetSessionUser(uuid string, userID int) (err error) { // {{{
|
||||||
|
_, err = db.Conn.Exec("UPDATE _webservice.session SET user_id=$1 WHERE uuid=$2", userID, uuid)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (db *T) CreateUser(username, password, name string) (err error) {// {{{
|
||||||
|
_, err = db.Conn.Exec(`
|
||||||
|
INSERT INTO _webservice.user(username, password, name)
|
||||||
|
VALUES(
|
||||||
|
$1,
|
||||||
|
_webservice.password_hash(
|
||||||
|
/* salt in hex */
|
||||||
|
ENCODE(_webservice.gen_random_bytes(16), 'hex'),
|
||||||
|
|
||||||
|
/* password */
|
||||||
|
$2::bytea
|
||||||
|
),
|
||||||
|
$3
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
// vim: foldmethod=marker
|
11
go.mod
Normal file
11
go.mod
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
module git.gibonuddevalla.se/go/webservice
|
||||||
|
|
||||||
|
go 1.21.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.gibonuddevalla.se/go/dbschema v1.3.0
|
||||||
|
github.com/google/uuid v1.5.0
|
||||||
|
github.com/jmoiron/sqlx v1.3.5
|
||||||
|
github.com/lib/pq v1.10.9
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
17
go.sum
Normal file
17
go.sum
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
git.gibonuddevalla.se/go/dbschema v1.2.0 h1:VhHFfkn/4UnlGy2Ax35Po8vb8E/x6DggtvNUKlGGQyY=
|
||||||
|
git.gibonuddevalla.se/go/dbschema v1.2.0/go.mod h1:BNw3q/574nXbGoeWyK+tLhRfggVkw2j2aXZzrBKC3ig=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
|
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
|
||||||
|
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
|
||||||
|
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
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=
|
||||||
|
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
217
pkg.go
Normal file
217
pkg.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
The webservice package is used to provide a webservice with sessions:
|
||||||
|
|
||||||
|
func sqlProvider(dbname string, version int) (sql []byte, found bool) {
|
||||||
|
var err error
|
||||||
|
sql, err = embeddedSQL.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := webservice.New("/etc/some/webservice.yaml")
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("application", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
service.SetDatabase(sqlProvider)
|
||||||
|
service.SetAuthenticationHandler(authenticate)
|
||||||
|
service.SetAuthorizationHandler(authorize)
|
||||||
|
service.Register("/foo", true, true, foo)
|
||||||
|
service.Register("/bar", true, false, bar)
|
||||||
|
err = service.Start()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("webserver", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
package webservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Internal
|
||||||
|
"git.gibonuddevalla.se/go/webservice/config"
|
||||||
|
"git.gibonuddevalla.se/go/webservice/database"
|
||||||
|
"git.gibonuddevalla.se/go/webservice/session"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const VERSION = "v0.1.0"
|
||||||
|
|
||||||
|
type HttpHandler func(http.ResponseWriter, *http.Request)
|
||||||
|
|
||||||
|
type ErrorHandler func(err error, w http.ResponseWriter)
|
||||||
|
type ServiceError struct {
|
||||||
|
OK bool
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
logger *slog.Logger
|
||||||
|
sessions map[string]*session.T
|
||||||
|
config config.Config
|
||||||
|
Db *database.T
|
||||||
|
|
||||||
|
errorHandler ErrorHandler
|
||||||
|
authenticationHandler AuthenticationHandler
|
||||||
|
authorizationHandler AuthorizationHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServiceHandler func(http.ResponseWriter, *http.Request, *session.T)
|
||||||
|
|
||||||
|
func New(configFilename string) (service *Service, err error) { // {{{
|
||||||
|
service = new(Service)
|
||||||
|
|
||||||
|
service.config, err = config.New(configFilename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := slog.HandlerOptions{}
|
||||||
|
service.logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))
|
||||||
|
service.sessions = make(map[string]*session.T, 128)
|
||||||
|
service.errorHandler = service.defaultErrorHandler
|
||||||
|
service.authenticationHandler = service.defaultAuthenticationHandler
|
||||||
|
service.authorizationHandler = service.defaultAuthorizationHandler
|
||||||
|
|
||||||
|
service.Register("/_session/new", false, false, service.sessionNew)
|
||||||
|
service.Register("/_session/authenticate", true, false, service.sessionAuthenticate)
|
||||||
|
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (service *Service) defaultAuthenticationHandler(req AuthenticationRequest, alreadyAuthenticated bool) (resp AuthenticationResponse, err error) { // {{{
|
||||||
|
resp.Authenticated = alreadyAuthenticated
|
||||||
|
service.logger.Info("webservice", "op", "authentication", "request", req, "authenticated", resp.Authenticated)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) defaultAuthorizationHandler(sess *session.T, r *http.Request) (resp bool, err error) { // {{{
|
||||||
|
service.logger.Error("webservice", "op", "authorization", "session", sess.UUID, "request", r, "authorized", false)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) defaultErrorHandler(err error, w http.ResponseWriter) { // {{{
|
||||||
|
service.logger.Error("webservice", "error", err)
|
||||||
|
errMsg := ServiceError{}
|
||||||
|
errMsg.OK = false
|
||||||
|
errMsg.Error = err.Error()
|
||||||
|
errJSON, _ := json.Marshal(errMsg)
|
||||||
|
w.Write(errJSON)
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (service *Service) SetLogger(logger *slog.Logger) { // {{{
|
||||||
|
service.logger = logger
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) SetErrorHandler(h ErrorHandler) { // {{{
|
||||||
|
service.errorHandler = h
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) SetAuthenticationHandler(h AuthenticationHandler) { // {{{
|
||||||
|
service.authenticationHandler = h
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) SetAuthorizationHandler(h AuthorizationHandler) { // {{{
|
||||||
|
service.authorizationHandler = h
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func (service *Service) SetDatabase(sqlProv database.SqlProvider) { // {{{
|
||||||
|
service.Db = database.New(service.config.Database)
|
||||||
|
service.Db.SetLogger(service.logger)
|
||||||
|
service.Db.SetSQLProvider(sqlProv)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) Register(path string, requireSession, requireAuthentication bool, handler ServiceHandler) { // {{{
|
||||||
|
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var session *session.T
|
||||||
|
var found bool
|
||||||
|
var authorized bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if requireAuthentication && !requireSession {
|
||||||
|
requireSession = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if requireSession {
|
||||||
|
headerSessionUUID, err := sessionUUID(r)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(fmt.Errorf("Header X-Session-ID missing"), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, found = service.sessionRetrieve(headerSessionUUID)
|
||||||
|
if !found {
|
||||||
|
service.errorHandler(fmt.Errorf("Session '%s' not found", headerSessionUUID), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if requireAuthentication {
|
||||||
|
if !session.Authenticated {
|
||||||
|
service.errorHandler(fmt.Errorf("Session '%s' not authenticated", session.UUID), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
authorized, err = service.authorizationHandler(session, r)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !authorized {
|
||||||
|
service.errorHandler(fmt.Errorf("Session '%s' not authorized for %s", session.UUID, r.URL.String()), w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(w, r, session)
|
||||||
|
})
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) InitDatabaseConnection() (err error) { // {{{
|
||||||
|
err = service.Db.Upgrade()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.Db.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) CreateUser(username, password, name string) (err error) { // {{{
|
||||||
|
if service.Db != nil {
|
||||||
|
err = service.InitDatabaseConnection()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.Db.CreateUser(username, password, name)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) Start() (err error) { // {{{
|
||||||
|
if service.Db != nil {
|
||||||
|
err = service.InitDatabaseConnection()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listen := fmt.Sprintf("%s:%d", service.config.Network.Address, service.config.Network.Port)
|
||||||
|
service.logger.Info("webserver", "listen", listen)
|
||||||
|
err = http.ListenAndServe(listen, nil)
|
||||||
|
return
|
||||||
|
} // }}}
|
||||||
|
|
||||||
|
func sessionUUID(r *http.Request) (string, error) { // {{{
|
||||||
|
headers := r.Header["X-Session-Id"]
|
||||||
|
if len(headers) > 0 {
|
||||||
|
return headers[0], nil
|
||||||
|
}
|
||||||
|
return "", errors.New("Invalid session")
|
||||||
|
} // }}}
|
136
session.go
Normal file
136
session.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package webservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
// External
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
"git.gibonuddevalla.se/go/webservice/session"
|
||||||
|
|
||||||
|
// Standard
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AuthenticationRequest struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticationResponse struct {
|
||||||
|
Authenticated bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticationHandler func(AuthenticationRequest, bool) (AuthenticationResponse, error)
|
||||||
|
type AuthorizationHandler func(*session.T, *http.Request) (bool, error)
|
||||||
|
|
||||||
|
func (service *Service) sessionNew(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var session session.T
|
||||||
|
var found bool
|
||||||
|
var err error
|
||||||
|
|
||||||
|
for {
|
||||||
|
session.UUID = uuid.NewString()
|
||||||
|
if service.Db == nil {
|
||||||
|
if _, found = service.sessions[session.UUID]; found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Authenticated = false
|
||||||
|
session.Created = time.Now()
|
||||||
|
service.sessions[session.UUID] = &session
|
||||||
|
break
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if _, found = service.sessionRetrieve(session.UUID); found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.Db.NewSession(session.UUID)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
respJSON, _ := json.Marshal(
|
||||||
|
struct {
|
||||||
|
OK bool
|
||||||
|
UUID string
|
||||||
|
}{
|
||||||
|
true,
|
||||||
|
session.UUID,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
w.Write(respJSON)
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) sessionAuthenticate(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
|
||||||
|
var authenticated bool
|
||||||
|
var authResponse AuthenticationResponse
|
||||||
|
var err error
|
||||||
|
reqBody, _ := io.ReadAll(r.Body)
|
||||||
|
|
||||||
|
// Username and password are provided in this JSON authentication request.
|
||||||
|
var authRequest AuthenticationRequest
|
||||||
|
err = json.Unmarshal(reqBody, &authRequest)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate against webservice user table if using a database.
|
||||||
|
if service.Db != nil {
|
||||||
|
var userID int
|
||||||
|
authenticated, userID, err = service.Db.Authenticate(authRequest.Username, authRequest.Password)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if authenticated && userID > 0 {
|
||||||
|
err = service.Db.SetSessionUser(sess.UUID, userID)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// The authentication handler is provided with the authenticated response of the possible database authentication,
|
||||||
|
// and given a chance to override it.
|
||||||
|
authResponse, err = service.authenticationHandler(authRequest, authenticated)
|
||||||
|
if err != nil {
|
||||||
|
service.errorHandler(err, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess.Authenticated = authResponse.Authenticated
|
||||||
|
|
||||||
|
authResp, _ := json.Marshal(authResponse)
|
||||||
|
w.Write(authResp)
|
||||||
|
} // }}}
|
||||||
|
func (service *Service) sessionRetrieve(uuid string) (session *session.T, found bool) { // {{{
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if service.Db == nil {
|
||||||
|
session, found = service.sessions[uuid]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session, err = service.Db.RetrieveSession(uuid)
|
||||||
|
if err != nil {
|
||||||
|
service.logger.Error("session", "error", err)
|
||||||
|
session = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
found = (session != nil)
|
||||||
|
return
|
||||||
|
} // }}}
|
16
session/pkg.go
Normal file
16
session/pkg.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package session
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type T struct {
|
||||||
|
UUID string
|
||||||
|
Created time.Time
|
||||||
|
LastUsed time.Time `db:"last_used"`
|
||||||
|
Authenticated bool
|
||||||
|
|
||||||
|
Username string
|
||||||
|
Name string
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user