1
0
Fork 0
mirror of https://github.com/anyproto/anytype-ts.git synced 2025-06-07 21:47:02 +09:00
anytype-ts/go/nativeMessagingHost.go
2024-06-11 16:22:23 +02:00

503 lines
13 KiB
Go

/*
- This is the native messaging host for the AnyType browser extension.
- It enables the web extension to find the open ports of the AnyType application and to start it if it is not running.
- It is installed by the Electron script found in electron/js/lib/installNativeMessagingHost.js
*/
package main
import (
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"unsafe"
)
// UTILITY FUNCTIONS
// splits stdout into an array of lines, removing empty lines
func splitStdOutLines(stdout string) []string {
lines := strings.Split(stdout, "\n")
filteredLines := make([]string, 0)
for _, line := range lines {
if len(line) > 0 {
filteredLines = append(filteredLines, line)
}
}
return filteredLines
}
// splits stdout into an array of tokens, replacing tabs with spaces
func splitStdOutTokens(line string) []string {
return strings.Fields(strings.Replace(line, "\t", " ", -1))
}
// executes a command and returns the stdout as string
func execCommand(command string) (string, error) {
if runtime.GOOS == "windows" {
return execCommandWin(command)
}
stdout, err := exec.Command("bash", "-c", command).Output()
return string(stdout), err
}
func execCommandWin(command string) (string, error) {
// Splitting the command into the executable and the arguments
// For Windows, commands are executed through cmd /C
cmd := exec.Command("cmd", "/C", command)
stdout, err := cmd.Output()
return string(stdout), err
}
// checks if a string is contained in an array of strings
func contains(s []string, e string) bool {
for _, a := range s {
if a == e {
return true
}
}
return false
}
// CORE LOGIC
// Windows: returns a list of open ports for all instances of anytypeHelper.exe found using cli utilities tasklist, netstat and findstr
// Windows: returns a list of open ports for all instances of anytypeHelper.exe found using cli utilities tasklist, netstat and findstr
func getOpenPortsWindows() (map[string][]string, error) {
appName := "anytypeHelper.exe"
stdout, err := execCommand(`tasklist`)
if err != nil {
return nil, err
}
lines := splitStdOutLines(stdout)
pids := map[string]bool{}
for _, line := range lines {
if !strings.Contains(line, appName) {
continue
}
tokens := splitStdOutTokens(line)
pids[tokens[1]] = true
}
if len(pids) == 0 {
return nil, errors.New("application not running")
}
result := map[string][]string{}
for pid := range pids {
stdout, err := execCommand(`netstat -ano`)
if err != nil {
return nil, err
}
lines := splitStdOutLines(stdout)
ports := map[string]bool{}
for _, line := range lines {
if !strings.Contains(line, pid) || !strings.Contains(line, "LISTENING") {
continue
}
tokens := splitStdOutTokens(line)
port := strings.Split(tokens[1], ":")[1]
ports[port] = true
}
portsSlice := []string{}
for port := range ports {
portsSlice = append(portsSlice, port)
}
result[pid] = portsSlice
}
return result, nil
}
func isFileGateway(port string) (bool, error) {
client := &http.Client{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "http://127.0.0.1:"+port+"/file", nil)
if err != nil {
return false, err
}
// disable follow redirect
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
resp, err := client.Do(req)
if err != nil {
return false, err
}
bu := bytes.NewBuffer(nil)
resp.Request.Write(bu)
ioutil.ReadAll(bu)
defer resp.Body.Close()
// should return 301 redirect Location: /file/
if resp.StatusCode == 301 {
return true, err
}
return false, err
}
func isGrpcWebServer(port string) (bool, error) {
client := &http.Client{}
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
var data = strings.NewReader(`AAAAAAIQFA==`)
req, err := http.NewRequestWithContext(ctx, "POST", "http://127.0.0.1:"+port+"/anytype.ClientCommands/AppGetVersion", data)
if err != nil {
return false, err
}
req.Header.Set("Content-Type", "application/grpc-web-text")
req.Header.Set("X-Grpc-Web", "1")
resp, err := client.Do(req)
if err != nil {
return false, err
}
defer resp.Body.Close()
// should has Content-Type: application/grpc-web-text
if resp.Header.Get("Content-Type") == "application/grpc-web-text" {
return true, nil
}
return false, fmt.Errorf("unexpected content type: %s", resp.Header.Get("Content-Type"))
}
// MacOS and Linux: returns a list of all open ports for all instances of anytype found using cli utilities lsof and grep
func getOpenPortsUnix() (map[string][]string, error) {
// execute the command
appName := "anytype"
stdout, err := execCommand(`lsof -i -P -n | grep LISTEN | grep "` + appName + `"`)
Trace.Print(`lsof -i -P -n | grep LISTEN | grep "` + appName + `"`)
if err != nil {
Trace.Print(err)
return nil, err
}
// initialize the result map
result := make(map[string][]string)
// split the output into lines
lines := splitStdOutLines(stdout)
for _, line := range lines {
// normalize whitespace and split into tokens
tokens := splitStdOutTokens(line)
pid := tokens[1]
port := strings.Split(tokens[8], ":")[1]
// add the port to the result map
if _, ok := result[pid]; !ok {
result[pid] = []string{}
}
if !contains(result[pid], port) {
result[pid] = append(result[pid], port)
}
}
if len(result) == 0 {
return nil, errors.New("application not running")
}
return result, nil
}
// Windows, MacOS and Linux: returns a list of all open ports for all instances of anytype found using cli utilities
func getOpenPorts() (map[string][]string, error) {
// Get Platform
platform := runtime.GOOS
var (
ports map[string][]string
err error
)
// Platform specific functions
if platform == "windows" {
ports, err = getOpenPortsWindows()
if err != nil {
return nil, err
}
} else if platform == "darwin" {
ports, err = getOpenPortsUnix()
if err != nil {
return nil, err
}
} else if platform == "linux" {
ports, err = getOpenPortsUnix()
if err != nil {
return nil, err
}
} else {
return nil, errors.New("unsupported platform")
}
totalPids := len(ports)
for pid, pidports := range ports {
var gatewayPort, grpcWebPort string
var errs []error
for _, port := range pidports {
var (
errDetectGateway, errDetectGrpcWeb error
serviceDetected bool
)
if gatewayPort == "" {
if serviceDetected, errDetectGateway = isFileGateway(port); serviceDetected {
gatewayPort = port
}
}
// in case we already detected grpcweb port skip this
if !serviceDetected && grpcWebPort == "" {
if serviceDetected, errDetectGrpcWeb = isGrpcWebServer(port); serviceDetected {
grpcWebPort = port
}
}
if !serviceDetected {
// means port failed to detect either gateway or grpcweb
errs = append(errs, fmt.Errorf("port: %s; gateway: %v; grpcweb: %v", port, errDetectGateway, errDetectGrpcWeb))
}
}
if gatewayPort != "" && grpcWebPort != "" {
ports[pid] = []string{grpcWebPort, gatewayPort}
} else {
Trace.Printf("can't detect ports. pid: %s; grpc: '%s'; gateway: '%s'; error: %v;", pid, grpcWebPort, gatewayPort, errs)
delete(ports, pid)
}
}
if len(ports) > 0 {
Trace.Printf("found ports: %v", ports)
} else {
Trace.Printf("ports no able to detect for %d pids", totalPids)
}
return ports, nil
}
// Windows, MacOS and Linux: Starts AnyType as a detached process and returns the PID
func startApplication() (int, error) {
platform := runtime.GOOS
executablePath, err := os.Executable()
if err != nil {
return 0, err
}
// /Resources/app.asar.unpacked/dist/executable
appPath := filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(executablePath))))
if platform == "windows" {
appPath = filepath.Join(appPath, "Anytype.exe")
} else if platform == "darwin" {
appPath = filepath.Join(appPath, "MacOS", "Anytype")
} else if platform == "linux" {
appPath = filepath.Join(appPath, "anytype")
} else {
return 0, errors.New("unsupported platform")
}
Trace.Print("Starting Application on Platform: " + platform + " with Path: " + appPath)
sub := exec.Command(appPath)
err = sub.Start()
sub.Process.Release()
if err != nil {
return 0, err
}
return sub.Process.Pid, nil
}
// MESSAGING LOGIC
// constants for Logger
var (
// Trace logs general information messages.
Trace *log.Logger
// Error logs error messages.
Error *log.Logger
)
// nativeEndian used to detect native byte order
var nativeEndian binary.ByteOrder
// bufferSize used to set size of IO buffer - adjust to accommodate message payloads
var bufferSize = 8192
// IncomingMessage represents a message sent to the native host.
type IncomingMessage struct {
Type string `json:"type"`
}
// OutgoingMessage respresents a response to an incoming message query.
type OutgoingMessage struct {
Type string `json:"type"`
Response interface{} `json:"response"`
Error interface{} `json:"error"`
}
// Init initializes logger and determines native byte order.
func Init(traceHandle io.Writer, errorHandle io.Writer) {
Trace = log.New(traceHandle, "TRACE: ", log.Ldate|log.Ltime|log.Lshortfile)
Error = log.New(errorHandle, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile)
// determine native byte order so that we can read message size correctly
var one int16 = 1
b := (*byte)(unsafe.Pointer(&one))
if *b == 0 {
nativeEndian = binary.BigEndian
} else {
nativeEndian = binary.LittleEndian
}
}
func main() {
// create temp file for logging
tmpFileName := filepath.Join(os.TempDir(), "anytype-nmh.log")
file, err := os.OpenFile(tmpFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
Init(os.Stdout, os.Stderr)
Error.Printf("Unable to create and/or open log file. Will log to Stdout and Stderr. Error: %v", err)
} else {
Init(file, file)
// ensure we close the log file when we're done
defer file.Close()
}
Trace.Printf("Chrome native messaging host started")
read()
Trace.Print("Chrome native messaging host exited.")
}
// read Creates a new buffered I/O reader and reads messages from Stdin.
func read() {
v := bufio.NewReader(os.Stdin)
// adjust buffer size to accommodate your json payload size limits; default is 4096
s := bufio.NewReaderSize(v, bufferSize)
lengthBytes := make([]byte, 4)
lengthNum := int(0)
// we're going to indefinitely read the first 4 bytes in buffer, which gives us the message length.
// if stdIn is closed we'll exit the loop and shut down host
for b, err := s.Read(lengthBytes); b > 0 && err == nil; b, err = s.Read(lengthBytes) {
// convert message length bytes to integer value
lengthNum = readMessageLength(lengthBytes)
// If message length exceeds size of buffer, the message will be truncated.
// This will likely cause an error when we attempt to unmarshal message to JSON.
if lengthNum > bufferSize {
Error.Printf("Message size of %d exceeds buffer size of %d. Message will be truncated and is unlikely to unmarshal to JSON.", lengthNum, bufferSize)
}
// read the content of the message from buffer
content := make([]byte, lengthNum)
_, err := s.Read(content)
if err != nil && err != io.EOF {
Error.Fatal(err)
}
// message has been read, now parse and process
parseMessage(content)
}
Trace.Print("Stdin closed.")
}
// readMessageLength reads and returns the message length value in native byte order.
func readMessageLength(msg []byte) int {
var length uint32
buf := bytes.NewBuffer(msg)
err := binary.Read(buf, nativeEndian, &length)
if err != nil {
Error.Printf("Unable to read bytes representing message length: %v", err)
}
return int(length)
}
// parseMessage parses incoming message
func parseMessage(msg []byte) {
iMsg := decodeMessage(msg)
Trace.Printf("Message received: %s: %s", iMsg.Type, msg)
// start building outgoing json message
oMsg := OutgoingMessage{
Type: iMsg.Type,
}
switch iMsg.Type {
case "getPorts":
// Get open ports
openPorts, err := getOpenPorts()
if err != nil {
oMsg.Error = err.Error()
} else {
oMsg.Response = openPorts
}
case "launchApp":
// Start application
pid, err := startApplication()
if err != nil {
oMsg.Error = err.Error()
} else {
oMsg.Response = pid
}
}
send(oMsg)
}
// decodeMessage unmarshals incoming json request and returns query value.
func decodeMessage(msg []byte) IncomingMessage {
var iMsg IncomingMessage
err := json.Unmarshal(msg, &iMsg)
if err != nil {
Error.Printf("Unable to unmarshal json to struct: %v", err)
}
return iMsg
}
// send sends an OutgoingMessage to os.Stdout.
func send(msg OutgoingMessage) {
byteMsg := dataToBytes(msg)
writeMessageLength(byteMsg)
var msgBuf bytes.Buffer
_, err := msgBuf.Write(byteMsg)
if err != nil {
Error.Printf("Unable to write message length to message buffer: %v", err)
}
_, err = msgBuf.WriteTo(os.Stdout)
if err != nil {
Error.Printf("Unable to write message buffer to Stdout: %v", err)
}
}
// dataToBytes marshals OutgoingMessage struct to slice of bytes
func dataToBytes(msg OutgoingMessage) []byte {
byteMsg, err := json.Marshal(msg)
if err != nil {
Error.Printf("Unable to marshal OutgoingMessage struct to slice of bytes: %v", err)
}
return byteMsg
}
// writeMessageLength determines length of message and writes it to os.Stdout.
func writeMessageLength(msg []byte) {
err := binary.Write(os.Stdout, nativeEndian, uint32(len(msg)))
if err != nil {
Error.Printf("Unable to write message length to Stdout: %v", err)
}
}