agent/apiserver/server.go

368 lines
8.3 KiB
Go
Raw Normal View History

2020-03-03 13:52:52 -05:00
package apiserver
import (
"context"
"fmt"
"log"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/indihub-space/agent/version"
"github.com/gorilla/websocket"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
elog "github.com/labstack/gommon/log"
"github.com/indihub-space/agent/lib"
"github.com/indihub-space/agent/logutil"
)
var allowedOrigins = map[string]bool{
"indihub.space": true,
"app.indihub.space": true,
"kids.indihub.space": true,
}
// AgentMode provides interface to operate with agent from API-server
type AgentMode interface {
Start()
Stop()
GetStatus() map[string]interface{}
}
type APIServer struct {
token string
indiServerAddr string
phd2ServerAddr string
port uint64
isTLS bool
origins string
e *echo.Echo
upgrader websocket.Upgrader
connList []net.Conn
indiProfile string
currMode string
agentModes map[string]AgentMode
}
func NewAPIServer(token string, indiServerAddr string, phd2ServerAddr string, port uint64, isTLS bool, origins string,
currMode string, indiProfile string, agentModes map[string]AgentMode) *APIServer {
apiServer := &APIServer{
token: token,
indiServerAddr: indiServerAddr,
phd2ServerAddr: phd2ServerAddr,
port: port,
isTLS: isTLS,
e: echo.New(),
upgrader: websocket.Upgrader{
EnableCompression: true,
},
connList: []net.Conn{},
indiProfile: indiProfile,
currMode: currMode,
agentModes: agentModes,
2020-03-03 13:52:52 -05:00
}
if logutil.IsDev {
allowedOrigins["localhost"] = true
}
// add optional additional origins
for _, orig := range strings.Split(origins, ",") {
allowedOrigins[strings.TrimSpace(orig)] = true
}
// allow WS connections only from number of domains
apiServer.upgrader.CheckOrigin = func(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return false
}
u, err := url.Parse(origin[0])
if err != nil {
return false
}
host, _, err := net.SplitHostPort(u.Host)
if err != nil {
return false
}
// check both host and host:port from --api-origins param values
return allowedOrigins[host] || allowedOrigins[u.Host]
2020-03-03 13:52:52 -05:00
}
return apiServer
}
func (s *APIServer) newIndiConnection(c echo.Context) error {
// upgrade to WS connection
ws, err := s.upgrader.Upgrade(c.Response(), c.Request(), nil)
if err != nil {
return err
}
defer ws.Close()
// open connection to INDI-Server
conn, err := net.Dial("tcp", s.indiServerAddr)
if err != nil {
return err
}
// add to connection list
s.connList = append(s.connList, conn)
// read messages from INDI-server and write them to WS
go func(indiConn net.Conn, wsConn *websocket.Conn) {
buf := make([]byte, lib.INDIServerMaxSendMsgSize, lib.INDIServerMaxSendMsgSize)
xmlFlattener := lib.NewXmlFlattener()
for {
// read from INDI-server
n, err := indiConn.Read(buf)
if err != nil {
indiConn.Close()
return
}
jsonMessages := xmlFlattener.ConvertChunkToJSON(buf[:n])
// Write to WS
for _, m := range jsonMessages {
err = wsConn.WriteMessage(websocket.TextMessage, m)
if err != nil {
indiConn.Close()
return
}
}
}
}(conn, ws)
// read messages from WS and write them to INDI-server
xmlFlattener := lib.NewXmlFlattener()
for {
// Read from WS
_, msg, err := ws.ReadMessage()
if err != nil {
conn.Close()
return err
}
xmlMsg, err := xmlFlattener.ConvertJSONToXML(msg)
if err != nil {
log.Printf("could not convert json '%s' to xml: %s", string(msg), err)
continue
}
// write to INDI server
_, err = conn.Write(xmlMsg)
if err != nil {
conn.Close()
return err
}
}
}
func (s *APIServer) newPHD2Connection(c echo.Context) error {
return nil
}
func (s *APIServer) getRestart(c echo.Context) error {
// restart current mode
if curAgentMode, ok := s.agentModes[s.currMode]; ok {
curAgentMode.Stop()
time.Sleep(1 * time.Second)
curAgentMode.Start()
}
c.JSONPretty(http.StatusOK, s.agentStatus(), " ")
return nil
}
func (s *APIServer) getStatus(c echo.Context) error {
c.JSONPretty(http.StatusOK, s.agentStatus(), " ")
return nil
}
func (s *APIServer) changeMode(c echo.Context) error {
newMode := c.Param("new_mode")
// don't do anything if agent is laready in this mode
if newMode == s.currMode {
c.JSONPretty(http.StatusOK, s.agentStatus(), " ")
return nil
}
// get agent new mode
newAgentMode, ok := s.agentModes[newMode]
if !ok {
c.JSON(
http.StatusBadRequest,
map[string]interface{}{
"message": "unknown indihub-agent mode: " + newMode,
},
)
return nil
}
// stop current mode
if curAgentMode, ok := s.agentModes[s.currMode]; ok {
curAgentMode.Stop()
}
time.Sleep(1 * time.Second)
// start agent in new mode
s.currMode = newMode
newAgentMode.Start()
c.JSONPretty(http.StatusOK, s.agentStatus(), " ")
return nil
}
func (s *APIServer) agentStatus() map[string]interface{} {
agentStatus := map[string]interface{}{
"version": version.AgentVersion,
"mode": s.currMode,
"indiProfile": s.indiProfile,
"indiServer": s.indiServerAddr,
"phd2Server": s.phd2ServerAddr,
}
supportedModes := make([]string, 0, len(s.agentModes))
for key := range s.agentModes {
supportedModes = append(supportedModes, key)
}
agentStatus["supportedModes"] = supportedModes
if agentMode, ok := s.agentModes[s.currMode]; ok {
for key, val := range agentMode.GetStatus() {
agentStatus[key] = val
}
}
return agentStatus
}
func (s *APIServer) Start() {
s.e.HideBanner = true
s.e.HidePort = true
if !logutil.IsDev {
s.e.Logger.SetLevel(elog.OFF)
}
// setup middle-wares
s.e.Use(middleware.Recover())
// set CORS in case browser decides to do pre-flight OPTIONS request
// default ones
allowOrigins := []string{}
for orig := range allowedOrigins {
allowOrigins = append(allowOrigins, "http://"+orig)
allowOrigins = append(allowOrigins, "https://"+orig)
}
// optional ones
for _, orig := range strings.Split(s.origins, ",") {
allowOrigins = append(allowOrigins, "http://"+strings.TrimSpace(orig))
allowOrigins = append(allowOrigins, "https://"+strings.TrimSpace(orig))
}
// add localhost for dev-mode
if logutil.IsDev {
allowOrigins = append(allowOrigins, "http://localhost:5000")
}
s.e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: allowOrigins,
AllowMethods: []string{
http.MethodGet,
http.MethodPost,
},
}))
// setup routing for WS and RESTful APIs
// protected WS-API
wsGroup := s.e.Group(
"/websocket",
// set auth middleware
middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
KeyLookup: "query:token",
Validator: func(token string, eCtx echo.Context) (b bool, err error) {
return token == s.token, nil
},
}),
)
wsGroup.GET("/indiserver", s.newIndiConnection)
wsGroup.GET("/phd2server", s.newPHD2Connection)
// protected RESTful API
s.e.POST(
"/mode/:new_mode",
s.changeMode,
// set auth middleware
middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
KeyLookup: "header:Authorization",
Validator: func(token string, eCtx echo.Context) (b bool, err error) {
return token == s.token, nil
},
}),
)
// public RESTful API
s.e.GET("/status", s.getStatus)
s.e.GET("/restart", s.getRestart)
// start agent in a required mode
agentMode, ok := s.agentModes[s.currMode]
if !ok {
log.Println("unknown agent mode:", s.currMode)
return
}
agentMode.Start()
// check if we are running TLS
if s.isTLS {
// generate self-signed cert to serve WS over TLS
keyFile, certFile, err := getSelfSignedCert()
if err != nil {
log.Println("could not start API-server, self-signed certificate generating failed:", err)
return
}
// start HTTP/WS API server over TLS
err = s.e.StartTLS(fmt.Sprintf(":%d", s.port), certFile, keyFile)
if err != nil && err != http.ErrServerClosed {
log.Println("API-server error:", err)
} else {
log.Println("API-server was shutdown gracefully")
}
return
}
// start HTTP/WS API server
err := s.e.Start(fmt.Sprintf(":%d", s.port))
if err != nil && err != http.ErrServerClosed {
log.Println("API-server error:", err)
} else {
log.Println("API-server was shutdown gracefully")
}
}
func (s *APIServer) Stop() {
if agentMode, ok := s.agentModes[s.currMode]; ok {
agentMode.Stop()
}
for _, conn := range s.connList {
conn.Close()
}
s.e.Shutdown(context.Background())
}