diff --git a/cmd/mobile/main.go b/cmd/mobile/main.go index 3a5448c..9b69828 100644 --- a/cmd/mobile/main.go +++ b/cmd/mobile/main.go @@ -1,30 +1,26 @@ -//go:build android -// +build android - package main import ( "context" + "crypto/tls" "os" - "sync" + "path/filepath" "forge.cadoles.com/arcad/arcast" "forge.cadoles.com/arcad/arcast/pkg/browser/gioui" - "forge.cadoles.com/arcad/arcast/pkg/selfsigned" + "forge.cadoles.com/arcad/arcast/pkg/config" "forge.cadoles.com/arcad/arcast/pkg/server" "gioui.org/app" "gioui.org/io/system" "gioui.org/layout" "gioui.org/op" "github.com/gioui-plugins/gio-plugins/plugin" - "github.com/gioui-plugins/gio-plugins/safedata" - "github.com/gioui-plugins/gio-plugins/safedata/giosafedata" "github.com/gioui-plugins/gio-plugins/webviewer/webview" "github.com/pkg/errors" "gitlab.com/wpetit/goweb/logger" ) -const instanceIDSecretIdentifier = "instance_id" +const packageName = "com.cadoles.arcast_player" func main() { ctx := context.Background() @@ -36,12 +32,6 @@ func main() { browser := gioui.NewBrowser(window) - var safeDataConfig safedata.Config - var safeDataConfigWaigGroup sync.WaitGroup - var initSafeDataConfig sync.Once - - safeDataConfigWaigGroup.Add(1) - go func() { ops := new(op.Ops) for { @@ -56,11 +46,6 @@ func main() { gtx := layout.NewContext(ops, evt) browser.Layout(gtx) evt.Frame(gtx.Ops) - case app.ViewEvent: - initSafeDataConfig.Do(func() { - defer safeDataConfigWaigGroup.Done() - safeDataConfig = giosafedata.NewConfigFromViewEvent(window, evt, "com.cadoles.arcast_player") - }) } } }() @@ -71,26 +56,32 @@ func main() { ctx, cancel := context.WithCancel(ctx) defer cancel() - safeDataConfigWaigGroup.Wait() - safe := safedata.NewSafeData(safeDataConfig) + conf := config.DefaultConfig() + configFiles := getConfigFiles(ctx) + for _, f := range configFiles { + logger.Info(ctx, "loading or creating configuration file", logger.F("filename", f)) + if err := config.LoadOrCreate(ctx, f, conf, config.DefaultTransforms...); err != nil { + logger.Error(ctx, "could not load configuration file", logger.CapturedE(errors.WithStack(err))) + continue + } - instanceID, err := getInstanceIDFromSafeData(ctx, safe) - if err != nil { - logger.Fatal(ctx, "could not retrieve instance id", logger.CapturedE(errors.WithStack(err))) + break } - cert, err := selfsigned.NewLANCert() + cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key) if err != nil { - logger.Fatal(ctx, "could not generate self signed certificate", logger.CapturedE(errors.WithStack(err))) + logger.Fatal(ctx, "could not parse x509 certificate", logger.CapturedE(errors.WithStack(err))) } server := server.New( browser, - server.WithInstanceID(instanceID), - server.WithAppsEnabled(true), - server.WithDefaultApp("home"), + server.WithInstanceID(conf.InstanceID), + server.WithAppsEnabled(conf.Apps.Enabled), + server.WithDefaultApp(conf.Apps.DefaultApp), server.WithApps(arcast.DefaultApps...), - server.WithTLSCertificate(cert), + server.WithTLSCertificate(&cert), + server.WithAddress(conf.HTTP.Address), + server.WithTLSAddress(conf.HTTPS.Address), ) if err := server.Start(); err != nil { @@ -112,26 +103,19 @@ func main() { app.Main() } -func getInstanceIDFromSafeData(ctx context.Context, safe *safedata.SafeData) (string, error) { - instanceIDSecret, err := safe.Get(instanceIDSecretIdentifier) - if err != nil && err.Error() != "not found" { - logger.Error(ctx, "could not retrieve instance id secret", logger.CapturedE(errors.WithStack(err))) - } +func getConfigFiles(ctx context.Context) []string { + configFiles := make([]string, 0) - var instanceID string - if len(instanceIDSecret.Data) > 0 { - instanceID = string(instanceIDSecret.Data) + sharedStorageConfigFile := filepath.Join("/storage/emulated/0/Android/data", packageName, "files/config.json") + configFiles = append(configFiles, sharedStorageConfigFile) + + dataDir, err := app.DataDir() + if err != nil { + logger.Error(ctx, "could not retrieve app data dir", logger.CapturedE(errors.WithStack(err))) } else { - instanceID = server.NewRandomInstanceID() - - instanceIDSecret.Identifier = instanceIDSecretIdentifier - instanceIDSecret.Data = []byte(instanceID) - instanceIDSecret.Description = "Arcast player instance identifier" - - if err := safe.Set(instanceIDSecret); err != nil { - return "", errors.Wrapf(err, "could not save instance id secret") - } + appDataConfigFile := filepath.Join(dataDir, "config.json") + configFiles = append(configFiles, appDataConfigFile) } - return instanceID, nil + return configFiles } diff --git a/internal/command/player/run.go b/internal/command/player/run.go index e1578f9..7b20082 100644 --- a/internal/command/player/run.go +++ b/internal/command/player/run.go @@ -1,12 +1,14 @@ package player import ( + "context" + "crypto/tls" "fmt" "os" "forge.cadoles.com/arcad/arcast" "forge.cadoles.com/arcad/arcast/pkg/browser/lorca" - "forge.cadoles.com/arcad/arcast/pkg/selfsigned" + "forge.cadoles.com/arcad/arcast/pkg/config" "forge.cadoles.com/arcad/arcast/pkg/server" "github.com/pkg/errors" "github.com/urfave/cli/v2" @@ -15,9 +17,15 @@ import ( func Run() *cli.Command { defaults := lorca.NewOptions() + return &cli.Command{ Name: "run", Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + EnvVars: []string{"ARCAST_DESKTOP_CONFIG"}, + Value: config.DefaultConfigFile(context.Background()), + }, &cli.StringSliceFlag{ Name: "additional-chrome-arg", EnvVars: []string{"ARCAST_DESKTOP_ADDITIONAL_CHROME_ARGS"}, @@ -34,8 +42,8 @@ func Run() *cli.Command { Value: ":", }, &cli.StringFlag{ - Name: "tls-address", - EnvVars: []string{"ARCAST_DESKTOP_TLS_ADDRESS"}, + Name: "https-address", + EnvVars: []string{"ARCAST_DESKTOP_HTTPS_ADDRESS"}, Value: ":", }, &cli.IntFlag{ @@ -55,12 +63,10 @@ func Run() *cli.Command { }, }, Action: func(ctx *cli.Context) error { + configFile := ctx.String("config") windowHeight := ctx.Int("window-height") windowWidth := ctx.Int("window-width") chromeArgs := addFlagsPrefix(ctx.StringSlice("additional-chrome-arg")...) - enableApps := ctx.Bool("apps") - serverAddress := ctx.String("address") - serverTLSAddress := ctx.String("tls-address") browser := lorca.NewBrowser( lorca.WithAdditionalChromeArgs(chromeArgs...), @@ -84,24 +90,43 @@ func Run() *cli.Command { } }() - instanceID := ctx.String("instance-id") - if instanceID == "" { - instanceID = server.NewRandomInstanceID() + conf := config.DefaultConfig() + + logger.Info(ctx.Context, "loading or creating configuration file", logger.F("filename", configFile)) + if err := config.LoadOrCreate(ctx.Context, configFile, conf, config.DefaultTransforms...); err != nil { + logger.Error(ctx.Context, "could not load configuration file", logger.CapturedE(errors.WithStack(err))) } - cert, err := selfsigned.NewLANCert() + instanceID := ctx.String("instance-id") + if instanceID != "" { + conf.InstanceID = instanceID + } + + cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key) if err != nil { - return errors.Wrap(err, "could not generate self signed certificate") + return errors.Wrap(err, "could not parse tls cert/key pair") + } + + if ctx.IsSet("apps") { + conf.Apps.Enabled = ctx.Bool("apps") + } + + if ctx.IsSet("address") { + conf.HTTP.Address = ctx.String("address") + } + + if ctx.IsSet("https-address") { + conf.HTTPS.Address = ctx.String("tls-address") } server := server.New(browser, - server.WithInstanceID(instanceID), - server.WithAppsEnabled(enableApps), - server.WithDefaultApp("home"), + server.WithInstanceID(conf.InstanceID), + server.WithAppsEnabled(conf.Apps.Enabled), + server.WithDefaultApp(conf.Apps.DefaultApp), server.WithApps(arcast.DefaultApps...), - server.WithAddress(serverAddress), - server.WithTLSAddress(serverTLSAddress), - server.WithTLSCertificate(cert), + server.WithAddress(conf.HTTP.Address), + server.WithTLSAddress(conf.HTTPS.Address), + server.WithTLSCertificate(&cert), ) if err := server.Start(); err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..6dfbc0e --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,114 @@ +package config + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + + "forge.cadoles.com/arcad/arcast/pkg/server" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +type Config struct { + InstanceID string `json:"instanceId"` + HTTP HTTPConfig `json:"http"` + HTTPS HTTPSConfig `json:"https"` + Apps AppsConfig `json:"apps"` +} + +type HTTPConfig struct { + Address string `json:"address"` +} + +type HTTPSConfig struct { + Address string `json:"address"` + Cert []byte `json:"cert"` + Key []byte `json:"key"` + SelfSigned SelfSignedCertConfig `json:"selfSigned"` +} + +type SelfSignedCertConfig struct { + Enabled bool `json:"enabled"` + NetworkFingerprint string `json:"networkFingerprint"` +} + +type AppsConfig struct { + Enabled bool `json:"enabled"` + DefaultApp string `json:"defaultApp"` +} + +type TransformFunc func(ctx context.Context, conf *Config) error + +func DefaultConfigFile(ctx context.Context) string { + configDir, err := os.UserConfigDir() + if err != nil { + logger.Error(ctx, "could not get user config dir", logger.CapturedE(errors.WithStack(err))) + configDir = "" + } + + if configDir != "" { + configDir = filepath.Join(configDir, "arcast-player") + } + + return filepath.Join(configDir, "config.json") +} + +func LoadOrCreate(ctx context.Context, filename string, conf *Config, funcs ...TransformFunc) error { + data, err := os.ReadFile(filename) + if err != nil && !os.IsNotExist(err) { + return errors.WithStack(err) + } + + if data != nil { + if err := json.Unmarshal(data, conf); err != nil { + return errors.WithStack(err) + } + } + + for _, fn := range funcs { + if err := fn(ctx, conf); err != nil { + return errors.WithStack(err) + } + } + + data, err = json.MarshalIndent(conf, "", " ") + if err != nil { + return errors.WithStack(err) + } + + dirname := filepath.Dir(filename) + + if _, err := os.Stat(dirname); os.IsNotExist(err) { + if err := os.MkdirAll(dirname, 0777); err != nil { + return errors.WithStack(err) + } + } + + if err := os.WriteFile(filename, data, 0640); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func DefaultConfig() *Config { + return &Config{ + InstanceID: server.NewRandomInstanceID(), + HTTP: HTTPConfig{ + Address: ":45555", + }, + HTTPS: HTTPSConfig{ + Address: ":45556", + SelfSigned: SelfSignedCertConfig{ + Enabled: true, + NetworkFingerprint: "", + }, + }, + Apps: AppsConfig{ + Enabled: true, + DefaultApp: "home", + }, + } +} diff --git a/pkg/config/default_transforms.go b/pkg/config/default_transforms.go new file mode 100644 index 0000000..f36a0e6 --- /dev/null +++ b/pkg/config/default_transforms.go @@ -0,0 +1,6 @@ +package config + +var DefaultTransforms = []TransformFunc{ + GenerateSelfSignedCert, + RenewExpiredSelfSignedCert, +} diff --git a/pkg/config/selfsigned.go b/pkg/config/selfsigned.go new file mode 100644 index 0000000..42ffa98 --- /dev/null +++ b/pkg/config/selfsigned.go @@ -0,0 +1,100 @@ +package config + +import ( + "context" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "fmt" + "slices" + "time" + + "forge.cadoles.com/arcad/arcast/pkg/network" + "forge.cadoles.com/arcad/arcast/pkg/selfsigned" + "github.com/pkg/errors" + "gitlab.com/wpetit/goweb/logger" +) + +func GenerateSelfSignedCert(ctx context.Context, conf *Config) error { + if !conf.HTTPS.SelfSigned.Enabled { + return nil + } + + if conf.HTTPS.Cert != nil && conf.HTTPS.Key != nil { + return nil + } + + rawCert, rawKey, err := selfsigned.NewLANKeyPair() + if err != nil { + return errors.Wrap(err, "could not generate self signed x509 key pair") + } + + conf.HTTPS.Cert = rawCert + conf.HTTPS.Key = rawKey + + networkFingerprint, err := getNetworkFingerprint() + if err != nil { + return errors.Wrap(err, "could not retrieve network fingerprint") + } + + conf.HTTPS.SelfSigned.NetworkFingerprint = networkFingerprint + + return nil +} + +func RenewExpiredSelfSignedCert(ctx context.Context, conf *Config) error { + if !conf.HTTPS.SelfSigned.Enabled { + return nil + } + + if conf.HTTPS.Cert == nil || conf.HTTPS.Key == nil { + if err := GenerateSelfSignedCert(ctx, conf); err != nil { + return errors.WithStack(err) + } + } + + cert, err := tls.X509KeyPair(conf.HTTPS.Cert, conf.HTTPS.Key) + if err != nil { + return errors.Wrap(err, "could not parse x509 cert/key pair") + } + + leaf, err := x509.ParseCertificate(cert.Certificate[0]) + if err != nil { + logger.Error(ctx, "could not parse x509 certificate, regenerating one", logger.CapturedE(errors.WithStack(err))) + + if err := GenerateSelfSignedCert(ctx, conf); err != nil { + return errors.WithStack(err) + } + } + + // Check that self-signed certificate is still valid + if time.Now().Before(leaf.NotAfter) { + return nil + } + + logger.Warn(ctx, "self-signed certificate has expired, regenerating one", logger.CapturedE(errors.WithStack(err))) + + if err := GenerateSelfSignedCert(ctx, conf); err != nil { + return errors.WithStack(err) + } + + return nil +} + +func getNetworkFingerprint() (string, error) { + ips, err := network.GetLANIPv4Addrs() + if err != nil { + return "", errors.WithStack(err) + } + + slices.Sort(ips) + + hash := sha256.New() + for _, ip := range ips { + if _, err := hash.Write([]byte(ip)); err != nil { + return "", errors.WithStack(err) + } + } + + return fmt.Sprintf("%x", hash.Sum(nil)), nil +} diff --git a/pkg/selfsigned/cert.go b/pkg/selfsigned/cert.go index 4789f19..12056b7 100644 --- a/pkg/selfsigned/cert.go +++ b/pkg/selfsigned/cert.go @@ -19,15 +19,24 @@ import ( "github.com/pkg/errors" ) -func NewLANCert() (*tls.Certificate, error) { +func NewLANKeyPair() ([]byte, []byte, error) { hosts, err := network.GetLANIPv4Addrs() if err != nil { - return nil, errors.WithStack(err) + return nil, nil, errors.WithStack(err) } hosts = append(hosts, "127.0.0.1") rawCert, rawKey, err := NewCertKeyPair(hosts...) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + return rawCert, rawKey, nil +} + +func NewLANCert() (*tls.Certificate, error) { + rawCert, rawKey, err := NewLANKeyPair() if err != nil { return nil, errors.WithStack(err) } diff --git a/pkg/server/broadcast.go b/pkg/server/broadcast.go index ffcf51e..cfe1044 100644 --- a/pkg/server/broadcast.go +++ b/pkg/server/broadcast.go @@ -41,7 +41,7 @@ func (s *Server) handleBroadcast(w http.ResponseWriter, r *http.Request) { for { messageType, message, err := c.ReadMessage() - if err != nil { + if err != nil && !websocket.IsCloseError(err, 1001) { logger.Error(ctx, "could not read message", logger.E(errors.WithStack(err))) break } diff --git a/pkg/server/templates/idle.html.gotmpl b/pkg/server/templates/idle.html.gotmpl index bc587cc..e1efd7b 100644 --- a/pkg/server/templates/idle.html.gotmpl +++ b/pkg/server/templates/idle.html.gotmpl @@ -3,7 +3,7 @@ - Arcast - Idle + Arcast - Ready to cast !