blob: 157c35982a00f0591116d483dfc4effc5299bec5 [file] [log] [blame]
// 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()
}