309 lines
8.4 KiB
Go
309 lines
8.4 KiB
Go
package sendaround
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/icedream/sendaround/internal"
|
|
"github.com/pion/webrtc/v2"
|
|
)
|
|
|
|
const sendaroundDataChannelLabel = "sendaround"
|
|
|
|
var defaultServerUrl = &url.URL{
|
|
Scheme: "http",
|
|
Host: "localhost:8080",
|
|
Path: "/",
|
|
}
|
|
|
|
// SendaroundClient is the interface describing all client functionality to send
|
|
// or receive file transmissions via the Sendaround service.
|
|
type SendaroundClient interface {
|
|
// Offer sets up a WebRTC connection to upload files to another peer.
|
|
// A token is generated by the handshake server along with a URL that can be
|
|
// used in the web browser to transfer files without any dedicated software.
|
|
// Both values will be available in the returned Uploader instance.
|
|
Offer() (conn *Uploader, err error)
|
|
|
|
// Receive sets up a WebRTC connection to download files from a peer.
|
|
// The token is communicated to the handshake server so that associated
|
|
// connection data can be exchanged. Afterwards, a peer-to-peer connection
|
|
// is started and data transmission can occur by requesting files through
|
|
// the returned Downloader instance.
|
|
Receive(token string) (conn *Downloader, err error)
|
|
}
|
|
|
|
type sendaroundClient struct {
|
|
serverURL *url.URL
|
|
httpClient *http.Client
|
|
webrtcConfiguration webrtc.Configuration
|
|
webrtc *webrtc.API
|
|
}
|
|
|
|
// SendaroundClientConfiguration is a data struct containing configuration
|
|
// parameters for the creation of a SendaroundClient instance.
|
|
type SendaroundClientConfiguration struct {
|
|
// ServerUrl is the URL of the Sendaround handshake server. Defaults to the main service URL.
|
|
ServerURL *url.URL
|
|
|
|
// WebrtcConfiguration defines specific configuration for WebRTC communication. Defaults to a configuration which uses Google STUN/ICE servers.
|
|
WebRTCConfiguration *webrtc.Configuration
|
|
|
|
// HttpClient can be set to a specific HTTP client instance to use for handshake server communication. Defaults to the Golang default HTTP client.
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// NewSendaroundClient creates a new instance of the SendaroundClient implementation to send or receive file transmissions via the Sendaround service.
|
|
func NewSendaroundClient(cfg *SendaroundClientConfiguration) SendaroundClient {
|
|
serverURL := cfg.ServerURL
|
|
if serverURL == nil {
|
|
serverURL = defaultServerUrl
|
|
}
|
|
|
|
httpClient := cfg.HTTPClient
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
|
|
webrtcConfiguration := cfg.WebRTCConfiguration
|
|
if webrtcConfiguration == nil {
|
|
webrtcConfiguration = &webrtc.Configuration{
|
|
ICEServers: []webrtc.ICEServer{
|
|
{
|
|
URLs: []string{"stun:stun.l.google.com:19302"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
return &sendaroundClient{
|
|
serverURL: serverURL,
|
|
webrtc: webrtc.NewAPI(),
|
|
httpClient: httpClient,
|
|
webrtcConfiguration: *webrtcConfiguration,
|
|
}
|
|
}
|
|
|
|
func (client *sendaroundClient) Offer() (conn *Uploader, err error) {
|
|
// Create a new RTCPeerConnection
|
|
peerConnection, err := client.webrtc.NewPeerConnection(client.webrtcConfiguration)
|
|
/*
|
|
peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
|
log.Println("ICE connection state changed:", state,
|
|
peerConnection.ConnectionState())
|
|
})
|
|
peerConnection.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) {
|
|
log.Println("ICE gathering state changed:", state)
|
|
})
|
|
peerConnection.OnSignalingStateChange(func(state webrtc.SignalingState) {
|
|
log.Println("Signaling state changed:", state)
|
|
})
|
|
*/
|
|
|
|
// Create a datachannel with label 'data'
|
|
ordered := true
|
|
priority := webrtc.PriorityTypeHigh
|
|
protocol := "sendaround"
|
|
dataChannel, err := peerConnection.CreateDataChannel(sendaroundDataChannelLabel, &webrtc.DataChannelInit{
|
|
Ordered: &ordered,
|
|
Protocol: &protocol,
|
|
Priority: &priority,
|
|
})
|
|
|
|
// Create an offer to send to the browser
|
|
offer, err := peerConnection.CreateOffer(nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Sets the LocalDescription, and starts our UDP listeners
|
|
err = peerConnection.SetLocalDescription(offer)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Send SDP to server and retrieve new tokens
|
|
req, err := http.NewRequest("POST",
|
|
client.serverURL.ResolveReference(&url.URL{Path: "offer"}).String(),
|
|
strings.NewReader(offer.SDP))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req.ContentLength = int64(len([]byte(offer.SDP)))
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := client.httpClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
return
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
log.Fatalf("Server says: %s", resp.Status)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
result := new(internal.CreateOfferResponse)
|
|
if err = json.NewDecoder(resp.Body).Decode(result); err != nil {
|
|
return
|
|
}
|
|
u, err := url.Parse(result.Url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
server := &Uploader{
|
|
dataChannel: internal.NewDataChannel(dataChannel),
|
|
url: u,
|
|
token: result.Token,
|
|
}
|
|
|
|
server.init()
|
|
|
|
go func() {
|
|
var err error
|
|
defer func() {
|
|
if err != nil {
|
|
server.changeState(ConnectionState{Type: Failed, Error: err})
|
|
}
|
|
}()
|
|
|
|
// Wait for answer SDP from handshake server
|
|
req, err := http.NewRequest("POST",
|
|
client.serverURL.ResolveReference(&url.URL{
|
|
Path: fmt.Sprintf("offer/%s", url.PathEscape(result.AnswerReceivalToken)),
|
|
}).String(),
|
|
strings.NewReader(offer.SDP))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req.ContentLength = int64(len([]byte(offer.SDP)))
|
|
req.Header.Set("Accept", "application/sdp")
|
|
resp, err := client.httpClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
return
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
log.Fatalf("Server says: %s", resp.Status)
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
answerSdp, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Set answer SDP as remote description
|
|
if err = peerConnection.SetRemoteDescription(webrtc.SessionDescription{
|
|
Type: webrtc.SDPTypeAnswer,
|
|
SDP: string(answerSdp),
|
|
}); err != nil {
|
|
return
|
|
}
|
|
}()
|
|
|
|
conn = server
|
|
return
|
|
}
|
|
|
|
func (client *sendaroundClient) Receive(token string) (conn *Downloader, err error) {
|
|
dataChannelChan := make(chan *webrtc.DataChannel, 1)
|
|
|
|
// Create a new RTCPeerConnection
|
|
peerConnection, err := client.webrtc.NewPeerConnection(client.webrtcConfiguration)
|
|
/*
|
|
peerConnection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
|
log.Println("ICE connection state changed:", state)
|
|
})
|
|
peerConnection.OnSignalingStateChange(func(state webrtc.SignalingState) {
|
|
log.Println("Signaling state changed:", state)
|
|
})
|
|
peerConnection.OnICEGatheringStateChange(func(state webrtc.ICEGathererState) {
|
|
log.Println("ICE gathering state changed:", state)
|
|
})
|
|
*/
|
|
|
|
peerConnection.OnDataChannel(func(dataChannel *webrtc.DataChannel) {
|
|
if dataChannel.Label() != sendaroundDataChannelLabel {
|
|
// We expected a sendaround data channel but got something else => protocol violation
|
|
peerConnection.Close()
|
|
}
|
|
dataChannelChan <- dataChannel
|
|
})
|
|
|
|
// Fetch offer SDP
|
|
u := client.serverURL.ResolveReference(&url.URL{
|
|
Path: fmt.Sprintf("offer/%s", url.PathEscape(token)),
|
|
}).String()
|
|
req, err := http.NewRequest("GET", u, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req.Header.Set("Accept", "application/sdp")
|
|
resp, err := client.httpClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
return
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
log.Fatalf("Server says: %s", resp.Status)
|
|
return
|
|
}
|
|
offerSdp, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = peerConnection.SetRemoteDescription(
|
|
webrtc.SessionDescription{
|
|
SDP: string(offerSdp),
|
|
Type: webrtc.SDPTypeOffer,
|
|
}); err != nil {
|
|
return
|
|
}
|
|
|
|
answer, err := peerConnection.CreateAnswer(nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Sets the LocalDescription, and starts our UDP listeners
|
|
err = peerConnection.SetLocalDescription(answer)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Send SDP to server
|
|
req, err = http.NewRequest("POST",
|
|
client.serverURL.ResolveReference(&url.URL{
|
|
Path: fmt.Sprintf("answer/%s", url.PathEscape(token)),
|
|
}).String(),
|
|
strings.NewReader(answer.SDP))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
req.ContentLength = int64(len([]byte(answer.SDP)))
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err = client.httpClient.Do(req)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
return
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
log.Fatalf("Server says: %s", resp.Status)
|
|
return
|
|
}
|
|
|
|
// TODO - Implement some way to cancel/timeout this
|
|
dataChannel := <-dataChannelChan
|
|
retval := &Downloader{
|
|
dataChannel: internal.NewDataChannel(dataChannel),
|
|
}
|
|
retval.init()
|
|
conn = retval
|
|
return
|
|
|
|
}
|