mirror of https://github.com/h44z/wg-portal.git
				
				
				
			
		
			
				
	
	
		
			287 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			287 lines
		
	
	
		
			8.3 KiB
		
	
	
	
		
			Go
		
	
	
	
| package configfile
 | |
| 
 | |
| import (
 | |
| 	"bufio"
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"log/slog"
 | |
| 	"os"
 | |
| 	"strings"
 | |
| 
 | |
| 	"github.com/yeqown/go-qrcode/v2"
 | |
| 	"github.com/yeqown/go-qrcode/writer/compressed"
 | |
| 
 | |
| 	"github.com/h44z/wg-portal/internal/app"
 | |
| 	"github.com/h44z/wg-portal/internal/config"
 | |
| 	"github.com/h44z/wg-portal/internal/domain"
 | |
| )
 | |
| 
 | |
| // region dependencies
 | |
| 
 | |
| type UserDatabaseRepo interface {
 | |
| 	// GetUser returns the user with the given identifier from the SQL database.
 | |
| 	GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
 | |
| }
 | |
| 
 | |
| type WireguardDatabaseRepo interface {
 | |
| 	// GetInterfaceAndPeers returns the interface and all peers associated with it.
 | |
| 	GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
 | |
| 	// GetPeer returns the peer with the given identifier.
 | |
| 	GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
 | |
| 	// GetInterface returns the interface with the given identifier.
 | |
| 	GetInterface(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, error)
 | |
| }
 | |
| 
 | |
| type FileSystemRepo interface {
 | |
| 	// WriteFile writes the contents to the file at the given path.
 | |
| 	WriteFile(path string, contents io.Reader) error
 | |
| 
 | |
| 	// DeleteFile deletes the file at the given path.
 | |
| 	DeleteFile(path string) error
 | |
| }
 | |
| 
 | |
| type TemplateRenderer interface {
 | |
| 	// GetInterfaceConfig returns the configuration file for the given interface.
 | |
| 	GetInterfaceConfig(iface *domain.Interface, peers []domain.Peer) (io.Reader, error)
 | |
| 	// GetPeerConfig returns the configuration file for the given peer.
 | |
| 	GetPeerConfig(peer *domain.Peer) (io.Reader, error)
 | |
| }
 | |
| 
 | |
| type EventBus interface {
 | |
| 	// Subscribe subscribes to the given topic.
 | |
| 	Subscribe(topic string, fn any) error
 | |
| }
 | |
| 
 | |
| // endregion dependencies
 | |
| 
 | |
| // Manager is responsible for managing the configuration files of the WireGuard interfaces and peers.
 | |
| type Manager struct {
 | |
| 	cfg *config.Config
 | |
| 	bus EventBus
 | |
| 
 | |
| 	tplHandler TemplateRenderer
 | |
| 	fsRepo     FileSystemRepo
 | |
| 	users      UserDatabaseRepo
 | |
| 	wg         WireguardDatabaseRepo
 | |
| }
 | |
| 
 | |
| // NewConfigFileManager creates a new Manager instance.
 | |
| func NewConfigFileManager(
 | |
| 	cfg *config.Config,
 | |
| 	bus EventBus,
 | |
| 	users UserDatabaseRepo,
 | |
| 	wg WireguardDatabaseRepo,
 | |
| 	fsRepo FileSystemRepo,
 | |
| ) (*Manager, error) {
 | |
| 	tplHandler, err := newTemplateHandler()
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to initialize template handler: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	m := &Manager{
 | |
| 		cfg:        cfg,
 | |
| 		bus:        bus,
 | |
| 		tplHandler: tplHandler,
 | |
| 
 | |
| 		fsRepo: fsRepo,
 | |
| 		users:  users,
 | |
| 		wg:     wg,
 | |
| 	}
 | |
| 
 | |
| 	if m.cfg.Advanced.ConfigStoragePath != "" {
 | |
| 		if err := m.createStorageDirectory(); err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 
 | |
| 		m.connectToMessageBus()
 | |
| 	}
 | |
| 
 | |
| 	return m, nil
 | |
| }
 | |
| 
 | |
| func (m Manager) createStorageDirectory() error {
 | |
| 	err := os.MkdirAll(m.cfg.Advanced.ConfigStoragePath, os.ModePerm)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to create configuration storage path %s: %w",
 | |
| 			m.cfg.Advanced.ConfigStoragePath, err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (m Manager) connectToMessageBus() {
 | |
| 	_ = m.bus.Subscribe(app.TopicInterfaceCreated, m.handleInterfaceSavedEvent)
 | |
| 	_ = m.bus.Subscribe(app.TopicInterfaceUpdated, m.handleInterfaceSavedEvent)
 | |
| 	_ = m.bus.Subscribe(app.TopicInterfaceDeleted, m.handleInterfaceDeleteEvent)
 | |
| 	_ = m.bus.Subscribe(app.TopicPeerInterfaceUpdated, m.handlePeerInterfaceUpdatedEvent)
 | |
| }
 | |
| 
 | |
| func (m Manager) handleInterfaceSavedEvent(iface domain.Interface) {
 | |
| 	if !iface.SaveConfig {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	slog.Debug("handling interface save event", "interface", iface.Identifier)
 | |
| 
 | |
| 	err := m.PersistInterfaceConfig(context.Background(), iface.Identifier)
 | |
| 	if err != nil {
 | |
| 		slog.Error("failed to automatically persist interface config",
 | |
| 			"interface", iface.Identifier, "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (m Manager) handleInterfaceDeleteEvent(iface domain.Interface) {
 | |
| 	if !iface.SaveConfig {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	slog.Debug("handling interface delete event", "interface", iface.Identifier)
 | |
| 
 | |
| 	err := m.UnpersistInterfaceConfig(context.Background(), iface.GetConfigFileName())
 | |
| 	if err != nil {
 | |
| 		slog.Error("failed to remove persisted interface config",
 | |
| 			"interface", iface.Identifier, "error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (m Manager) handlePeerInterfaceUpdatedEvent(id domain.InterfaceIdentifier) {
 | |
| 	peerInterface, err := m.wg.GetInterface(context.Background(), id)
 | |
| 	if err != nil {
 | |
| 		slog.Error("failed to load interface",
 | |
| 			"interface", id,
 | |
| 			"error", err)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if !peerInterface.SaveConfig {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	slog.Debug("handling peer interface updated event", "interface", id)
 | |
| 
 | |
| 	err = m.PersistInterfaceConfig(context.Background(), peerInterface.Identifier)
 | |
| 	if err != nil {
 | |
| 		slog.Error("failed to automatically persist interface config",
 | |
| 			"interface", peerInterface.Identifier,
 | |
| 			"error", err)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // GetInterfaceConfig returns the configuration file for the given interface.
 | |
| // The file is structured in wg-quick format.
 | |
| func (m Manager) GetInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) (io.Reader, error) {
 | |
| 	if err := domain.ValidateAdminAccessRights(ctx); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	iface, peers, err := m.wg.GetInterfaceAndPeers(ctx, id)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to fetch interface %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	return m.tplHandler.GetInterfaceConfig(iface, peers)
 | |
| }
 | |
| 
 | |
| // GetPeerConfig returns the configuration file for the given peer.
 | |
| // The file is structured in wg-quick format.
 | |
| func (m Manager) GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
 | |
| 	peer, err := m.wg.GetPeer(ctx, id)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return m.tplHandler.GetPeerConfig(peer)
 | |
| }
 | |
| 
 | |
| // GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer.
 | |
| func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error) {
 | |
| 	peer, err := m.wg.GetPeer(ctx, id)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to fetch peer %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	cfgData, err := m.tplHandler.GetPeerConfig(peer)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to get peer config for %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	// remove comments from qr-code config as it is not needed
 | |
| 	sb := strings.Builder{}
 | |
| 	scanner := bufio.NewScanner(cfgData)
 | |
| 	for scanner.Scan() {
 | |
| 		line := strings.TrimSpace(scanner.Text())
 | |
| 		if !strings.HasPrefix(line, "#") {
 | |
| 			sb.WriteString(line)
 | |
| 			sb.WriteString("\n")
 | |
| 		}
 | |
| 	}
 | |
| 	if err := scanner.Err(); err != nil {
 | |
| 		return nil, fmt.Errorf("failed to read peer config for %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	code, err := qrcode.NewWith(sb.String(),
 | |
| 		qrcode.WithErrorCorrectionLevel(qrcode.ErrorCorrectionLow), qrcode.WithEncodingMode(qrcode.EncModeByte))
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to initialize qr code for %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	buf := bytes.NewBuffer(nil)
 | |
| 	wr := nopCloser{Writer: buf}
 | |
| 	option := compressed.Option{
 | |
| 		Padding:   8, // padding pixels around the qr code.
 | |
| 		BlockSize: 4, // block pixels which represents a bit data.
 | |
| 	}
 | |
| 	qrWriter := compressed.NewWithWriter(wr, &option)
 | |
| 	err = code.Save(qrWriter)
 | |
| 	if err != nil {
 | |
| 		return nil, fmt.Errorf("failed to write code for %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	return buf, nil
 | |
| }
 | |
| 
 | |
| // PersistInterfaceConfig writes the configuration file for the given interface to the file system.
 | |
| func (m Manager) PersistInterfaceConfig(ctx context.Context, id domain.InterfaceIdentifier) error {
 | |
| 	iface, peers, err := m.wg.GetInterfaceAndPeers(ctx, id)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to fetch interface %s: %w", id, err)
 | |
| 	}
 | |
| 
 | |
| 	cfg, err := m.tplHandler.GetInterfaceConfig(iface, peers)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to get interface config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := m.fsRepo.WriteFile(iface.GetConfigFileName(), cfg); err != nil {
 | |
| 		return fmt.Errorf("failed to write interface config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // UnpersistInterfaceConfig removes the configuration file for the given interface from the file system.
 | |
| func (m Manager) UnpersistInterfaceConfig(_ context.Context, filename string) error {
 | |
| 	if err := m.fsRepo.DeleteFile(filename); err != nil {
 | |
| 		return fmt.Errorf("failed to remove interface config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| type nopCloser struct {
 | |
| 	io.Writer
 | |
| }
 | |
| 
 | |
| // Close is a no-op for the nopCloser.
 | |
| func (nopCloser) Close() error { return nil }
 |