diff --git a/internal/app/ipc/openuc2/com.openuc2.deviceadmin.openuc2.varlink b/internal/app/ipc/openuc2/com.openuc2.deviceadmin.openuc2.varlink new file mode 100644 index 0000000..f51579b --- /dev/null +++ b/internal/app/ipc/openuc2/com.openuc2.deviceadmin.openuc2.varlink @@ -0,0 +1,18 @@ +# com.openuc2.deviceadmin.openuc2 manages openUC2 OS-specific settings. +interface com.openuc2.deviceadmin.openuc2 + +# The service was unable to perform the requested operation for an unspecified reason. +error Unknown (description: string) + +# NetworkManager + +# UpdatePSKDropInFile updates the specified connection profile (as specified via file-based name, +# e.g. "wlan0-hotspot")'s Wi-Fi PSK drop-in snippet file (which is automatically determined) +# with the specified cleartext password. +# This operation does not try to regenerate the connection profile itself from the drop-in files. +method UpdatePSKDropInFile(connProfile: string, newPw: string) -> () + +# RegenerateConnProfile reassembles the file for the NetworkManager connection profile (specified by +# its file-based name, e.g. "wlan0-hotspot") from its constituent drop-in snippet files. +# This operation does not try to make NetworkManager reload the updated connection profile. +method RegenerateDropInConnProfile(connProfile: string) -> () diff --git a/internal/app/ipc/openuc2/comopenuc2deviceadminopenuc2.go b/internal/app/ipc/openuc2/comopenuc2deviceadminopenuc2.go new file mode 100644 index 0000000..5e2e90b --- /dev/null +++ b/internal/app/ipc/openuc2/comopenuc2deviceadminopenuc2.go @@ -0,0 +1,277 @@ +// Code generated by github.com/varlink/go/cmd/varlink-go-interface-generator, DO NOT EDIT. + +// com.openuc2.deviceadmin.openuc2 manages openUC2 OS-specific settings. +package comopenuc2deviceadminopenuc2 + +import ( + "context" + "encoding/json" + "fmt" + "github.com/varlink/go/varlink" +) + +// Generated type declarations + +// The service was unable to perform the requested operation for an unspecified reason. +type Unknown struct { + Description string `json:"description"` +} + +func (e Unknown) Error() string { + s := "com.openuc2.deviceadmin.openuc2.Unknown" + s += fmt.Sprintf("(Description: %v)", e.Description) + return s +} + +func Dispatch_Error(err error) error { + if e, ok := err.(*varlink.Error); ok { + switch e.Name { + case "com.openuc2.deviceadmin.openuc2.Unknown": + errorRawParameters := e.Parameters.(*json.RawMessage) + if errorRawParameters == nil { + return e + } + var param Unknown + err := json.Unmarshal(*errorRawParameters, ¶m) + if err != nil { + return e + } + return ¶m + } + } + return err +} + +// Generated client method calls + +// UpdatePSKDropInFile updates the specified connection profile (as specified via file-based name, +// e.g. "wlan0-hotspot")'s Wi-Fi PSK drop-in snippet file (which is automatically determined) +// with the specified cleartext password. +// This operation does not try to regenerate the connection profile itself from the drop-in files. +type UpdatePSKDropInFile_methods struct{} + +func UpdatePSKDropInFile() UpdatePSKDropInFile_methods { return UpdatePSKDropInFile_methods{} } + +func (m UpdatePSKDropInFile_methods) Call(ctx context.Context, c *varlink.Connection, connProfile_in_ string, newPw_in_ string) (err_ error) { + receive, err_ := m.Send(ctx, c, 0, connProfile_in_, newPw_in_) + if err_ != nil { + return + } + _, err_ = receive(ctx) + return +} + +func (m UpdatePSKDropInFile_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, connProfile_in_ string, newPw_in_ string) (func(ctx context.Context) (uint64, error), error) { + var in struct { + ConnProfile string `json:"connProfile"` + NewPw string `json:"newPw"` + } + in.ConnProfile = connProfile_in_ + in.NewPw = newPw_in_ + receive, err := c.Send(ctx, "com.openuc2.deviceadmin.openuc2.UpdatePSKDropInFile", in, flags) + if err != nil { + return nil, err + } + return func(context.Context) (flags uint64, err error) { + flags, err = receive(ctx, nil) + if err != nil { + err = Dispatch_Error(err) + return + } + return + }, nil +} + +func (m UpdatePSKDropInFile_methods) Upgrade(ctx context.Context, c *varlink.Connection, connProfile_in_ string, newPw_in_ string) (func(ctx context.Context) (flags uint64, conn varlink.ReadWriterContext, err_ error), error) { + var in struct { + ConnProfile string `json:"connProfile"` + NewPw string `json:"newPw"` + } + in.ConnProfile = connProfile_in_ + in.NewPw = newPw_in_ + receive, err := c.Upgrade(ctx, "com.openuc2.deviceadmin.openuc2.UpdatePSKDropInFile", in) + if err != nil { + return nil, err + } + return func(context.Context) (flags uint64, conn varlink.ReadWriterContext, err error) { + flags, conn, err = receive(ctx, nil) + if err != nil { + err = Dispatch_Error(err) + return + } + return + }, nil +} + +// RegenerateConnProfile reassembles the file for the NetworkManager connection profile (specified by +// its file-based name, e.g. "wlan0-hotspot") from its constituent drop-in snippet files. +// This operation does not try to make NetworkManager reload the updated connection profile. +type RegenerateDropInConnProfile_methods struct{} + +func RegenerateDropInConnProfile() RegenerateDropInConnProfile_methods { + return RegenerateDropInConnProfile_methods{} +} + +func (m RegenerateDropInConnProfile_methods) Call(ctx context.Context, c *varlink.Connection, connProfile_in_ string) (err_ error) { + receive, err_ := m.Send(ctx, c, 0, connProfile_in_) + if err_ != nil { + return + } + _, err_ = receive(ctx) + return +} + +func (m RegenerateDropInConnProfile_methods) Send(ctx context.Context, c *varlink.Connection, flags uint64, connProfile_in_ string) (func(ctx context.Context) (uint64, error), error) { + var in struct { + ConnProfile string `json:"connProfile"` + } + in.ConnProfile = connProfile_in_ + receive, err := c.Send(ctx, "com.openuc2.deviceadmin.openuc2.RegenerateDropInConnProfile", in, flags) + if err != nil { + return nil, err + } + return func(context.Context) (flags uint64, err error) { + flags, err = receive(ctx, nil) + if err != nil { + err = Dispatch_Error(err) + return + } + return + }, nil +} + +func (m RegenerateDropInConnProfile_methods) Upgrade(ctx context.Context, c *varlink.Connection, connProfile_in_ string) (func(ctx context.Context) (flags uint64, conn varlink.ReadWriterContext, err_ error), error) { + var in struct { + ConnProfile string `json:"connProfile"` + } + in.ConnProfile = connProfile_in_ + receive, err := c.Upgrade(ctx, "com.openuc2.deviceadmin.openuc2.RegenerateDropInConnProfile", in) + if err != nil { + return nil, err + } + return func(context.Context) (flags uint64, conn varlink.ReadWriterContext, err error) { + flags, conn, err = receive(ctx, nil) + if err != nil { + err = Dispatch_Error(err) + return + } + return + }, nil +} + +// Generated service interface with all methods + +type comopenuc2deviceadminopenuc2Interface interface { + UpdatePSKDropInFile(ctx context.Context, c VarlinkCall, connProfile_ string, newPw_ string) error + RegenerateDropInConnProfile(ctx context.Context, c VarlinkCall, connProfile_ string) error +} + +// Generated service object with all methods + +type VarlinkCall struct{ varlink.Call } + +// Generated reply methods for all varlink errors + +// The service was unable to perform the requested operation for an unspecified reason. +func (c *VarlinkCall) ReplyUnknown(ctx context.Context, description_ string) error { + var out Unknown + out.Description = description_ + return c.ReplyError(ctx, "com.openuc2.deviceadmin.openuc2.Unknown", &out) +} + +// Generated reply methods for all varlink methods + +func (c *VarlinkCall) ReplyUpdatePSKDropInFile(ctx context.Context) error { + return c.Reply(ctx, nil) +} + +func (c *VarlinkCall) ReplyRegenerateDropInConnProfile(ctx context.Context) error { + return c.Reply(ctx, nil) +} + +// Generated dummy implementations for all varlink methods + +// UpdatePSKDropInFile updates the specified connection profile (as specified via file-based name, +// e.g. "wlan0-hotspot")'s Wi-Fi PSK drop-in snippet file (which is automatically determined) +// with the specified cleartext password. +// This operation does not try to regenerate the connection profile itself from the drop-in files. +func (s *VarlinkInterface) UpdatePSKDropInFile(ctx context.Context, c VarlinkCall, connProfile_ string, newPw_ string) error { + return c.ReplyMethodNotImplemented(ctx, "com.openuc2.deviceadmin.openuc2.UpdatePSKDropInFile") +} + +// RegenerateConnProfile reassembles the file for the NetworkManager connection profile (specified by +// its file-based name, e.g. "wlan0-hotspot") from its constituent drop-in snippet files. +// This operation does not try to make NetworkManager reload the updated connection profile. +func (s *VarlinkInterface) RegenerateDropInConnProfile(ctx context.Context, c VarlinkCall, connProfile_ string) error { + return c.ReplyMethodNotImplemented(ctx, "com.openuc2.deviceadmin.openuc2.RegenerateDropInConnProfile") +} + +// Generated method call dispatcher + +func (s *VarlinkInterface) VarlinkDispatch(ctx context.Context, call varlink.Call, methodname string) error { + switch methodname { + case "UpdatePSKDropInFile": + var in struct { + ConnProfile string `json:"connProfile"` + NewPw string `json:"newPw"` + } + err := call.GetParameters(&in) + if err != nil { + return call.ReplyInvalidParameter(ctx, "parameters") + } + return s.comopenuc2deviceadminopenuc2Interface.UpdatePSKDropInFile(ctx, VarlinkCall{call}, in.ConnProfile, in.NewPw) + + case "RegenerateDropInConnProfile": + var in struct { + ConnProfile string `json:"connProfile"` + } + err := call.GetParameters(&in) + if err != nil { + return call.ReplyInvalidParameter(ctx, "parameters") + } + return s.comopenuc2deviceadminopenuc2Interface.RegenerateDropInConnProfile(ctx, VarlinkCall{call}, in.ConnProfile) + + default: + return call.ReplyMethodNotFound(ctx, methodname) + } +} + +// Generated varlink interface name + +func (s *VarlinkInterface) VarlinkGetName() string { + return `com.openuc2.deviceadmin.openuc2` +} + +// Generated varlink interface description + +func (s *VarlinkInterface) VarlinkGetDescription() string { + return `# com.openuc2.deviceadmin.openuc2 manages openUC2 OS-specific settings. +interface com.openuc2.deviceadmin.openuc2 + +# The service was unable to perform the requested operation for an unspecified reason. +error Unknown (description: string) + +# NetworkManager + +# UpdatePSKDropInFile updates the specified connection profile (as specified via file-based name, +# e.g. "wlan0-hotspot")'s Wi-Fi PSK drop-in snippet file (which is automatically determined) +# with the specified cleartext password. +# This operation does not try to regenerate the connection profile itself from the drop-in files. +method UpdatePSKDropInFile(connProfile: string, newPw: string) -> () + +# RegenerateConnProfile reassembles the file for the NetworkManager connection profile (specified by +# its file-based name, e.g. "wlan0-hotspot") from its constituent drop-in snippet files. +# This operation does not try to make NetworkManager reload the updated connection profile. +method RegenerateDropInConnProfile(connProfile: string) -> () +` +} + +// Generated service interface + +type VarlinkInterface struct { + comopenuc2deviceadminopenuc2Interface +} + +func VarlinkNew(m comopenuc2deviceadminopenuc2Interface) *VarlinkInterface { + return &VarlinkInterface{m} +} diff --git a/internal/app/ipc/openuc2/generate.go b/internal/app/ipc/openuc2/generate.go new file mode 100644 index 0000000..ac4fb37 --- /dev/null +++ b/internal/app/ipc/openuc2/generate.go @@ -0,0 +1,3 @@ +package comopenuc2deviceadminopenuc2 + +//go:generate go tool varlink-go-interface-generator com.openuc2.deviceadmin.openuc2.varlink diff --git a/internal/app/server/routes/boot/routes.go b/internal/app/server/routes/boot/routes.go index ece9999..b103e29 100644 --- a/internal/app/server/routes/boot/routes.go +++ b/internal/app/server/routes/boot/routes.go @@ -116,14 +116,8 @@ func shutdownViaSidecar(ctx context.Context, method string, scc *sc.Client, l go if err != nil { return errors.Wrap(err, "couldn't open connection to sidecar") } - defer func() { - if conn == nil { - return - } - if err := conn.Close(); err != nil { - l.Error(errors.New("couldn't close connection to sidecar")) - } - }() + defer sc.CloseConn(conn, l) + switch method { default: return errors.Errorf("unknown sidecar method %s", method) diff --git a/internal/app/server/routes/internet/conn-profiles.go b/internal/app/server/routes/internet/conn-profiles.go index c139a75..9fc9e67 100644 --- a/internal/app/server/routes/internet/conn-profiles.go +++ b/internal/app/server/routes/internet/conn-profiles.go @@ -6,6 +6,7 @@ import ( "math" "net/http" "net/url" + "path" "strconv" "strings" "time" @@ -17,7 +18,8 @@ import ( "github.com/sargassum-world/godest/handling" "github.com/sargassum-world/godest/turbostreams" - ipc "github.com/openUC2/device-admin/internal/app/ipc/networkmanager" + nmipc "github.com/openUC2/device-admin/internal/app/ipc/networkmanager" + uc2ipc "github.com/openUC2/device-admin/internal/app/ipc/openuc2" sh "github.com/openUC2/device-admin/internal/app/server/handling" nm "github.com/openUC2/device-admin/internal/clients/networkmanager" sc "github.com/openUC2/device-admin/internal/clients/sidecar" @@ -51,15 +53,9 @@ func reloadConnProfilesViaSidecar(ctx context.Context, scc *sc.Client, l godest. if err != nil { return errors.Wrap(err, "couldn't open connection to sidecar") } - defer func() { - if conn == nil { - return - } - if err := conn.Close(); err != nil { - l.Error(errors.New("couldn't close connection to sidecar")) - } - }() - if err := ipc.ReloadConnProfiles().Call(ctx, conn); err != nil { + defer sc.CloseConn(conn, l) + + if err := nmipc.ReloadConnProfiles().Call(ctx, conn); err != nil { return errors.Wrap(err, "couldn't call sidecar's ReloadConnProfiles method") } return nil @@ -176,7 +172,13 @@ func (h *Handlers) HandleConnProfilePostByUUID() echo.HandlerFunc { if err != nil { return errors.Wrap(err, "couldn't load form parameters") } - state := c.FormValue("state") + rawTrue := "true" + dropInUpdate := c.FormValue("state:drop-in-updated") == rawTrue + regenerate := c.FormValue("state:regenerated") == rawTrue + reload := c.FormValue("state:reloaded") == rawTrue + update := c.FormValue("state:updated") == rawTrue + updateType := c.FormValue("update-type") + activate := c.FormValue("state:activated") == rawTrue redirectTarget := c.FormValue("redirect-target") // Run queries @@ -185,39 +187,79 @@ func (h *Handlers) HandleConnProfilePostByUUID() echo.HandlerFunc { // network interface down before bringing it back up), the operation is not interrupted by // context cancellation from the loss ofthe client-server connection: ctx := context.Background() - switch state { - default: - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf( - "invalid connection profiles state %s", state, - )) - case "reloaded": - if err := reloadConnProfileViaSidecar(ctx, uid, h.scc, h.l); err != nil { - return errors.Wrapf(err, "couldn't reload connection profile %s", rawUUID) - } - case "activated-transiently": - if err := h.nmc.ActivateConnProfile(ctx, uid); err != nil { - return errors.Wrapf(err, "couldn't activate connection profile %s", rawUUID) + if dropInUpdate { + if err := dropInUpdateConnProfileViaSidecar( + ctx, uid, c.FormValue("802-11-wireless-security.psk"), h.nmc, h.scc, h.l, + ); err != nil { + return errors.Wrapf(err, "couldn't regenerate connection profile %s", uid.String()) } - case "simplified-updated", "simplified-updated-activated": - if err := updateConnProfile(ctx, uid, "save and apply", formValues, h.nmc); err != nil { - return errors.Wrapf(err, "couldn't update connection profile %s", rawUUID) + } + if regenerate { + if err := regenerateConnProfileViaSidecar(ctx, uid, h.nmc, h.scc, h.l); err != nil { + return errors.Wrapf(err, "couldn't regenerate connection profile %s", uid.String()) } - if state == "simplified-updated-activated" { - if err := h.nmc.ActivateConnProfile(ctx, uid); err != nil { - return errors.Wrapf(err, "couldn't activate connection profile %s", rawUUID) - } + } + if reload { + if err := reloadConnProfileViaSidecar(ctx, uid, h.scc, h.l); err != nil { + return errors.Wrapf(err, "couldn't reload connection profile %s", uid.String()) } - case "updated": - updateType := c.FormValue("update-type") + } + if update { if err := updateConnProfile(ctx, uid, updateType, formValues, h.nmc); err != nil { - return errors.Wrapf(err, "couldn't update connection profile %s", rawUUID) + return errors.Wrapf(err, "couldn't update connection profile %s", uid.String()) + } + } + if activate { + if err := h.nmc.ActivateConnProfile(ctx, uid); err != nil { + return errors.Wrapf(err, "couldn't activate connection profile %s", uid.String()) } } + // Redirect user return c.Redirect(http.StatusSeeOther, redirectTarget) } } +func dropInUpdateConnProfileViaSidecar( + ctx context.Context, uid uuid.UUID, newPw string, nmc *nm.Client, scc *sc.Client, l godest.Logger, +) error { + conn, err := scc.Open(ctx) + if err != nil { + return errors.Wrap(err, "couldn't open connection to sidecar") + } + defer sc.CloseConn(conn, l) + + filename, err := nmc.GetConnProfileFilename(ctx, uid) + if err != nil { + return err + } + filename = strings.TrimSuffix(path.Base(filename), ".nmconnection") + if err := uc2ipc.UpdatePSKDropInFile().Call(ctx, conn, filename, newPw); err != nil { + return errors.Wrap(err, "couldn't call sidecar's UpdatePSKDropInFile method") + } + return nil +} + +func regenerateConnProfileViaSidecar( + ctx context.Context, uid uuid.UUID, nmc *nm.Client, scc *sc.Client, l godest.Logger, +) error { + conn, err := scc.Open(ctx) + if err != nil { + return errors.Wrap(err, "couldn't open connection to sidecar") + } + defer sc.CloseConn(conn, l) + + filename, err := nmc.GetConnProfileFilename(ctx, uid) + if err != nil { + return err + } + filename = strings.TrimSuffix(path.Base(filename), ".nmconnection") + if err := uc2ipc.RegenerateDropInConnProfile().Call(ctx, conn, filename); err != nil { + return errors.Wrap(err, "couldn't call sidecar's RegenerateConnProfile method") + } + return nil +} + func reloadConnProfileViaSidecar( ctx context.Context, uid uuid.UUID, scc *sc.Client, l godest.Logger, ) error { @@ -225,16 +267,11 @@ func reloadConnProfileViaSidecar( if err != nil { return errors.Wrap(err, "couldn't open connection to sidecar") } - defer func() { - if conn == nil { - return - } - if err := conn.Close(); err != nil { - l.Error(errors.New("couldn't close connection to sidecar")) - } - }() - if err := ipc.ReloadConnProfile().Call(ctx, conn, uid.String()); err != nil { - return errors.Wrap(err, "couldn't call sidecar's ReloadConnProfiles method") + defer sc.CloseConn(conn, l) + + connProfileName := uid.String() + if err := nmipc.ReloadConnProfile().Call(ctx, conn, connProfileName); err != nil { + return errors.Wrap(err, "couldn't call sidecar's ReloadConnProfile method") } return nil } diff --git a/internal/app/server/routes/internet/routes.go b/internal/app/server/routes/internet/routes.go index 68bcc54..aa92294 100644 --- a/internal/app/server/routes/internet/routes.go +++ b/internal/app/server/routes/internet/routes.go @@ -85,10 +85,13 @@ func (h *Handlers) HandleInternetGet() echo.HandlerFunc { type InternetViewData struct { NM nm.NetworkManager - AvailableSSIDs []string - Wlan0HotspotConnProfile nm.ConnProfile + Wlan0HotspotConnProfile nm.ConnProfile + Wlan0Device nm.Device + Wlan0HotspotPasswordInsecure bool + Wlan1InternetConnProfile nm.ConnProfile Wlan1Device nm.Device + AvailableSSIDs []string WifiDevices []nm.Device EthernetDevices []nm.Device @@ -119,16 +122,30 @@ func getInternetViewData(ctx context.Context, nmc *nm.Client) (vd InternetViewDa } slices.Sort(vd.AvailableSSIDs) + if err := collectDevices(ctx, nmc, &vd); err != nil { + return vd, err + } + if err := collectConnProfiles(ctx, nmc, &vd); err != nil { + return vd, err + } + + return vd, nil +} + +func collectDevices(ctx context.Context, nmc *nm.Client, vd *InternetViewData) error { allDevices, err := nmc.GetDevices(ctx) if err != nil { - return vd, errors.Wrap(err, "couldn't list network devices") + return errors.Wrap(err, "couldn't list network devices") } for _, device := range allDevices { switch device.Type.Info().Short { default: vd.OtherDevices = append(vd.OtherDevices, device) case "wifi": - if cmp.Or(device.IpInterface, device.ControlInterface) == "wlan1" { + switch cmp.Or(device.IpInterface, device.ControlInterface) { + case "wlan0": + vd.Wlan0Device = device + case "wlan1": vd.Wlan1Device = device } vd.WifiDevices = append(vd.WifiDevices, device) @@ -136,10 +153,13 @@ func getInternetViewData(ctx context.Context, nmc *nm.Client) (vd InternetViewDa vd.EthernetDevices = append(vd.EthernetDevices, device) } } + return nil +} +func collectConnProfiles(ctx context.Context, nmc *nm.Client, vd *InternetViewData) error { connProfiles, err := nmc.ListConnProfiles(ctx) if err != nil { - return vd, errors.Wrap(err, "couldn't list connection profiles") + return errors.Wrap(err, "couldn't list connection profiles") } for _, connProfile := range connProfiles { switch connProfile.Settings.Conn.Type.Info().Short { @@ -157,8 +177,7 @@ func getInternetViewData(ctx context.Context, nmc *nm.Client) (vd InternetViewDa vd.OtherConnProfiles = append(vd.OtherConnProfiles, connProfile.Settings.Conn) } } - - return vd, nil + return nil } func (h *Handlers) HandleInternetPub() turbostreams.HandlerFunc { diff --git a/internal/app/sidecar/handling/varlink.go b/internal/app/sidecar/handling/varlink.go new file mode 100644 index 0000000..cf75796 --- /dev/null +++ b/internal/app/sidecar/handling/varlink.go @@ -0,0 +1,35 @@ +// Package handling provides utilities for handlers +package handling + +import ( + "context" + "encoding/json" + + "github.com/pkg/errors" + "github.com/sargassum-world/godest" +) + +func LogMethod(request *[]byte, l godest.Logger) { + if request != nil { + var req struct { + Method string `json:"method"` + } + if err := json.Unmarshal(*request, &req); err == nil { + l.Info(req.Method) + } + } +} + +type UnknownErrorReplier interface { + ReplyUnknown(ctx context.Context, description string) error +} + +func ReportUnknownError( + ctx context.Context, errReplier UnknownErrorReplier, err error, l godest.Logger, +) error { + l.Error(err) + if replyErr := errReplier.ReplyUnknown(ctx, err.Error()); replyErr != nil { + return errors.Wrapf(replyErr, "couldn't report error (%s)", err.Error()) + } + return nil +} diff --git a/internal/app/sidecar/routes/boot/routes.go b/internal/app/sidecar/routes/boot/routes.go index 1d4a3ea..c8c8388 100644 --- a/internal/app/sidecar/routes/boot/routes.go +++ b/internal/app/sidecar/routes/boot/routes.go @@ -2,13 +2,12 @@ package boot import ( "context" - "encoding/json" - "github.com/pkg/errors" "github.com/sargassum-world/godest" "github.com/varlink/go/varlink" ipc "github.com/openUC2/device-admin/internal/app/ipc/boot" + "github.com/openUC2/device-admin/internal/app/sidecar/handling" sd "github.com/openUC2/device-admin/internal/clients/systemd" ) @@ -32,64 +31,28 @@ func (h *Handlers) Register(service *varlink.Service) error { } func (h *Handlers) Poweroff(ctx context.Context, call ipc.VarlinkCall) error { - if call.Request != nil { - var req struct { - Method string `json:"method"` - } - if err := json.Unmarshal(*call.Request, &req); err == nil { - h.l.Info(req.Method) - } - } + handling.LogMethod(call.Request, h.l) if err := h.sdc.Poweroff(ctx); err != nil { - if replyErr := call.ReplyError( - ctx, "com.openuc2.deviceadmin.boot.Unknown", ipc.Unknown{Description: err.Error()}, - ); replyErr != nil { - h.l.Error(err) - return errors.Wrapf(replyErr, "couldn't report error (%s) in method call reply", err.Error()) - } + return handling.ReportUnknownError(ctx, &call, err, h.l) } return call.ReplyPoweroff(ctx) } func (h *Handlers) Reboot(ctx context.Context, call ipc.VarlinkCall) error { - if call.Request != nil { - var req struct { - Method string `json:"method"` - } - if err := json.Unmarshal(*call.Request, &req); err == nil { - h.l.Info(req.Method) - } - } + handling.LogMethod(call.Request, h.l) if err := h.sdc.Reboot(ctx); err != nil { - if replyErr := call.ReplyError( - ctx, "com.openuc2.deviceadmin.boot.Unknown", ipc.Unknown{Description: err.Error()}, - ); replyErr != nil { - h.l.Error(err) - return errors.Wrapf(replyErr, "couldn't report error (%s) in method call reply", err.Error()) - } + return handling.ReportUnknownError(ctx, &call, err, h.l) } return call.ReplyReboot(ctx) } func (h *Handlers) SoftReboot(ctx context.Context, call ipc.VarlinkCall) error { - if call.Request != nil { - var req struct { - Method string `json:"method"` - } - if err := json.Unmarshal(*call.Request, &req); err == nil { - h.l.Info(req.Method) - } - } + handling.LogMethod(call.Request, h.l) if err := h.sdc.SoftReboot(ctx); err != nil { - if replyErr := call.ReplyError( - ctx, "com.openuc2.deviceadmin.boot.Unknown", ipc.Unknown{Description: err.Error()}, - ); replyErr != nil { - h.l.Error(err) - return errors.Wrapf(replyErr, "couldn't report error (%s) in method call reply", err.Error()) - } + return handling.ReportUnknownError(ctx, &call, err, h.l) } return call.ReplySoftReboot(ctx) } diff --git a/internal/app/sidecar/routes/networkmanager/routes.go b/internal/app/sidecar/routes/networkmanager/routes.go index aa92272..2b12fbe 100644 --- a/internal/app/sidecar/routes/networkmanager/routes.go +++ b/internal/app/sidecar/routes/networkmanager/routes.go @@ -2,7 +2,6 @@ package networkmanager import ( "context" - "encoding/json" "github.com/google/uuid" "github.com/pkg/errors" @@ -10,6 +9,7 @@ import ( "github.com/varlink/go/varlink" ipc "github.com/openUC2/device-admin/internal/app/ipc/networkmanager" + "github.com/openUC2/device-admin/internal/app/sidecar/handling" nm "github.com/openUC2/device-admin/internal/clients/networkmanager" ) @@ -33,23 +33,10 @@ func (h *Handlers) Register(service *varlink.Service) error { } func (h *Handlers) ReloadConnProfiles(ctx context.Context, call ipc.VarlinkCall) error { - if call.Request != nil { - var req struct { - Method string `json:"method"` - } - if err := json.Unmarshal(*call.Request, &req); err == nil { - h.l.Info(req.Method) - } - } + handling.LogMethod(call.Request, h.l) if err := h.nmc.ReloadConnProfiles(ctx); err != nil { - if replyErr := call.ReplyError( - ctx, "com.openuc2.deviceadmin.networkmanager.Unknown", ipc.Unknown{Description: err.Error()}, - ); replyErr != nil { - h.l.Error(err) - return errors.Wrapf(replyErr, "couldn't report error (%s) in method call reply", err.Error()) - } - return err + return handling.ReportUnknownError(ctx, &call, err, h.l) } return call.ReplyReloadConnProfiles(ctx) } @@ -57,35 +44,16 @@ func (h *Handlers) ReloadConnProfiles(ctx context.Context, call ipc.VarlinkCall) func (h *Handlers) ReloadConnProfile( ctx context.Context, call ipc.VarlinkCall, rawUUID string, ) error { - if call.Request != nil { - var req struct { - Method string `json:"method"` - } - if err := json.Unmarshal(*call.Request, &req); err == nil { - h.l.Info(req.Method) - } - } + handling.LogMethod(call.Request, h.l) uid, err := uuid.Parse(rawUUID) if err != nil { - err = errors.Wrapf(err, "couldn't parse uuid %s", rawUUID) - if replyErr := call.ReplyError( - ctx, "com.openuc2.deviceadmin.networkmanager.InvalidUUID", - ipc.InvalidUUID{Description: err.Error()}, - ); replyErr != nil { - h.l.Error(err) - return errors.Wrapf(replyErr, "couldn't report error (%s) in method call reply", err.Error()) - } - return err + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't parse uuid %s", rawUUID, + ), h.l) } if err := h.nmc.ReloadConnProfile(ctx, uid); err != nil { - if replyErr := call.ReplyError( - ctx, "com.openuc2.deviceadmin.networkmanager.Unknown", ipc.Unknown{Description: err.Error()}, - ); replyErr != nil { - h.l.Error(err) - return errors.Wrapf(replyErr, "couldn't report error (%s) in method call reply", err.Error()) - } - return err + return handling.ReportUnknownError(ctx, &call, err, h.l) } return call.ReplyReloadConnProfiles(ctx) } diff --git a/internal/app/sidecar/routes/openuc2/routes.go b/internal/app/sidecar/routes/openuc2/routes.go new file mode 100644 index 0000000..515c4b3 --- /dev/null +++ b/internal/app/sidecar/routes/openuc2/routes.go @@ -0,0 +1,152 @@ +package openuc2 + +import ( + "context" + "fmt" + "os" + "path" + "regexp" + "strings" + + "github.com/pkg/errors" + "github.com/sargassum-world/godest" + "github.com/varlink/go/varlink" + + ipc "github.com/openUC2/device-admin/internal/app/ipc/openuc2" + "github.com/openUC2/device-admin/internal/app/sidecar/handling" + sd "github.com/openUC2/device-admin/internal/clients/systemd" +) + +type Handlers struct { + ipc.VarlinkInterface + + sdc *sd.Client + + l godest.Logger +} + +func New(sdc *sd.Client, l godest.Logger) *Handlers { + return &Handlers{ + sdc: sdc, + l: l, + } +} + +func (h *Handlers) Register(service *varlink.Service) error { + return service.RegisterInterface(ipc.VarlinkNew(h)) +} + +func (h *Handlers) UpdatePSKDropInFile( + ctx context.Context, call ipc.VarlinkCall, connProfile string, newPw string, +) error { + handling.LogMethod(call.Request, h.l) + + dropInDir := path.Join("/etc/NetworkManager/system-connections.d", connProfile) + fsys, err := os.OpenRoot(dropInDir) + if err != nil { + return errors.Wrapf(err, "couldn't open drop-in directory %s", dropInDir) + } + const dropInFile = "51-wifi-security-password.nmconnection" + lines, err := readLines(fsys, dropInFile) + if err != nil { + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't read PSK drop-in file %s", path.Join(dropInDir, dropInFile), + ), h.l) + } + + if lines, err = setKey(lines, "psk", newPw); err != nil { + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't update PSK for drop-in file %s", path.Join(dropInDir, dropInFile), + ), h.l) + } + + const mode = 0o600 // -rw------- + if err = writeAtomically(fsys, dropInFile, lines, mode); err != nil { + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't atomically write updated drop-in file %s", path.Join(dropInDir, dropInFile), + ), h.l) + } + + return call.ReplyUpdatePSKDropInFile(ctx) +} + +func readLines(fsys *os.Root, filePath string) ([]string, error) { + contents, err := fsys.ReadFile(filePath) + if err != nil { + return nil, errors.Wrapf(err, "couldn't read file %s", filePath) + } + + return strings.Split(string(contents), "\n"), nil +} + +func setKey(lines []string, key string, newValue string) ([]string, error) { + pattern := fmt.Sprintf("^%s[ ]*=", key) + re, err := regexp.Compile(pattern) + if err != nil { + return nil, errors.Wrapf(err, "couldn't compile regexp for key %s", key) + } + setLine := fmt.Sprintf("%s=%s", key, newValue) + + hasKey := false + for i, line := range lines { + if !re.MatchString(line) { + continue + } + hasKey = true + lines[i] = setLine + } + if !hasKey { + lines = append(lines, setLine) + } + return lines, nil +} + +func writeAtomically(fsys *os.Root, filePath string, lines []string, perm os.FileMode) error { + data := []byte(strings.Join(lines, "\n")) + swapFilePath := filePath + ".swp" + if err := fsys.WriteFile(swapFilePath, data, perm.Perm()); err != nil { + return errors.Wrapf( + err, "couldn't write drop-in file %s to swap file %s", filePath, swapFilePath, + ) + } + if err := fsys.Rename(swapFilePath, filePath); err != nil { + return errors.Wrapf( + err, "couldn't move temporary drop-in file %s to %s", swapFilePath, filePath, + ) + } + return nil +} + +func (h *Handlers) RegenerateDropInConnProfile( + ctx context.Context, call ipc.VarlinkCall, connProfile string, +) error { + handling.LogMethod(call.Request, h.l) + + templatedAssembleUnit := fmt.Sprintf( + "assemble-networkmanager-connection-templated@%s.service", connProfile, + ) + hasTemplatedAssemble, err := h.sdc.UnitExists(ctx, templatedAssembleUnit) + if err != nil { + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't check whether templated drop-in assembly service %s exists for %s", + templatedAssembleUnit, connProfile, + ), h.l) + } + if hasTemplatedAssemble { + if err := h.sdc.RestartUnit(ctx, templatedAssembleUnit); err != nil { + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't restart templated drop-in assembly service %s for %s", + templatedAssembleUnit, connProfile, + ), h.l) + } + } + + assembleUnit := fmt.Sprintf("assemble-networkmanager-connection@%s.service", connProfile) + if err := h.sdc.RestartUnit(ctx, assembleUnit); err != nil { + return handling.ReportUnknownError(ctx, &call, errors.Wrapf( + err, "couldn't restart drop-in assembly service %s for %s", assembleUnit, connProfile, + ), h.l) + } + + return call.ReplyRegenerateDropInConnProfile(ctx) +} diff --git a/internal/app/sidecar/routes/routes.go b/internal/app/sidecar/routes/routes.go index dc3ceea..6d86356 100644 --- a/internal/app/sidecar/routes/routes.go +++ b/internal/app/sidecar/routes/routes.go @@ -7,6 +7,7 @@ import ( "github.com/openUC2/device-admin/internal/app/sidecar/client" "github.com/openUC2/device-admin/internal/app/sidecar/routes/boot" "github.com/openUC2/device-admin/internal/app/sidecar/routes/networkmanager" + "github.com/openUC2/device-admin/internal/app/sidecar/routes/openuc2" ) type Handlers struct { @@ -27,5 +28,8 @@ func (s *Handlers) Register(service *varlink.Service) error { if err := networkmanager.New(s.globals.NetworkManager, l).Register(service); err != nil { return errors.Wrap(err, "couldn't register networkmanager handlers") } + if err := openuc2.New(s.globals.Systemd, l).Register(service); err != nil { + return errors.Wrap(err, "couldn't register openUC2 OS handlers") + } return nil } diff --git a/internal/clients/networkmanager/conn-profiles.go b/internal/clients/networkmanager/conn-profiles.go index 7f56b97..1cb739a 100644 --- a/internal/clients/networkmanager/conn-profiles.go +++ b/internal/clients/networkmanager/conn-profiles.go @@ -140,18 +140,9 @@ func (c *Client) ReloadConnProfiles(ctx context.Context) error { } func (c *Client) ReloadConnProfile(ctx context.Context, uid uuid.UUID) error { - conno, err := c.findConnProfileByUUID(ctx, uid) + filename, err := c.GetConnProfileFilename(ctx, uid) if err != nil { - return errors.Wrapf(err, "couldn't find connection profile with uuid %s", uid) - } - - var filename string - const connName = nmName + ".Settings.Connection" - if err = conno.StoreProperty(connName+".Filename", &filename); err != nil { - return errors.Wrap(err, "couldn't query for filename") - } - if filename == "" { - return errors.Wrapf(err, "connection with uuid %s is not backed by a file!", uid) + return errors.Wrapf(err, "couldn't determine filename of connection with uuid %s", uid) } nm := c.getNetworkManagerSettings() @@ -169,6 +160,24 @@ func (c *Client) ReloadConnProfile(ctx context.Context, uid uuid.UUID) error { return nil } +func (c *Client) GetConnProfileFilename( + ctx context.Context, uid uuid.UUID, +) (filename string, err error) { + conno, err := c.findConnProfileByUUID(ctx, uid) + if err != nil { + return "", errors.Wrapf(err, "couldn't find connection profile with uuid %s", uid) + } + + const connName = nmName + ".Settings.Connection" + if err = conno.StoreProperty(connName+".Filename", &filename); err != nil { + return "", errors.Wrap(err, "couldn't query for filename") + } + if filename == "" { + return "", errors.Wrapf(err, "connection with uuid %s is not backed by a file!", uid) + } + return filename, nil +} + func (c *Client) ActivateConnProfile(ctx context.Context, uid uuid.UUID) error { nm := c.getNetworkManager() conno, err := c.findConnProfileByUUID(ctx, uid) diff --git a/internal/clients/sidecar/client.go b/internal/clients/sidecar/client.go index 41d27c0..f56fb42 100644 --- a/internal/clients/sidecar/client.go +++ b/internal/clients/sidecar/client.go @@ -5,6 +5,7 @@ import ( "context" "github.com/pkg/errors" + "github.com/sargassum-world/godest" "github.com/varlink/go/varlink" ) @@ -26,3 +27,12 @@ func (c *Client) Open(ctx context.Context) (conn *varlink.Connection, err error) } return conn, nil } + +func CloseConn(conn *varlink.Connection, l godest.Logger) { + if conn == nil { + return + } + if err := conn.Close(); err != nil { + l.Error(errors.New("couldn't close connection to sidecar")) + } +} diff --git a/internal/clients/systemd/client.go b/internal/clients/systemd/client.go index 916352e..f1c3956 100644 --- a/internal/clients/systemd/client.go +++ b/internal/clients/systemd/client.go @@ -33,7 +33,10 @@ func (c *Client) Open(ctx context.Context) (err error) { return nil } -const sdName = "org.freedesktop.systemd1" +const ( + sdName = "org.freedesktop.systemd1" + sdManagerName = "org.freedesktop.systemd1.Manager" +) func (c *Client) getSystemdManager() dbus.BusObject { return c.bus.Object(sdName, "/org/freedesktop/systemd1") @@ -43,7 +46,7 @@ func (c *Client) getSystemdManager() dbus.BusObject { func (c *Client) Poweroff(ctx context.Context) error { sdm := c.getSystemdManager() - if err := sdm.CallWithContext(ctx, sdName+".Manager.PowerOff", 0).Store(); err != nil { + if err := sdm.CallWithContext(ctx, sdManagerName+".PowerOff", 0).Store(); err != nil { return errors.Wrap(err, "couldn't power-off") } return nil @@ -51,7 +54,7 @@ func (c *Client) Poweroff(ctx context.Context) error { func (c *Client) Reboot(ctx context.Context) error { sdm := c.getSystemdManager() - if err := sdm.CallWithContext(ctx, sdName+".Manager.Reboot", 0).Store(); err != nil { + if err := sdm.CallWithContext(ctx, sdManagerName+".Reboot", 0).Store(); err != nil { return errors.Wrap(err, "couldn't reboot") } return nil @@ -59,8 +62,33 @@ func (c *Client) Reboot(ctx context.Context) error { func (c *Client) SoftReboot(ctx context.Context) error { sd := c.getSystemdManager() - if err := sd.CallWithContext(ctx, sdName+".Manager.SoftReboot", 0, "").Store(); err != nil { + if err := sd.CallWithContext(ctx, sdManagerName+".SoftReboot", 0, "").Store(); err != nil { return errors.Wrap(err, "couldn't soft-reboot") } return nil } + +// Units + +func (c *Client) UnitExists(ctx context.Context, name string) (bool, error) { + sd := c.getSystemdManager() + var unitPath dbus.ObjectPath + err := sd.CallWithContext( + ctx, sdManagerName+".GetUnit", 0, name, + ).Store(&unitPath) + if err != nil { + return false, errors.Wrapf(err, "couldn't find %s", name) + } + return true, nil +} + +func (c *Client) RestartUnit(ctx context.Context, name string) error { + sd := c.getSystemdManager() + var jobPath dbus.ObjectPath + if err := sd.CallWithContext( + ctx, sdManagerName+".RestartUnit", 0, name, "replace", + ).Store(&jobPath); err != nil { + return errors.Wrapf(err, "couldn't restart %s", name) + } + return nil +} diff --git a/web/app/package.json b/web/app/package.json index 65526c8..5d7e47b 100644 --- a/web/app/package.json +++ b/web/app/package.json @@ -8,7 +8,7 @@ "lint": "prettier --check --write ./src/**/*.{js,html} --no-error-on-unmatched-pattern && eslint src/**/*.{js} --no-error-on-unmatched-pattern", "lint:fix": "prettier --write ./src/**/*.{js,html} --no-error-on-unmatched-pattern && eslint --fix src/**/*.{js} --no-error-on-unmatched-pattern" }, - "packageManager": "pnpm@10.32.1+sha512.a706938f0e89ac1456b6563eab4edf1d1faf3368d1191fc5c59790e96dc918e4456ab2e67d613de1043d2e8c81f87303e6b40d4ffeca9df15ef1ad567348f2be", + "packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319", "devDependencies": { "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", diff --git a/web/app/pnpm-workspace.yaml b/web/app/pnpm-workspace.yaml new file mode 100644 index 0000000..620a7bb --- /dev/null +++ b/web/app/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + '@parcel/watcher': true diff --git a/web/templates/internet/conn-profiles/index.page.tmpl b/web/templates/internet/conn-profiles/index.page.tmpl index 741ab5d..c379c46 100644 --- a/web/templates/internet/conn-profiles/index.page.tmpl +++ b/web/templates/internet/conn-profiles/index.page.tmpl @@ -107,7 +107,7 @@ data-form-submission-target="submitter" class="mt-4 mb-3" > - + - + - + + + - {{if ne (or $device.IpInterface $device.ControlInterface) "wlan1"}} - - {{else}} - + {{if eq (or $device.IpInterface $device.ControlInterface) "wlan1"}} + {{end}} + +
+ + + + {{if eq (or $device.IpInterface $device.ControlInterface) "wlan0"}} + + {{end}} + + +
+ +
+
+

{{toString $wifi.SSID}}

+
+
+
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ + diff --git a/web/templates/internet/index.page.tmpl b/web/templates/internet/index.page.tmpl index f30aa98..7c73752 100644 --- a/web/templates/internet/index.page.tmpl +++ b/web/templates/internet/index.page.tmpl @@ -65,22 +65,22 @@ {{else}} -

External Wi-Fi network

- - {{if ne (or .Data.Wlan1Device.IpInterface .Data.Wlan1Device.ControlInterface) "wlan1"}} -
-
- No recognized USB Wi-Fi module is plugged into the machine! Such a module will be - needed before this machine can connect to an external Wi-Fi network. -
-
- {{end}} -
+

External Wi-Fi network

+ + {{if ne (or .Data.Wlan1Device.IpInterface .Data.Wlan1Device.ControlInterface) "wlan1"}} +
+
+ No recognized USB Wi-Fi module is plugged into the machine! Such a module will + be needed before this machine can connect to an external Wi-Fi network. +
+
+ {{end}} +
{{ template "internet/external-network-form.partial.tmpl" dict "ConnProfile" .Data.Wlan1InternetConnProfile @@ -92,6 +92,47 @@
{{end}} + {{if not .Data.Wlan0HotspotConnProfile.HasData}} +
+
+ The basic configuration file for the Wi-Fi hotspot could not be found! Was it removed? + You might be able to troubleshoot this by opening the + advanced view of this page to check whether + the "wlan0-hotspot" connection profile is listed, and if so, what its contents are. + It should have ID "wlan0-hotspot" and be of type "wifi". +
+
+ {{else}} +
+
+

Wi-Fi hotspot

+ + {{if ne (or .Data.Wlan0Device.IpInterface .Data.Wlan0Device.ControlInterface) "wlan0"}} +
+
+ The machine's internal Wi-Fi module is not working! As a result, this machine + may be unable to make its Wi-Fi hotspot. +
+
+ {{end}} +
+ {{ + template "internet/hotspot-form.partial.tmpl" dict + "ConnProfile" .Data.Wlan0HotspotConnProfile + "Device" .Data.Wlan0Device + "AvailableSSIDs" .Data.AvailableSSIDs + "Meta" .Meta + }} +
+
+ {{end}} +

All modules