Compare commits

...

17 commits
v0.2.9 ... main

Author SHA1 Message Date
58050a44a4 Fixed double pointer for ParseApplicationConfig 2025-03-06 07:51:26 +01:00
63f484e269 Honor the useStaticDirectory variable in the static handler to minimize unnecessary os.Stat 2025-03-06 07:01:18 +01:00
1cf9318bc8 Added html_template package for easier HTML page generation 2024-12-19 13:46:22 +01:00
ac7887b6c7 Add README.md 2024-09-17 07:28:04 +02:00
53efcaedc9 Execute websocket handlers as goroutines. Bumped to 0.2.17 2024-07-30 07:45:41 +02:00
256dda9a48 Removed validation on session since a lot of other software is depending on this package 2024-06-30 11:41:03 +02:00
6623db9574 Added caching headers for static content 2024-05-22 07:58:46 +02:00
cff9082aac Added application config parsing 2024-05-22 07:58:46 +02:00
452a109204 Read session UUID from cookie if header is missing. Bump to v0.2.14 2024-05-08 18:10:06 +02:00
aecafc2986 Bumped to v0.2.13 2024-05-08 17:52:15 +02:00
a62d2287ae Added X-Session-ID cookie 2024-05-08 17:51:41 +02:00
9ce19b94b4 Updated doc, exposed Config and bumped to v0.2.12 2024-04-27 11:54:33 +02:00
57c1cfb412 Bumped to v0.2.11 2024-04-22 09:19:38 +02:00
d650542b72 Authentication with lowercased username 2024-04-22 09:19:21 +02:00
548cddb773 Bumped version string to 0.2.10 2024-03-29 08:45:07 +01:00
ad1cb56a06 Merge branch 'main' of ssh://git.gibonuddevalla.se:2222/go/webservice 2024-02-20 08:05:42 +01:00
825bc4d8b2 Added a function to change password 2024-02-20 08:05:33 +01:00
8 changed files with 294 additions and 30 deletions

10
README.md Normal file
View file

@ -0,0 +1,10 @@
# godoc
Installera verktyget godoc:
```
go install golang.org/x/tools/cmd/godoc@latest
```
Kör godoc i katalogen för webservice-repot.
Gå till http://localhost:6060/

View file

@ -5,7 +5,6 @@ import (
"gopkg.in/yaml.v3"
// Standard
"errors"
"os"
)
@ -33,6 +32,8 @@ type Config struct {
Session struct {
DaysValid int
}
Application any
}
func New(filename string) (config Config, err error) {
@ -47,9 +48,10 @@ func New(filename string) (config Config, err error) {
return
}
if config.Session.DaysValid == 0 {
err = errors.New("Configuration: session.daysvalid needs to be higher than 0.")
}
return
}
func (config *Config) ParseApplicationConfig(v any) {
yStr, _ := yaml.Marshal(config.Application)
yaml.Unmarshal(yStr, v)
}

View file

@ -164,7 +164,7 @@ func (db *T) Authenticate(username, password string) (authenticated bool, userID
SELECT id
FROM _webservice.user
WHERE
username = $1 AND
LOWER(username) = LOWER($1) AND
password = _webservice.password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
`,
username,
@ -286,5 +286,36 @@ func (db *T) CreateUser(username, password, name string) (userID int64, err erro
err = row.Scan(&userID)
return
} // }}}
func (db *T) ChangePassword(userID int, currentPassword, newPassword string) (changed bool, err error) { // {{{
var res sql.Result
res, err = db.Conn.Exec(`
UPDATE _webservice.user
SET
"password" = _webservice.password_hash(
/* salt in hex */
ENCODE(_webservice.gen_random_bytes(16), 'hex'),
/* password */
$3::bytea
)
WHERE
id = $1 AND
"password" = _webservice.password_hash(SUBSTRING(password FROM 1 FOR 32), $2::bytea)
`,
userID,
currentPassword,
newPassword,
)
var rowsAffected int64
rowsAffected, err = res.RowsAffected()
if err != nil {
return
}
changed = (rowsAffected == 1)
return
} // }}}
// vim: foldmethod=marker

31
html_template/page.go Normal file
View file

@ -0,0 +1,31 @@
package HTMLTemplate
type Page interface {
GetVersion() string
GetLayout() string
GetPage() string
GetData() any
}
type SimplePage struct {
Version string
Layout string
Page string
Data any
}
func (s SimplePage) GetVersion() string {
return s.Version
}
func (s SimplePage) GetLayout() string {
return s.Layout
}
func (s SimplePage) GetPage() string {
return s.Page
}
func (s SimplePage) GetData() any {
return s.Data
}

158
html_template/pkg.go Normal file
View file

@ -0,0 +1,158 @@
package HTMLTemplate
import (
// External
werr "git.gibonuddevalla.se/go/wrappederror"
// Standard
"fmt"
"html/template"
"io/fs"
"net/http"
"os"
"regexp"
)
type Engine struct {
parsedTemplates map[string]*template.Template
viewFS fs.FS
staticEmbeddedFS http.Handler
staticLocalFS http.Handler
componentFilenames []string
DevMode bool
}
func NewEngine(viewFS, staticFS fs.FS, devmode bool) (e Engine, err error) { // {{{
e.parsedTemplates = make(map[string]*template.Template)
e.viewFS = viewFS
e.DevMode = devmode
e.componentFilenames, err = e.getComponentFilenames()
// Set up fileservers for static resources.
// The embedded FS is using the embedded files intented for production use.
// The local FS is for development of Javascript to avoid server rebuild (devmode).
var staticSubFS fs.FS
staticSubFS, err = fs.Sub(staticFS, "static")
if err != nil {
return
}
e.staticEmbeddedFS = http.FileServer(http.FS(staticSubFS))
e.staticLocalFS = http.FileServer(http.Dir("static"))
return
} // }}}
func (e *Engine) getComponentFilenames() (files []string, err error) { // {{{
files = []string{}
if err := fs.WalkDir(e.viewFS, "views/components", func(path string, d fs.DirEntry, err error) error {
if d == nil {
return nil
}
if d.IsDir() {
return nil
}
files = append(files, path)
return nil
}); err != nil {
return nil, err
}
return files, nil
} // }}}
func (e *Engine) ReloadTemplates() { // {{{
e.parsedTemplates = make(map[string]*template.Template)
} // }}}
func (e *Engine) StaticResource(w http.ResponseWriter, r *http.Request) { // {{{
var err error
// URLs with pattern /(css|images)/v1.0.0/foobar are stripped of the version.
// To get rid of problems with cached content in browser on a new version release,
// while also not disabling cache altogether.
if r.URL.Path == "/favicon.ico" {
e.staticEmbeddedFS.ServeHTTP(w, r)
return
}
rxp := regexp.MustCompile("^/(css|images|js|fonts)/v[0-9]+/(.*)$")
if comp := rxp.FindStringSubmatch(r.URL.Path); comp != nil {
w.Header().Add("Pragma", "public")
w.Header().Add("Cache-Control", "max-age=604800")
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
if e.DevMode {
p := fmt.Sprintf("static/%s/%s", comp[1], comp[2])
_, err = os.Stat(p)
if err == nil {
e.staticLocalFS.ServeHTTP(w, r)
}
return
}
}
e.staticEmbeddedFS.ServeHTTP(w, r)
} // }}}
func (e *Engine) getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)
if tmpl, found := e.parsedTemplates[page]; found {
return tmpl, nil
}
funcMap := template.FuncMap{
/*
"format_time": func(t time.Time) template.HTML {
return template.HTML(
t.In(smonConfig.Timezone()).Format(`<span class="date">2006-01-02</span> <span class="time">15:04:05<span class="seconds">:05</span></span>`),
)
},
*/
}
filenames := []string{layoutFilename, pageFilename}
filenames = append(filenames, e.componentFilenames...)
if e.DevMode {
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
} else {
tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(e.viewFS, filenames...)
}
if err != nil {
err = werr.Wrap(err).Log()
return
}
e.parsedTemplates[page] = tmpl
return
} // }}}
func (e *Engine) Render(p Page, w http.ResponseWriter, r *http.Request) (err error) { // {{{
if e.DevMode {
e.ReloadTemplates()
}
var tmpl *template.Template
tmpl, err = e.getPage(p.GetLayout(), p.GetPage())
if err != nil {
err = werr.Wrap(err)
return
}
data := map[string]any{
"VERSION": p.GetVersion(),
"LAYOUT": p.GetLayout(),
"PAGE": p.GetPage(),
"ERROR": r.URL.Query().Get("_err"),
"Data": p.GetData(),
}
err = tmpl.Execute(w, data)
if err != nil {
err = werr.Wrap(err)
}
return
} // }}}
// vim: foldmethod=marker

71
pkg.go
View file

@ -1,6 +1,10 @@
/*
The webservice package is used to provide a webservice with sessions:
const VERSION = "v1"
var logger *slog.Logger
func sqlProvider(dbname string, version int) (sql []byte, found bool) {
var err error
sql, err = embeddedSQL.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
@ -11,21 +15,29 @@ The webservice package is used to provide a webservice with sessions:
return
}
service, err := webservice.New("/etc/some/webservice.yaml")
if err != nil {
logger.Error("application", "error", err)
os.Exit(1)
func init() {
opts := slog.HandlerOptions{}
logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))
}
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)
func main() {
service, err := webservice.New("/etc/some/webservice.yaml", VERSION, logger)
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
@ -52,7 +64,7 @@ import (
"strings"
)
const VERSION = "v0.1.0"
const VERSION = "v0.2.17"
type HttpHandler func(http.ResponseWriter, *http.Request)
@ -66,7 +78,7 @@ type ServiceError struct {
type Service struct {
logger *slog.Logger
sessions map[string]*session.T
config config.Config
Config config.Config
Db *database.T
Version string
WsConnectionManager ws_conn_manager.ConnectionManager
@ -87,11 +99,11 @@ type ServiceHandler func(http.ResponseWriter, *http.Request, *session.T)
func New(configFilename, version string, logger *slog.Logger) (service *Service, err error) { // {{{
service = new(Service)
service.config, err = config.New(configFilename)
service.Config, err = config.New(configFilename)
if err != nil {
return
}
logger.Debug("config", "config", service.config)
logger.Debug("config", "config", service.Config)
service.Version = version
service.logger = logger
@ -99,7 +111,7 @@ func New(configFilename, version string, logger *slog.Logger) (service *Service,
service.errorHandler = service.defaultErrorHandler
service.authenticationHandler = service.defaultAuthenticationHandler
service.authorizationHandler = service.defaultAuthorizationHandler
service.WsConnectionManager = ws_conn_manager.NewConnectionManager(service.logger, service.config.Websocket.Domains)
service.WsConnectionManager = ws_conn_manager.NewConnectionManager(service.logger, service.Config.Websocket.Domains)
service.Register("/_session/new", false, false, service.sessionNew)
service.Register("/_session/authenticate", true, false, service.sessionAuthenticate)
@ -160,7 +172,7 @@ func (service *Service) SetStaticDirectory(directory string, useDirectory bool)
} // }}}
func (service *Service) SetDatabase(sqlProv database.SqlProvider) { // {{{
service.Db = database.New(service.config.Database)
service.Db = database.New(service.Config.Database)
service.Db.SetLogger(service.logger)
service.Db.SetSQLProvider(sqlProv)
return
@ -267,7 +279,7 @@ func (service *Service) Start() (err error) { // {{{
go service.WsConnectionManager.BroadcastLoop()
listen := fmt.Sprintf("%s:%d", service.config.Network.Address, service.config.Network.Port)
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
@ -288,11 +300,19 @@ func (service *Service) StaticHandler(w http.ResponseWriter, r *http.Request, se
rxp := regexp.MustCompile("^/(css|images|js|fonts)/v[0-9]+/(.*)$")
if comp := rxp.FindStringSubmatch(r.URL.Path); comp != nil {
w.Header().Add("Pragma", "public")
w.Header().Add("Cache-Control", "max-age=604800")
r.URL.Path = fmt.Sprintf("/%s/%s", comp[1], comp[2])
p := fmt.Sprintf(service.staticDirectory+"/%s/%s", comp[1], comp[2])
_, err = os.Stat(p)
if err == nil {
service.staticLocalFileserver.ServeHTTP(w, r)
if service.useStaticDirectory {
_, err = os.Stat(p)
if err == nil {
service.staticLocalFileserver.ServeHTTP(w, r)
} else {
service.staticEmbeddedFileserver.ServeHTTP(w, r)
}
} else {
service.staticEmbeddedFileserver.ServeHTTP(w, r)
}
@ -364,6 +384,11 @@ func sessionUUID(r *http.Request) (string, error) { // {{{
headers := r.Header["X-Session-Id"]
if len(headers) > 0 {
return headers[0], nil
} else {
cookie, err := r.Cookie("X-Session-ID")
if err == nil && cookie.Value != "" {
return cookie.Value, nil
}
}
return "", errors.New("Invalid session")
} // }}}

View file

@ -75,6 +75,13 @@ func (service *Service) sessionNew(w http.ResponseWriter, r *http.Request, foo *
},
)
cookie := http.Cookie{}
cookie.Name = "X-Session-ID"
cookie.Value = sess.UUID
cookie.MaxAge = 86400 * 365
cookie.Path = "/"
http.SetCookie(w, &cookie)
w.Write(respJSON)
} // }}}
func (service *Service) sessionAuthenticate(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{

View file

@ -117,7 +117,7 @@ func (cm *ConnectionManager) ReadLoop(wsConn *WsConnection) { // {{{
cm.logger.Debug("websocket", "op", "read", "data", data)
for _, handler := range cm.readHandlers {
handler(cm, wsConn, data)
go handler(cm, wsConn, data)
}
}
} // }}}