control RESTful API added

This commit is contained in:
dencoded 2020-03-03 13:52:52 -05:00
parent f8855ca56a
commit 51691334b3
10 changed files with 885 additions and 464 deletions

192
README.md
View file

@ -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
<getProperties version="1.7" />
```
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
<defNumberVector device="iOptron CEM25" name="TELESCOPE_INFO" label="Scope Properties" group="Options" state="Ok" perm="rw" timeout="60" timestamp="2020-02-20T21:52:23">
<defNumber name="TELESCOPE_APERTURE" label="Aperture (mm)" format="%g" min="10" max="5000" step="0">
120
</defNumber>
<defNumber name="TELESCOPE_FOCAL_LENGTH" label="Focal Length (mm)" format="%g" min="10" max="10000" step="0">
600
</defNumber>
<defNumber name="GUIDER_APERTURE" label="Guider Aperture (mm)" format="%g" min="10" max="5000" step="0">
50
</defNumber>
<defNumber name="GUIDER_FOCAL_LENGTH" label="Guider Focal Length (mm)" format="%g" min="10" max="10000" step="0">
162
</defNumber>
</defNumberVector>
```
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/).

363
apiserver/server.go Normal file
View file

@ -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())
}

View file

@ -1,4 +1,4 @@
package websockets
package apiserver
import (
"crypto/ecdsa"

View file

@ -6,4 +6,8 @@ const (
INDIServerMaxRecvMsgSize = 49152
INDIServerMaxSendMsgSize = 2048
ModeSolo = "solo"
ModeShare = "share"
ModeRobotic = "robotic"
)

291
main.go
View file

@ -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()
}

View file

@ -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 {

178
share/mode.go Normal file
View file

@ -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,
}
}

70
solo/mode.go Normal file
View file

@ -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,
}
}

View file

@ -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(" ")
}
}

View file

@ -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())
}