webextensions

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 18, 2021 License: GPL-3.0 Imports: 11 Imported by: 0

README

#+PROPERTY: header-args :eval never :exports code :results silent
#+title: Go native applications for browser extensions

Essentially this is an adapter for the Go [[https://golang.org/pkg/net/rpc/jsonrpc][net/rpc/jsonrpc]] standard package
that implements communication with browsers through =stdin= and =stdout=
pipes using a kind of streaming JSON protocol with package size
as a binary header.

* Examples of code

If you are eager to see a code example, the following is a snippet
of native application:
#+begin_src go
  rpc.RegisterName("example", &ExampleBackend{})
  rpc.ServeCodec(webextensions.NewServerCodecSplit(
	  os.Stdin, os.Stdout, jsonrpc.NewServerCodec))
#+end_src

In add-on code you would likely prefer to have ~Promise~ based
interface. Under the hood it would do something like
#+begin_src js
  const port = browser.runtime.connectNative(BACKEND_ID);
  port.onMessage.addListener(console.log);
  port.postMessage({id, method: "example.Method", params: [arg]});
#+end_src

Have a look at [[file:../../examples][=../../examples/]] directory for a complete example
of a backend and a Firefox extension.

* Rationale of native messaging application for browser add-ons

To allow browser extensions (WebExtensions) access to local files or
running external applications, it is necessary to install
a special application and to use native messaging protocol
for communication with it.

See developer reference for details:

- Firefox: <https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging>
- Chrome: <https://developer.chrome.com/apps/nativeMessaging>

Requirement of intermediate application originates from security reasons.
Browsers protect users by sandboxing web content and add-ons.
Native backend is a way to escape this jail and to allow
operation with local files. At the same time it is additional risk
that, having full access to filesystem, a malicious
add-on might steal sensitive data with help of backend application.
However installing and configuring of external application in addition
to a browser extension is an extra step for users that reduces the chance
that additional privileges are unintentional.
The expected and unavoidable consequence of such policy is that native
backends are rarely used.

Local HTTP server is not always a replacement of a native
messaging application. Various sites inspect open ports in your
local network sending requests from your browser.
Starter guides how to create an RPC server often skip issues
related to authentication and local TLS certificate authority.
Provided examples usually even do not limit listen addresses
of a TCP port to the loopback interface.
As a result backend is exposed to whole local network
that may be large enough when such server is used in an office.
Even if 127.0.0.1 of =::1=  optional argument of listen call is not missed,
any application (maybe one belongs to other user) on the local machine
can connect to the TCP port.
The opposite side of hassle with configuration of native messaging
host is more direct and so private access to local files
(of course, only if the backend is a trusted application).

* Go package for native messaging hosts in details

JSON is a natural way to exchange data from browser's JavaScript.
Additional layer of JSON-RPC protocol allows to reliably
discriminate regular results, runtime errors handled by application,
and serious problems lead to crash.

Since packages have identifiers, it is possible to process
several queries in parallel. However some measures
to limit concurrent heavy queries may be required
and they have not been taken yet.

This package has been developed for the RPC framework
from the standard [[https://golang.org/pkg/net/rpc][net/rpc]] package (a frozen one). More precisely
it provides a decorator for ~ServerCodec~
from the [[https://golang.org/pkg/net/rpc/jsonrpc][net/rpc/jsonrpc]] package. Such approach
has some limitations:

- JSON-RPC 1.0 only.
- Single parameter passed as single element array.
- No support of contexts[fn:gocontext] that allows to associate some data
  with particular request, to impose a timeout, or to cancel
  processing.
- Structured errors are impossible due to a filed of string type in
  the intermediate structure from =net/rpc= package.


Maybe it is possible to use utilities from this package
with a third-party Go RPC package following contemporary best practices.

[fn:gocontext] Sameer Ajmani. [[https://blog.golang.org/context][Go Concurrency Patterns: Context]].
The Go Blog. 29 July 2014.


This library provides just a couple of things that allow
WebExtensions native messaging protocol to fit into the Go RPC
framework.
- It handles message size in binary form before text message,
  a particular kind of [[https://en.wikipedia.org/wiki/JSON_streaming][JSON Streaming]]
- Unlike usual for =net/rpc= network connections, WebExtensions native
  messaging protocol uses separate =stdin= and =stdout= channels, so
  this library glues them into a single object.

Standard =net/rpc/jsonrpc= package uses capitalized method names
since they are directly mapped to Go exported functions.
As a workaround, another wrapper for =jsonrpc.ServerCodec= may be added

#+begin_src go
  if err := rpc.RegisterName("Example", backend); err != nil {
	  return err
  }
  methodMap := map[string]string{
	  "example.greet": "Example.Greet",
  }
  rpc.ServeCodec(webextensions.NewServerCodecSplit(
	  os.Stdin, os.Stdout,
	  webextensions.MappedServerCodecFactory(methodMap, jsonrpc.NewServerCodec)
  ))
#+end_src

~Manifest~ struct might help to generate native messaging manifest file.

~ClientCodec~ wrapper in this package facilitates
writing of backend test utilities.

This is a toy project that was started to get some impression of Go
programming in general and of standard library interfaces in
particular. It can be considered a bit over-engineered but it allows
to avoid dumb code with hand-written serialization and
deserialization. I hope it might be still useful to someone.

Documentation

Overview

WebExtensions native messaging support based on net/rpc and net/rpc/jsonrpc facilities from the standard library.

Most of the types and functions can be wiped from the public interface but such building blocks might be reused for similar protocols.

Example
package main

import (
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"

	"github.com/maxnikulin/burl/pkg/webextensions"
)

type Backend struct{}

func (b *Backend) Ping(query *string, reply *string) error {
	*reply = *query + "pong"
	return nil
}

func main() {
	rpc.RegisterName("Example", new(Backend))
	rpc.ServeCodec(webextensions.NewServerCodecSplit(os.Stdin, os.Stdout, jsonrpc.NewServerCodec))
}
Output:

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrNoCommand = errors.New("Command is not specified")
View Source
var ErrOutBufferNotEmpty = errors.New("Output buffer is not empty on close")
View Source
var ErrReadBody = errors.New("Frame body has not read till its end")
View Source
var MaxInSize uint32 = 1024 * 1024 * 1024

Said to protect browser from buggy backends

View Source
var NativeEndian = binary.LittleEndian

Conditional compilation hack to bypass attempts of Go authors to make world better by prohibiting usage of native encoding. Real world spec originating from Chrome developers states:

"Each message ... is preceded with a 32-bit value containing
 the message length in native byte order."

Portable alternative https://github.com/koneu/natend relies on unsafe. Refused request for a similar feature in the "binary/encoding" package: https://groups.google.com/d/topic/golang-nuts/3GEzwKfRRQw

View Source
var SizeMismatchError = errors.New("Written frame size differs from actual")

Do not know if it may happen

View Source
var TooLargeError = errors.New("Frame size is too large")

Functions

func NewClientCodecSplit

func NewClientCodecSplit(reader io.ReadCloser, writer io.WriteCloser,
	parentFactory func(io.ReadWriteCloser) rpc.ClientCodec,
) rpc.ClientCodec

The purpose of client codec is usage in test applications that allows to debug backend without a browser. RPC client created with such codec is a usual RPC client.

Example
package main

import (
	"fmt"
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"
	"os/exec"

	"github.com/maxnikulin/burl/pkg/webextensions"
)

func main() {
	runClient := func() error {
		cmd := exec.Command("we_example", "--option", "argument")
		cmd.Stderr = os.Stderr
		cmdStdin, err := cmd.StdinPipe()
		if err != nil {
			return fmt.Errorf("piping command stdin: %w", err)
		}
		cmdStdout, err := cmd.StdoutPipe()
		if err != nil {
			return fmt.Errorf("piping command stdout: %w", err)
		}
		if err := cmd.Start(); err != nil {
			return fmt.Errorf("run backend: %w", err)
		}
		rpcClient := rpc.NewClientWithCodec(webextensions.NewClientCodecSplit(cmdStdout, cmdStdin, jsonrpc.NewClientCodec))
		query := "ping"
		var response string
		if err := rpcClient.Call("Backend.Ping", &query, &response); err != nil {
			return err
		} else {
			fmt.Printf("response: %s\n", response)
		}
		if err := rpcClient.Close(); err != nil {
			return fmt.Errorf("client close: %w", err)
		}
		if err := cmd.Wait(); err != nil {
			return fmt.Errorf("exec wait: %w", err)
		}
		return nil
	}
	runIt := false // compile-only example
	if runIt {
		if err := runClient(); err != nil {
			fmt.Printf("error: %s", err)
		}
	}
}
Output:

func NewServerCodecSplit

func NewServerCodecSplit(reader io.ReadCloser, writer io.WriteCloser,
	parentFactory CodecFactory,
) rpc.ServerCodec

The primary function in this library. Wrap server codec created by parentFactory (e.g. jsonrpc.NewServerCodec) for usage with split input and output streams and to handle preceding packet size.

Example
package main

import (
	"net/rpc"
	"net/rpc/jsonrpc"
	"os"

	"github.com/maxnikulin/burl/pkg/webextensions"
)

func main() {
	rpc.ServeCodec(webextensions.NewServerCodecSplit(os.Stdin, os.Stdout, jsonrpc.NewServerCodec))
}
Output:

Types

type ClientCommand

type ClientCommand struct {
	*rpc.Client
	Command *exec.Cmd
}

func NewClientCommand

func NewClientCommand(args []string) (client *ClientCommand, err error)

func (*ClientCommand) Close

func (c *ClientCommand) Close() error

type CodecFactory

type CodecFactory func(io.ReadWriteCloser) rpc.ServerCodec

func MappedServerCodecFactory

func MappedServerCodecFactory(methodMap map[string]string, parentFactory CodecFactory) CodecFactory

Usage:

methodMap := map[string]string{ "hello": "Addon.Hello" }
f := webextensions.MappedServerCodecFactory(methodMap, jsonrpc.NewServerCodec)))

Result can be passed to NewServerCodecSplit.

type FrameBufferedWriter

type FrameBufferedWriter struct {
	bytes.Buffer
	W io.Writer
}

func (*FrameBufferedWriter) Discard

func (w *FrameBufferedWriter) Discard()

func (*FrameBufferedWriter) Empty

func (w *FrameBufferedWriter) Empty() bool

func (*FrameBufferedWriter) WriteFrame

func (w *FrameBufferedWriter) WriteFrame() error

type FrameLimitedReader

type FrameLimitedReader struct {
	io.LimitedReader
}

FrameReader with binary uint32 size header as in Webextensions native messaging protocol. Internal helper that might be helpful in other application.

func (*FrameLimitedReader) ReadHeader

func (r *FrameLimitedReader) ReadHeader() error

type FrameReadWriteCloser

type FrameReadWriteCloser interface {
	FrameReader
	FrameWriter
	io.Closer
}

Wrapper interface that behaves as ordinary connection for RPC codec and use "size,data" format for communication with other end. It is an internal interface, but there is no reason to hide it.

func NewSplitFrameReadWriteCloser

func NewSplitFrameReadWriteCloser(reader io.ReadCloser, writer io.WriteCloser) FrameReadWriteCloser

type FrameReader

type FrameReader interface {
	io.Reader
	// Read packet size
	ReadHeader() error
}

Split continuous data into packets using "size,data" protocol. Call ReadHeader() then consume packet data using Read. More internal interface than public one.

func NewFrameLimitedReader

func NewFrameLimitedReader(reader io.ReadCloser) FrameReader

type FrameWriter

type FrameWriter interface {
	io.Writer
	WriteFrame() error
	// Likely have an underlying buffer. Returns if it is empty.
	Empty() bool
	// Annoying item, unsure if it is worth to check for non-empty buffer error
	Discard()
}

func NewFrameBufferedWriter

func NewFrameBufferedWriter(w io.WriteCloser) FrameWriter

type Manifest

type Manifest struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Path        string `json:"path"`
	Type        string `json:"type"`
	// Chrome
	AllowedOrigins []string `json:"allowed_origins,omitempty"`
	// Firefox
	AllowedExtensions []string `json:"allowed_extensions,omitempty"`
	ManifestPath      string   `json:"-"`
}

Due to excessive number of fields, there is no point in New function. Call Init to initialize Type field and validate.

func (*Manifest) Init

func (m *Manifest) Init() error

type MappedServerCodec

type MappedServerCodec struct {
	rpc.ServerCodec
	// contains filtered or unexported fields
}

Mapping for arbitrary names of RPC methods. To overcome limitations that method names must start with a capital letter otherwise they are not exported.

func (*MappedServerCodec) ReadRequestHeader

func (c *MappedServerCodec) ReadRequestHeader(r *rpc.Request) error

type SplitFrameReadWriteCloser

type SplitFrameReadWriteCloser struct {
	FrameReader
	FrameWriter
	R io.Closer // might be obtained from FrameReader using type cast
	W io.Closer
}

A mediator that merges stdin and stdout into unified connection expected by a RPC codec. Handle packet size before data during data exchange over IO streams. Is not supposed to be used directly in ordinary cases.

func (*SplitFrameReadWriteCloser) Close

func (s *SplitFrameReadWriteCloser) Close() error

func (*SplitFrameReadWriteCloser) Discard

func (s *SplitFrameReadWriteCloser) Discard()

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL