Newer
Older
wg-portal / internal / server / core.go
@Christoph Haas Christoph Haas on 17 Nov 2020 7 KB auto create account, sync ldap disabled flag
package server

import (
	"encoding/gob"
	"errors"
	"html/template"
	"math/rand"
	"net/url"
	"os"
	"path/filepath"
	"time"

	"github.com/h44z/wg-portal/internal/wireguard"

	"github.com/h44z/wg-portal/internal/common"

	"github.com/h44z/wg-portal/internal/ldap"
	log "github.com/sirupsen/logrus"

	"github.com/gin-contrib/sessions"
	"github.com/gin-contrib/sessions/memstore"
	"github.com/gin-gonic/gin"
)

const SessionIdentifier = "wgPortalSession"
const CacheRefreshDuration = 5 * time.Minute

func init() {
	gob.Register(SessionData{})
	gob.Register(FlashData{})
	gob.Register(User{})
	gob.Register(Device{})
	gob.Register(LdapCreateForm{})
}

type SessionData struct {
	LoggedIn      bool
	IsAdmin       bool
	UID           string
	UserName      string
	Firstname     string
	Lastname      string
	Email         string
	SortedBy      string
	SortDirection string
	Search        string
	AlertData     string
	AlertType     string
	FormData      interface{}
}

type FlashData struct {
	HasAlert bool
	Message  string
	Type     string
}

type StaticData struct {
	WebsiteTitle string
	WebsiteLogo  string
	CompanyName  string
	Year         int
	LdapDisabled bool
}

type Server struct {
	// Core components
	config  *common.Config
	server  *gin.Engine
	users   *UserManager
	mailTpl *template.Template

	// WireGuard stuff
	wg *wireguard.Manager

	// LDAP stuff
	ldapDisabled     bool
	ldapAuth         ldap.Authentication
	ldapUsers        *ldap.SynchronizedUserCacheHolder
	ldapCacheUpdater *ldap.UserCache
}

func (s *Server) Setup() error {
	dir := s.getExecutableDirectory()
	rDir, _ := filepath.Abs(filepath.Dir(os.Args[0]))
	log.Infof("Real working directory: %s", rDir)
	log.Infof("Current working directory: %s", dir)

	// Init rand
	rand.Seed(time.Now().UnixNano())

	s.config = common.NewConfig()

	// Setup LDAP stuff
	s.ldapAuth = ldap.NewAuthentication(s.config.LDAP)
	s.ldapUsers = &ldap.SynchronizedUserCacheHolder{}
	s.ldapUsers.Init()
	s.ldapCacheUpdater = ldap.NewUserCache(s.config.LDAP, s.ldapUsers)
	if s.ldapCacheUpdater.LastError != nil {
		log.Warnf("LDAP error: %v", s.ldapCacheUpdater.LastError)
		log.Warnf("LDAP features disabled!")
		s.ldapDisabled = true
	}

	// Setup WireGuard stuff
	s.wg = &wireguard.Manager{Cfg: &s.config.WG}
	if err := s.wg.Init(); err != nil {
		return err
	}

	// Setup user manager
	if s.users = NewUserManager(filepath.Join(dir, s.config.Core.DatabasePath), s.wg, s.ldapUsers); s.users == nil {
		return errors.New("unable to setup user manager")
	}
	if err := s.users.InitFromCurrentInterface(); err != nil {
		return errors.New("unable to initialize user manager")
	}
	if err := s.RestoreWireGuardInterface(); err != nil {
		return errors.New("unable to restore wirguard state")
	}

	// Setup mail template
	var err error
	s.mailTpl, err = template.New("email.html").ParseFiles(filepath.Join(dir, "/assets/tpl/email.html"))
	if err != nil {
		return errors.New("unable to pare mail template")
	}

	// Setup http server
	s.server = gin.Default()
	s.server.SetFuncMap(template.FuncMap{
		"formatBytes": common.ByteCountSI,
		"urlEncode":   url.QueryEscape,
	})

	// Setup templates
	log.Infof("Loading templates from: %s", filepath.Join(dir, "/assets/tpl/*.html"))
	s.server.LoadHTMLGlob(filepath.Join(dir, "/assets/tpl/*.html"))
	s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte("secret")))) // TODO: change key?

	// Serve static files
	s.server.Static("/css", filepath.Join(dir, "/assets/css"))
	s.server.Static("/js", filepath.Join(dir, "/assets/js"))
	s.server.Static("/img", filepath.Join(dir, "/assets/img"))
	s.server.Static("/fonts", filepath.Join(dir, "/assets/fonts"))

	// Setup all routes
	SetupRoutes(s)

	log.Infof("Setup of service completed!")
	return nil
}

func (s *Server) Run() {
	// Start ldap group watcher
	if !s.ldapDisabled {
		go func(s *Server) {
			for {
				time.Sleep(CacheRefreshDuration)
				if err := s.ldapCacheUpdater.Update(true, true); err != nil {
					log.Warnf("Failed to update ldap group cache: %v", err)
				}
				log.Debugf("Refreshed LDAP permissions!")
			}
		}(s)
	}

	if !s.ldapDisabled && s.config.Core.SyncLdapStatus {
		go func(s *Server) {
			for {
				time.Sleep(CacheRefreshDuration)
				if err := s.SyncLdapAttributesWithWireGuard(); err != nil {
					log.Warnf("Failed to synchronize ldap attributes: %v", err)
				}
				log.Debugf("Synced LDAP attributes!")
			}
		}(s)
	}

	// Run web service
	err := s.server.Run(s.config.Core.ListeningAddress)
	if err != nil {
		log.Errorf("Failed to listen and serve on %s: %v", s.config.Core.ListeningAddress, err)
	}
}

func (s *Server) getExecutableDirectory() string {
	dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
	if err != nil {
		log.Errorf("Failed to get executable directory: %v", err)
	}

	if _, err := os.Stat(filepath.Join(dir, "assets")); os.IsNotExist(err) {
		return "." // assets directory not found -> we are developing in goland =)
	}

	return dir
}

func (s *Server) getSessionData(c *gin.Context) SessionData {
	session := sessions.Default(c)
	rawSessionData := session.Get(SessionIdentifier)

	var sessionData SessionData
	if rawSessionData != nil {
		sessionData = rawSessionData.(SessionData)
	} else {
		sessionData = SessionData{
			SortedBy:      "mail",
			SortDirection: "asc",
			Email:         "",
			Firstname:     "",
			Lastname:      "",
			IsAdmin:       false,
			LoggedIn:      false,
		}
		session.Set(SessionIdentifier, sessionData)
		if err := session.Save(); err != nil {
			log.Errorf("Failed to store session: %v", err)
		}
	}

	return sessionData
}

func (s *Server) getFlashes(c *gin.Context) []FlashData {
	session := sessions.Default(c)
	flashes := session.Flashes()
	if err := session.Save(); err != nil {
		log.Errorf("Failed to store session after setting flash: %v", err)
	}

	flashData := make([]FlashData, len(flashes))
	for i := range flashes {
		flashData[i] = flashes[i].(FlashData)
	}

	return flashData
}

func (s *Server) updateSessionData(c *gin.Context, data SessionData) error {
	session := sessions.Default(c)
	session.Set(SessionIdentifier, data)
	if err := session.Save(); err != nil {
		log.Errorf("Failed to store session: %v", err)
		return err
	}
	return nil
}

func (s *Server) destroySessionData(c *gin.Context) error {
	session := sessions.Default(c)
	session.Delete(SessionIdentifier)
	if err := session.Save(); err != nil {
		log.Errorf("Failed to destroy session: %v", err)
		return err
	}
	return nil
}

func (s *Server) getStaticData() StaticData {
	return StaticData{
		WebsiteTitle: s.config.Core.Title,
		WebsiteLogo:  "/img/header-logo.png",
		CompanyName:  s.config.Core.CompanyName,
		LdapDisabled: s.ldapDisabled,
		Year:         time.Now().Year(),
	}
}

func (s *Server) setFlashMessage(c *gin.Context, message, typ string) {
	session := sessions.Default(c)
	session.AddFlash(FlashData{
		Message: message,
		Type:    typ,
	})
	if err := session.Save(); err != nil {
		log.Errorf("Failed to store session after setting flash: %v", err)
	}
}

func (s SessionData) GetSortIcon(field string) string {
	if s.SortedBy != field {
		return "fa-sort"
	}
	if s.SortDirection == "asc" {
		return "fa-sort-alpha-down"
	} else {
		return "fa-sort-alpha-up"
	}
}