| // Copyright 2015 The Chromium OS 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 overlord |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "github.com/googollee/go-socket.io" |
| "github.com/gorilla/mux" |
| "github.com/gorilla/websocket" |
| "github.com/satori/go.uuid" |
| "io/ioutil" |
| "log" |
| "net" |
| "net/http" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "time" |
| ) |
| |
| const ( |
| SYSTEM_APP_DIR = "/usr/share/overlord" |
| WEBSERVER_ADDR = "localhost:9000" |
| LD_INTERVAL = 5 |
| KEEP_ALIVE_PERIOD = 1 |
| ) |
| |
| type SpawnTerminalCmd struct { |
| Sid string |
| } |
| |
| type SpawnShellCmd struct { |
| Sid string |
| Command string |
| } |
| |
| type ConnectLogcatCmd struct { |
| Conn *websocket.Conn |
| } |
| |
| // WebSocketContext is used for maintaining the session information of |
| // WebSocket requests. When requests come from Web Server, we create a new |
| // WebSocketConext to store the session ID and WebSocket connection. ConnServer |
| // will request a new terminal connection with client ID equals the session ID. |
| // This way, the ConnServer can retreive the connresponding WebSocketContext |
| // with it's client (session) ID and get the WebSocket. |
| type WebSocketContext struct { |
| Sid string |
| Conn *websocket.Conn |
| } |
| |
| func NewWebsocketContext(conn *websocket.Conn) *WebSocketContext { |
| return &WebSocketContext{ |
| Sid: uuid.NewV4().String(), |
| Conn: conn, |
| } |
| } |
| |
| type Overlord struct { |
| lanDiscInterface string // Network interface used for broadcasting LAN discovery packet |
| noAuth bool // Disable HTTP basic authentication |
| TLSSettings string // TLS settings in the form of "cert.pem,key.pem". Empty to disable TLS |
| agents map[string]*ConnServer // Normal ghost agents |
| logcats map[string]map[string]*ConnServer // logcat clients |
| wsctxs map[string]*WebSocketContext // (cid, WebSocketContext) mapping |
| ioserver *socketio.Server |
| } |
| |
| func NewOverlord(lanDiscInterface string, noAuth bool, TLSSettings string) *Overlord { |
| return &Overlord{ |
| lanDiscInterface: lanDiscInterface, |
| noAuth: noAuth, |
| TLSSettings: TLSSettings, |
| agents: make(map[string]*ConnServer), |
| logcats: make(map[string]map[string]*ConnServer), |
| wsctxs: make(map[string]*WebSocketContext), |
| } |
| } |
| |
| // Register a client. |
| func (self *Overlord) Register(conn *ConnServer) (*websocket.Conn, error) { |
| msg, err := json.Marshal(map[string]interface{}{ |
| "mid": conn.Mid, |
| "cid": conn.Cid, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| var wsconn *websocket.Conn |
| |
| switch conn.Mode { |
| case AGENT: |
| if _, ok := self.agents[conn.Mid]; ok { |
| return nil, errors.New("Register: duplicate machine ID: " + conn.Mid) |
| } |
| |
| self.agents[conn.Mid] = conn |
| self.ioserver.BroadcastTo("monitor", "agent joined", string(msg)) |
| case TERMINAL, SHELL: |
| if ctx, ok := self.wsctxs[conn.Cid]; !ok { |
| return nil, errors.New("Register: client " + conn.Cid + |
| " registered without context") |
| } else { |
| wsconn = ctx.Conn |
| } |
| case LOGCAT: |
| if _, ok := self.logcats[conn.Mid]; !ok { |
| self.logcats[conn.Mid] = make(map[string]*ConnServer) |
| } |
| if _, ok := self.logcats[conn.Mid][conn.Cid]; ok { |
| return nil, errors.New("Register: duplicate client ID: " + conn.Cid) |
| } |
| self.logcats[conn.Mid][conn.Cid] = conn |
| self.ioserver.BroadcastTo("monitor", "logcat joined", string(msg)) |
| default: |
| return nil, errors.New("Register: Unknown client mode") |
| } |
| |
| var id string |
| if conn.Mode == AGENT { |
| id = conn.Mid |
| } else { |
| id = conn.Cid |
| } |
| |
| log.Printf("%s %s registered\n", ModeStr(conn.Mode), id) |
| |
| return wsconn, nil |
| } |
| |
| // Unregister a client. |
| func (self *Overlord) Unregister(conn *ConnServer) { |
| msg, err := json.Marshal(map[string]interface{}{ |
| "mid": conn.Mid, |
| "cid": conn.Cid, |
| }) |
| |
| if err != nil { |
| panic(err) |
| } |
| |
| switch conn.Mode { |
| case AGENT: |
| self.ioserver.BroadcastTo("monitor", "agent left", string(msg)) |
| delete(self.agents, conn.Mid) |
| case LOGCAT: |
| if _, ok := self.logcats[conn.Mid]; ok { |
| self.ioserver.BroadcastTo("monitor", "logcat left", string(msg)) |
| delete(self.logcats[conn.Mid], conn.Cid) |
| if len(self.logcats[conn.Mid]) == 0 { |
| delete(self.logcats, conn.Mid) |
| } |
| } |
| default: |
| if _, ok := self.wsctxs[conn.Cid]; ok { |
| delete(self.wsctxs, conn.Cid) |
| } |
| } |
| |
| var id string |
| if conn.Mode == AGENT { |
| id = conn.Mid |
| } else { |
| id = conn.Cid |
| } |
| log.Printf("%s %s unregistered\n", ModeStr(conn.Mode), id) |
| } |
| |
| func (self *Overlord) AddWebsocketContext(wc *WebSocketContext) { |
| self.wsctxs[wc.Sid] = wc |
| } |
| |
| // Handle TCP Connection. |
| func (self *Overlord) handleConnection(conn net.Conn) { |
| handler := NewConnServer(self, conn) |
| go handler.Listen() |
| } |
| |
| // Socket server main routine. |
| func (self *Overlord) ServSocket(port int) { |
| addrStr := fmt.Sprintf("0.0.0.0:%d", port) |
| addr, err := net.ResolveTCPAddr("tcp", addrStr) |
| if err != nil { |
| panic(err) |
| } |
| ln, err := net.ListenTCP("tcp", addr) |
| if err != nil { |
| panic(err) |
| } |
| log.Printf("Overlord started, listening at %s", addr) |
| for { |
| conn, err := ln.AcceptTCP() |
| if err != nil { |
| panic(err) |
| } |
| log.Printf("Incomming connection from %s\n", conn.RemoteAddr()) |
| conn.SetKeepAlive(true) |
| conn.SetKeepAlivePeriod(KEEP_ALIVE_PERIOD * time.Second) |
| self.handleConnection(conn) |
| } |
| } |
| |
| // Initialize the Socket.io server. |
| func (self *Overlord) InitSocketIOServer() { |
| server, err := socketio.NewServer(nil) |
| if err != nil { |
| log.Fatal(err) |
| } |
| |
| server.On("connection", func(so socketio.Socket) { |
| so.Join("monitor") |
| }) |
| |
| server.On("error", func(so socketio.Socket, err error) { |
| log.Println("error:", err) |
| }) |
| |
| self.ioserver = server |
| } |
| |
| func (self *Overlord) GetAppDir() string { |
| wd, err := os.Getwd() |
| if err != nil { |
| panic(err) |
| } |
| |
| appDir := filepath.Join(wd, filepath.Dir(os.Args[0]), "app") |
| if _, err := os.Stat(appDir); os.IsNotExist(err) { |
| // Try system install direcotry |
| appDir = filepath.Join(SYSTEM_APP_DIR, "app") |
| if _, err := os.Stat(appDir); os.IsNotExist(err) { |
| log.Fatalf("Can not find app directory\n") |
| } |
| } |
| return appDir |
| } |
| |
| func (self *Overlord) GetAppNames(ignoreSpecial bool) ([]string, error) { |
| var appNames []string |
| |
| apps, err := ioutil.ReadDir(self.GetAppDir()) |
| if err != nil { |
| return nil, nil |
| } |
| |
| for _, app := range apps { |
| if !app.IsDir() || |
| (ignoreSpecial && (app.Name() == "common" || app.Name() == "index")) { |
| continue |
| } |
| appNames = append(appNames, app.Name()) |
| } |
| return appNames, nil |
| } |
| |
| func AuthPassThrough(h http.Handler) http.Handler { |
| return h |
| } |
| |
| // Web server main routine. |
| func (self *Overlord) ServHTTP(addr string) { |
| var upgrader = websocket.Upgrader{ |
| ReadBufferSize: BUFSIZ, |
| WriteBufferSize: BUFSIZ, |
| CheckOrigin: func(r *http.Request) bool { |
| return true |
| }, |
| } |
| |
| // Helper function for writing error message to WebSocket |
| WebSocketSendError := func(ws *websocket.Conn, err string) { |
| log.Println(err) |
| msg := websocket.FormatCloseMessage(websocket.CloseProtocolError, err) |
| ws.WriteMessage(websocket.CloseMessage, msg) |
| ws.Close() |
| } |
| |
| // List all apps available on Overlord. |
| AppsListHandler := func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| |
| apps, err := self.GetAppNames(true) |
| if err != nil { |
| w.Write([]byte(fmt.Sprintf(`{"error", "%s"}`, err.Error()))) |
| } |
| |
| result, err := json.Marshal(map[string][]string{"apps": apps}) |
| if err != nil { |
| w.Write([]byte(fmt.Sprintf(`{"error", "%s"}`, err.Error()))) |
| } else { |
| w.Write(result) |
| } |
| } |
| |
| // List all agents connected to the Overlord. |
| AgentsListHandler := func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| data := make([]map[string]string, len(self.agents)) |
| idx := 0 |
| for _, agent := range self.agents { |
| data[idx] = map[string]string{ |
| "mid": agent.Mid, |
| "cid": agent.Cid, |
| } |
| idx++ |
| } |
| |
| result, err := json.Marshal(data) |
| if err != nil { |
| w.Write([]byte(fmt.Sprintf(`{"error", "%s"}`, err.Error()))) |
| } else { |
| w.Write(result) |
| } |
| } |
| |
| // List all logcat clients connected to the Overlord. |
| LogcatsListHandler := func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| data := make([]map[string]interface{}, len(self.logcats)) |
| idx := 0 |
| for mid, logcats := range self.logcats { |
| var cids []string |
| for cid, _ := range logcats { |
| cids = append(cids, cid) |
| } |
| data[idx] = map[string]interface{}{ |
| "mid": mid, |
| "cids": cids, |
| } |
| idx++ |
| } |
| |
| result, err := json.Marshal(data) |
| if err != nil { |
| w.Write([]byte(fmt.Sprintf(`{"error", "%s"}`, err.Error()))) |
| } else { |
| w.Write(result) |
| } |
| } |
| |
| // PTY stream request handler. |
| // We first create a WebSocketContext to store the connection, then send a |
| // command to Overlord to client to spawn a terminal connection. |
| PtyHandler := func(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Terminal request from %s\n", r.RemoteAddr) |
| |
| conn, err := upgrader.Upgrade(w, r, nil) |
| if err != nil { |
| log.Println(err) |
| return |
| } |
| |
| vars := mux.Vars(r) |
| mid := vars["mid"] |
| if agent, ok := self.agents[mid]; ok { |
| wc := NewWebsocketContext(conn) |
| self.AddWebsocketContext(wc) |
| agent.Bridge <- SpawnTerminalCmd{wc.Sid} |
| } else { |
| WebSocketSendError(conn, "No client with mid "+mid) |
| } |
| } |
| |
| // Shell command request handler. |
| // We first create a WebSocketContext to store the connection, then send a |
| // command to ConnServer to client to spawn a shell connection. |
| ShellHandler := func(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Shell request from %s\n", r.RemoteAddr) |
| |
| conn, err := upgrader.Upgrade(w, r, nil) |
| if err != nil { |
| log.Println(err) |
| return |
| } |
| |
| vars := mux.Vars(r) |
| mid := vars["mid"] |
| if agent, ok := self.agents[mid]; ok { |
| if command, ok := r.URL.Query()["command"]; ok { |
| wc := NewWebsocketContext(conn) |
| self.AddWebsocketContext(wc) |
| agent.Bridge <- SpawnShellCmd{wc.Sid, command[0]} |
| } else { |
| WebSocketSendError(conn, "No command specified for shell request "+mid) |
| } |
| } else { |
| WebSocketSendError(conn, "No client with mid "+mid) |
| } |
| } |
| |
| // Logcat request handler. |
| // We directly send the WebSocket connection to ConnServer for forwarding |
| // the log stream. |
| LogcatHandler := func(w http.ResponseWriter, r *http.Request) { |
| log.Printf("Logcat request from %s\n", r.RemoteAddr) |
| |
| conn, err := upgrader.Upgrade(w, r, nil) |
| if err != nil { |
| log.Println(err) |
| return |
| } |
| |
| vars := mux.Vars(r) |
| mid := vars["mid"] |
| cid := vars["cid"] |
| |
| if logcats, ok := self.logcats[mid]; ok { |
| if logcat, ok := logcats[cid]; ok { |
| logcat.Bridge <- ConnectLogcatCmd{conn} |
| } else { |
| WebSocketSendError(conn, "No client with cid "+cid) |
| } |
| } else { |
| WebSocketSendError(conn, "No client with mid "+mid) |
| } |
| } |
| |
| // Get agent properties as JSON |
| AgentPropertiesHandler := func(w http.ResponseWriter, r *http.Request) { |
| w.Header().Set("Content-Type", "application/json") |
| |
| vars := mux.Vars(r) |
| mid := vars["mid"] |
| if agent, ok := self.agents[mid]; ok { |
| jsonResult, err := json.Marshal(agent.Properties) |
| if err != nil { |
| w.Write([]byte(fmt.Sprintf(`{"error", "%s"}`, err.Error()))) |
| return |
| } |
| w.Write(jsonResult) |
| } else { |
| w.Write([]byte(fmt.Sprintf(`{"error", "No client with mid` + mid + `"}`))) |
| } |
| } |
| |
| appDir := self.GetAppDir() |
| |
| // HTTP basic auth |
| auth := NewBasicAuth("Overlord", filepath.Join(appDir, "overlord.htpasswd"), |
| self.noAuth) |
| |
| // Initialize socket IO server |
| self.InitSocketIOServer() |
| |
| // Register the request handlers and start the WebServer. |
| r := mux.NewRouter() |
| |
| r.HandleFunc("/api/apps/list", AppsListHandler) |
| r.HandleFunc("/api/agents/list", AgentsListHandler) |
| r.HandleFunc("/api/logcats/list", LogcatsListHandler) |
| |
| // Logcat methods |
| r.HandleFunc("/api/log/{mid}/{cid}", LogcatHandler) |
| |
| // Agent methods |
| r.HandleFunc("/api/agent/pty/{mid}", PtyHandler) |
| r.HandleFunc("/api/agent/shell/{mid}", ShellHandler) |
| r.HandleFunc("/api/agent/properties/{mid}", AgentPropertiesHandler) |
| |
| http.Handle("/api/", auth.Wrap(r)) |
| http.Handle("/api/socket.io/", auth.Wrap(self.ioserver)) |
| http.Handle("/vendor/", auth.Wrap(http.FileServer(http.Dir(filepath.Join(appDir, "common"))))) |
| http.Handle("/", auth.Wrap(http.FileServer(http.Dir(filepath.Join(appDir, "dashboard"))))) |
| |
| // Serve all apps |
| appNames, err := self.GetAppNames(false) |
| if err != nil { |
| panic(err) |
| } |
| |
| for _, app := range appNames { |
| if app != "common" && app != "index" { |
| log.Printf("Serving app `%s' ...\n", app) |
| } |
| prefix := fmt.Sprintf("/%s/", app) |
| http.Handle(prefix, http.StripPrefix(prefix, |
| auth.Wrap(http.FileServer(http.Dir(filepath.Join(appDir, app)))))) |
| } |
| |
| if self.TLSSettings != "" { |
| parts := strings.Split(self.TLSSettings, ",") |
| if len(parts) != 2 { |
| log.Fatalf("TLSSettings: invalid key assignment") |
| } |
| err = http.ListenAndServeTLS(WEBSERVER_ADDR, parts[0], parts[1], nil) |
| } else { |
| err = http.ListenAndServe(WEBSERVER_ADDR, nil) |
| } |
| if err != nil { |
| log.Fatalf("net.http could not listen on address '%s': %s\n", |
| WEBSERVER_ADDR, err) |
| } |
| } |
| |
| // Broadcast LAN discovery message. |
| func (self *Overlord) StartUDPBroadcast(port int) { |
| ifaceIP := "" |
| bcastIP := net.IPv4bcast.String() |
| |
| if self.lanDiscInterface != "" { |
| interfaces, err := net.Interfaces() |
| if err != nil { |
| panic(err) |
| } |
| |
| outter: |
| for _, iface := range interfaces { |
| if iface.Name == self.lanDiscInterface { |
| addrs, err := iface.Addrs() |
| if err != nil { |
| panic(err) |
| } |
| for _, addr := range addrs { |
| ip, ipnet, err := net.ParseCIDR(addr.String()) |
| if err != nil { |
| continue |
| } |
| // Calculate broadcast IP |
| ip4 := ip.To4() |
| |
| // We only care about IPv4 address |
| if ip4 == nil { |
| continue |
| } |
| bcastIPraw := make(net.IP, 4) |
| for i := 0; i < 4; i++ { |
| bcastIPraw[i] = ip4[i] | ^ipnet.Mask[i] |
| } |
| ifaceIP = ip.String() |
| bcastIP = bcastIPraw.String() |
| break outter |
| } |
| } |
| } |
| |
| if ifaceIP == "" { |
| log.Fatalf("can not found any interface with name %s\n", |
| self.lanDiscInterface) |
| } |
| } |
| |
| conn, err := net.Dial("udp", fmt.Sprintf("%s:%d", bcastIP, port)) |
| if err != nil { |
| panic(err) |
| } |
| |
| ticker := time.NewTicker(time.Duration(LD_INTERVAL * time.Second)) |
| |
| for { |
| select { |
| case <-ticker.C: |
| conn.Write([]byte(fmt.Sprintf("OVERLORD %s:%d", ifaceIP, OVERLORD_PORT))) |
| } |
| } |
| } |
| |
| func (self *Overlord) Serv() { |
| go self.ServSocket(OVERLORD_PORT) |
| go self.ServHTTP(WEBSERVER_ADDR) |
| go self.StartUDPBroadcast(OVERLORD_LD_PORT) |
| |
| ticker := time.NewTicker(time.Duration(60 * time.Second)) |
| |
| for { |
| select { |
| case <-ticker.C: |
| log.Printf("#Goroutines, #Ghostclient: %d, %d\n", |
| runtime.NumGoroutine(), len(self.agents)) |
| } |
| } |
| } |
| |
| func StartOverlord(lanDiscInterface string, noAuth bool, TLSSettings string) { |
| ovl := NewOverlord(lanDiscInterface, noAuth, TLSSettings) |
| ovl.Serv() |
| } |