First session system
All checks were successful
the build was successful

This commit is contained in:
Antoine Bartuccio 2018-07-18 00:00:51 +02:00
parent f24b1d2ed5
commit e68bc648ca
Signed by: klmp200
GPG Key ID: E7245548C53F904B
8 changed files with 401 additions and 8 deletions

View File

@ -3,4 +3,5 @@ pipeline:
image: golang
commands:
- go get github.com/oxtoacart/bpool
- go get github.com/google/uuid
- go build

View File

@ -15,10 +15,17 @@ Le fichier de configuration est passé dans `Configure()` et doit être au forma
"ServerPort": ":8000",
"Domain": "http://git.an",
"MainTemplate": "{{define \"main\" }} {{ template \"base\" . }} {{ end }}",
"Debug": false
"Debug": false,
"SessionProvider": "FileProvider"
}
```
Des paramètres par défaut sont utilisé dans tous les cas.
Le système de session utilise un dossier `.session` généré automatiquement, attention aux permissions !
Il est possible de rajouter ses propres backend pour le système de session en les enregistrant grâce à la fonction `SessionProviderRegister`. Les backends doivent respecter l'interface `SessionProvider`.
Le MainTemplate est utilisé pour charger tous les templates, il peut être utilisé tel quel ou modifié si vous savez ce que fous faites. Dans le doute, laissez celui par défaut.
On lance enfin le serveur en utilisant `Start()`

22
cookie.go Normal file
View File

@ -0,0 +1,22 @@
/*
* @Author: Bartuccio Antoine
* @Date: 2018-07-16 12:22:24
* @Last Modified by: klmp200
* @Last Modified time: 2018-07-17 23:27:27
*/
package gowebframework
import (
"net/http"
"time"
)
func AddCookie(w http.ResponseWriter, name string, data string, lifetime time.Duration) {
cookie := http.Cookie{
Name: name,
Value: data,
Expires: time.Now().Add(lifetime),
}
http.SetCookie(w, &cookie)
}

157
file-session-provider.go Normal file
View File

@ -0,0 +1,157 @@
/*
* @Author: Bartuccio Antoine
* @Date: 2018-07-16 15:37:29
* @Last Modified by: klmp200
* @Last Modified time: 2018-07-17 23:47:56
*/
package gowebframework
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"path/filepath"
"time"
)
var session_folder_path string = ".session"
var folder_perm os.FileMode = 0770
type FileSessionProvider struct{}
// Used to hide FileSession variables and still be able to write inside json with json.Marshal
type unexportedFileSession struct {
UUID string
LastUse time.Time
Data map[string]interface{}
}
type FileSession struct {
uuid string
lastUse time.Time
data map[string]interface{}
}
// Convert a unexportedFileSession to a FileSession
func (session unexportedFileSession) fileSession() FileSession {
return FileSession{
uuid: session.UUID,
lastUse: session.LastUse,
data: session.Data,
}
}
func (provider FileSessionProvider) checkOrCreateFolder() {
folder, err := os.Open(session_folder_path)
if err != nil {
// Auto create folder if doesn't exist
log.Println("No session folder found, creating " + session_folder_path)
err = os.Mkdir(session_folder_path, folder_perm)
if err != nil {
log.Fatal("Could not create session directory, exiting")
}
return
}
folder_infos, err := folder.Stat()
if err != nil {
log.Fatal(err)
}
if !folder_infos.IsDir() {
log.Fatal(session_folder_path + " is not a directory, exiting")
}
if !isWritable(session_folder_path) {
log.Fatal(session_folder_path + " directory is not writable, exiting")
}
}
func (provider FileSessionProvider) SessionInit(uuid string) (Session, error) {
provider.checkOrCreateFolder()
provider.SessionDestroy(uuid)
session := FileSession{}
session.uuid = uuid
session.data = make(map[string]interface{})
session.lastUse = time.Now()
return session, session.writeSession()
}
func (provider FileSessionProvider) SessionRead(uuid string) (Session, error) {
provider.checkOrCreateFolder()
data, err := ioutil.ReadFile(filepath.Join(session_folder_path, uuid+".json"))
if err != nil {
return provider.SessionInit(uuid)
}
unexported_session := unexportedFileSession{}
err = json.Unmarshal(data, &unexported_session)
if err != nil {
return nil, err
}
session := unexported_session.fileSession()
return session, session.writeSession()
}
func (provider FileSessionProvider) SessionDestroy(uuid string) error {
provider.checkOrCreateFolder()
return os.Remove(filepath.Join(session_folder_path, uuid+".json"))
}
func (provider FileSessionProvider) SessionClearExpired(maxLifetime time.Duration) {
provider.checkOrCreateFolder()
directory, _ := ioutil.ReadDir(session_folder_path)
for _, file := range directory {
session := unexportedFileSession{}
data, err := ioutil.ReadFile(filepath.Join(session_folder_path, file.Name()))
if err != nil {
log.Println(err)
} else {
err = json.Unmarshal(data, &session)
if err != nil {
log.Println(err)
} else {
if session.LastUse.Add(maxLifetime).Sub(time.Now()) <= 0 {
// Session expired
provider.SessionDestroy(session.UUID)
}
}
}
}
}
func (session FileSession) unexportedFileSession() unexportedFileSession {
return unexportedFileSession{
UUID: session.uuid,
LastUse: session.lastUse,
Data: session.data,
}
}
// Write a session on the disk
func (session FileSession) writeSession() error {
session.lastUse = time.Now()
data, _ := json.Marshal(session.unexportedFileSession())
err := ioutil.WriteFile(filepath.Join(session_folder_path, session.uuid+".json"), data, folder_perm)
return err
}
func (session FileSession) Set(key string, value interface{}) error {
session.data[key] = value
return session.writeSession()
}
func (session FileSession) Get(key string) interface{} {
value, ok := session.data[key]
if !ok {
return nil
}
return value
}
func (session FileSession) Delete(key string) error {
delete(session.data, key)
return session.writeSession()
}
func (session FileSession) SessionUUID() string {
return session.uuid
}

View File

@ -2,7 +2,7 @@
* @Author: Bartuccio Antoine
* @Date: 2018-07-14 11:32:11
* @Last Modified by: klmp200
* @Last Modified time: 2018-07-14 13:18:53
* @Last Modified time: 2018-07-17 23:55:22
*/
package gowebframework
@ -16,6 +16,7 @@ import (
"log"
"net/http"
"path/filepath"
"time"
)
var templates map[string]*template.Template
@ -28,6 +29,7 @@ type Config struct {
StaticFolderPath string
TemplateExtensionPattern string
ServerPort string
SessionProvider string
Domain string
Debug bool
}
@ -38,6 +40,15 @@ var ServerConfig Config
func Configure(config_file_name string, custom_config_file_name string) {
loadConfiguration(config_file_name, custom_config_file_name)
loadTemplates()
// initialise sessions
SessionProviderRegister(FileSessionProvider{}, "FileProvider")
// Put a session of 1 day duration
ses, err := newSessionManager(ServerConfig.SessionProvider, "GOSESSID", time.Hour*24)
if err != nil {
log.Fatal(err)
}
SESSION = ses
go clearExpiredSessionsCron()
}
// Starts server
@ -47,15 +58,29 @@ func Start() {
log.Fatal(http.ListenAndServe(ServerConfig.ServerPort, nil))
}
func loadDefaultConfiguration() {
ServerConfig.StaticFolderPath = "statics/"
ServerConfig.TemplateIncludePath = "templates/"
ServerConfig.TemplateLayoutPath = "templates/layouts/"
ServerConfig.TemplateExtensionPattern = "*.gohtml"
ServerConfig.ServerPort = ":8000"
ServerConfig.Domain = "localhost"
ServerConfig.MainTemplate = `{{define "main" }} {{ template "base" . }} {{ end }}`
ServerConfig.Debug = false
ServerConfig.SessionProvider = "FileProvider"
}
func loadConfiguration(config_file_name string, custom_config_file_name string) {
loadDefaultConfiguration()
default_settings, err := ioutil.ReadFile(config_file_name)
if err != nil {
log.Fatal("No " + config_file_name + " found, exiting")
}
log.Println("Importing settings")
err = json.Unmarshal(default_settings, &ServerConfig)
if err != nil {
log.Fatal("Malformed " + config_file_name)
log.Println("No " + config_file_name + " found, using default conf instead")
} else {
log.Println("Importing settings")
err = json.Unmarshal(default_settings, &ServerConfig)
if err != nil {
log.Fatal("Malformed " + config_file_name)
}
}
custom_settings, err := ioutil.ReadFile(custom_config_file_name)

46
is-writable.go Normal file
View File

@ -0,0 +1,46 @@
/*
* @Author: Bartuccio Antoine
* @Date: 2018-07-17 18:37:38
* @Last Modified by: klmp200
* @Last Modified time: 2018-07-17 19:35:18
*/
// +build !windows
package gowebframework
import (
"log"
"os"
"syscall"
)
// Check if a file or directory is writable
func isWritable(path string) bool {
os_stats, err := os.Stat(path)
if err != nil {
// Error reading file or folder
log.Println(err)
return false
}
if os_stats.Mode().Perm()&(1<<(uint(7))) == 0 {
// Check if user bit is enabled
log.Println("User bit on " + path + " is not enabled")
return false
}
var syscall_stats syscall.Stat_t
err = syscall.Stat(path, &syscall_stats)
if err != nil {
// Error getting stats
log.Println(err)
return false
}
if uint32(os.Getuid()) != syscall_stats.Uid {
// User doesn't have permission to write
log.Println("No permission to write on " + path)
return false
}
return true
}

30
is-writable_windows.go Normal file
View File

@ -0,0 +1,30 @@
/*
* @Author: Bartuccio Antoine
* @Date: 2018-07-17 18:51:12
* @Last Modified by: klmp200
* @Last Modified time: 2018-07-17 18:54:20
*/
// WARNING : Not tested !
package gowebframework
import (
"os"
)
func isWritable(path string) bool {
stats, err := os.Stat(path)
if err != nil {
// Folder or file doesn't exist
return false
}
if stats.Mode().Perm()&(1<<(uint(7))) == 0 {
// Check if the user bit is enabled in file permission
// It's the only thing we can do on windows
return false
}
return true
}

105
session.go Normal file
View File

@ -0,0 +1,105 @@
/*
* @Author: Bartuccio Antoine
* @Date: 2018-07-16 14:07:07
* @Last Modified by: klmp200
* @Last Modified time: 2018-07-17 23:57:10
*/
package gowebframework
import (
"fmt"
"github.com/google/uuid"
"log"
"net/http"
"net/url"
"sync"
"time"
)
var SESSION *SessionManager
var providers = make(map[string]Provider)
type SessionManager struct {
cookieName string // Name of the cookie on the client side
lock sync.Mutex
provider Provider
maxLifetime time.Duration
}
type Session interface {
Set(key string, value interface{}) error // set a session value
Get(key string) interface{} // get session value
Delete(key string) error // delete session value
SessionUUID() string // get the current session UUID
}
type Provider interface {
SessionInit(uuid string) (Session, error)
SessionRead(uuid string) (Session, error)
SessionDestroy(uuid string) error
SessionClearExpired(maxLifetime time.Duration)
}
func (manager *SessionManager) generateUUID() string {
return uuid.New().String()
}
// Launch a Clear Expired Sessions task at fixed rate
func clearExpiredSessionsCron() {
SESSION.lock.Lock()
lifetime := SESSION.maxLifetime
SESSION.lock.Unlock()
for {
log.Println("pute")
SESSION.lock.Lock()
SESSION.provider.SessionClearExpired(lifetime)
SESSION.lock.Unlock()
time.Sleep(lifetime)
}
}
func SessionProviderRegister(provider Provider, providerName string) error {
if provider == nil {
return fmt.Errorf("No provider found for provider name %s", providerName)
}
if _, exists := providers[providerName]; exists {
return fmt.Errorf("Provider with name %s already exists", providerName)
}
providers[providerName] = provider
return nil
}
func (manager *SessionManager) GetSession(w http.ResponseWriter, r *http.Request) Session {
var session Session
manager.lock.Lock()
defer manager.lock.Unlock()
cookie, err := r.Cookie(manager.cookieName)
if err != nil || cookie.Value == "" {
// Create a new session
session_id := manager.generateUUID()
s, err := manager.provider.SessionInit(session_id)
if err != nil {
log.Fatal(err)
}
session = s
AddCookie(w, manager.cookieName, url.QueryEscape(session_id), manager.maxLifetime)
} else {
// Get the session
session_id, _ := url.QueryUnescape(cookie.Value)
s, err := manager.provider.SessionRead(session_id)
if err != nil {
log.Fatal(err)
}
session = s
}
return session
}
func newSessionManager(providerName string, cookieName string, maxLifetime time.Duration) (*SessionManager, error) {
provider, ok := providers[providerName]
if !ok {
return nil, fmt.Errorf("Session error: unknown provider %s", providerName)
}
return &SessionManager{provider: provider, cookieName: cookieName, maxLifetime: maxLifetime}, nil
}