+
+ ֍
+ X
+ diff --git a/cmd/apps/skydex/README.md b/cmd/apps/skydex/README.md new file mode 100644 index 0000000000..043b2ca650 --- /dev/null +++ b/cmd/apps/skydex/README.md @@ -0,0 +1,52 @@ +# Skywire Chat app + +Chat implements basic text messaging between skywire visors. + +Messaging UI is exposed via web interface. + +Chat only supports one WEB client user at a time. + +## Local setup + +Create 2 visor config files: + +`skywire1.json` + +```json +{ + "apps": [ + { + "app": "skychat", + "version": "1.0", + "auto_start": true, + "port": 1 + } + ] +} +``` + +`skywire2.json` + +```json +{ + "apps": [ + { + "app": "skychat", + "version": "1.0", + "auto_start": true, + "port": 1, + "args": ["-addr", ":8002"] + } + ] +} +``` + +Compile binaries and start 2 visors: + +```bash +$ go build -o apps/skychat.v1.0 ./cmd/apps/skychat +$ ./skywire-visor skywire1.json +$ ./skywire-visor skywire2.json +``` + +Chat interface will be available on ports `8001` and `8002`. diff --git a/cmd/apps/skydex/skydex.go b/cmd/apps/skydex/skydex.go new file mode 100644 index 0000000000..fc2691ac01 --- /dev/null +++ b/cmd/apps/skydex/skydex.go @@ -0,0 +1,233 @@ +/* +skydex app for skywire visor +*/ +package main + +import ( + "context" + "embed" + "encoding/json" + "flag" + "fmt" + "io/fs" + "net" + "net/http" + "os" + "sync" + "time" + + ipc "github.com/james-barrow/golang-ipc" + "github.com/skycoin/skycoin/src/util/logging" + + "github.com/skycoin/skywire-utilities/pkg/cipher" + "github.com/skycoin/skywire-utilities/pkg/netutil" + "github.com/skycoin/skywire/pkg/app" + "github.com/skycoin/skywire/pkg/app/appnet" + "github.com/skycoin/skywire/pkg/app/launcher" + "github.com/skycoin/skywire/pkg/routing" + "github.com/skycoin/skywire/pkg/skyenv" +) + +const ( + netType = appnet.TypeSkynet + port = routing.Port(1) +) + +var log = logging.MustGetLogger("chat") +var addr = flag.String("addr", ":8585", "address to bind") +var r = netutil.NewRetrier(log, 50*time.Millisecond, netutil.DefaultMaxBackoff, 5, 2) + +var ( + appCl *app.Client + clientCh chan string + conns map[cipher.PubKey]net.Conn // Chat connections + connsMu sync.Mutex +) + +// the go embed static points to skywire/cmd/apps/skychat/static + +//go:embed static +var embededFiles embed.FS + +func main() { + appCl = app.NewClient(nil) + defer appCl.Close() + + fmt.Println(appCl.Config().VisorPK) + + err := http.ListenAndServe(*addr, nil) + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func listenLoop() { + l, err := appCl.Listen(netType, port) + if err != nil { + fmt.Printf("Error listening network %v on port %d: %v\n", netType, port, err) + return + } + + for { + fmt.Print("Accepting skychat conn...") + conn, err := l.Accept() + if err != nil { + fmt.Print("Failed to accept conn:", err) + return + } + fmt.Print("Accepted skychat conn") + + raddr := conn.RemoteAddr().(appnet.Addr) + connsMu.Lock() + conns[raddr.PubKey] = conn + connsMu.Unlock() + fmt.Printf("Accepted skychat conn on %s from %s\n", conn.LocalAddr(), raddr.PubKey) + + go handleConn(conn) + } +} + +func handleConn(conn net.Conn) { + raddr := conn.RemoteAddr().(appnet.Addr) + for { + buf := make([]byte, 32*1024) + n, err := conn.Read(buf) + if err != nil { + fmt.Print("Failed to read packet:", err) + raddr := conn.RemoteAddr().(appnet.Addr) + connsMu.Lock() + delete(conns, raddr.PubKey) + connsMu.Unlock() + return + } + + clientMsg, err := json.Marshal(map[string]string{"sender": raddr.PubKey.Hex(), "message": string(buf[:n])}) + if err != nil { + fmt.Printf("Failed to marshal json: %v", err) + } + select { + case clientCh <- string(clientMsg): + fmt.Printf("Received and sent to ui: %s\n", clientMsg) + default: + fmt.Printf("Received and trashed: %s\n", clientMsg) + } + } +} + +func messageHandler(ctx context.Context) func(w http.ResponseWriter, rreq *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + + data := map[string]string{} + if err := json.NewDecoder(req.Body).Decode(&data); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + pk := cipher.PubKey{} + if err := pk.UnmarshalText([]byte(data["recipient"])); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + addr := appnet.Addr{ + Net: netType, + PubKey: pk, + Port: 1, + } + connsMu.Lock() + conn, ok := conns[pk] + connsMu.Unlock() + + if !ok { + var err error + err = r.Do(ctx, func() error { + conn, err = appCl.Dial(addr) + return err + }) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + connsMu.Lock() + conns[pk] = conn + connsMu.Unlock() + + go handleConn(conn) + } + + _, err := conn.Write([]byte(data["message"])) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + + connsMu.Lock() + delete(conns, pk) + connsMu.Unlock() + + return + } + } +} + +func sseHandler(w http.ResponseWriter, req *http.Request) { + f, ok := w.(http.Flusher) + if !ok { + http.Error(w, "Streaming unsupported!", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + w.Header().Set("Transfer-Encoding", "chunked") + + for { + select { + case msg, ok := <-clientCh: + if !ok { + return + } + _, _ = fmt.Fprintf(w, "data: %s\n\n", msg) + f.Flush() + + case <-req.Context().Done(): + fmt.Print("SSE connection were closed.") + return + } + } +} + +func getFileSystem() http.FileSystem { + fsys, err := fs.Sub(embededFiles, "static") + if err != nil { + panic(err) + } + return http.FS(fsys) +} + +func handleIPCSignal(client *ipc.Client) { + for { + m, err := client.Read() + if err != nil { + fmt.Printf("%s IPC received error: %v", skyenv.SkychatName, err) + } + if m.MsgType == skyenv.IPCShutdownMessageType { + fmt.Println("Stopping " + skyenv.SkychatName + " via IPC") + break + } + } + os.Exit(0) +} + +func setAppStatus(appCl *app.Client, status launcher.AppDetailedStatus) { + if err := appCl.SetDetailedStatus(string(status)); err != nil { + fmt.Printf("Failed to set status %v: %v\n", status, err) + } +} + +func setAppError(appCl *app.Client, appErr error) { + if err := appCl.SetError(appErr.Error()); err != nil { + fmt.Printf("Failed to set error %v: %v\n", appErr, err) + } +} diff --git a/cmd/apps/skydex/static/index.html b/cmd/apps/skydex/static/index.html new file mode 100644 index 0000000000..df658eae03 --- /dev/null +++ b/cmd/apps/skydex/static/index.html @@ -0,0 +1,745 @@ + + + + +
+ + + + + + + + +