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 }