| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Daemon browsprd implements the wspr web socket proxy as a Native Client |
| // executable, to be run as a Chrome extension. |
| package main |
| |
| import ( |
| "bytes" |
| "crypto/ecdsa" |
| "encoding/base64" |
| "fmt" |
| "runtime" |
| "runtime/ppapi" |
| |
| "v.io/v23" |
| "v.io/v23/logging" |
| "v.io/v23/security" |
| "v.io/v23/vdl" |
| "v.io/x/lib/vlog" |
| "v.io/x/ref/internal/logger" |
| vsecurity "v.io/x/ref/lib/security" |
| _ "v.io/x/ref/runtime/factories/chrome" |
| "v.io/x/ref/runtime/internal/lib/xwebsocket" |
| "v.io/x/ref/services/wspr/internal/app" |
| "v.io/x/ref/services/wspr/internal/browspr" |
| "v.io/x/ref/services/wspr/internal/channel/channel_nacl" |
| "v.io/x/ref/services/wspr/internal/principal" |
| "v.io/x/ref/services/wspr/internal/rpc/server" |
| ) |
| |
| const ( |
| browsprDir = "/browspr/data" |
| browsprKeyFile = browsprDir + "/privateKey.pem." |
| blessingRootsData = browsprDir + "/blessingroots.data" |
| blessingRootsSig = browsprDir + "/blessingroots.sig" |
| blessingStoreData = browsprDir + "/blessingstore.data" |
| blessingStoreSig = browsprDir + "/blessingstore.sig" |
| ) |
| |
| func main() { |
| security.OverrideCaveatValidation(server.CaveatValidation) |
| ppapi.Init(newBrowsprInstance) |
| } |
| |
| // browsprInstance represents an instance of a PPAPI client and receives |
| // callbacks from PPAPI to handle events. |
| type browsprInstance struct { |
| ppapi.Instance |
| fs ppapi.FileSystem |
| browspr *browspr.Browspr |
| channel *channel_nacl.Channel |
| logger logging.Logger |
| } |
| |
| var _ ppapi.InstanceHandlers = (*browsprInstance)(nil) |
| |
| func newBrowsprInstance(inst ppapi.Instance) ppapi.InstanceHandlers { |
| runtime.GOMAXPROCS(4) |
| browsprInst := &browsprInstance{Instance: inst, logger: logger.Global()} |
| browsprInst.initFileSystem() |
| |
| // Give the websocket interface the ppapi instance. |
| xwebsocket.PpapiInstance = inst |
| |
| // Set up the channel and register start rpc handler. |
| browsprInst.channel = channel_nacl.NewChannel(inst) |
| browsprInst.channel.RegisterRequestHandler("start", browsprInst.HandleStartMessage) |
| |
| return browsprInst |
| } |
| |
| func (inst *browsprInstance) initFileSystem() { |
| var err error |
| // Create a filesystem. |
| if inst.fs, err = inst.CreateFileSystem(ppapi.PP_FILESYSTEMTYPE_LOCALPERSISTENT); err != nil { |
| panic(err.Error()) |
| } |
| if ty := inst.fs.Type(); ty != ppapi.PP_FILESYSTEMTYPE_LOCALPERSISTENT { |
| panic(fmt.Errorf("unexpected filesystem type: %d", ty)) |
| } |
| // Open filesystem with expected size of 2K |
| if err = inst.fs.OpenFS(1 << 11); err != nil { |
| panic(fmt.Errorf("failed to open filesystem:%s", err)) |
| } |
| // Create directory to store browspr keys |
| if err = inst.fs.MkdirAll(browsprDir); err != nil { |
| panic(fmt.Errorf("failed to create directory:%s", err)) |
| } |
| } |
| |
| func (inst *browsprInstance) loadKeyFromStorage(browsprKeyFile string) (*ecdsa.PrivateKey, error) { |
| inst.logger.VI(1).Infof("Attempting to read key from file %v", browsprKeyFile) |
| |
| rFile, err := inst.fs.Open(browsprKeyFile) |
| if err != nil { |
| inst.logger.VI(1).Infof("Key not found in file %v", browsprKeyFile) |
| return nil, err |
| } |
| |
| inst.logger.VI(1).Infof("Attempting to load cached browspr ecdsaPrivateKey in file %v", browsprKeyFile) |
| defer rFile.Release() |
| key, err := vsecurity.LoadPEMKey(rFile, nil) |
| if err != nil { |
| return nil, fmt.Errorf("failed to load browspr key:%s", err) |
| } |
| if ecdsaKey, ok := key.(*ecdsa.PrivateKey); !ok { |
| return nil, fmt.Errorf("got key of type %T, want *ecdsa.PrivateKey", key) |
| } else { |
| return ecdsaKey, nil |
| } |
| } |
| |
| // Loads a saved key if one exists, otherwise creates a new one and persists it. |
| func (inst *browsprInstance) initKey() (*ecdsa.PrivateKey, error) { |
| if ecdsaKey, err := inst.loadKeyFromStorage(browsprKeyFile); err == nil { |
| return ecdsaKey, nil |
| } else { |
| inst.logger.VI(1).Infof("inst.loadKeyFromStorage(%v) failed: %v", browsprKeyFile, err) |
| } |
| |
| inst.logger.VI(1).Infof("Generating new browspr ecdsaPrivateKey") |
| |
| // Generate new keys and store them. |
| var ecdsaKey *ecdsa.PrivateKey |
| var err error |
| if _, ecdsaKey, err = vsecurity.NewPrincipalKey(); err != nil { |
| return nil, fmt.Errorf("failed to generate security key:%s", err) |
| } |
| // Persist the keys in a local file. |
| wFile, err := inst.fs.Create(browsprKeyFile) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create file to persist browspr keys:%s", err) |
| } |
| defer wFile.Release() |
| var b bytes.Buffer |
| if err = vsecurity.SavePEMKey(&b, ecdsaKey, nil); err != nil { |
| return nil, fmt.Errorf("failed to save browspr key:%s", err) |
| } |
| if n, err := wFile.Write(b.Bytes()); n != b.Len() || err != nil { |
| return nil, fmt.Errorf("failed to write browspr key:%s", err) |
| } |
| return ecdsaKey, nil |
| } |
| |
| func (inst *browsprInstance) newPrincipal(ecdsaKey *ecdsa.PrivateKey, blessingRootsData, blessingRootsSig, blessingStoreData, blessingStoreSig string) (security.Principal, error) { |
| roots, err := principal.NewFileSerializer(blessingRootsData, blessingRootsSig, inst.fs) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create blessing roots serializer:%s", err) |
| } |
| store, err := principal.NewFileSerializer(blessingStoreData, blessingStoreSig, inst.fs) |
| if err != nil { |
| return nil, fmt.Errorf("failed to create blessing store serializer:%s", err) |
| } |
| state := &vsecurity.PrincipalStateSerializer{ |
| BlessingRoots: roots, |
| BlessingStore: store, |
| } |
| return vsecurity.NewPrincipalFromSigner(security.NewInMemoryECDSASigner(ecdsaKey), state) |
| } |
| |
| func (inst *browsprInstance) newPersistentPrincipal(peerNames []string) (security.Principal, error) { |
| ecdsaKey, err := inst.initKey() |
| if err != nil { |
| return nil, fmt.Errorf("failed to initialize ecdsa key:%s", err) |
| } |
| |
| principal, err := inst.newPrincipal(ecdsaKey, blessingRootsData, blessingRootsSig, blessingStoreData, blessingStoreSig) |
| if err != nil { |
| inst.logger.VI(1).Infof("inst.newPrincipal(%v, %v, %v, %v, %v) failed: %v", ecdsaKey, blessingRootsData, blessingRootsSig, blessingStoreData, blessingStoreSig) |
| |
| // Delete the files and try again. |
| if err := inst.cleanupBlessings(); err != nil { |
| return nil, err |
| } |
| principal, err = inst.newPrincipal(ecdsaKey, blessingRootsData, blessingRootsSig, blessingStoreData, blessingStoreSig) |
| } |
| return principal, err |
| } |
| |
| // cleanupBlessings removes the persisted blessing roots and store. |
| func (inst *browsprInstance) cleanupBlessings() error { |
| inst.logger.VI(1).Info("Cleaning up blessing roots and store.") |
| for _, file := range []string{blessingRootsData, blessingRootsSig, blessingStoreData, blessingStoreSig} { |
| if err := inst.fs.Remove(file); err != nil { |
| return fmt.Errorf("fs.Remove(%s) failed: %v", file, err) |
| } |
| } |
| return nil |
| } |
| |
| // Base64-decode and unmarshal a public key. |
| func decodeAndUnmarshalPublicKey(k string) (security.PublicKey, error) { |
| decodedK, err := base64.URLEncoding.DecodeString(k) |
| if err != nil { |
| return nil, err |
| } |
| return security.UnmarshalPublicKey(decodedK) |
| } |
| |
| func (inst *browsprInstance) HandleStartMessage(val *vdl.Value) (*vdl.Value, error) { |
| inst.logger.VI(1).Info("Starting Browspr") |
| var msg browspr.StartMessage |
| if err := vdl.Convert(&msg, val); err != nil { |
| return nil, fmt.Errorf("HandleStartMessage did not receive StartMessage, received: %v, %v", val, err) |
| } |
| |
| // The extension starts the nacl plugin with CleanupBlessings=true when it |
| // detects it has been upgraded. This prevents the plugin from breaking if |
| // non-backwards-compatible changes have been made to the format of |
| // blessings or roots. |
| if msg.CleanupBlessings { |
| if err := inst.cleanupBlessings(); err != nil { |
| return nil, err |
| } |
| } |
| |
| p, err := inst.newPersistentPrincipal(msg.IdentitydBlessingRoot.Names) |
| if err != nil { |
| return nil, err |
| } |
| |
| blessingName := "browspr-default-blessing" |
| blessing, err := p.BlessSelf(blessingName) |
| if err != nil { |
| return nil, fmt.Errorf("p.BlessSelf(%v) failed: %v", blessingName, err) |
| } |
| |
| // If msg.IdentitydBlessingRoot has a public key and names, then add |
| // the public key to our set of trusted roots, and limit our blessing |
| // to only talk to those names. |
| if msg.IdentitydBlessingRoot.PublicKey != "" { |
| if len(msg.IdentitydBlessingRoot.Names) == 0 { |
| return nil, fmt.Errorf("invalid IdentitydBlessingRoot: Names is empty") |
| } |
| |
| inst.logger.VI(1).Infof("Using blessing roots for identity with key %v and names %v", msg.IdentitydBlessingRoot.PublicKey, msg.IdentitydBlessingRoot.Names) |
| keybytes, err := base64.URLEncoding.DecodeString(msg.IdentitydBlessingRoot.PublicKey) |
| if err != nil { |
| inst.logger.Fatalf("failed to decode public key (%v): %v", msg.IdentitydBlessingRoot.PublicKey, err) |
| } |
| |
| for _, name := range msg.IdentitydBlessingRoot.Names { |
| pattern := security.BlessingPattern(name) |
| |
| // Trust the identity servers blessing root. |
| p.Roots().Add(keybytes, pattern) |
| |
| // Use our blessing to only talk to the identity server. |
| if _, err := p.BlessingStore().Set(blessing, pattern); err != nil { |
| return nil, fmt.Errorf("p.BlessingStore().Set(%v, %v) failed: %v", blessing, pattern, err) |
| } |
| } |
| } else { |
| inst.logger.VI(1).Infof("IdentitydBlessingRoot.PublicKey is empty. Will allow browspr blessing to be shareable with all principals.") |
| // Set our blessing as shareable with all peers. |
| if _, err := p.BlessingStore().Set(blessing, security.AllPrincipals); err != nil { |
| return nil, fmt.Errorf("p.BlessingStore().Set(%v, %v) failed: %v", blessing, security.AllPrincipals, err) |
| } |
| } |
| |
| // Initialize the runtime. |
| // TODO(suharshs,mattr): Should we worried about not shutting down here? |
| ctx, _ := v23.Init() |
| |
| ctx, err = v23.WithPrincipal(ctx, p) |
| if err != nil { |
| return nil, err |
| } |
| |
| // TODO(cnicolaou): provide a means of configuring logging that |
| // doesn't depend on vlog - e.g. ConfigureFromArgs(args []string) to |
| // pair with ConfigureFromFlags(). See v.io/i/556 |
| // Configure logger with level and module from start message. |
| inst.logger.VI(1).Infof("Configuring vlog with v=%v, modulesSpec=%v", msg.LogLevel, msg.LogModule) |
| moduleSpec := vlog.ModuleSpec{} |
| moduleSpec.Set(msg.LogModule) |
| if err := vlog.Log.Configure(vlog.OverridePriorConfiguration(true), vlog.Level(msg.LogLevel), moduleSpec); err != nil { |
| return nil, err |
| } |
| |
| // TODO(ataly, bprosnitz, caprita): The runtime MUST be cleaned up |
| // after use. Figure out the appropriate place to add the Cleanup call. |
| |
| v23.GetNamespace(ctx).SetRoots(msg.NamespaceRoot) |
| |
| listenSpec := v23.GetListenSpec(ctx) |
| listenSpec.Proxy = msg.Proxy |
| |
| principalSerializer, err := principal.NewFileSerializer(browsprDir+"/principalData", browsprDir+"/principalSignature", inst.fs) |
| if err != nil { |
| return nil, fmt.Errorf("principal.NewFileSerializer() failed: %v", err) |
| } |
| |
| inst.logger.VI(1).Infof("Starting browspr with config: proxy=%q mounttable=%q identityd=%q identitydBlessingRoot=%q ", msg.Proxy, msg.NamespaceRoot, msg.Identityd, msg.IdentitydBlessingRoot) |
| inst.browspr = browspr.NewBrowspr(ctx, |
| inst.BrowsprOutgoingPostMessage, |
| &listenSpec, |
| msg.Identityd, |
| []string{msg.NamespaceRoot}, |
| principalSerializer) |
| |
| // Add the rpc handlers that depend on inst.browspr. |
| inst.channel.RegisterRequestHandler("auth:create-account", inst.browspr.HandleAuthCreateAccountRpc) |
| inst.channel.RegisterRequestHandler("auth:associate-account", inst.browspr.HandleAuthAssociateAccountRpc) |
| inst.channel.RegisterRequestHandler("auth:get-accounts", inst.browspr.HandleAuthGetAccountsRpc) |
| inst.channel.RegisterRequestHandler("auth:origin-has-account", inst.browspr.HandleAuthOriginHasAccountRpc) |
| inst.channel.RegisterRequestHandler("create-instance", inst.browspr.HandleCreateInstanceRpc) |
| inst.channel.RegisterRequestHandler("cleanup", inst.browspr.HandleCleanupRpc) |
| |
| return nil, nil |
| } |
| |
| func (inst *browsprInstance) BrowsprOutgoingPostMessage(instanceId int32, ty string, message string) { |
| if message == "" { |
| // TODO(nlacasse,bprosnitz): VarFromString crashes if the |
| // string is empty, so we must use a placeholder. |
| message = "." |
| } |
| dict := ppapi.NewDictVar() |
| instVar := ppapi.VarFromInt(instanceId) |
| bodyVar := ppapi.VarFromString(message) |
| tyVar := ppapi.VarFromString(ty) |
| dict.DictionarySet("instanceId", instVar) |
| dict.DictionarySet("type", tyVar) |
| dict.DictionarySet("body", bodyVar) |
| inst.PostMessage(dict) |
| instVar.Release() |
| bodyVar.Release() |
| tyVar.Release() |
| dict.Release() |
| } |
| |
| // HandleBrowsprMessage handles one-way messages of the type "browsprMsg" by |
| // sending them to browspr's handler. |
| func (inst *browsprInstance) HandleBrowsprMessage(instanceId int32, origin string, varMsg ppapi.Var) error { |
| msg, err := varToMessage(varMsg) |
| if err != nil { |
| return fmt.Errorf("Invalid message: %v", err) |
| } |
| |
| inst.logger.VI(1).Infof("Calling browspr's HandleMessage: instanceId %d origin %s message %s", instanceId, origin, msg) |
| if err := inst.browspr.HandleMessage(instanceId, origin, msg); err != nil { |
| return fmt.Errorf("Error while handling message in browspr: %v", err) |
| } |
| return nil |
| } |
| |
| // HandleIntentionalPanic intentionally triggers a panic. This is used in tests |
| // of the extension's crash handling behavior. |
| // TODO(bprosnitz) We probably should conditionally compile this in via build |
| // tags so we don't hit it in production code. |
| func (inst *browsprInstance) HandleIntentionalPanic(instanceId int32, origin string, message ppapi.Var) error { |
| // NOTE(nlacasse): Calling panic directly (not inside a goroutine) |
| // sometimes blocks during the panic, causing the plugin to not emit a |
| // "crash" event. I'm not sure why this happens, but it breaks the |
| // test-nacl-plugin-crash test. Calling panic from within a goroutine seems |
| // to reliably panic "all the way" and emit a "crash" event. |
| go panic("Crashing intentionally") |
| return nil |
| } |
| |
| // HandleBrowsprRpc handles two-way rpc messages of the type "browsprRpc" |
| // sending them to the channel's handler. |
| func (inst *browsprInstance) HandleBrowsprRpc(instanceId int32, origin string, message ppapi.Var) error { |
| inst.logger.VI(1).Infof("Got to HandleBrowsprRpc: instanceId: %d origin %s", instanceId, origin) |
| inst.channel.HandleMessage(message) |
| return nil |
| } |
| |
| // handleGoError handles error returned by go code. |
| func (inst *browsprInstance) handleGoError(err error) { |
| inst.logger.VI(2).Info(err) |
| inst.LogString(ppapi.PP_LOGLEVEL_ERROR, fmt.Sprintf("Error in go code: %v", err.Error())) |
| inst.logger.Error(err) |
| } |
| |
| // HandleMessage receives messages from Javascript and uses them to perform actions. |
| // A message is of the form {"type": "typeName", "body": { stuff here }}, |
| // where the body is passed to the message handler. |
| func (inst *browsprInstance) HandleMessage(message ppapi.Var) { |
| inst.logger.VI(2).Infof("Got to HandleMessage") |
| instanceId, err := message.LookupIntValuedKey("instanceId") |
| if err != nil { |
| inst.handleGoError(err) |
| return |
| } |
| origin, err := message.LookupStringValuedKey("origin") |
| if err != nil { |
| inst.handleGoError(err) |
| return |
| } |
| ty, err := message.LookupStringValuedKey("type") |
| if err != nil { |
| inst.handleGoError(err) |
| return |
| } |
| var messageHandlers = map[string]func(int32, string, ppapi.Var) error{ |
| "browsprMsg": inst.HandleBrowsprMessage, |
| "browsprRpc": inst.HandleBrowsprRpc, |
| "intentionallyPanic": inst.HandleIntentionalPanic, |
| } |
| h, ok := messageHandlers[ty] |
| if !ok { |
| inst.handleGoError(fmt.Errorf("No handler found for message type: %q", ty)) |
| return |
| } |
| body, err := message.LookupKey("body") |
| if err != nil { |
| body = ppapi.VarUndefined |
| } |
| err = h(int32(instanceId), origin, body) |
| body.Release() |
| if err != nil { |
| inst.handleGoError(err) |
| } |
| } |
| |
| func (inst browsprInstance) DidCreate(args map[string]string) bool { |
| inst.logger.VI(2).Infof("Got to DidCreate") |
| return true |
| } |
| |
| func (inst *browsprInstance) DidDestroy() { |
| inst.logger.VI(2).Infof("Got to DidDestroy()") |
| } |
| |
| func (inst *browsprInstance) DidChangeView(view ppapi.View) { |
| inst.logger.VI(2).Infof("Got to DidChangeView(%v)", view) |
| } |
| |
| func (inst *browsprInstance) DidChangeFocus(has_focus bool) { |
| inst.logger.VI(2).Infof("Got to DidChangeFocus(%v)", has_focus) |
| } |
| |
| func (inst *browsprInstance) HandleDocumentLoad(url_loader ppapi.Resource) bool { |
| inst.logger.VI(2).Infof("Got to HandleDocumentLoad(%v)", url_loader) |
| return true |
| } |
| |
| func (inst *browsprInstance) HandleInputEvent(event ppapi.InputEvent) bool { |
| inst.logger.VI(2).Infof("Got to HandleInputEvent(%v)", event) |
| return true |
| } |
| |
| func (inst *browsprInstance) Graphics3DContextLost() { |
| inst.logger.VI(2).Infof("Got to Graphics3DContextLost()") |
| } |
| |
| func (inst *browsprInstance) MouseLockLost() { |
| inst.logger.VI(2).Infof("Got to MouseLockLost()") |
| } |
| |
| func varToMessage(v ppapi.Var) (app.Message, error) { |
| var msg app.Message |
| id, err := v.LookupIntValuedKey("id") |
| if err != nil { |
| return msg, err |
| } |
| ty, err := v.LookupIntValuedKey("type") |
| if err != nil { |
| return msg, err |
| } |
| data, err := v.LookupStringValuedKey("data") |
| if err != nil { |
| // OK for message to have empty data. |
| data = "" |
| } |
| |
| msg.Id = int32(id) |
| msg.Data = data |
| msg.Type = app.MessageType(ty) |
| return msg, nil |
| } |