mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-11 08:12:09 -04:00
Wifi QR code generation (requires testing) (#1794)
* fix: add mockery v2 to nix flake pkgs * feat: add requests for generating wifi qr codes as png files in /tmp, and to delete them later. only supports NetworkManager backend for now. * feat: add modal for sharing wifi via qr code and saving the code as png file. * fix: uncomment QR code file deletion * network: light refactor and cleanup for QR code generation --------- Co-authored-by: bbedward <bbedward@gmail.com>
This commit is contained in:
@@ -10,6 +10,7 @@ type Backend interface {
|
||||
ScanWiFi() error
|
||||
ScanWiFiDevice(device string) error
|
||||
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
|
||||
GetWiFiQRCodeContent(ssid string) (string, error)
|
||||
GetWiFiDevices() []WiFiDevice
|
||||
|
||||
ConnectWiFi(req ConnectionRequest) error
|
||||
|
||||
@@ -111,6 +111,10 @@ func (b *HybridIwdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkI
|
||||
return b.wifi.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
return b.wifi.GetWiFiQRCodeContent(ssid)
|
||||
}
|
||||
|
||||
func (b *HybridIwdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
if err := b.wifi.ConnectWiFi(req); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package network
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func (b *IWDBackend) GetWiredConnections() ([]WiredConnection, error) {
|
||||
return nil, fmt.Errorf("wired connections not supported by iwd")
|
||||
@@ -112,3 +115,19 @@ func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
|
||||
Networks: b.state.WiFiNetworks,
|
||||
}}
|
||||
}
|
||||
|
||||
func (b *IWDBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
path := iwdConfigPath(ssid)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no saved iwd config for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
passphrase, err := parseIWDPassphrase(string(data))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read passphrase for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
return FormatWiFiQRString("WPA", ssid, passphrase), nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ func (b *SystemdNetworkdBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInf
|
||||
return nil, fmt.Errorf("WiFi details not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
return "", fmt.Errorf("WiFi QR Code not supported by networkd backend")
|
||||
}
|
||||
|
||||
func (b *SystemdNetworkdBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
return fmt.Errorf("WiFi connect not supported by networkd backend")
|
||||
}
|
||||
|
||||
@@ -196,6 +196,65 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||
conn, err := b.findConnection(ssid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("no saved connection for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
connSettings, err := conn.GetSettings()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get settings for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
secSettings, ok := connSettings["802-11-wireless-security"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("network `%s` has no security settings", ssid)
|
||||
}
|
||||
|
||||
keyMgmt, ok := secSettings["key-mgmt"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to identify security type of network `%s`", ssid)
|
||||
}
|
||||
|
||||
var securityType string
|
||||
switch keyMgmt {
|
||||
case "none":
|
||||
authAlg, _ := secSettings["auth-alg"].(string)
|
||||
switch authAlg {
|
||||
case "open":
|
||||
securityType = "nopass"
|
||||
default:
|
||||
securityType = "WEP"
|
||||
}
|
||||
case "ieee8021x":
|
||||
securityType = "WEP"
|
||||
default:
|
||||
securityType = "WPA"
|
||||
}
|
||||
|
||||
if securityType != "WPA" {
|
||||
return "", fmt.Errorf("QR code generation only supports WPA connections, `%s` uses %s", ssid, securityType)
|
||||
}
|
||||
|
||||
secrets, err := conn.GetSecrets("802-11-wireless-security")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to retrieve connection secrets for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
secSecrets, ok := secrets["802-11-wireless-security"]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
|
||||
}
|
||||
|
||||
psk, ok := secSecrets["psk"].(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to retrieve password for `%s`", ssid)
|
||||
}
|
||||
|
||||
return FormatWiFiQRString(securityType, ssid, psk), nil
|
||||
}
|
||||
|
||||
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
|
||||
devInfo, err := b.getWifiDeviceForConnection(req.Device)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
@@ -40,6 +41,10 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
handleSetPreference(conn, req, manager)
|
||||
case "network.info":
|
||||
handleGetNetworkInfo(conn, req, manager)
|
||||
case "network.qrcode":
|
||||
handleGetNetworkQRCode(conn, req, manager)
|
||||
case "network.delete-qrcode":
|
||||
handleDeleteQRCode(conn, req, manager)
|
||||
case "network.ethernet.info":
|
||||
handleGetWiredNetworkInfo(conn, req, manager)
|
||||
case "network.subscribe":
|
||||
@@ -320,6 +325,42 @@ func handleGetNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, network)
|
||||
}
|
||||
|
||||
func handleGetNetworkQRCode(conn net.Conn, req models.Request, manager *Manager) {
|
||||
ssid, err := params.String(req.Params, "ssid")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
content, err := manager.GetNetworkQRCode(ssid)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, content)
|
||||
}
|
||||
|
||||
func handleDeleteQRCode(conn net.Conn, req models.Request, _ *Manager) {
|
||||
path, err := params.String(req.Params, "path")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidQRCodePath(path) {
|
||||
models.RespondError(conn, req.ID, "invalid QR code path")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "QR code file deleted"})
|
||||
}
|
||||
|
||||
func handleGetWiredNetworkInfo(conn net.Conn, req models.Request, manager *Manager) {
|
||||
uuid, err := params.String(req.Params, "uuid")
|
||||
if err != nil {
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/yeqown/go-qrcode/v2"
|
||||
"github.com/yeqown/go-qrcode/writer/standard"
|
||||
)
|
||||
|
||||
func NewManager() (*Manager, error) {
|
||||
@@ -438,6 +440,43 @@ func (m *Manager) GetNetworkInfoDetailed(ssid string) (*NetworkInfoResponse, err
|
||||
return m.backend.GetWiFiNetworkDetails(ssid)
|
||||
}
|
||||
|
||||
func (m *Manager) GetNetworkQRCode(ssid string) ([2]string, error) {
|
||||
content, err := m.backend.GetWiFiQRCodeContent(ssid)
|
||||
if err != nil {
|
||||
return [2]string{}, err
|
||||
}
|
||||
|
||||
qrc, err := qrcode.New(content)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
pathThemed, pathNormal := qrCodePaths(ssid)
|
||||
|
||||
wThemed, err := standard.New(
|
||||
pathThemed,
|
||||
standard.WithBuiltinImageEncoder(standard.PNG_FORMAT),
|
||||
standard.WithBgTransparent(),
|
||||
standard.WithFgColorRGBHex("#ffffff"),
|
||||
)
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(wThemed); err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
wNormal, err := standard.New(pathNormal, standard.WithBuiltinImageEncoder(standard.PNG_FORMAT))
|
||||
if err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to create QR code writer: %w", err)
|
||||
}
|
||||
if err := qrc.Save(wNormal); err != nil {
|
||||
return [2]string{}, fmt.Errorf("failed to save QR code for `%s`: %w", ssid, err)
|
||||
}
|
||||
|
||||
return [2]string{pathThemed, pathNormal}, nil
|
||||
}
|
||||
|
||||
func (m *Manager) ToggleWiFi() error {
|
||||
enabled, err := m.backend.GetWiFiEnabled()
|
||||
if err != nil {
|
||||
|
||||
59
core/internal/server/network/wifi_qrcode.go
Normal file
59
core/internal/server/network/wifi_qrcode.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const qrCodeTmpPrefix = "/tmp/dank-wifi-qrcode-"
|
||||
|
||||
func FormatWiFiQRString(securityType, ssid, password string) string {
|
||||
return fmt.Sprintf("WIFI:T:%s;S:%s;P:%s;;", securityType, ssid, password)
|
||||
}
|
||||
|
||||
func qrCodePaths(ssid string) (themed, normal string) {
|
||||
safe := sanitizeSSIDForPath(ssid)
|
||||
themed = fmt.Sprintf("%s%s-themed.png", qrCodeTmpPrefix, safe)
|
||||
normal = fmt.Sprintf("%s%s-normal.png", qrCodeTmpPrefix, safe)
|
||||
return
|
||||
}
|
||||
|
||||
func isValidQRCodePath(path string) bool {
|
||||
clean := filepath.Clean(path)
|
||||
return strings.HasPrefix(clean, qrCodeTmpPrefix) && strings.HasSuffix(clean, ".png")
|
||||
}
|
||||
|
||||
var safePathChar = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
|
||||
|
||||
func sanitizeSSIDForPath(ssid string) string {
|
||||
return safePathChar.ReplaceAllString(ssid, "_")
|
||||
}
|
||||
|
||||
var iwdVerbatimSSID = regexp.MustCompile(`^[a-zA-Z0-9 _-]+$`)
|
||||
|
||||
func iwdConfigPath(ssid string) string {
|
||||
switch {
|
||||
case iwdVerbatimSSID.MatchString(ssid):
|
||||
return fmt.Sprintf("/var/lib/iwd/%s.psk", ssid)
|
||||
default:
|
||||
return fmt.Sprintf("/var/lib/iwd/=%x.psk", []byte(ssid))
|
||||
}
|
||||
}
|
||||
|
||||
func parseIWDPassphrase(data string) (string, error) {
|
||||
inSecurity := false
|
||||
for _, line := range strings.Split(data, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
switch {
|
||||
case line == "[Security]":
|
||||
inSecurity = true
|
||||
case strings.HasPrefix(line, "["):
|
||||
inSecurity = false
|
||||
case inSecurity && strings.HasPrefix(line, "Passphrase="):
|
||||
return strings.TrimPrefix(line, "Passphrase="), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no passphrase found in iwd config")
|
||||
}
|
||||
Reference in New Issue
Block a user