package main import ( "encoding/binary" "encoding/json" "errors" "flag" "fmt" "io" "net" "os" "time" ) const ( defaultSocketPath = "/var/lib/iot/ctl.sock" maxPacketSize = 65535 dialTimeout = 5 * time.Second requestTimeout = 10 * time.Second ) type request struct { Method string `json:"method"` Params map[string]string `json:"params,omitempty"` } type response struct { Result json.RawMessage `json:"result,omitempty"` Error *responseError `json:"error,omitempty"` } type responseError struct { Message string `json:"message"` } func main() { if err := run(os.Args[1:]); err != nil { fmt.Fprintf(os.Stderr, "iot_ctrl: %v\n", err) os.Exit(1) } } func run(args []string) error { flags := flag.NewFlagSet("iot_ctrl", flag.ContinueOnError) flags.SetOutput(io.Discard) socketPath := defaultSocketPath if envSocketPath := os.Getenv("IOT_CTRL_SOCKET_PATH"); envSocketPath != "" { socketPath = envSocketPath } flags.StringVar(&socketPath, "socket", socketPath, "control socket path") if err := flags.Parse(args); err != nil { return usageError() } rest := flags.Args() if len(rest) == 0 { return usageError() } req, err := parseCommand(rest) if err != nil { return err } result, err := call(socketPath, req) if err != nil { return err } if result != "" { fmt.Println(result) } return nil } func parseCommand(args []string) (request, error) { switch args[0] { case "ping": if len(args) != 1 { return request{}, usageError() } return request{Method: "ping"}, nil case "add-client": flags := flag.NewFlagSet("add-client", flag.ContinueOnError) flags.SetOutput(io.Discard) var uuid string var token string flags.StringVar(&uuid, "uuid", "", "client uuid") flags.StringVar(&token, "token", "", "client token") if err := flags.Parse(args[1:]); err != nil { return request{}, usageError() } if uuid == "" || token == "" || flags.NArg() != 0 { return request{}, usageError() } return request{ Method: "add_client", Params: map[string]string{ "uuid": uuid, "token": token, }, }, nil default: return request{}, usageError() } } func call(socketPath string, req request) (string, error) { conn, err := net.DialTimeout("unix", socketPath, dialTimeout) if err != nil { return "", err } defer conn.Close() if err := conn.SetDeadline(time.Now().Add(requestTimeout)); err != nil { return "", err } body, err := json.Marshal(req) if err != nil { return "", err } if err := writePacket(conn, body); err != nil { return "", err } reply, err := readPacket(conn) if err != nil { return "", err } var resp response if err := json.Unmarshal(reply, &resp); err != nil { return "", err } if resp.Error != nil { return "", errors.New(resp.Error.Message) } if len(resp.Result) == 0 { return "", nil } var text string if err := json.Unmarshal(resp.Result, &text); err == nil { return text, nil } return string(resp.Result), nil } func writePacket(w io.Writer, body []byte) error { if len(body) > maxPacketSize { return fmt.Errorf("request exceeds packet=2 limit: %d bytes", len(body)) } header := make([]byte, 2) binary.BigEndian.PutUint16(header, uint16(len(body))) if _, err := w.Write(header); err != nil { return err } _, err := w.Write(body) return err } func readPacket(r io.Reader) ([]byte, error) { header := make([]byte, 2) if _, err := io.ReadFull(r, header); err != nil { return nil, err } size := binary.BigEndian.Uint16(header) body := make([]byte, int(size)) if _, err := io.ReadFull(r, body); err != nil { return nil, err } return body, nil } func usageError() error { return errors.New("usage: iot_ctrl [--socket path] ping | iot_ctrl [--socket path] add-client --uuid --token ") }