2016-01-22 19:08:26 -06:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2016-01-23 13:48:33 -06:00
|
|
|
"crypto/hmac"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/json"
|
2020-04-22 20:00:49 +02:00
|
|
|
"io"
|
2017-09-12 10:18:28 -04:00
|
|
|
"time"
|
2016-07-29 06:10:43 -05:00
|
|
|
|
2020-01-20 17:47:53 +01:00
|
|
|
"github.com/go-zeromq/zmq4"
|
2019-12-08 19:17:08 +01:00
|
|
|
"github.com/gofrs/uuid"
|
2016-01-22 19:08:26 -06:00
|
|
|
)
|
|
|
|
|
2016-07-29 16:52:27 -05:00
|
|
|
// MsgHeader encodes header info for ZMQ messages.
|
2016-01-23 13:48:33 -06:00
|
|
|
type MsgHeader struct {
|
2017-09-12 10:18:28 -04:00
|
|
|
MsgID string `json:"msg_id"`
|
|
|
|
Username string `json:"username"`
|
|
|
|
Session string `json:"session"`
|
|
|
|
MsgType string `json:"msg_type"`
|
|
|
|
ProtocolVersion string `json:"version"`
|
|
|
|
Timestamp string `json:"date"`
|
2016-01-22 19:08:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// ComposedMsg represents an entire message in a high-level structure.
|
|
|
|
type ComposedMsg struct {
|
2016-01-24 00:16:23 -06:00
|
|
|
Header MsgHeader
|
|
|
|
ParentHeader MsgHeader
|
|
|
|
Metadata map[string]interface{}
|
|
|
|
Content interface{}
|
2016-01-22 19:08:26 -06:00
|
|
|
}
|
|
|
|
|
2017-07-22 16:49:22 -04:00
|
|
|
// msgReceipt represents a received message, its return identities, and
|
|
|
|
// the sockets for communication.
|
|
|
|
type msgReceipt struct {
|
|
|
|
Msg ComposedMsg
|
|
|
|
Identities [][]byte
|
|
|
|
Sockets SocketGroup
|
|
|
|
}
|
|
|
|
|
2019-04-10 22:15:44 +02:00
|
|
|
// MIMEMap holds data that can be presented in multiple formats. The keys are MIME types
|
2019-04-11 22:22:37 +02:00
|
|
|
// and the values are the data formatted with respect to its MIME type.
|
|
|
|
// All maps should contain at least a "text/plain" representation with a string value.
|
2019-04-10 22:15:44 +02:00
|
|
|
type MIMEMap = map[string]interface{}
|
2018-06-04 21:55:35 +02:00
|
|
|
|
2019-04-11 22:22:37 +02:00
|
|
|
// Data is the exact structure returned to Jupyter.
|
|
|
|
// It allows to fully specify how a value should be displayed.
|
2019-04-10 22:15:44 +02:00
|
|
|
type Data = struct {
|
|
|
|
Data MIMEMap
|
|
|
|
Metadata MIMEMap
|
|
|
|
Transient MIMEMap
|
2018-06-04 21:55:35 +02:00
|
|
|
}
|
2017-07-22 16:49:22 -04:00
|
|
|
|
2016-01-22 19:08:26 -06:00
|
|
|
// InvalidSignatureError is returned when the signature on a received message does not
|
|
|
|
// validate.
|
2016-01-23 13:48:33 -06:00
|
|
|
type InvalidSignatureError struct{}
|
|
|
|
|
2016-01-22 19:08:26 -06:00
|
|
|
func (e *InvalidSignatureError) Error() string {
|
2016-01-23 13:48:33 -06:00
|
|
|
return "A message had an invalid signature"
|
2016-01-22 19:08:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// WireMsgToComposedMsg translates a multipart ZMQ messages received from a socket into
|
|
|
|
// a ComposedMsg struct and a slice of return identities. This includes verifying the
|
|
|
|
// message signature.
|
2016-07-29 16:52:27 -05:00
|
|
|
func WireMsgToComposedMsg(msgparts [][]byte, signkey []byte) (ComposedMsg, [][]byte, error) {
|
|
|
|
|
2016-01-23 13:48:33 -06:00
|
|
|
i := 0
|
|
|
|
for string(msgparts[i]) != "<IDS|MSG>" {
|
|
|
|
i++
|
|
|
|
}
|
2016-07-29 16:52:27 -05:00
|
|
|
identities := msgparts[:i]
|
2016-01-22 19:08:26 -06:00
|
|
|
|
2017-07-22 16:49:22 -04:00
|
|
|
// Validate signature.
|
2016-07-29 16:52:27 -05:00
|
|
|
var msg ComposedMsg
|
2016-01-23 13:48:33 -06:00
|
|
|
if len(signkey) != 0 {
|
|
|
|
mac := hmac.New(sha256.New, signkey)
|
2017-09-07 20:51:43 -04:00
|
|
|
for _, msgpart := range msgparts[i+2 : i+6] {
|
2016-01-23 13:48:33 -06:00
|
|
|
mac.Write(msgpart)
|
|
|
|
}
|
|
|
|
signature := make([]byte, hex.DecodedLen(len(msgparts[i+1])))
|
|
|
|
hex.Decode(signature, msgparts[i+1])
|
|
|
|
if !hmac.Equal(mac.Sum(nil), signature) {
|
|
|
|
return msg, nil, &InvalidSignatureError{}
|
|
|
|
}
|
|
|
|
}
|
2017-07-22 16:49:22 -04:00
|
|
|
|
|
|
|
// Unmarshal contents.
|
2016-01-23 13:48:33 -06:00
|
|
|
json.Unmarshal(msgparts[i+2], &msg.Header)
|
2016-01-24 00:16:23 -06:00
|
|
|
json.Unmarshal(msgparts[i+3], &msg.ParentHeader)
|
2016-01-23 13:48:33 -06:00
|
|
|
json.Unmarshal(msgparts[i+4], &msg.Metadata)
|
|
|
|
json.Unmarshal(msgparts[i+5], &msg.Content)
|
2016-07-29 16:52:27 -05:00
|
|
|
return msg, identities, nil
|
2016-01-22 19:08:26 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// ToWireMsg translates a ComposedMsg into a multipart ZMQ message ready to send, and
|
|
|
|
// signs it. This does not add the return identities or the delimiter.
|
2016-07-29 07:21:46 -05:00
|
|
|
func (msg ComposedMsg) ToWireMsg(signkey []byte) ([][]byte, error) {
|
|
|
|
|
|
|
|
msgparts := make([][]byte, 5)
|
2017-07-22 16:49:22 -04:00
|
|
|
|
2016-07-29 07:21:46 -05:00
|
|
|
header, err := json.Marshal(msg.Header)
|
|
|
|
if err != nil {
|
2017-07-22 16:49:22 -04:00
|
|
|
return msgparts, err
|
2016-07-29 07:21:46 -05:00
|
|
|
}
|
2016-01-23 13:48:33 -06:00
|
|
|
msgparts[1] = header
|
2016-07-29 07:21:46 -05:00
|
|
|
|
|
|
|
parentHeader, err := json.Marshal(msg.ParentHeader)
|
|
|
|
if err != nil {
|
2017-07-22 16:49:22 -04:00
|
|
|
return msgparts, err
|
2016-07-29 07:21:46 -05:00
|
|
|
}
|
2016-01-24 00:16:23 -06:00
|
|
|
msgparts[2] = parentHeader
|
2016-07-29 07:21:46 -05:00
|
|
|
|
2016-01-23 13:48:33 -06:00
|
|
|
if msg.Metadata == nil {
|
|
|
|
msg.Metadata = make(map[string]interface{})
|
|
|
|
}
|
2017-07-22 16:49:22 -04:00
|
|
|
|
2016-07-29 07:21:46 -05:00
|
|
|
metadata, err := json.Marshal(msg.Metadata)
|
|
|
|
if err != nil {
|
2017-07-22 16:49:22 -04:00
|
|
|
return msgparts, err
|
2016-07-29 07:21:46 -05:00
|
|
|
}
|
2016-01-23 13:48:33 -06:00
|
|
|
msgparts[3] = metadata
|
2016-07-29 07:21:46 -05:00
|
|
|
|
|
|
|
content, err := json.Marshal(msg.Content)
|
|
|
|
if err != nil {
|
2017-07-22 16:49:22 -04:00
|
|
|
return msgparts, err
|
2016-07-29 07:21:46 -05:00
|
|
|
}
|
2016-01-23 13:48:33 -06:00
|
|
|
msgparts[4] = content
|
2016-01-22 19:08:26 -06:00
|
|
|
|
2016-07-29 16:52:27 -05:00
|
|
|
// Sign the message.
|
2016-01-23 13:48:33 -06:00
|
|
|
if len(signkey) != 0 {
|
|
|
|
mac := hmac.New(sha256.New, signkey)
|
|
|
|
for _, msgpart := range msgparts[1:] {
|
|
|
|
mac.Write(msgpart)
|
|
|
|
}
|
|
|
|
msgparts[0] = make([]byte, hex.EncodedLen(mac.Size()))
|
|
|
|
hex.Encode(msgparts[0], mac.Sum(nil))
|
|
|
|
}
|
2016-01-22 19:08:26 -06:00
|
|
|
|
2017-07-22 16:49:22 -04:00
|
|
|
return msgparts, nil
|
2016-01-22 19:08:26 -06:00
|
|
|
}
|
|
|
|
|
2020-01-20 17:27:01 +01:00
|
|
|
// SendResponse sends a message back to return identities of the received message.
|
2020-01-20 17:47:53 +01:00
|
|
|
func (receipt *msgReceipt) SendResponse(socket zmq4.Socket, msg ComposedMsg) error {
|
2016-07-29 07:21:46 -05:00
|
|
|
|
2017-08-01 17:22:40 -04:00
|
|
|
msgParts, err := msg.ToWireMsg(receipt.Sockets.Key)
|
2016-07-29 07:21:46 -05:00
|
|
|
if err != nil {
|
2017-07-22 16:49:22 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-01-20 17:27:01 +01:00
|
|
|
var frames = make([][]byte, 0, len(receipt.Identities)+1+len(msgParts))
|
|
|
|
frames = append(frames, receipt.Identities...)
|
|
|
|
frames = append(frames, []byte("<IDS|MSG>"))
|
|
|
|
frames = append(frames, msgParts...)
|
|
|
|
|
2020-01-20 17:47:53 +01:00
|
|
|
err = socket.SendMulti(zmq4.NewMsgFrom(frames...))
|
2017-07-22 16:49:22 -04:00
|
|
|
if err != nil {
|
|
|
|
return err
|
2016-07-29 07:21:46 -05:00
|
|
|
}
|
2017-08-01 17:22:40 -04:00
|
|
|
|
|
|
|
return nil
|
2016-01-22 19:08:26 -06:00
|
|
|
}
|
|
|
|
|
2017-07-22 16:49:22 -04:00
|
|
|
// NewMsg creates a new ComposedMsg to respond to a parent message.
|
|
|
|
// This includes setting up its headers.
|
|
|
|
func NewMsg(msgType string, parent ComposedMsg) (ComposedMsg, error) {
|
2017-08-01 17:22:40 -04:00
|
|
|
var msg ComposedMsg
|
|
|
|
|
2016-01-24 00:16:23 -06:00
|
|
|
msg.ParentHeader = parent.Header
|
2016-01-23 13:48:33 -06:00
|
|
|
msg.Header.Session = parent.Header.Session
|
|
|
|
msg.Header.Username = parent.Header.Username
|
2016-01-24 00:16:23 -06:00
|
|
|
msg.Header.MsgType = msgType
|
2017-09-12 10:18:28 -04:00
|
|
|
msg.Header.ProtocolVersion = ProtocolVersion
|
|
|
|
msg.Header.Timestamp = time.Now().UTC().Format(time.RFC3339)
|
2017-07-22 16:49:22 -04:00
|
|
|
|
2016-07-29 07:21:46 -05:00
|
|
|
u, err := uuid.NewV4()
|
|
|
|
if err != nil {
|
2017-07-22 16:49:22 -04:00
|
|
|
return msg, err
|
2016-07-29 07:21:46 -05:00
|
|
|
}
|
2016-01-24 00:16:23 -06:00
|
|
|
msg.Header.MsgID = u.String()
|
2017-07-22 16:49:22 -04:00
|
|
|
|
|
|
|
return msg, nil
|
2016-01-23 13:48:33 -06:00
|
|
|
}
|
2017-08-25 21:56:25 -04:00
|
|
|
|
|
|
|
// Publish creates a new ComposedMsg and sends it back to the return identities over the
|
2017-09-07 20:51:43 -04:00
|
|
|
// IOPub channel.
|
2017-08-25 21:56:25 -04:00
|
|
|
func (receipt *msgReceipt) Publish(msgType string, content interface{}) error {
|
|
|
|
msg, err := NewMsg(msgType, receipt.Msg)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
msg.Content = content
|
2020-01-20 17:47:53 +01:00
|
|
|
return receipt.Sockets.IOPubSocket.RunWithSocket(func(iopub zmq4.Socket) error {
|
2018-06-02 18:50:13 +02:00
|
|
|
return receipt.SendResponse(iopub, msg)
|
|
|
|
})
|
2017-08-25 21:56:25 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Reply creates a new ComposedMsg and sends it back to the return identities over the
|
2017-09-07 20:51:43 -04:00
|
|
|
// Shell channel.
|
2017-08-25 21:56:25 -04:00
|
|
|
func (receipt *msgReceipt) Reply(msgType string, content interface{}) error {
|
|
|
|
msg, err := NewMsg(msgType, receipt.Msg)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
msg.Content = content
|
2020-01-20 17:47:53 +01:00
|
|
|
return receipt.Sockets.ShellSocket.RunWithSocket(func(shell zmq4.Socket) error {
|
2018-06-02 18:50:13 +02:00
|
|
|
return receipt.SendResponse(shell, msg)
|
|
|
|
})
|
2017-08-25 23:45:04 -04:00
|
|
|
}
|
|
|
|
|
2017-09-12 11:12:59 -04:00
|
|
|
// PublishKernelStatus publishes a status message notifying front-ends of the state the kernel is in. Supports
|
|
|
|
// states "starting", "busy", and "idle".
|
|
|
|
func (receipt *msgReceipt) PublishKernelStatus(status string) error {
|
2017-09-07 20:51:43 -04:00
|
|
|
return receipt.Publish("status",
|
|
|
|
struct {
|
2017-09-12 11:12:59 -04:00
|
|
|
ExecutionState string `json:"execution_state"`
|
2017-09-07 20:51:43 -04:00
|
|
|
}{
|
|
|
|
ExecutionState: status,
|
2017-08-25 23:45:04 -04:00
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// PublishExecutionInput publishes a status message notifying front-ends of what code is
|
|
|
|
// currently being executed.
|
2017-09-07 20:51:43 -04:00
|
|
|
func (receipt *msgReceipt) PublishExecutionInput(execCount int, code string) error {
|
|
|
|
return receipt.Publish("execute_input",
|
|
|
|
struct {
|
|
|
|
ExecCount int `json:"execution_count"`
|
|
|
|
Code string `json:"code"`
|
|
|
|
}{
|
2017-08-25 23:45:04 -04:00
|
|
|
ExecCount: execCount,
|
|
|
|
Code: code,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-04-10 22:15:44 +02:00
|
|
|
func ensure(bundle MIMEMap) MIMEMap {
|
|
|
|
if bundle == nil {
|
|
|
|
bundle = make(MIMEMap)
|
|
|
|
}
|
|
|
|
return bundle
|
|
|
|
}
|
|
|
|
|
2019-04-13 22:57:40 +02:00
|
|
|
func merge(a MIMEMap, b MIMEMap) MIMEMap {
|
|
|
|
if len(b) == 0 {
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
if a == nil {
|
|
|
|
a = make(MIMEMap)
|
|
|
|
}
|
|
|
|
for k, v := range b {
|
|
|
|
a[k] = v
|
|
|
|
}
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
|
2017-09-07 20:51:43 -04:00
|
|
|
// PublishExecuteResult publishes the result of the `execCount` execution as a string.
|
2018-06-11 01:51:40 +02:00
|
|
|
func (receipt *msgReceipt) PublishExecutionResult(execCount int, data Data) error {
|
2019-04-10 22:15:44 +02:00
|
|
|
return receipt.Publish("execute_result", struct {
|
|
|
|
ExecCount int `json:"execution_count"`
|
|
|
|
Data MIMEMap `json:"data"`
|
|
|
|
Metadata MIMEMap `json:"metadata"`
|
|
|
|
}{
|
|
|
|
ExecCount: execCount,
|
|
|
|
Data: data.Data,
|
|
|
|
Metadata: ensure(data.Metadata),
|
|
|
|
})
|
2017-08-25 23:45:04 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// PublishExecuteResult publishes a serialized error that was encountered during execution.
|
2017-09-07 20:51:43 -04:00
|
|
|
func (receipt *msgReceipt) PublishExecutionError(err string, trace []string) error {
|
|
|
|
return receipt.Publish("error",
|
|
|
|
struct {
|
|
|
|
Name string `json:"ename"`
|
|
|
|
Value string `json:"evalue"`
|
|
|
|
Trace []string `json:"traceback"`
|
|
|
|
}{
|
2017-08-25 23:45:04 -04:00
|
|
|
Name: "ERROR",
|
|
|
|
Value: err,
|
2017-09-07 20:51:43 -04:00
|
|
|
Trace: trace,
|
2017-08-25 23:45:04 -04:00
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
2017-09-24 21:28:21 -04:00
|
|
|
|
2018-04-07 16:34:50 +02:00
|
|
|
// PublishDisplayData publishes a single image.
|
2018-06-10 23:09:07 +02:00
|
|
|
func (receipt *msgReceipt) PublishDisplayData(data Data) error {
|
2019-04-10 22:15:44 +02:00
|
|
|
// copy Data in a struct with appropriate json tags
|
|
|
|
return receipt.Publish("display_data", struct {
|
|
|
|
Data MIMEMap `json:"data"`
|
|
|
|
Metadata MIMEMap `json:"metadata"`
|
|
|
|
Transient MIMEMap `json:"transient"`
|
|
|
|
}{
|
|
|
|
Data: data.Data,
|
|
|
|
Metadata: ensure(data.Metadata),
|
|
|
|
Transient: ensure(data.Transient),
|
|
|
|
})
|
2018-04-07 16:34:50 +02:00
|
|
|
}
|
|
|
|
|
2017-09-24 21:28:21 -04:00
|
|
|
const (
|
|
|
|
// StreamStdout defines the stream name for standard out on the front-end. It
|
|
|
|
// is used in `PublishWriteStream` to specify the stream to write to.
|
|
|
|
StreamStdout = "stdout"
|
|
|
|
|
|
|
|
// StreamStderr defines the stream name for standard error on the front-end. It
|
|
|
|
// is used in `PublishWriteStream` to specify the stream to write to.
|
|
|
|
StreamStderr = "stderr"
|
|
|
|
)
|
|
|
|
|
|
|
|
// PublishWriteStream prints the data string to a stream on the front-end. This is
|
|
|
|
// either `StreamStdout` or `StreamStderr`.
|
|
|
|
func (receipt *msgReceipt) PublishWriteStream(stream string, data string) error {
|
|
|
|
return receipt.Publish("stream",
|
|
|
|
struct {
|
|
|
|
Stream string `json:"name"`
|
|
|
|
Data string `json:"text"`
|
|
|
|
}{
|
|
|
|
Stream: stream,
|
|
|
|
Data: data,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// JupyterStreamWriter is an `io.Writer` implementation that writes the data to the notebook
|
|
|
|
// front-end.
|
|
|
|
type JupyterStreamWriter struct {
|
|
|
|
stream string
|
|
|
|
receipt *msgReceipt
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write implements `io.Writer.Write` by publishing the data via `PublishWriteStream`
|
2017-09-30 01:51:15 -04:00
|
|
|
func (writer *JupyterStreamWriter) Write(p []byte) (int, error) {
|
2017-09-24 21:28:21 -04:00
|
|
|
data := string(p)
|
2017-09-30 01:51:15 -04:00
|
|
|
n := len(p)
|
2017-09-24 21:28:21 -04:00
|
|
|
|
2017-09-30 01:51:15 -04:00
|
|
|
if err := writer.receipt.PublishWriteStream(writer.stream, data); err != nil {
|
|
|
|
return 0, err
|
2017-09-24 21:28:21 -04:00
|
|
|
}
|
|
|
|
|
2017-09-30 01:51:15 -04:00
|
|
|
return n, nil
|
2017-09-24 21:28:21 -04:00
|
|
|
}
|
2020-04-22 20:00:49 +02:00
|
|
|
|
|
|
|
type OutErr struct {
|
|
|
|
out io.Writer
|
|
|
|
err io.Writer
|
|
|
|
}
|