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, style string) (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, style string) (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, style)
 | 
						|
}
 | 
						|
 | 
						|
// GetPeerConfigQrCode returns a QR code image containing the configuration for the given peer.
 | 
						|
func (m Manager) GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier, style string) (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, style)
 | 
						|
	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 }
 |