diff --git a/README.md b/README.md
index f7a5dd9..a25caf0 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,198 @@ To get usage of all parameters just run `indihub-agent -help`.
The latest `indihub-agent` release can be downloaded from [releases](https://github.com/indihub-space/agent/releases) or [indihub.space](https://indihub.space) Web-site.
+## API
+
+There is an API-server running as part of `indihub-agent` and listening on port `:2020` (or on port specified via `-api-port=N` parameter) which provides two different APIs to control or use `indihub-agent`:
+
+- RESTful API to get status or switch modes of the the agent
+- Websocket API to control equipment via websocket-connections
+
+By default API-server works over HTTP-protocol. You can switch it to work over TLS by providing `-api-tls` parameter. This will make `indihub-agent` to generate self-signed CA and certificate.
+
+### RESTful API
+
+You can use this simple RESTful API to control `indihub-agent`.
+
+NOTE: CORS protection can be specified with comma-separated list of allowed origins via agent parameter:
+
+`-api-origins=host1,host2,hostN`.
+
+Also, all examples assume `indihub-agent` is running on host `raspberrypi.local`.
+
+#### 1. Get indihub-agent status (public)
+
+`curl -X GET http://raspberrypi.local:2020/status`
+
+Response example:
+```json
+{
+ "indiProfile": "NEO-remote",
+ "indiServer": "raspberrypi.local:7624",
+ "mode": "solo",
+ "phd2Server": "",
+ "status": "running",
+ "supportedModes": [
+ "solo",
+ "share",
+ "robotic"
+ ],
+ "version": "1.0.3"
+}
+```
+
+#### 2. Restart indihub-agent current mode (public)
+
+`curl -X GET "http://raspberrypi.local:2020/status"`
+
+#### 3. Switch indihub-agent mode (protected via token)
+
+You need to do HTTP-request `POST "http://indihub-agent-host:2020/mode/{mode}"` specifying required mode in a `mode` URI-parameter and supplying your token in `Authorization` header, i.e.:
+
+`curl -X POST "http://raspberrypi.local:2020/mode/solo" -H "Authorization: Bearer cca13ac2951efd6d912ead20a7ab4882"`
+
+Response will have status of agent running in new mode:
+
+```json
+{
+ "indiProfile": "NEO-remote",
+ "indiServer": "raspberrypi.local:7624",
+ "mode": "share",
+ "phd2Server": "",
+ "publicEndpoints": [
+ {
+ "name": "INDI-Server",
+ "addr": "node-1.indihub.io:55642"
+ }
+ ],
+ "status": "running",
+ "supportedModes": [
+ "share",
+ "robotic",
+ "solo"
+ ],
+ "version": "1.0.3"
+}
+```
+
+### Websocket API
+
+You can use `indihub-agent` to control your equipment via Websocket API, i.e. from your Web-app open int the Web-browser.
+
+NOTE: Websocket upgrade-requests are allowed only from origins specified with comma-separated list via agent parameter:
+
+`-api-origins=host1,host2,hostN`
+
+#### 1. Open WS-connection to INDI-server (protected via token)
+
+To establish new WS-connection to INDI-server you will need to use URL with format:
+
+`ws://raspberrypi.local:2020/websocket/indiserver?token=cca13ac2951efd6d912ead20a7ab4882`
+
+NOTE: we connect to `raspberrypi.local:2020` which is `indihub-agent` API-server and we provide token via `token` query string parameter.
+
+This will open WS-connection to you your equipment via `indihub-agent` API-server where all outgoing messages will be INDI-protocol commands and all incoming messages will be INDI-protocol replies from your equipment.
+
+#### 2. Message format for INDI-server Websocket connection
+
+All commands are expected to be in JSON-format where INDI-command XML attributes translate to fields with `attr_`-prefix.
+
+I.e. to send this INDI XML-command over WS-connection:
+
+```xml
+
+```
+
+You will need to send message over WS-connection in JSON-format:
+```json
+{
+ "getProperties": {
+ "attr_version":"1.7"
+ }
+}
+```
+
+The INDI-protocol responses will be converted from XML to JSON-messages and sent over WS-connection as messages.
+
+I.e. this INDI-response about telescopes installed on the mount in XML-format:
+```xml
+
+
+120
+
+
+600
+
+
+50
+
+
+162
+
+
+```
+
+Will be converted in to JSON-format like this:
+
+```json
+{
+ "defNumberVector": {
+ "attr_device":"iOptron CEM25",
+ "attr_group":"Options",
+ "attr_label":"Scope Properties",
+ "attr_name":"TELESCOPE_INFO",
+ "attr_perm":"rw",
+ "attr_state":"Ok",
+ "attr_timeout":60,
+ "attr_timestamp":"2020-02-20T21:52:23",
+ "defNumber": [
+ {
+ "#text":120,
+ "attr_format":"%g",
+ "attr_label":"Aperture (mm)",
+ "attr_max":5000,
+ "attr_min":10,
+ "attr_name":"TELESCOPE_APERTURE",
+ "attr_step":0
+ },
+ {
+ "#text":600,
+ "attr_format":"%g",
+ "attr_label":"Focal Length (mm)",
+ "attr_max":10000,
+ "attr_min":10,
+ "attr_name":"TELESCOPE_FOCAL_LENGTH",
+ "attr_step":0
+ },
+ {
+ "#text":50,
+ "attr_format":"%g",
+ "attr_label":"Guider Aperture (mm)",
+ "attr_max":5000,
+ "attr_min":10,
+ "attr_name":"GUIDER_APERTURE",
+ "attr_step":0
+ },
+ {
+ "#text":162,
+ "attr_format":"%g",
+ "attr_label":"Guider Focal Length (mm)",
+ "attr_max":10000,
+ "attr_min":10,
+ "attr_name":"GUIDER_FOCAL_LENGTH",
+ "attr_step":0
+ }
+ ]
+ }
+}
+```
+
+Please NOTE:
+
+- XML element attributes get converted into JSON-fields with `attr_` prefix
+- XML element value get converted into JSON-field with name `#text`
+- vector like child-elements get converted into JSON-arrays
+
## Building indihub-agent
You will need to install [Golang](https://golang.org/dl/).
diff --git a/apiserver/server.go b/apiserver/server.go
new file mode 100644
index 0000000..7850c96
--- /dev/null
+++ b/apiserver/server.go
@@ -0,0 +1,363 @@
+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{},
+ connList: []net.Conn{},
+ indiProfile: indiProfile,
+ currMode: currMode,
+ agentModes: agentModes,
+ }
+
+ 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
+ }
+ return allowedOrigins[host]
+ }
+
+ 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())
+}
diff --git a/websockets/tls.go b/apiserver/tls.go
similarity index 99%
rename from websockets/tls.go
rename to apiserver/tls.go
index f821690..69dbd8c 100644
--- a/websockets/tls.go
+++ b/apiserver/tls.go
@@ -1,4 +1,4 @@
-package websockets
+package apiserver
import (
"crypto/ecdsa"
diff --git a/lib/const.go b/lib/const.go
index e2462bb..4a390f3 100644
--- a/lib/const.go
+++ b/lib/const.go
@@ -6,4 +6,8 @@ const (
INDIServerMaxRecvMsgSize = 49152
INDIServerMaxSendMsgSize = 2048
+
+ ModeSolo = "solo"
+ ModeShare = "share"
+ ModeRobotic = "robotic"
)
diff --git a/main.go b/main.go
index 05f4365..3a992ba 100644
--- a/main.go
+++ b/main.go
@@ -11,32 +11,25 @@ import (
"os"
"os/signal"
"runtime"
- "sync"
- "time"
- "github.com/fatih/color"
+ "github.com/indihub-space/agent/apiserver"
+
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
_ "google.golang.org/grpc/encoding/gzip"
"github.com/indihub-space/agent/config"
- "github.com/indihub-space/agent/hostutils"
"github.com/indihub-space/agent/lib"
"github.com/indihub-space/agent/logutil"
"github.com/indihub-space/agent/manager"
"github.com/indihub-space/agent/proto/indihub"
- "github.com/indihub-space/agent/proxy"
+ "github.com/indihub-space/agent/share"
"github.com/indihub-space/agent/solo"
"github.com/indihub-space/agent/version"
- "github.com/indihub-space/agent/websockets"
)
const (
- defaultWSPort uint64 = 2020
-
- modeSolo = "solo"
- modeShare = "share"
- modeRobotic = "robotic"
+ defaultAPIPort uint64 = 2020
)
var (
@@ -47,10 +40,9 @@ var (
flagConfFile string
flagSoloINDIServerAddr string
flagCompress bool
- flagWSServer bool
- flagWSIsTLS bool
- flagWSPort uint64
- flagWSOrigins string
+ flagAPITLS bool
+ flagAPIPort uint64
+ flagAPIOrigins string
flagMode string
indiServerAddr string
@@ -68,7 +60,7 @@ func init() {
flag.StringVar(
&flagMode,
"mode",
- modeSolo,
+ lib.ModeSolo,
`indihub-agent mode (deafult value is "solo"), there four modes:\n
solo - equipment sharing is not possible, you are connected to INDIHUB and contributing images
share - you are sharing equipment with another INDIHUB user (agent will output connection info)
@@ -112,35 +104,29 @@ robotic - equipment sharing is not possible, your equipment is controlled by IND
"Name of INDI-profile to share via indihub",
)
flag.BoolVar(
- &flagWSServer,
- "ws-server",
- true,
- "launch Websocket server to control equipment via Websocket API",
- )
- flag.BoolVar(
- &flagWSIsTLS,
- "ws-tls",
+ &flagAPITLS,
+ "api-tls",
false,
- "serve web-socket over TLS with self-signed certificate",
+ "serve API-server over TLS with self-signed certificate",
)
flag.Uint64Var(
- &flagWSPort,
- "ws-port",
- defaultWSPort,
- "port to start web socket-server on",
+ &flagAPIPort,
+ "api-port",
+ defaultAPIPort,
+ "port to start API-server on",
)
flag.StringVar(
- &flagWSOrigins,
- "ws-origins",
+ &flagAPIOrigins,
+ "api-origins",
"",
- "comma-separated list of origins allowed to connect to WS-server",
+ "comma-separated list of origins allowed to connect to API-server",
)
}
func main() {
flag.Parse()
- if flagMode != modeSolo && flagMode != modeShare && flagMode != modeRobotic {
+ if flagMode != lib.ModeSolo && flagMode != lib.ModeShare && flagMode != lib.ModeRobotic {
log.Fatalf("Unknown mode '%s' provided\n", flagMode)
}
@@ -239,9 +225,9 @@ func main() {
Autoconnect: indiProfile.AutoConnect,
},
Drivers: make([]*indihub.INDIDriver, len(indiDrivers)),
- SoloMode: flagMode == modeSolo,
+ SoloMode: flagMode == lib.ModeSolo,
IsPHD2: flagPHD2ServerAddr != "",
- IsRobotic: flagMode == modeRobotic,
+ IsRobotic: flagMode == lib.ModeRobotic,
IsBroadcast: false,
AgentVersion: version.AgentVersion,
Os: runtime.GOOS,
@@ -287,6 +273,8 @@ func main() {
log.Fatal(err)
}
log.Println("...OK")
+ // close grpc client connection at the very end
+ defer conn.Close()
indiHubClient := indihub.NewINDIHubClient(conn)
@@ -296,21 +284,7 @@ func main() {
log.Fatal(err)
}
- log.Println("Current agent version:", version.AgentVersion)
- log.Println("Latest agent version:", regInfo.AgentVersion)
-
- if version.AgentVersion < regInfo.AgentVersion {
- yc := color.New(color.FgYellow)
- yc.Println()
- yc.Println(" ************************************************************")
- yc.Println(" * WARNING: you version of agent is outdated! *")
- yc.Println(" * *")
- yc.Println(" * Please download the latest version from: *")
- yc.Println(" * https://indihub.space/downloads *")
- yc.Println(" * *")
- yc.Println(" ************************************************************")
- yc.Println(" ")
- }
+ version.CheckAgentVersion(regInfo.AgentVersion)
log.Printf("Access token: %s\n", regInfo.Token)
log.Printf("Host session token: %s\n", regInfo.SessionIDPublic)
@@ -325,203 +299,40 @@ func main() {
}
}
- // start WS-server
- wsServer := websockets.NewWsServer(
+ // prepare all modes
+ soloMode := solo.NewMode(indiHubClient, regInfo, indiServerAddr, ccdDrivers)
+ shareMode := share.NewMode(indiHubClient, regInfo, indiServerAddr, flagPHD2ServerAddr, lib.ModeShare)
+ roboticMode := share.NewMode(indiHubClient, regInfo, indiServerAddr, flagPHD2ServerAddr, lib.ModeRobotic)
+
+ // start API-server
+ apiServer := apiserver.NewAPIServer(
regInfo.Token,
indiServerAddr,
flagPHD2ServerAddr,
- flagWSPort,
- flagWSIsTLS,
- flagWSOrigins,
+ flagAPIPort,
+ flagAPITLS,
+ flagAPIOrigins,
+ flagMode,
+ flagINDIProfile,
+ map[string]apiserver.AgentMode{
+ lib.ModeSolo: soloMode,
+ lib.ModeShare: shareMode,
+ lib.ModeRobotic: roboticMode,
+ },
)
- go wsServer.Start()
- // start session
- switch flagMode {
+ go func() {
+ sigint := make(chan os.Signal, 1)
+ signal.Notify(sigint, os.Interrupt, os.Kill)
- case modeSolo:
- // solo mode - equipment sharing is not available but host still sends all images to INDIHUB
- log.Println("'solo' parameter was provided. Your session is in solo-mode: equipment sharing is not available")
- log.Println("Starting INDIHUB agent in solo mode!")
+ <-sigint
- soloClient, err := indiHubClient.SoloMode(context.Background())
- if err != nil {
- log.Fatalf("Could not start agent in solo mode: %v", err)
- }
+ log.Println("Stopping API-server gracefully")
- soloAgent := solo.New(
- indiServerAddr,
- soloClient,
- ccdDrivers,
- )
+ // close connections to local INDI-server
+ apiServer.Stop()
+ }()
- go func() {
- sigint := make(chan os.Signal, 1)
- signal.Notify(sigint, os.Interrupt, os.Kill)
-
- <-sigint
-
- // stop WS-server
- wsServer.Stop()
-
- log.Println("Closing INDIHUB solo-session")
-
- // close connections to local INDI-server and to INDI client
- soloAgent.Close()
-
- time.Sleep(1 * time.Second)
-
- // close grpc client connection
- conn.Close()
- }()
-
- // start solo mode INDI-server tcp-proxy
- wg := sync.WaitGroup{}
- wg.Add(1)
- go func() {
- defer wg.Done()
- soloAgent.Start(regInfo.SessionID, regInfo.SessionIDPublic)
- }()
-
- wg.Wait()
-
- case modeShare, modeRobotic:
- // main equipment sharing mode
- if flagMode == modeRobotic {
- log.Println("'robotic' parameter was provided. Your session is in robotic-mode: equipment sharing is not available")
- }
- // open INDI server tunnel
- log.Println("Starting INDI-Server in the cloud...")
- indiServTunnel, err := indiHubClient.INDIServer(
- context.Background(),
- )
- if err != nil {
- log.Fatal(err)
- }
- log.Println("...OK")
-
- indiFilterConf := &hostutils.INDIFilterConfig{} // TODO: add reading config
- indiFilter := hostutils.NewINDIFilter(indiFilterConf)
- indiServerProxy := proxy.New("INDI-Server", indiServerAddr, indiServTunnel, indiFilter)
-
- // start PHD2 server proxy if specified
- var phd2ServerProxy *proxy.TcpProxy
- if flagPHD2ServerAddr != "" {
- // open PHD2 server tunnel
- log.Println("Starting PHD2-Server in the cloud...")
- phd2ServTunnel, err := indiHubClient.PHD2Server(
- context.Background(),
- )
- if err != nil {
- log.Fatal(err)
- }
- log.Println("...OK")
- phd2ServerProxy = proxy.New("PHD2-Server", flagPHD2ServerAddr, phd2ServTunnel, nil)
- }
-
- go func() {
- sigint := make(chan os.Signal, 1)
- signal.Notify(sigint, os.Interrupt, os.Kill)
-
- <-sigint
-
- // stop WS-server
- wsServer.Stop()
-
- // close connections to tunnels
- indiServTunnel.CloseSend()
- if phd2ServerProxy != nil {
- phd2ServerProxy.Tunnel.CloseSend()
- }
-
- // close grpc client connection
- conn.Close()
-
- // close connections to local INDI-server and PHD2-Server
- indiServerProxy.Close()
- if phd2ServerProxy != nil {
- phd2ServerProxy.Close()
- }
- }()
-
- serverAddrChan := make(chan proxy.PublicServerAddr, 3)
-
- wg := sync.WaitGroup{}
-
- // INDI Server Proxy start
- waitNum := 1
- wg.Add(1)
- go func() {
- defer wg.Done()
- indiServerProxy.Start(serverAddrChan, regInfo.SessionID, regInfo.SessionIDPublic)
- }()
-
- if flagPHD2ServerAddr != "" {
- waitNum = 2
- wg.Add(1)
- go func() {
- defer wg.Done()
- phd2ServerProxy.Start(serverAddrChan, regInfo.SessionID, regInfo.SessionIDPublic)
- }()
- }
-
- addrData := []proxy.PublicServerAddr{}
- for i := 0; i < waitNum; i++ {
- sAddr := <-serverAddrChan
- addrData = append(addrData, sAddr)
- }
-
- c := color.New(color.FgCyan)
- gc := color.New(color.FgGreen)
- yc := color.New(color.FgYellow)
- rc := color.New(color.FgMagenta)
- if flagMode != modeRobotic {
- c.Println()
- c.Println(" ************************************************************")
- c.Println(" * INDIHUB public address list!! *")
- c.Println(" ************************************************************")
- c.Println(" ")
- for _, sAddr := range addrData {
- gc.Printf(" %s: %s\n", sAddr.Name, sAddr.Addr)
- }
- c.Println(" ")
- c.Println(" ************************************************************")
- c.Println()
- c.Println(" Please provide your guest with this information:")
- c.Println()
- c.Println(" 1. Public address list from the above")
- c.Println(" 2. Focal length and aperture of your main telescope")
- c.Println(" 3. Focal length and aperture of your guiding telescope")
- c.Println(" 4. Type of guiding you use: PHD2 or guiding via camera")
- c.Println(" 5. Names of your imaging camera and guiding cameras")
- c.Println()
- yc.Println(" NOTE: These public addresses will be available ONLY until")
- yc.Println(" agent is running! (Ctrl+C will stop the session)")
- c.Println()
- } else {
- c.Println()
- c.Println(" ************************************************************")
- c.Println(" * INDIHUB robotic-session started!! *")
- c.Println(" ************************************************************")
- c.Println(" ")
- }
-
- wg.Wait()
-
- c.Println()
- c.Println(" ************************************************************")
- c.Println(" * INDIHUB session finished!! *")
- c.Println(" ************************************************************")
- c.Println(" ")
- if flagMode != modeRobotic {
- for _, sAddr := range addrData {
- rc.Printf(" %s: %s - CLOSED!!\n", sAddr.Name, sAddr.Addr)
- }
- } else {
- c.Println(" * INDIHUB robotic-session finished. *")
- c.Println(" * Thank you for your contribution! *")
- }
- c.Println(" ")
- c.Println(" ************************************************************")
- }
+ // start API-server and indihub-agent in the current mode
+ apiServer.Start()
}
diff --git a/proxy/proxy.go b/proxy/proxy.go
index 0da0bf5..f141768 100644
--- a/proxy/proxy.go
+++ b/proxy/proxy.go
@@ -30,8 +30,8 @@ type TcpProxy struct {
}
type PublicServerAddr struct {
- Name string
- Addr string
+ Name string `json:"name"`
+ Addr string `json:"addr"`
}
func New(name string, addr string, tunnel INDIHubTunnel, filter *hostutils.INDIFilter) *TcpProxy {
diff --git a/share/mode.go b/share/mode.go
new file mode 100644
index 0000000..533e936
--- /dev/null
+++ b/share/mode.go
@@ -0,0 +1,178 @@
+package share
+
+import (
+ "context"
+ "log"
+
+ "github.com/fatih/color"
+
+ "github.com/indihub-space/agent/hostutils"
+ "github.com/indihub-space/agent/lib"
+ "github.com/indihub-space/agent/proto/indihub"
+ "github.com/indihub-space/agent/proxy"
+)
+
+type Mode struct {
+ indiHubClient indihub.INDIHubClient
+ regInfo *indihub.RegisterInfo
+
+ indiServerAddr string
+ phd2ServerAddr string
+
+ addrData []proxy.PublicServerAddr
+
+ stopCh chan struct{}
+ status string
+ mode string
+}
+
+func NewMode(indiHubClient indihub.INDIHubClient, regInfo *indihub.RegisterInfo, indiServerAddr string, phd2ServerAddr string, mode string) *Mode {
+ return &Mode{
+ indiHubClient: indiHubClient,
+ regInfo: regInfo,
+ indiServerAddr: indiServerAddr,
+ phd2ServerAddr: phd2ServerAddr,
+ mode: mode,
+ addrData: []proxy.PublicServerAddr{},
+ stopCh: make(chan struct{}, 1),
+ }
+}
+
+func (m *Mode) Start() {
+ // main equipment sharing mode
+ if m.mode == lib.ModeRobotic {
+ log.Println("'robotic' parameter was provided. Your session is in robotic-mode: equipment sharing is not available")
+ }
+ // open INDI server tunnel
+ log.Println("Starting INDI-Server in the cloud...")
+ indiServTunnel, err := m.indiHubClient.INDIServer(
+ context.Background(),
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println("...OK")
+
+ indiFilterConf := &hostutils.INDIFilterConfig{} // TODO: add reading config
+ indiFilter := hostutils.NewINDIFilter(indiFilterConf)
+ indiServerProxy := proxy.New("INDI-Server", m.indiServerAddr, indiServTunnel, indiFilter)
+
+ // start PHD2 server proxy if specified
+ var phd2ServerProxy *proxy.TcpProxy
+ if m.phd2ServerAddr != "" {
+ // open PHD2 server tunnel
+ log.Println("Starting PHD2-Server in the cloud...")
+ phd2ServTunnel, err := m.indiHubClient.PHD2Server(
+ context.Background(),
+ )
+ if err != nil {
+ log.Fatal(err)
+ }
+ log.Println("...OK")
+ phd2ServerProxy = proxy.New("PHD2-Server", m.phd2ServerAddr, phd2ServTunnel, nil)
+ }
+
+ go func() {
+ <-m.stopCh
+
+ log.Printf("Closing %s-session\n", m.mode)
+
+ // close connections to tunnels
+ indiServTunnel.CloseSend()
+ if phd2ServerProxy != nil {
+ phd2ServerProxy.Tunnel.CloseSend()
+ }
+
+ // close connections to local INDI-server and PHD2-Server
+ indiServerProxy.Close()
+ if phd2ServerProxy != nil {
+ phd2ServerProxy.Close()
+ }
+ }()
+
+ serverAddrChan := make(chan proxy.PublicServerAddr, 3)
+
+ // INDI Server Proxy start
+ go indiServerProxy.Start(serverAddrChan, m.regInfo.SessionID, m.regInfo.SessionIDPublic)
+ sAddr := <-serverAddrChan
+ if m.mode != lib.ModeRobotic {
+ m.addrData = append(m.addrData, sAddr)
+ }
+
+ // PHD2 Server proxy start
+ if m.phd2ServerAddr != "" {
+ go phd2ServerProxy.Start(serverAddrChan, m.regInfo.SessionID, m.regInfo.SessionIDPublic)
+ sAddr := <-serverAddrChan
+ if m.mode != lib.ModeRobotic {
+ m.addrData = append(m.addrData, sAddr)
+ }
+ }
+
+ c := color.New(color.FgCyan)
+ gc := color.New(color.FgGreen)
+ yc := color.New(color.FgYellow)
+ if m.mode != lib.ModeRobotic {
+ c.Println()
+ c.Println(" ************************************************************")
+ c.Println(" * INDIHUB public address list!! *")
+ c.Println(" ************************************************************")
+ c.Println(" ")
+ for _, sAddr := range m.addrData {
+ gc.Printf(" %s: %s\n", sAddr.Name, sAddr.Addr)
+ }
+ c.Println(" ")
+ c.Println(" ************************************************************")
+ c.Println()
+ c.Println(" Please provide your guest with this information:")
+ c.Println()
+ c.Println(" 1. Public address list from the above")
+ c.Println(" 2. Focal length and aperture of your main telescope")
+ c.Println(" 3. Focal length and aperture of your guiding telescope")
+ c.Println(" 4. Type of guiding you use: PHD2 or guiding via camera")
+ c.Println(" 5. Names of your imaging camera and guiding cameras")
+ c.Println()
+ yc.Println(" NOTE: These public addresses will be available ONLY until")
+ yc.Println(" agent is running! (Ctrl+C will stop the session)")
+ c.Println()
+ } else {
+ c.Println()
+ c.Println(" ************************************************************")
+ c.Println(" * INDIHUB robotic-session started!! *")
+ c.Println(" ************************************************************")
+ c.Println(" ")
+ }
+
+ m.status = "running"
+}
+
+func (m *Mode) Stop() {
+ m.status = "stopped"
+ m.stopCh <- struct{}{}
+
+ c := color.New(color.FgCyan)
+ rc := color.New(color.FgMagenta)
+ c.Println()
+ c.Println(" ************************************************************")
+ c.Println(" * INDIHUB session finished!! *")
+ c.Println(" ************************************************************")
+ c.Println(" ")
+ if m.mode != lib.ModeRobotic {
+ for _, sAddr := range m.addrData {
+ rc.Printf(" %s: %s - CLOSED!!\n", sAddr.Name, sAddr.Addr)
+ }
+ } else {
+ c.Println(" * INDIHUB robotic-session finished. *")
+ c.Println(" * Thank you for your contribution! *")
+ }
+ c.Println(" ")
+ c.Println(" ************************************************************")
+
+ m.addrData = []proxy.PublicServerAddr{}
+}
+
+func (m *Mode) GetStatus() map[string]interface{} {
+ return map[string]interface{}{
+ "status": m.status,
+ "publicEndpoints": m.addrData,
+ }
+}
diff --git a/solo/mode.go b/solo/mode.go
new file mode 100644
index 0000000..238864c
--- /dev/null
+++ b/solo/mode.go
@@ -0,0 +1,70 @@
+package solo
+
+import (
+ "context"
+ "log"
+
+ "github.com/indihub-space/agent/proto/indihub"
+)
+
+type Mode struct {
+ indiServerAddr string
+ indiHubClient indihub.INDIHubClient
+ regInfo *indihub.RegisterInfo
+ ccdDrivers []string
+
+ stopCh chan struct{}
+ status string
+}
+
+func NewMode(indiHubClient indihub.INDIHubClient, regInfo *indihub.RegisterInfo, indiServerAddr string, ccdDrivers []string) *Mode {
+ return &Mode{
+ indiServerAddr: indiServerAddr,
+ indiHubClient: indiHubClient,
+ regInfo: regInfo,
+ ccdDrivers: ccdDrivers,
+ stopCh: make(chan struct{}, 1),
+ }
+}
+
+func (s *Mode) Start() {
+ // solo mode - equipment sharing is not available but host still sends all images to INDIHUB
+ log.Println("'solo' parameter was provided. Your session is in solo-mode: equipment sharing is not available")
+ log.Println("Starting INDIHUB agent in solo mode!")
+
+ soloClient, err := s.indiHubClient.SoloMode(context.Background())
+ if err != nil {
+ log.Fatalf("Could not start agent in solo mode: %v", err)
+ }
+
+ soloAgent := New(
+ s.indiServerAddr,
+ soloClient,
+ s.ccdDrivers,
+ )
+
+ go func() {
+ <-s.stopCh
+ log.Println("Closing INDIHUB solo-session")
+ // close connections to local INDI-server
+ soloAgent.Close()
+ }()
+
+ // start agent in solo-mode
+ go func() {
+ soloAgent.Start(s.regInfo.SessionID, s.regInfo.SessionIDPublic)
+ }()
+
+ s.status = "running"
+}
+
+func (s *Mode) Stop() {
+ s.status = "stopped"
+ s.stopCh <- struct{}{}
+}
+
+func (s *Mode) GetStatus() map[string]interface{} {
+ return map[string]interface{}{
+ "status": s.status,
+ }
+}
diff --git a/version/version.go b/version/version.go
index c4746eb..ff3fa0b 100644
--- a/version/version.go
+++ b/version/version.go
@@ -1,3 +1,27 @@
package version
+import (
+ "log"
+
+ "github.com/fatih/color"
+)
+
var AgentVersion = "1.0.2"
+
+func CheckAgentVersion(latestVer string) {
+ log.Println("Current agent version:", AgentVersion)
+ log.Println("Latest agent version:", latestVer)
+
+ if AgentVersion < latestVer {
+ yc := color.New(color.FgYellow)
+ yc.Println()
+ yc.Println(" ************************************************************")
+ yc.Println(" * WARNING: you version of agent is outdated! *")
+ yc.Println(" * *")
+ yc.Println(" * Please download the latest version from: *")
+ yc.Println(" * https://indihub.space/downloads *")
+ yc.Println(" * *")
+ yc.Println(" ************************************************************")
+ yc.Println(" ")
+ }
+}
diff --git a/websockets/server.go b/websockets/server.go
deleted file mode 100644
index 213c186..0000000
--- a/websockets/server.go
+++ /dev/null
@@ -1,221 +0,0 @@
-package websockets
-
-import (
- "context"
- "fmt"
- "log"
- "net"
- "net/http"
- "net/url"
- "strings"
-
- "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,
-}
-
-type WsServer struct {
- token string
- indiServerAddr string
- phd2ServerAddr string
- wsPort uint64
- isTLS bool
- origins string
-
- e *echo.Echo
- upgrader websocket.Upgrader
- connList []net.Conn
-}
-
-func NewWsServer(token string, indiServerAddr string, phd2ServerAddr string, wsPort uint64, isTLS bool, origins string) *WsServer {
- wsServer := &WsServer{
- token: token,
- indiServerAddr: indiServerAddr,
- phd2ServerAddr: phd2ServerAddr,
- wsPort: wsPort,
- isTLS: isTLS,
- e: echo.New(),
- upgrader: websocket.Upgrader{},
- connList: []net.Conn{},
- }
-
- 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
- wsServer.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
- }
- return allowedOrigins[host]
- }
-
- return wsServer
-}
-
-func (s *WsServer) 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 *WsServer) newPHD2Connection(c echo.Context) error {
- return nil
-}
-
-func (s *WsServer) Start() {
- s.e.HideBanner = true
- s.e.HidePort = true
-
- if !logutil.IsDev {
- s.e.Logger.SetLevel(elog.OFF)
- }
-
- 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))
- }
- // 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},
- }))
-
- // set auth middleware
- s.e.Use(middleware.KeyAuthWithConfig(middleware.KeyAuthConfig{
- KeyLookup: "query:token",
- Validator: func(token string, eCtx echo.Context) (b bool, err error) {
- return token == s.token, nil
- },
- }))
-
- s.e.GET("/indiserver", s.newIndiConnection)
- s.e.GET("/phd2server", s.newPHD2Connection)
-
- // 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 WSS server, cert generation failed:", err)
- return
- }
- err = s.e.StartTLS(fmt.Sprintf(":%d", s.wsPort), certFile, keyFile)
- if err != nil {
- log.Println("WSS server error:", err)
- }
- return
- }
-
- // run WS
- err := s.e.Start(fmt.Sprintf(":%d", s.wsPort))
- if err != nil {
- log.Println("WSS server error:", err)
- }
-}
-
-func (s *WsServer) Stop() {
- for _, conn := range s.connList {
- conn.Close()
- }
- s.e.Shutdown(context.Background())
-}