diff --git a/cmd/debora/auth.go b/cmd/debora/auth.go new file mode 100644 index 00000000..83a81121 --- /dev/null +++ b/cmd/debora/auth.go @@ -0,0 +1,58 @@ +package main + +import acm "github.com/tendermint/tendermint/account" + +type Validator struct { + VotingPower uint64 + PubKey acm.PubKey +} + +func validate(signBytes []byte, validators []Validator, signatures []acm.Signature) bool { + var signedPower uint64 + var totalPower uint64 + for i, val := range validators { + if val.PubKey.VerifyBytes(signBytes, signatures[i]) { + signedPower += val.VotingPower + totalPower += val.VotingPower + } else { + totalPower += val.VotingPower + } + } + return signedPower > totalPower*2/3 +} + +/* +NOTE: Not used, just here in case we want it later. +func ValidateHandler(handler http.Handler, validators []Validator) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sigStrs := r.Header[http.CanonicalHeaderKey("debora-signatures")] + log.Debug("Woot", "sigstrs", sigStrs, "len", len(sigStrs)) + // from https://medium.com/@xoen/golang-read-from-an-io-readwriter-without-loosing-its-content-2c6911805361 + // Read the content + var bodyBytes []byte + if r.Body != nil { + bodyBytes, _ = ioutil.ReadAll(r.Body) + } + // Restore the io.ReadCloser to its original state + r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) + // Get body string + bodyString := string(bodyBytes) + // Also read the path+"?"+query + pathQuery := fmt.Sprintf("%v?%v", r.URL.Path, r.URL.RawQuery) + // Concatenate into tuple of two strings. + tuple := struct { + Body string + Path string + }{bodyString, pathQuery} + // Get sign bytes + signBytes := binary.BinaryBytes(tuple) + // Validate the sign bytes. + //if validate(signBytes, validators, + log.Debug("Should sign", "bytes", signBytes) + // If validation fails + // XXX + // If validation passes + handler.ServeHTTP(w, r) + }) +} +*/ diff --git a/cmd/debora/main.go b/cmd/debora/main.go index 5eb4ddde..ca638eca 100644 --- a/cmd/debora/main.go +++ b/cmd/debora/main.go @@ -1,11 +1,15 @@ package main +// TODO: Nonrepudiable command log + import ( + "errors" "fmt" "io" "io/ioutil" "net/http" "os" + "reflect" "sync" acm "github.com/tendermint/tendermint/account" @@ -16,28 +20,23 @@ import ( ) var Routes = map[string]*rpc.RPCFunc{ - "RunProcess": rpc.NewRPCFunc(RunProcess, []string{"wait", "label", "execPath", "args"}), - "ListProcesses": rpc.NewRPCFunc(ListProcesses, []string{}), - "StopProcess": rpc.NewRPCFunc(StopProcess, []string{"label", "kill"}), - // NOTE: also, two special non-JSONRPC routes called - // "download" and "upload". -} - -type Validator struct { - VotingPower uint64 - PubKey acm.PubKey + "run_auth_command": rpc.NewRPCFunc(Run, []string{"auth_command"}), + // NOTE: also, two special non-JSONRPC routes called "download" and "upload" } type Options struct { Validators []Validator ListenAddress string + StartNonce uint64 } // Global instance var debora = struct { - mtx sync.Mutex - processes map[string]*pcm.Process -}{sync.Mutex{}, make(map[string]*pcm.Process)} + mtx sync.Mutex + processes map[string]*pcm.Process + validators []Validator + nonce uint64 +}{sync.Mutex{}, make(map[string]*pcm.Process), nil, 0} func main() { @@ -51,11 +50,14 @@ func main() { if err != nil { panic(Fmt("Error parsing input: %v", err)) } + debora.nonce = options.StartNonce + debora.validators = options.Validators // Debug. - fmt.Printf("Validators: %v\n", options.Validators) + fmt.Printf("Options: %v\n", options) + fmt.Printf("Debora: %v\n", debora) - // start rpc server. + // Start rpc server. mux := http.NewServeMux() mux.HandleFunc("/download", ServeFile) // TODO: mux.HandleFunc("/upload", UploadFile) @@ -68,7 +70,102 @@ func main() { } //------------------------------------------------------------------------------ -// RPC functions +// RPC main function + +func Run(authCommand AuthCommand) (interface{}, error) { + command, err := parseValidateCommand(authCommand) + if err != nil { + return nil, err + } + log.Info(Fmt("Run() received command %v", reflect.TypeOf(command))) + // Issue command + switch c := command.(type) { + case CommandRunProcess: + return RunProcess(c.Wait, c.Label, c.ExecPath, c.Args, c.Input) + case CommandStopProcess: + return StopProcess(c.Label, c.Kill) + case CommandListProcesses: + return ListProcesses() + default: + return nil, errors.New("Invalid endpoint for command") + } +} + +func parseValidateCommandStr(authCommandStr string) (Command, error) { + var err error + authCommand := binary.ReadJSON(AuthCommand{}, []byte(authCommandStr), &err).(AuthCommand) + if err != nil { + fmt.Printf("Failed to parse auth_command") + return nil, errors.New("AuthCommand parse error") + } + return parseValidateCommand(authCommand) +} + +func parseValidateCommand(authCommand AuthCommand) (Command, error) { + commandJSONStr := authCommand.CommandJSONStr + signatures := authCommand.Signatures + // Validate commandJSONStr + if !validate([]byte(commandJSONStr), debora.validators, signatures) { + fmt.Printf("Failed validation attempt") + return nil, errors.New("Validation error") + } + // Parse command + var err error + command := binary.ReadJSON(NoncedCommand{}, []byte(commandJSONStr), &err).(NoncedCommand) + if err != nil { + fmt.Printf("Failed to parse command") + return nil, errors.New("Command parse error") + } + // Prevent replays + if debora.nonce+1 != command.Nonce { + return nil, errors.New("Replay error") + } else { + debora.nonce += 1 + } + return command.Command, nil +} + +type AuthCommand struct { + CommandJSONStr string + Signatures []acm.Signature +} + +type NoncedCommand struct { + Nonce uint64 + Command +} + +type Command interface{} + +// for binary.readReflect +var _ = binary.RegisterInterface( + struct{ Command }{}, + binary.ConcreteType{CommandRunProcess{}}, + binary.ConcreteType{CommandStopProcess{}}, + binary.ConcreteType{CommandListProcesses{}}, + binary.ConcreteType{CommandServeFile{}}, +) + +const ( + typeByteRunProcess = 0x01 + typeByteStopProcess = 0x02 + typeByteListProcesses = 0x03 + typeByteServeFile = 0x04 +) + +//------------------------------------------------------------------------------ +// RPC base commands +// WARNING Not validated, do not export to routes. + +type CommandRunProcess struct { + Wait bool + Label string + ExecPath string + Args []string + Input string +} + +func (_ CommandRunProcess) TypeByte() byte { return typeByteRunProcess } type ResponseRunProcess struct { } @@ -98,24 +195,12 @@ func RunProcess(wait bool, label string, execPath string, args []string, input s //-------------------------------------- -type ResponseListProcesses struct { - Processes []*pcm.Process +type CommandStopProcess struct { + Label string + Kill bool } -func ListProcesses() (*ResponseListProcesses, error) { - var procs = []*pcm.Process{} - debora.mtx.Lock() - for _, proc := range debora.processes { - procs = append(procs, proc) - } - debora.mtx.Unlock() - - return &ResponseListProcesses{ - Processes: procs, - }, nil -} - -//-------------------------------------- +func (_ CommandStopProcess) TypeByte() byte { return typeByteStopProcess } type ResponseStopProcess struct { } @@ -133,10 +218,49 @@ func StopProcess(label string, kill bool) (*ResponseStopProcess, error) { return &ResponseStopProcess{}, err } +//-------------------------------------- + +type CommandListProcesses struct{} + +func (_ CommandListProcesses) TypeByte() byte { return typeByteListProcesses } + +type ResponseListProcesses struct { + Processes []*pcm.Process +} + +func ListProcesses() (*ResponseListProcesses, error) { + var procs = []*pcm.Process{} + debora.mtx.Lock() + for _, proc := range debora.processes { + procs = append(procs, proc) + } + debora.mtx.Unlock() + + return &ResponseListProcesses{ + Processes: procs, + }, nil +} + //------------------------------------------------------------------------------ +type CommandServeFile struct { + Path string +} + +func (_ CommandServeFile) TypeByte() byte { return typeByteServeFile } + func ServeFile(w http.ResponseWriter, req *http.Request) { - path := req.FormValue("path") + + authCommandStr := req.FormValue("auth_command") + command, err := parseValidateCommandStr(authCommandStr) + if err != nil { + http.Error(w, Fmt("Invalid command: %v", err), 400) + } + serveCommand, ok := command.(CommandServeFile) + if !ok { + http.Error(w, "Invalid command", 400) + } + path := serveCommand.Path if path == "" { http.Error(w, "Must specify path", 400) return diff --git a/node/node.go b/node/node.go index 1d46405f..f7749a80 100644 --- a/node/node.go +++ b/node/node.go @@ -152,8 +152,7 @@ func (n *Node) StartRPC() { mux := http.NewServeMux() rpc.RegisterEventsHandler(mux, n.evsw) rpc.RegisterRPCFuncs(mux, core.Routes) - handler := rpc.AuthenticateHandler(mux) - rpc.StartHTTPServer(listenAddr, handler) + rpc.StartHTTPServer(listenAddr, mux) } func (n *Node) Switch() *p2p.Switch { diff --git a/rpc/http_server.go b/rpc/http_server.go index c86e0087..14bcc9ee 100644 --- a/rpc/http_server.go +++ b/rpc/http_server.go @@ -4,7 +4,6 @@ package rpc import ( "bytes" "fmt" - "io/ioutil" "net/http" "runtime/debug" "time" @@ -38,39 +37,6 @@ func WriteRPCResponse(w http.ResponseWriter, res RPCResponse) { //----------------------------------------------------------------------------- -func AuthenticateHandler(handler http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // from https://medium.com/@xoen/golang-read-from-an-io-readwriter-without-loosing-its-content-2c6911805361 - // Read the content - var bodyBytes []byte - if r.Body != nil { - bodyBytes, _ = ioutil.ReadAll(r.Body) - } - // Restore the io.ReadCloser to its original state - r.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) - // Get body string - bodyString := string(bodyBytes) - // Also read the path+"?"+query - pathQuery := Fmt("%v?%v", r.URL.Path, r.URL.RawQuery) - // Concatenate into tuple - tuple := struct { - Body string - Path string - }{bodyString, pathQuery} - // Get sign bytes - signBytes := binary.BinaryBytes(tuple) - // Validate the sign bytes. - // XXX - log.Debug("Should sign", "bytes", signBytes) - // If validation fails - // XXX - // If validation passes - handler.ServeHTTP(w, r) - }) -} - -//----------------------------------------------------------------------------- - // Wraps an HTTP handler, adding error logging. // If the inner function panics, the outer function recovers, logs, sends an // HTTP 500 error response.