Skip to content
This repository was archived by the owner on Jan 14, 2022. It is now read-only.

Commit ac1c10c

Browse files
Implement connection, authentication, and heartbeat
1 parent 5bb458c commit ac1c10c

File tree

10 files changed

+263
-33
lines changed

10 files changed

+263
-33
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ language: go
22
go:
33
- 1.10.x
44
- master
5+
notifications:
6+
email: false

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
21
# go-obs-websocket
32

3+
[![Build Status](https://travis-ci.com/christopher-dG/go-obs-websocket.svg?branch=master)](https://travis-ci.com/christopher-dG/go-obs-websocket)
4+
45
`go-obs-websocket` is a package for interacting with [`obs-websocket`](https://github.com/Palakis/obs-websocket).
56

67
## Installation
@@ -12,9 +13,19 @@ go get github.com/christopher-dG/go-obs-websocket
1213
## Usage
1314

1415
```go
15-
import obs "github.com/christopher-dG/go-obs-websocket"
16+
package main
17+
18+
import (
19+
"log"
20+
21+
obs "github.com/christopher-dG/go-obs-websocket"
22+
)
1623

17-
client := obs.NewClientClient("localhost", 4444, "")
24+
client := obs.NewClient("localhost", 4444, "")
25+
if err := client.Connect(); err != nil {
26+
log.Fatal(err)
27+
}
28+
defer client.Close()
1829

1930
// TODO
2031
```

client.go

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,19 @@
11
package obsws
22

3-
// Client is the interface to OBS's websockets.
4-
type Client struct {
5-
host string
6-
port int
7-
password string
3+
import "github.com/gorilla/websocket"
4+
5+
// client is the interface to obs-websocket.
6+
type client struct {
7+
Host string // Host (probably "localhost").
8+
Port int // Port (OBS default is 4444).
9+
Password string // Password (OBS default is "").
10+
conn *websocket.Conn
11+
id int
812
}
913

10-
// NewClient creates a new Client. If you haven't configured obs-websocket at
14+
// NewClient creates a new client. If you haven't configured obs-websocket at
1115
// all, then host should be "localhost", port should be 4444, and password
1216
// should be "".
13-
func NewClient(host string, port int, password string) *Client {
14-
return &Client{host: host, port: port, password: password}
15-
}
16-
17-
// Host returns the Client's host.
18-
func (c *Client) Host() string {
19-
return c.host
20-
}
21-
22-
// Port returns the Client's port.
23-
func (c *Client) Port() int {
24-
return c.port
25-
26-
}
27-
28-
// Password returns the Client's password.
29-
func (c *Client) Password() string {
30-
return c.password
17+
func NewClient(host string, port int, password string) *client {
18+
return &client{Host: host, Port: port, Password: password}
3119
}

client_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import "testing"
44

55
func TestNewClient(t *testing.T) {
66
c := NewClient("localhost", 4444, "")
7-
if c.Host() != "localhost" {
8-
t.Errorf("expected c.Host() == 'localhost', got '%s'", c.Host())
7+
if c.Host != "localhost" {
8+
t.Errorf("expected c.Host == 'localhost', got '%s'", c.Host)
99
}
10-
if c.Port() != 4444 {
11-
t.Errorf("expected c.Port() == 4444, got '%d'", c.Port())
10+
if c.Port != 4444 {
11+
t.Errorf("expected c.Port == 4444, got '%d'", c.Port)
1212
}
13-
if c.Password() != "" {
14-
t.Errorf("expected c.Password() == '', got '%s'", c.Password())
13+
if c.Password != "" {
14+
t.Errorf("expected c.Password == '', got '%s'", c.Password)
1515
}
1616
}

connection.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package obsws
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/base64"
6+
"fmt"
7+
8+
"github.com/gorilla/websocket"
9+
"github.com/pkg/errors"
10+
)
11+
12+
// https://github.com/Palakis/obs-websocket/blob/master/docs/generated/protocol.md#authentication
13+
// https://github.com/Palakis/obs-websocket/blob/master/docs/generated/protocol.md#getauthrequired
14+
// https://github.com/Palakis/obs-websocket/blob/master/docs/generated/protocol.md#authenticate
15+
16+
type getAuthRequiredRequest struct {
17+
MessageID string `json:"message-id"`
18+
RequestType string `json:"request-type"`
19+
}
20+
21+
func (r *getAuthRequiredRequest) ID() string {
22+
return r.MessageID
23+
}
24+
25+
type getAuthRequiredResponse struct {
26+
MessageID string `json:"message-id"`
27+
Status string `json:"status"`
28+
Error string `json:"error"`
29+
AuthRequired bool `json:"authRequired"`
30+
Challenge string `json:"challenge"`
31+
Salt string `json:"salt"`
32+
}
33+
34+
func (r *getAuthRequiredResponse) ID() string {
35+
return r.MessageID
36+
}
37+
38+
type authenticateRequest struct {
39+
MessageID string `json:"message-id"`
40+
RequestType string `json:"request-type"`
41+
Auth string `json:"auth"`
42+
}
43+
44+
func (r *authenticateRequest) ID() string {
45+
return r.MessageID
46+
}
47+
48+
// Connect makes a WebSocket connection and authenticates if necessary.
49+
func (c *client) Connect() error {
50+
conn, err := connect(c.Host, c.Port)
51+
if err != nil {
52+
return err
53+
}
54+
c.conn = conn
55+
56+
reqGAR := getAuthRequiredRequest{
57+
MessageID: c.getMessageID(),
58+
RequestType: "GetAuthRequired",
59+
}
60+
61+
if err = c.conn.WriteJSON(reqGAR); err != nil {
62+
return errors.Wrap(err, "write Authenticate")
63+
}
64+
65+
respGAR := &getAuthRequiredResponse{}
66+
if err = c.conn.ReadJSON(respGAR); err != nil {
67+
return errors.Wrap(err, "read GetAuthRequired")
68+
}
69+
70+
if !respGAR.AuthRequired {
71+
logger.Info("no authentication required")
72+
return nil
73+
}
74+
75+
auth := getAuth(c.Password, respGAR.Salt, respGAR.Challenge)
76+
logger.Debugf("auth: %s", auth)
77+
78+
reqA := authenticateRequest{
79+
MessageID: c.getMessageID(),
80+
RequestType: "Authenticate",
81+
Auth: auth,
82+
}
83+
if err = c.conn.WriteJSON(reqA); err != nil {
84+
return errors.Wrap(err, "write Authenticate")
85+
}
86+
87+
logger.Info("logged in")
88+
return nil
89+
}
90+
91+
// Close closes the WebSocket connection.
92+
func (c *client) Close() error {
93+
return c.conn.Close()
94+
}
95+
96+
func connect(host string, port int) (*websocket.Conn, error) {
97+
url := fmt.Sprintf("ws://%s:%d", host, port)
98+
logger.Infof("connecting to %s", url)
99+
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
100+
if err != nil {
101+
return nil, err
102+
}
103+
return conn, nil
104+
}
105+
106+
func getAuth(password, salt, challenge string) string {
107+
sha := sha256.Sum256([]byte(password + salt))
108+
b64 := base64.StdEncoding.EncodeToString([]byte(sha[:]))
109+
110+
sha = sha256.Sum256([]byte(b64 + challenge))
111+
b64 = base64.StdEncoding.EncodeToString([]byte(sha[:]))
112+
113+
return b64
114+
}

connection_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package obsws
2+
3+
import "testing"
4+
5+
func TestGetAuth(t *testing.T) {
6+
expected := "zTM5ki6L2vVvBQiTG9ckH1Lh64AbnCf6XZ226UmnkIA="
7+
observed := getAuth("password", "salt", "challenge")
8+
if observed != expected {
9+
t.Errorf("expected auth == '%s', got '%s'", expected, observed)
10+
}
11+
}

heartbeat.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package obsws
2+
3+
import (
4+
"github.com/pkg/errors"
5+
)
6+
7+
// https://github.com/Palakis/obs-websocket/blob/master/docs/generated/protocol.md#setheartbeat
8+
// https://github.com/Palakis/obs-websocket/blob/master/docs/generated/protocol.md#heartbeat
9+
10+
type setHeartbeatRequest struct {
11+
MessageID string `json:"message-id"`
12+
RequestType string `json:"request-type"`
13+
Enable bool `json:"enable"`
14+
}
15+
16+
func (r *setHeartbeatRequest) ID() string {
17+
return r.MessageID
18+
}
19+
20+
type heartbeatResponse struct {
21+
MessageID string `json:"message-id"`
22+
Status string `json:"status"`
23+
Error string `json:"error"`
24+
Pulse bool `json:"pulse"`
25+
CurrentProfile string `json:"current-profile"`
26+
CurrentScene string `json:"current-scene"`
27+
Streaming bool `json:"streaming"`
28+
TotalStreamTime int `json:"total-stream-time"`
29+
TotalStreamBytes int `json:"total-stream-bytes"`
30+
TotalStreamFrames int `json:"total-stream-frames"`
31+
Recording int `json:"recording"`
32+
TotalRecordTime int `json:"total-record-time"`
33+
TotalRecordBytes int `json:"total-record-bytes"`
34+
TotalRecordFrames int `json:"total-record-frames"`
35+
}
36+
37+
func (r *heartbeatResponse) ID() string {
38+
return r.MessageID
39+
}
40+
41+
// SetHeartbeat enables or disables sending of the Heartbeat event.
42+
func (c *client) SetHeartbeat(enable bool) error {
43+
reqH := setHeartbeatRequest{
44+
MessageID: c.getMessageID(),
45+
RequestType: "SetHeartbeat",
46+
Enable: enable,
47+
}
48+
if err := c.conn.WriteJSON(reqH); err != nil {
49+
return errors.Wrap(err, "write SetHeartbeat")
50+
}
51+
52+
logger.Infof("set heartbeat to %t", enable)
53+
return nil
54+
}

logger.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package obsws
2+
3+
import "github.com/op/go-logging"
4+
5+
var logger = logging.MustGetLogger("obsws")

message_id.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package obsws
2+
3+
import "strconv"
4+
5+
func (c *client) getMessageID() string {
6+
c.id++
7+
return strconv.Itoa(c.id)
8+
}
9+
10+
func (c *client) validateMessageID(req, resp reqOrResp) bool {
11+
return req.ID() == resp.ID()
12+
}

request_response.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package obsws
2+
3+
type reqOrResp interface {
4+
ID() string
5+
}
6+
7+
type genericRequest struct {
8+
MessageID string `json:"message-id"`
9+
RequestType string `json:"request-type"`
10+
}
11+
12+
func (r *genericRequest) ID() string {
13+
return r.MessageID
14+
}
15+
16+
type genericResponse struct {
17+
MessageID string `json:"message-id"`
18+
Status string `json:"status"`
19+
Error string `json:"error"`
20+
}
21+
22+
func (r *genericResponse) ID() string {
23+
return r.MessageID
24+
}
25+
26+
// ReceiveGeneric receives a minimal response with only the required fields.
27+
func (c *client) ReceiveGeneric() (*genericResponse, error) {
28+
resp := &genericResponse{}
29+
if err := c.conn.ReadJSON(resp); err != nil {
30+
return nil, err
31+
}
32+
return resp, nil
33+
}

0 commit comments

Comments
 (0)