blob: 1d3ace5f59126df3ab61580a42823a9fa06eacde [file] [log] [blame]
// Copyright 2016 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.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"time"
"v.io/v23"
"v.io/v23/context"
"v.io/v23/namespace"
"v.io/v23/naming"
"v.io/v23/rpc"
"v.io/v23/vdl"
"v.io/v23/vdlroot/signature"
_ "v.io/x/ref/runtime/factories/generic"
)
const (
RPC_TIMEOUT = 15 // 15 seconds per RPC
// TODO(alexfandrianto): Make this configurable here and on the JS end.
// https://github.com/vanadium/issues/issues/1268
SERVER_ADDRESS = "localhost:9002"
WEB_SERVER_ADDRESS = "localhost:9001"
HTML_DIR = "public"
)
type NamespaceBrowser struct {
// The base Vanadium context. Used for all RPCs.
ctx *context.T
namespace namespace.T
client rpc.Client
}
// NamespaceBrowser factory
func NewNamespaceBrowser(ctx *context.T) *NamespaceBrowser {
return &NamespaceBrowser{
ctx: ctx,
namespace: v23.GetNamespace(ctx),
client: v23.GetClient(ctx),
}
}
func (b *NamespaceBrowser) timed() *context.T {
ctx, _ := context.WithTimeout(b.ctx, RPC_TIMEOUT*time.Second)
return ctx
}
func writeAndFlush(rw http.ResponseWriter, data interface{}) {
// Make sure that the writer supports flushing.
flusher, ok := rw.(http.Flusher)
if !ok {
http.Error(rw, "The HTTP response writer cannot flush, so we cannot stream results!", http.StatusInternalServerError)
return
}
// Write data and flush
encoded, err := json.Marshal(data)
if err != nil {
log.Printf("Failed to encode data: %v, %v", data, err)
}
fmt.Fprintf(rw, "data: %s\n\n", string(encoded))
flusher.Flush()
}
func extractJsonString(params string) (s string, err error) {
err = json.Unmarshal([]byte(params), &s)
return
}
func extractJsonMap(params string) (m map[string]interface{}, err error) {
err = json.Unmarshal([]byte(params), &m)
return
}
/* ServeHTTP must handle EventSource requests from the browser.
* The requests have a "request" and "params" portion.
* The format is as follows:
*
* accountName: <no parameters> => { accountName: <string>, err: <err> }
* glob: string pattern => a stream of responses
* { globRes: <glob res>, globErr: <glob err>, globEnd: <bool>, err: <err> }
* permissions: string name => { permissions: <permissions>, err: <err> }
* deleteMountPoint: string name => { err: <err string> }
* resolveToMounttable: string name => { addresses: []<string>, err: <err> }
* objectAddresses: string name => { addresses: []<string>, err: <err> }
* remoteBlessings: string name => { blessings: []<string>, err: <err> }
* signature: string name => { signature: <signature>, err: <err> }
* makeRPC: { name: <string>, methodName: <string>, args: []<string>,
* numOutArgs: <int> } =>
* { response: <undefined, output, OR []outputs>, err: <err> }
*/
func (b *NamespaceBrowser) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// Set the headers related to event streaming.
rw.Header().Set("Content-Type", "text/event-stream")
rw.Header().Set("Cache-Control", "no-cache")
rw.Header().Set("Connection", "keep-alive")
rw.Header().Set("Access-Control-Allow-Origin", "*")
request, err := url.QueryUnescape(req.FormValue("request"))
if err != nil {
fmt.Println(err)
return
}
params, err := url.QueryUnescape(req.FormValue("params"))
if err != nil {
fmt.Println(err)
return
}
fmt.Println("request", request, "params", params)
// The response depends on the request type.
switch request {
case "accountName":
// Obtain the default blessing and return that.
blessing, _ := v23.GetPrincipal(b.ctx).BlessingStore().Default()
writeAndFlush(rw, accountNameReturn{AccountName: blessing.String()})
case "glob":
pattern, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Obtain the glob stream.
globCh, err := b.namespace.Glob(b.timed(), pattern)
if err != nil {
writeAndFlush(rw, globReturn{Err: fmt.Sprintf("%v", err)})
return
}
// Afterwards, go through the stream and forward results and errors.
writeAndFlush(rw, globReturn{})
for entry := range globCh { // These GlobReply could be a reply or an error.
switch v := entry.(type) {
case *naming.GlobReplyEntry:
writeAndFlush(rw, globReturn{GlobRes: &v.Value})
case *naming.GlobReplyError:
writeAndFlush(rw, globReturn{GlobErr: &v.Value})
}
}
writeAndFlush(rw, globReturn{GlobEnd: true})
case "deleteMountPoint":
name, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Delete the chosen name from the namespace.
err = b.namespace.Delete(b.timed(), name, true)
if err != nil {
writeAndFlush(rw, deleteReturn{Err: fmt.Sprintf("%v", err)})
return
}
writeAndFlush(rw, deleteReturn{})
case "resolveToMounttable":
name, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Use the MountEntry for this name to find its server addresses.
entry, err := b.namespace.ResolveToMountTable(b.timed(), name)
if err != nil {
writeAndFlush(rw, addressesReturn{Err: fmt.Sprintf("%v", err)})
return
}
addrs := []string{}
for _, server := range entry.Servers {
addrs = append(addrs, server.Server)
}
writeAndFlush(rw, addressesReturn{Addresses: addrs})
case "objectAddresses":
name, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Use the MountEntry for this name to find its object addresses.
entry, err := b.namespace.Resolve(b.timed(), name)
if err != nil {
writeAndFlush(rw, addressesReturn{Err: fmt.Sprintf("%v", err)})
return
}
addrs := []string{}
for _, server := range entry.Servers {
addrs = append(addrs, server.Server)
}
writeAndFlush(rw, addressesReturn{Addresses: addrs})
case "permissions":
name, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Obtain the mount table permissions at this name.
perms, _, err := b.namespace.GetPermissions(b.timed(), name)
if err != nil {
writeAndFlush(rw, permissionsReturn{Err: fmt.Sprintf("%v", err)})
return
}
writeAndFlush(rw, permissionsReturn{Permissions: perms})
case "remoteBlessings":
name, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Obtain the remote blessings for the server running at this name.
clientCall, err := b.client.StartCall(b.timed(), name, rpc.ReservedMethodSignature, nil)
defer clientCall.Finish()
if err != nil {
writeAndFlush(rw, blessingsReturn{Err: fmt.Sprintf("%v", err)})
return
}
rbs, _ := clientCall.RemoteBlessings()
writeAndFlush(rw, blessingsReturn{Blessings: rbs})
case "signature":
name, err := extractJsonString(params)
if err != nil {
fmt.Println(err)
return
}
// Obtain the signature(s) of the server running at this name.
var sig []signature.Interface
err = b.client.Call(b.timed(), name, rpc.ReservedSignature, nil, []interface{}{&sig})
if err != nil {
writeAndFlush(rw, signatureReturn{Err: fmt.Sprintf("%v", err)})
return
}
convertedSig := convertSignature(sig)
writeAndFlush(rw, signatureReturn{Signature: convertedSig})
case "makeRPC":
data, err := extractJsonMap(params)
if err != nil {
fmt.Println(err)
return
}
name := data["name"].(string)
method := data["methodName"].(string)
params := data["args"].([]interface{})
fmt.Printf("Make RPC: %s %s %v\n", name, method, params)
numOutArgs := int(data["numOutArgs"].(float64))
// Prepare outargs as *vdl.Value
outargs := make([]*vdl.Value, numOutArgs)
outptrs := make([]interface{}, numOutArgs)
for i := range outargs {
outptrs[i] = &outargs[i]
}
// Make the call to name's method with the given params.
err = b.client.Call(b.timed(), name, method, params, outptrs)
if err != nil {
writeAndFlush(rw, makeRPCReturn{Err: fmt.Sprintf("%v", err)})
return
}
// Convert the *vdl.Value outputs to readable strings
resStrings := []string{}
for _, outarg := range outargs {
resStrings = append(resStrings, outarg.String())
}
writeAndFlush(rw, makeRPCReturn{Response: resStrings})
default:
writeAndFlush(rw, "Please connect from the namespace browser.")
}
// I think this keeps the connection open until the other side closes it?
notify := rw.(http.CloseNotifier).CloseNotify()
<-notify
}
func main() {
ctx, shutdown := v23.Init()
defer shutdown()
fmt.Printf("\nPlease Visit http://%s to see Namespace Browser.\n\n", WEB_SERVER_ADDRESS)
go func() {
http.ListenAndServe(WEB_SERVER_ADDRESS, http.FileServer(http.Dir("public")))
log.Fatal("Web server error: ", http.ListenAndServe(WEB_SERVER_ADDRESS, http.FileServer(http.Dir(HTML_DIR))))
}()
browser := NewNamespaceBrowser(ctx)
log.Fatal("HTTP server error: ", http.ListenAndServe(SERVER_ADDRESS, browser))
}