SandpointsChromeExtension/native-host/src/main.go

267 lines
7.4 KiB
Go

package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"unsafe"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
// this project started by copying:
// https://github.com/jfarleyx/chrome-native-messaging-golang
// 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 * 8
// IncomingMessage represents a message sent to the native host.
type IncomingMessage struct {
Hugopage string `json:"hugopage"`
Filepath string `json:"filepath"`
Gitpath string `json:"gitpath"`
Publishpath string `json:"publishpath"`
Publish bool `json:"publish"`
Offline bool `json:"offline"`
Protocol string `json:"protocol"`
}
// OutgoingMessage respresents a response to an incoming message query.
type OutgoingMessage struct {
Query string `json:"query"`
Response string `json:"response"`
}
// 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() {
file, err := os.OpenFile(filepath.Join(os.TempDir(), "sandpoints-chromeext-log.txt"), 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. Native byte order: %v.", nativeEndian)
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)
Trace.Printf("IO buffer reader created with buffer size of %v.", s.Size())
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)
Trace.Printf("Message size in bytes: %v", lengthNum)
// 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)
}
// write incoming message to file
func writeMessageToFile(fpath string, content string) {
f, err := os.Create(fpath)
if err != nil {
Error.Printf("Write to file failed: %v", err)
}
defer f.Close()
f.WriteString(content)
}
// commit edit to git repo
func commitChangeToGit(iMsg IncomingMessage) {
g, err := git.PlainOpen(iMsg.Gitpath)
if err != nil {
Error.Printf("ERROR: Go-git Plain Open:", err)
}
w, err := g.Worktree()
if err != nil {
Error.Printf("ERROR: Go-git Worktree:", err)
}
// filePath := filepath.Base(iMsg.Filepath)
// editFile := filepath.Join(iMsg.Gitpath, filePath)
relPathFile := filepath.Join(strings.ReplaceAll(iMsg.Filepath, iMsg.Gitpath+"/", ""))
// err = ioutil.WriteFile(iMsg.Filepath, []byte(iMsg.Hugopage), 0644)
// check(err)
writeMessageToFile(iMsg.Filepath, iMsg.Hugopage)
_, err = w.Add(relPathFile)
if err != nil {
Error.Printf("ERROR: Go-git Add:", err)
}
tn := time.Now()
w.Commit(fmt.Sprintf("sandpoints-ext - %s - editing via browser...", tn), &git.CommitOptions{
Author: &object.Signature{
Name: "Sandpoints Edit Page",
Email: "editpage@sandpoints.org",
When: tn,
},
})
}
// trigger git hook
func triggerGitHook(gitpath string, publish bool, offline bool, protocol string) {
cmd := exec.Command(filepath.Join(gitpath, ".git", "hooks", "post-commit"))
stdin, err := cmd.StdinPipe()
if err != nil {
Error.Printf("StdinPipe:", err)
}
pub := "nope"
if publish {
pub = "publish"
}
offl := "nope"
if offline {
offl = "offline"
}
go func() {
defer stdin.Close()
io.WriteString(stdin, fmt.Sprintf("sandpoints-ext %s %s %s", pub, offl, protocol))
}()
out, err := cmd.CombinedOutput()
if err != nil {
Error.Printf("ERROR Combined output: %s", err)
}
Trace.Printf("Git Hook Combined Output: %s", out)
}
// parseMessage parses incoming message
func parseMessage(msg []byte) {
iMsg := decodeMessage(msg)
Trace.Printf("Message received: %s", msg)
commitChangeToGit(iMsg)
triggerGitHook(iMsg.Gitpath, iMsg.Publish, iMsg.Offline, iMsg.Protocol)
// start building outgoing json message
oMsg := OutgoingMessage{
Query: iMsg.Protocol,
}
switch iMsg.Protocol {
case "file":
oMsg.Response = iMsg.Publishpath
default:
oMsg.Response = "false"
}
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
}
// 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)
}
}