github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/overlord/hookstate/ctlcmd/ctlcmd.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2016 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 // Package ctlcmd contains the various snapctl subcommands. 21 package ctlcmd 22 23 import ( 24 "bytes" 25 "fmt" 26 "io" 27 28 "github.com/snapcore/snapd/logger" 29 "github.com/snapcore/snapd/overlord/hookstate" 30 31 "github.com/jessevdk/go-flags" 32 ) 33 34 type baseCommand struct { 35 stdout io.Writer 36 stderr io.Writer 37 c *hookstate.Context 38 } 39 40 func (c *baseCommand) setStdout(w io.Writer) { 41 c.stdout = w 42 } 43 44 func (c *baseCommand) printf(format string, a ...interface{}) { 45 if c.stdout != nil { 46 fmt.Fprintf(c.stdout, format, a...) 47 } 48 } 49 50 func (c *baseCommand) setStderr(w io.Writer) { 51 c.stderr = w 52 } 53 54 func (c *baseCommand) errorf(format string, a ...interface{}) { 55 if c.stderr != nil { 56 fmt.Fprintf(c.stderr, format, a...) 57 } 58 } 59 60 func (c *baseCommand) setContext(context *hookstate.Context) { 61 c.c = context 62 } 63 64 func (c *baseCommand) context() *hookstate.Context { 65 return c.c 66 } 67 68 type command interface { 69 setStdout(w io.Writer) 70 setStderr(w io.Writer) 71 72 setContext(context *hookstate.Context) 73 context() *hookstate.Context 74 75 Execute(args []string) error 76 } 77 78 type commandInfo struct { 79 shortHelp string 80 longHelp string 81 generator func() command 82 hidden bool 83 } 84 85 var commands = make(map[string]*commandInfo) 86 87 func addCommand(name, shortHelp, longHelp string, generator func() command) *commandInfo { 88 cmd := &commandInfo{ 89 shortHelp: shortHelp, 90 longHelp: longHelp, 91 generator: generator, 92 } 93 commands[name] = cmd 94 return cmd 95 } 96 97 // UnsuccessfulError carries a specific exit code to be returned to the client. 98 type UnsuccessfulError struct { 99 ExitCode int 100 } 101 102 func (e UnsuccessfulError) Error() string { 103 return fmt.Sprintf("unsuccessful with exit code: %d", e.ExitCode) 104 } 105 106 // ForbiddenCommandError conveys that a command cannot be invoked in some context 107 type ForbiddenCommandError struct { 108 Message string 109 } 110 111 func (f ForbiddenCommandError) Error() string { 112 return f.Message 113 } 114 115 // ForbiddenCommand contains information about an attempt to use a command in a context where it is not allowed. 116 type ForbiddenCommand struct { 117 Uid uint32 118 Name string 119 } 120 121 func (f *ForbiddenCommand) Execute(args []string) error { 122 return &ForbiddenCommandError{Message: fmt.Sprintf("cannot use %q with uid %d, try with sudo", f.Name, f.Uid)} 123 } 124 125 // Run runs the requested command. 126 func Run(context *hookstate.Context, args []string, uid uint32) (stdout, stderr []byte, err error) { 127 parser := flags.NewNamedParser("snapctl", flags.PassDoubleDash|flags.HelpFlag) 128 129 // Create stdout/stderr buffers, and make sure commands use them. 130 var stdoutBuffer bytes.Buffer 131 var stderrBuffer bytes.Buffer 132 for name, cmdInfo := range commands { 133 var data interface{} 134 // commands listed here will be allowed for regular users 135 // note: commands still need valid context and snaps can only access own config. 136 if uid == 0 || name == "get" || name == "services" || name == "set-health" || name == "is-connected" { 137 cmd := cmdInfo.generator() 138 cmd.setStdout(&stdoutBuffer) 139 cmd.setStderr(&stderrBuffer) 140 cmd.setContext(context) 141 data = cmd 142 } else { 143 data = &ForbiddenCommand{Uid: uid, Name: name} 144 } 145 theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, data) 146 theCmd.Hidden = cmdInfo.hidden 147 if err != nil { 148 logger.Panicf("cannot add command %q: %s", name, err) 149 } 150 } 151 152 _, err = parser.ParseArgs(args) 153 return stdoutBuffer.Bytes(), stderrBuffer.Bytes(), err 154 }