├── .codacy.yml ├── .gitignore ├── .goreleaser.yml ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── _client └── web │ ├── emitter.go │ ├── index.html │ ├── main.go │ ├── receiver.go │ ├── stream_saver.js │ ├── utils.go │ └── wasm_exec.js ├── cmd ├── bench │ └── cmd.go ├── install.go ├── receive │ └── cmd.go └── send │ └── cmd.go ├── go.mod ├── go.sum ├── internal ├── buffer │ └── buffer.go ├── session │ ├── getters.go │ ├── session.go │ └── session_test.go └── utils │ ├── stun_arg.go │ └── stun_arg_test.go ├── main.go ├── main_test.go └── pkg ├── session ├── bench │ ├── benchmark.go │ ├── benchmark_test.go │ ├── id.go │ ├── id_test.go │ ├── init.go │ ├── session.go │ ├── session_test.go │ ├── state.go │ ├── state_download.go │ ├── state_upload.go │ └── timeout_test.go ├── common │ └── config.go ├── receiver │ ├── init.go │ ├── receiver.go │ ├── receiver_test.go │ └── state.go ├── sender │ ├── getters.go │ ├── init.go │ ├── io.go │ ├── sender.go │ ├── sender_test.go │ └── state.go ├── session.go └── session_test.go ├── stats ├── bytes.go ├── bytes_test.go ├── ctrl.go ├── ctrl_test.go ├── data.go ├── data_test.go └── stats.go └── utils ├── utils.go └── utils_test.go /.codacy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | exclude_paths: 3 | - '_client/web/stream_saver.js' 4 | - '_client/web/wasm_exec.js' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | build/ 15 | gfile 16 | dist/ 17 | cover/ 18 | 19 | *.wasm -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Documentation: https://goreleaser.com/quick-start/ 3 | project_name: gfile 4 | env: 5 | - GO111MODULE=on 6 | before: 7 | hooks: 8 | - go mod download 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - darwin 15 | - windows 16 | goarch: 17 | - 386 18 | - amd64 19 | - arm 20 | - arm64 21 | checksum: 22 | name_template: '{{ .ProjectName }}_checksums.txt' 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | - Merge pull request 30 | - Merge branch 31 | archive: 32 | name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 33 | replacements: 34 | darwin: Darwin 35 | linux: Linux 36 | windows: Windows 37 | 386: i386 38 | amd64: x86_64 39 | format_overrides: 40 | - goos: windows 41 | format: zip -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.x" # use the latest Go release 5 | 6 | env: 7 | - GO111MODULE=on 8 | 9 | before_script: 10 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b $GOPATH/bin v1.14.0 11 | - go get github.com/mattn/goveralls 12 | 13 | script: 14 | - golangci-lint run ./... 15 | - go test -coverpkg=$(go list ./... | tr '\n' ',') -coverprofile=cover.out -v -race -covermode=atomic ./... 16 | - goveralls -coverprofile=cover.out -service=travis-ci 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Antoine Baché 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RM ?= rm -f 2 | ECHO ?= echo 3 | GO ?= go 4 | 5 | NAME := gfile 6 | 7 | PKG_LIST := $(shell go list ./... | grep -v /vendor/) 8 | 9 | deps: 10 | @$(ECHO) "==> Installing deps ..." 11 | @go get ./... 12 | 13 | build: deps 14 | @$(ECHO) "==> Building ..." 15 | @go build -o $(NAME) . 16 | 17 | build-all: deps 18 | @$(ECHO) "==> Building all binaries..." 19 | @CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o build/$(NAME)-macos main.go 20 | @CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags '-w -extldflags "-static"' -o build/$(NAME)-linux-x86_64 main.go 21 | @CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -a -ldflags '-w -extldflags "-static"' -o build/$(NAME)-linux-i386 main.go 22 | 23 | generate: 24 | @goreleaser --snapshot --skip-publish --rm-dist 25 | 26 | lint: 27 | @$(GOPATH)/bin/golint -set_exit_status ./... | grep -v vendor/ && exit 1 || exit 0 28 | 29 | test: 30 | @$(ECHO) "==> Running tests..." 31 | @go test -short ${PKG_LIST} 32 | 33 | clean: 34 | @$(RM) $(NAME) 35 | 36 | race: deps 37 | @go test -race -short ${PKG_LIST} 38 | 39 | msan: deps 40 | @go test -msan -short ${PKG_LIST} 41 | 42 | coverage: 43 | @mkdir -p cover/ 44 | @go test ${PKG_LIST} -v -coverprofile cover/testCoverage.txt 45 | 46 | coverhtml: coverage 47 | @go tool cover -html=cover/testCoverage.txt -o cover/coverage.html 48 | 49 | .PHONY: deps build build-all test clean lint generate -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Antonito/gfile.svg?branch=master)](https://travis-ci.org/Antonito/gfile) 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/Antonito/gfile)](https://goreportcard.com/report/github.com/Antonito/gfile) 3 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/5888662aebd54d2681f9a737dfd33913)](https://www.codacy.com/app/Antonito/gfile?utm_source=github.com&utm_medium=referral&utm_content=Antonito/gfile&utm_campaign=Badge_Grade) 4 | [![Coverage Status](https://coveralls.io/repos/github/Antonito/gfile/badge.svg?branch=master)](https://coveralls.io/github/Antonito/gfile?branch=master) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 7 | 8 | # GFile 9 | 10 | gfile is a WebRTC based file exchange software. 11 | 12 | It allows to share a file directly between two computers, without the need of a third party. 13 | 14 | ![ezgif-5-9936f8008e4d](https://user-images.githubusercontent.com/11705040/55694519-686e2d80-5969-11e9-9bc1-f7a59b62732f.gif) 15 | 16 | ## Note 17 | 18 | This project is still in its early stage. 19 | 20 | ## How does it work ? 21 | 22 | ![Schema](https://user-images.githubusercontent.com/11705040/55741923-4dd89a80-59e3-11e9-917c-daf9f08f164d.png) 23 | 24 | The [STUN server](https://en.wikipedia.org/wiki/STUN) is only used to retrieve informations metadata (how to connect the two clients). The data you transfer with `gfile` **does not transit through it**. 25 | 26 | > More informations [here](https://webrtc.org/) 27 | 28 | ## Usage 29 | 30 | ### Sender 31 | 32 | ```bash 33 | gfile send --file filename 34 | ``` 35 | 36 | - Run the command 37 | - A base64 encoded [SDP](https://tools.ietf.org/html/rfc4566) will appear, send it to the remote client 38 | - Follow the instructions to send the client's SDP to your process 39 | - The file transfer should start 40 | 41 | ### Receiver 42 | 43 | ```bash 44 | # SDP being the base64 SDP gotten from the other client 45 | echo "$SDP" | gfile receive -o filename 46 | ``` 47 | 48 | - Pipe the other client's SDP to gfile 49 | - A base64 encoded SDP will appear, send it to the remote client 50 | - The file transfer should start 51 | 52 | ### Benchmark 53 | 54 | `gfile` is able to benchmark the network speed between 2 clients (1 _master_ and 1 _slave_) with the `bench` command. 55 | For detailed instructions, see `Sender` and `Receiver` instructions. 56 | 57 | This feature is still an experiment. 58 | 59 | ```bash 60 | # Run as 'master' 61 | gfile bench -m 62 | 63 | # Run as 'slave' 64 | echo "$SDP" | gfile bench 65 | ``` 66 | 67 | ### Web Interface 68 | 69 | A web interface is being developed via WebAssembly. It is currently **not** working. 70 | 71 | ### Debug 72 | 73 | In order to obtain a more verbose output, it is possible to define the logging level via the `GFILE_LOG` environment variable. 74 | 75 | > Example: `export GFILE_LOG="TRACE"` 76 | > See function `setupLogger` in `main.go` for more information 77 | 78 | ## Contributors 79 | 80 | - Antoine Baché ([https://github.com/Antonito](https://github.com/Antonito)) **Original author** 81 | 82 | Special thanks to [Sean DuBois](https://github.com/Sean-Der) for his help with [pion/webrtc](https://github.com/pion/webrtc) and [Yutaka Takeda](https://github.com/enobufs) for his work on [pion/sctp](https://github.com/pion/sctp) 83 | -------------------------------------------------------------------------------- /_client/web/emitter.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "reflect" 9 | "syscall/js" 10 | "unsafe" 11 | 12 | "github.com/antonito/gfile/pkg/session/common" 13 | "github.com/antonito/gfile/pkg/session/sender" 14 | "github.com/antonito/gfile/pkg/utils" 15 | ) 16 | 17 | func updateFilePlaceholder(_ js.Value, _ []js.Value) interface{} { 18 | // Check if file was selected 19 | fileSelector := getElementByID("send-file-input") 20 | fileList := fileSelector.Get("files") 21 | fileSelectorLabels := fileSelector.Get("labels") 22 | fileListLen := fileList.Length() 23 | 24 | if fileListLen == 0 { 25 | fileSelectorLabels.Index(0).Set("textContent", "Choose file") 26 | } else if fileListLen == 1 { 27 | filename := fileList.Index(0).Get("name").String() 28 | fileSelectorLabels.Index(0).Set("textContent", filename) 29 | } else { 30 | // Should never reach this part, but 31 | // TODO: Pop-up error 32 | fmt.Printf("Error, too many files") 33 | } 34 | return js.Undefined() 35 | } 36 | 37 | func sendFile(fileContent js.Value) { 38 | // Manually allocate a memory zone, and get its raw pointer 39 | // make it point to the JS internal's memory array 40 | fileContentLength := fileContent.Length() 41 | fileBuffer := make([]byte, fileContentLength) 42 | hdr := (*reflect.SliceHeader)(unsafe.Pointer(&fileBuffer)) 43 | ptr := uintptr(unsafe.Pointer(hdr.Data)) 44 | js.Global().Get("window").Call("setMemory", fileContent, ptr) 45 | 46 | reader := bytes.NewReader(fileBuffer) 47 | 48 | // Retrieve remote SDP 49 | sdpInputBox := getElementByID("send-sdpInput") 50 | sdpInputBoxText := sdpInputBox.Get("value").String() 51 | 52 | // Access session 53 | sess := globalSess.(*sender.Session) 54 | 55 | sdpInput.WriteString(sdpInputBoxText + "\n") 56 | 57 | sess.SetStream(reader) 58 | 59 | // Notify client, in progress 60 | if err := sess.Start(); err != nil { 61 | // Notifiy client of error 62 | // TODO: Handle error 63 | } 64 | // Notifiy client of end 65 | processDone <- struct{}{} 66 | } 67 | 68 | func onSendFileButtonClick(_ js.Value, _ []js.Value) interface{} { 69 | go func() { 70 | // Check if file was selected 71 | fileSelector := getElementByID("send-file-input") 72 | fileList := fileSelector.Get("files") 73 | if fileList.Length() == 0 { 74 | // TODO: Pop-up error 75 | fmt.Println("No file selected") 76 | return 77 | } 78 | fileToSend := fileList.Index(0) 79 | 80 | js.Global().Call("readFileHelper", fileToSend, js.FuncOf(func(_ js.Value, res []js.Value) interface{} { 81 | if len(res) >= 1 { 82 | go sendFile(res[0]) 83 | } 84 | return js.Undefined() 85 | })) 86 | }() 87 | return js.Undefined() 88 | } 89 | 90 | func onMenuSendFileClickHandler(_ js.Value, _ []js.Value) interface{} { 91 | go func() { 92 | // Update UI 93 | getElementByID("menu-container").Set("hidden", true) 94 | getElementByID("menu-send-container").Set("hidden", false) 95 | 96 | sdpOutputBox := getElementByID("send-sdpOutput") 97 | sdpOutputBox.Set("textContent", "Generating SDP...") 98 | 99 | sess := sender.NewWith(sender.Config{ 100 | Stream: nil, 101 | Configuration: common.Configuration{ 102 | SDPProvider: sdpInput, 103 | SDPOutput: sdpOutput, 104 | OnCompletion: func() { 105 | // TODO: Notify user ? 106 | }, 107 | }, 108 | }) 109 | globalSess = sess 110 | sess.Initialize() 111 | sdp, err := utils.MustReadStream(sdpOutput) 112 | if err != nil { 113 | // TODO: Notify error 114 | } 115 | 116 | // Show SDP to the user 117 | sdpOutputBox.Set("textContent", sdp) 118 | }() 119 | 120 | return js.Undefined() 121 | } 122 | 123 | func setupEmitter() { 124 | setCallback("onMenuSendAFileClick", onMenuSendFileClickHandler) 125 | setCallback("onSendFileButtonClick", onSendFileButtonClick) 126 | setCallback("updateFilePlaceholder", updateFilePlaceholder) 127 | } 128 | -------------------------------------------------------------------------------- /_client/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 52 | 53 | 54 | 55 |
56 |

gfile Web Client

57 | 58 | 59 | 69 | 70 | 71 | 96 | 97 | 98 | 115 | 116 |
117 | 118 | 119 | -------------------------------------------------------------------------------- /_client/web/main.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | 8 | "github.com/antonito/gfile/pkg/session" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // TODO: Store in a struct 13 | var globalSess session.Session 14 | var sdpOutput *bytes.Buffer 15 | var sdpInput *bytes.Buffer 16 | var processDone chan struct{} 17 | 18 | func main() { 19 | log.SetLevel(log.TraceLevel) 20 | processDone = make(chan struct{}) 21 | 22 | sdpOutput = &bytes.Buffer{} 23 | sdpInput = &bytes.Buffer{} 24 | setupEmitter() 25 | setupReceiver() 26 | 27 | <-processDone 28 | } 29 | -------------------------------------------------------------------------------- /_client/web/receiver.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package main 4 | 5 | import ( 6 | "bufio" 7 | "bytes" 8 | "fmt" 9 | "syscall/js" 10 | 11 | "github.com/antonito/gfile/pkg/session/common" 12 | "github.com/antonito/gfile/pkg/session/receiver" 13 | "github.com/antonito/gfile/pkg/utils" 14 | ) 15 | 16 | func onMenuReceiveFileClickHandler(_ js.Value, _ []js.Value) interface{} { 17 | go func() { 18 | getElementByID("menu-container").Set("hidden", true) 19 | getElementByID("menu-receive-container").Set("hidden", false) 20 | }() 21 | return js.Undefined() 22 | } 23 | 24 | func onReceiveFileButtonClick(_ js.Value, _ []js.Value) interface{} { 25 | go func() { 26 | sdpInputBox := getElementByID("receive-sdpInput") 27 | sdpInputBoxText := sdpInputBox.Get("value").String() 28 | 29 | sdpInput.WriteString(sdpInputBoxText + "\n") 30 | 31 | sess := receiver.NewWith(receiver.Config{ 32 | Configuration: common.Configuration{ 33 | SDPProvider: sdpInput, 34 | SDPOutput: sdpOutput, 35 | OnCompletion: func() { 36 | }, 37 | }, 38 | }) 39 | 40 | globalSess = sess 41 | sess.Initialize() 42 | 43 | fmt.Printf("Reading SDP\n") 44 | sdp, err := utils.MustReadStream(sdpOutput) 45 | if err != nil { 46 | fmt.Printf("Got error -> %s\n", err) 47 | // TODO: Notify error 48 | } 49 | 50 | sdpOutputBox := getElementByID("receive-sdpOutput") 51 | sdpOutputBox.Set("textContent", sdp) 52 | 53 | buffer := &bytes.Buffer{} 54 | writerBuffer := bufio.NewWriter(buffer) 55 | sess.SetStream(writerBuffer) 56 | if err := sess.Start(); err != nil { 57 | fmt.Printf("Got an error: %v\n", err) 58 | // TOOD: Notify error 59 | } else { 60 | // Write file 61 | fmt.Println("Ready to write content") 62 | filename := "donwload.lol" 63 | bufferBytes := buffer.Bytes() 64 | array := js.TypedArrayOf(bufferBytes) 65 | js.Global().Get("window").Call("saveFile", filename, array) 66 | array.Release() 67 | } 68 | 69 | processDone <- struct{}{} 70 | }() 71 | 72 | return js.Undefined() 73 | } 74 | 75 | func setupReceiver() { 76 | setCallback("onMenuReceiveFileClick", onMenuReceiveFileClickHandler) 77 | setCallback("onReceiveFileButtonClick", onReceiveFileButtonClick) 78 | } 79 | -------------------------------------------------------------------------------- /_client/web/stream_saver.js: -------------------------------------------------------------------------------- 1 | /* global location WritableStream ReadableStream define MouseEvent MessageChannel TransformStream */ 2 | ;((name, definition) => { 3 | typeof module !== 'undefined' 4 | ? module.exports = definition() 5 | : typeof define === 'function' && typeof define.amd === 'object' 6 | ? define(definition) 7 | : this[name] = definition() 8 | })('streamSaver', () => { 9 | 'use strict' 10 | 11 | const secure = location.protocol === 'https:' || 12 | location.protocol === 'chrome-extension:' || 13 | location.hostname === 'localhost' 14 | let iframe 15 | let loaded 16 | let transfarableSupport = false 17 | let streamSaver = { 18 | createWriteStream, 19 | supported: false, 20 | version: { 21 | full: '1.2.0', 22 | major: 1, 23 | minor: 2, 24 | dot: 0 25 | } 26 | } 27 | 28 | streamSaver.mitm = 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=' + 29 | streamSaver.version.full 30 | 31 | try { 32 | // Some browser has it but ain't allowed to construct a stream yet 33 | streamSaver.supported = 'serviceWorker' in navigator && !!new ReadableStream() 34 | } catch (err) {} 35 | 36 | try { 37 | const { readable } = new TransformStream() 38 | const mc = new MessageChannel() 39 | mc.port1.postMessage(readable, [readable]) 40 | mc.port1.close() 41 | mc.port2.close() 42 | transfarableSupport = readable.locked === true 43 | } catch (err) { 44 | // Was first enabled in chrome v73 45 | } 46 | 47 | function createWriteStream (filename, queuingStrategy, size) { 48 | // normalize arguments 49 | if (Number.isFinite(queuingStrategy)) { 50 | [size, queuingStrategy] = [queuingStrategy, size] 51 | } 52 | 53 | let channel = new MessageChannel() 54 | let popup 55 | let setupChannel = readableStream => new Promise(resolve => { 56 | const args = [ { filename, size }, '*', [ channel.port2 ] ] 57 | 58 | // Pass along transfarable stream 59 | if (readableStream) { 60 | args[0].readableStream = readableStream 61 | args[2].push(readableStream) 62 | } 63 | 64 | channel.port1.onmessage = evt => { 65 | // Service worker sent us a link from where 66 | // we recive the readable link (stream) 67 | if (evt.data.download) { 68 | resolve() // Signal that the writestream are ready to recive data 69 | if (!secure) popup.close() // don't need the popup any longer 70 | if (window.chrome && chrome.extension && 71 | chrome.extension.getBackgroundPage && 72 | chrome.extension.getBackgroundPage() === window) { 73 | chrome.tabs.create({ url: evt.data.download, active: false }) 74 | } else { 75 | window.location = evt.data.download 76 | } 77 | 78 | // Cleanup 79 | if (readableStream) { 80 | // We don't need postMessages now when stream are transferable 81 | channel.port1.close() 82 | channel.port2.close() 83 | } 84 | 85 | channel.port1.onmessage = null 86 | } 87 | } 88 | 89 | if (secure && !iframe) { 90 | iframe = document.createElement('iframe') 91 | iframe.src = streamSaver.mitm 92 | iframe.hidden = true 93 | document.body.appendChild(iframe) 94 | } 95 | 96 | if (secure && !loaded) { 97 | let fn 98 | iframe.addEventListener('load', fn = () => { 99 | loaded = true 100 | iframe.removeEventListener('load', fn) 101 | iframe.contentWindow.postMessage(...args) 102 | }) 103 | } 104 | 105 | if (secure && loaded) { 106 | iframe.contentWindow.postMessage(...args) 107 | } 108 | 109 | if (!secure) { 110 | popup = window.open(streamSaver.mitm, Math.random()) 111 | let onready = evt => { 112 | if (evt.source === popup) { 113 | popup.postMessage(...args) 114 | window.removeEventListener('message', onready) 115 | } 116 | } 117 | 118 | // Another problem that cross origin don't allow is scripting 119 | // so popup.onload() don't work but postMessage still dose 120 | // work cross origin 121 | window.addEventListener('message', onready) 122 | } 123 | }) 124 | 125 | if (transfarableSupport) { 126 | const ts = new TransformStream({ 127 | start () { 128 | return new Promise(resolve => 129 | setTimeout(() => setupChannel(ts.readable).then(resolve)) 130 | ) 131 | } 132 | }, queuingStrategy) 133 | 134 | return ts.writable 135 | } 136 | 137 | return new WritableStream({ 138 | start () { 139 | // is called immediately, and should perform any actions 140 | // necessary to acquire access to the underlying sink. 141 | // If this process is asynchronous, it can return a promise 142 | // to signal success or failure. 143 | return setupChannel() 144 | }, 145 | write (chunk) { 146 | // is called when a new chunk of data is ready to be written 147 | // to the underlying sink. It can return a promise to signal 148 | // success or failure of the write operation. The stream 149 | // implementation guarantees that this method will be called 150 | // only after previous writes have succeeded, and never after 151 | // close or abort is called. 152 | 153 | // TODO: Kind of important that service worker respond back when 154 | // it has been written. Otherwise we can't handle backpressure 155 | // EDIT: Transfarable streams solvs this... 156 | channel.port1.postMessage(chunk) 157 | }, 158 | close () { 159 | channel.port1.postMessage('end') 160 | }, 161 | abort () { 162 | channel.port1.postMessage('abort') 163 | } 164 | }, queuingStrategy) 165 | } 166 | 167 | return streamSaver 168 | }) 169 | -------------------------------------------------------------------------------- /_client/web/utils.go: -------------------------------------------------------------------------------- 1 | // +build js,wasm 2 | 3 | package main 4 | 5 | import "syscall/js" 6 | 7 | func getElementByID(id string) js.Value { 8 | return js.Global().Get("document").Call("getElementById", id) 9 | } 10 | 11 | type jsCallback func(_ js.Value, _ []js.Value) interface{} 12 | 13 | func setCallback(id string, cb jsCallback) { 14 | js.Global().Set(id, js.FuncOf(cb)) 15 | } 16 | -------------------------------------------------------------------------------- /_client/web/wasm_exec.js: -------------------------------------------------------------------------------- 1 | // Copyright 2018 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | (() => { 6 | // Map multiple JavaScript environments to a single common API, 7 | // preferring web standards over Node.js API. 8 | // 9 | // Environments considered: 10 | // - Browsers 11 | // - Node.js 12 | // - Electron 13 | // - Parcel 14 | 15 | if (typeof global !== "undefined") { 16 | // global already exists 17 | } else if (typeof window !== "undefined") { 18 | window.global = window; 19 | } else if (typeof self !== "undefined") { 20 | self.global = self; 21 | } else { 22 | throw new Error("cannot export Go (neither global, window nor self is defined)"); 23 | } 24 | 25 | if (!global.require && typeof require !== "undefined") { 26 | global.require = require; 27 | } 28 | 29 | if (!global.fs && global.require) { 30 | global.fs = require("fs"); 31 | } 32 | 33 | if (!global.fs) { 34 | let outputBuf = ""; 35 | global.fs = { 36 | constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused 37 | writeSync(fd, buf) { 38 | outputBuf += decoder.decode(buf); 39 | const nl = outputBuf.lastIndexOf("\n"); 40 | if (nl != -1) { 41 | console.log(outputBuf.substr(0, nl)); 42 | outputBuf = outputBuf.substr(nl + 1); 43 | } 44 | return buf.length; 45 | }, 46 | write(fd, buf, offset, length, position, callback) { 47 | if (offset !== 0 || length !== buf.length || position !== null) { 48 | throw new Error("not implemented"); 49 | } 50 | const n = this.writeSync(fd, buf); 51 | callback(null, n); 52 | }, 53 | open(path, flags, mode, callback) { 54 | const err = new Error("not implemented"); 55 | err.code = "ENOSYS"; 56 | callback(err); 57 | }, 58 | read(fd, buffer, offset, length, position, callback) { 59 | const err = new Error("not implemented"); 60 | err.code = "ENOSYS"; 61 | callback(err); 62 | }, 63 | fsync(fd, callback) { 64 | callback(null); 65 | }, 66 | }; 67 | } 68 | 69 | if (!global.crypto) { 70 | const nodeCrypto = require("crypto"); 71 | global.crypto = { 72 | getRandomValues(b) { 73 | nodeCrypto.randomFillSync(b); 74 | }, 75 | }; 76 | } 77 | 78 | if (!global.performance) { 79 | global.performance = { 80 | now() { 81 | const [sec, nsec] = process.hrtime(); 82 | return sec * 1000 + nsec / 1000000; 83 | }, 84 | }; 85 | } 86 | 87 | if (!global.TextEncoder) { 88 | global.TextEncoder = require("util").TextEncoder; 89 | } 90 | 91 | if (!global.TextDecoder) { 92 | global.TextDecoder = require("util").TextDecoder; 93 | } 94 | 95 | // End of polyfills for common API. 96 | 97 | const encoder = new TextEncoder("utf-8"); 98 | const decoder = new TextDecoder("utf-8"); 99 | 100 | global.Go = class { 101 | constructor() { 102 | this.argv = ["js"]; 103 | this.env = {}; 104 | this.exit = (code) => { 105 | if (code !== 0) { 106 | console.warn("exit code:", code); 107 | } 108 | }; 109 | this._exitPromise = new Promise((resolve) => { 110 | this._resolveExitPromise = resolve; 111 | }); 112 | this._pendingEvent = null; 113 | this._scheduledTimeouts = new Map(); 114 | this._nextCallbackTimeoutID = 1; 115 | 116 | const mem = () => { 117 | // The buffer may change when requesting more memory. 118 | return new DataView(this._inst.exports.mem.buffer); 119 | } 120 | 121 | const setInt64 = (addr, v) => { 122 | mem().setUint32(addr + 0, v, true); 123 | mem().setUint32(addr + 4, Math.floor(v / 4294967296), true); 124 | } 125 | 126 | const getInt64 = (addr) => { 127 | const low = mem().getUint32(addr + 0, true); 128 | const high = mem().getInt32(addr + 4, true); 129 | return low + high * 4294967296; 130 | } 131 | 132 | const loadValue = (addr) => { 133 | const f = mem().getFloat64(addr, true); 134 | if (f === 0) { 135 | return undefined; 136 | } 137 | if (!isNaN(f)) { 138 | return f; 139 | } 140 | 141 | const id = mem().getUint32(addr, true); 142 | return this._values[id]; 143 | } 144 | 145 | const storeValue = (addr, v) => { 146 | const nanHead = 0x7FF80000; 147 | 148 | if (typeof v === "number") { 149 | if (isNaN(v)) { 150 | mem().setUint32(addr + 4, nanHead, true); 151 | mem().setUint32(addr, 0, true); 152 | return; 153 | } 154 | if (v === 0) { 155 | mem().setUint32(addr + 4, nanHead, true); 156 | mem().setUint32(addr, 1, true); 157 | return; 158 | } 159 | mem().setFloat64(addr, v, true); 160 | return; 161 | } 162 | 163 | switch (v) { 164 | case undefined: 165 | mem().setFloat64(addr, 0, true); 166 | return; 167 | case null: 168 | mem().setUint32(addr + 4, nanHead, true); 169 | mem().setUint32(addr, 2, true); 170 | return; 171 | case true: 172 | mem().setUint32(addr + 4, nanHead, true); 173 | mem().setUint32(addr, 3, true); 174 | return; 175 | case false: 176 | mem().setUint32(addr + 4, nanHead, true); 177 | mem().setUint32(addr, 4, true); 178 | return; 179 | } 180 | 181 | let ref = this._refs.get(v); 182 | if (ref === undefined) { 183 | ref = this._values.length; 184 | this._values.push(v); 185 | this._refs.set(v, ref); 186 | } 187 | let typeFlag = 0; 188 | switch (typeof v) { 189 | case "string": 190 | typeFlag = 1; 191 | break; 192 | case "symbol": 193 | typeFlag = 2; 194 | break; 195 | case "function": 196 | typeFlag = 3; 197 | break; 198 | } 199 | mem().setUint32(addr + 4, nanHead | typeFlag, true); 200 | mem().setUint32(addr, ref, true); 201 | } 202 | 203 | const loadSlice = (addr) => { 204 | const array = getInt64(addr + 0); 205 | const len = getInt64(addr + 8); 206 | return new Uint8Array(this._inst.exports.mem.buffer, array, len); 207 | } 208 | 209 | const loadSliceOfValues = (addr) => { 210 | const array = getInt64(addr + 0); 211 | const len = getInt64(addr + 8); 212 | const a = new Array(len); 213 | for (let i = 0; i < len; i++) { 214 | a[i] = loadValue(array + i * 8); 215 | } 216 | return a; 217 | } 218 | 219 | const loadString = (addr) => { 220 | const saddr = getInt64(addr + 0); 221 | const len = getInt64(addr + 8); 222 | return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len)); 223 | } 224 | 225 | const timeOrigin = Date.now() - performance.now(); 226 | this.importObject = { 227 | go: { 228 | // Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters) 229 | // may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported 230 | // function. A goroutine can switch to a new stack if the current stack is too small (see morestack function). 231 | // This changes the SP, thus we have to update the SP used by the imported function. 232 | 233 | // func wasmExit(code int32) 234 | "runtime.wasmExit": (sp) => { 235 | const code = mem().getInt32(sp + 8, true); 236 | this.exited = true; 237 | delete this._inst; 238 | delete this._values; 239 | delete this._refs; 240 | this.exit(code); 241 | }, 242 | 243 | // func wasmWrite(fd uintptr, p unsafe.Pointer, n int32) 244 | "runtime.wasmWrite": (sp) => { 245 | const fd = getInt64(sp + 8); 246 | const p = getInt64(sp + 16); 247 | const n = mem().getInt32(sp + 24, true); 248 | fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n)); 249 | }, 250 | 251 | // func nanotime() int64 252 | "runtime.nanotime": (sp) => { 253 | setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000); 254 | }, 255 | 256 | // func walltime() (sec int64, nsec int32) 257 | "runtime.walltime": (sp) => { 258 | const msec = (new Date).getTime(); 259 | setInt64(sp + 8, msec / 1000); 260 | mem().setInt32(sp + 16, (msec % 1000) * 1000000, true); 261 | }, 262 | 263 | // func scheduleTimeoutEvent(delay int64) int32 264 | "runtime.scheduleTimeoutEvent": (sp) => { 265 | const id = this._nextCallbackTimeoutID; 266 | this._nextCallbackTimeoutID++; 267 | this._scheduledTimeouts.set(id, setTimeout( 268 | () => { 269 | this._resume(); 270 | while (this._scheduledTimeouts.has(id)) { 271 | // for some reason Go failed to register the timeout event, log and try again 272 | // (temporary workaround for https://github.com/golang/go/issues/28975) 273 | console.warn("scheduleTimeoutEvent: missed timeout event"); 274 | this._resume(); 275 | } 276 | }, 277 | getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early 278 | )); 279 | mem().setInt32(sp + 16, id, true); 280 | }, 281 | 282 | // func clearTimeoutEvent(id int32) 283 | "runtime.clearTimeoutEvent": (sp) => { 284 | const id = mem().getInt32(sp + 8, true); 285 | clearTimeout(this._scheduledTimeouts.get(id)); 286 | this._scheduledTimeouts.delete(id); 287 | }, 288 | 289 | // func getRandomData(r []byte) 290 | "runtime.getRandomData": (sp) => { 291 | crypto.getRandomValues(loadSlice(sp + 8)); 292 | }, 293 | 294 | // func stringVal(value string) ref 295 | "syscall/js.stringVal": (sp) => { 296 | storeValue(sp + 24, loadString(sp + 8)); 297 | }, 298 | 299 | // func valueGet(v ref, p string) ref 300 | "syscall/js.valueGet": (sp) => { 301 | const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16)); 302 | sp = this._inst.exports.getsp(); // see comment above 303 | storeValue(sp + 32, result); 304 | }, 305 | 306 | // func valueSet(v ref, p string, x ref) 307 | "syscall/js.valueSet": (sp) => { 308 | Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32)); 309 | }, 310 | 311 | // func valueIndex(v ref, i int) ref 312 | "syscall/js.valueIndex": (sp) => { 313 | storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16))); 314 | }, 315 | 316 | // valueSetIndex(v ref, i int, x ref) 317 | "syscall/js.valueSetIndex": (sp) => { 318 | Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24)); 319 | }, 320 | 321 | // func valueCall(v ref, m string, args []ref) (ref, bool) 322 | "syscall/js.valueCall": (sp) => { 323 | try { 324 | const v = loadValue(sp + 8); 325 | const m = Reflect.get(v, loadString(sp + 16)); 326 | const args = loadSliceOfValues(sp + 32); 327 | const result = Reflect.apply(m, v, args); 328 | sp = this._inst.exports.getsp(); // see comment above 329 | storeValue(sp + 56, result); 330 | mem().setUint8(sp + 64, 1); 331 | } catch (err) { 332 | storeValue(sp + 56, err); 333 | mem().setUint8(sp + 64, 0); 334 | } 335 | }, 336 | 337 | // func valueInvoke(v ref, args []ref) (ref, bool) 338 | "syscall/js.valueInvoke": (sp) => { 339 | try { 340 | const v = loadValue(sp + 8); 341 | const args = loadSliceOfValues(sp + 16); 342 | const result = Reflect.apply(v, undefined, args); 343 | sp = this._inst.exports.getsp(); // see comment above 344 | storeValue(sp + 40, result); 345 | mem().setUint8(sp + 48, 1); 346 | } catch (err) { 347 | storeValue(sp + 40, err); 348 | mem().setUint8(sp + 48, 0); 349 | } 350 | }, 351 | 352 | // func valueNew(v ref, args []ref) (ref, bool) 353 | "syscall/js.valueNew": (sp) => { 354 | try { 355 | const v = loadValue(sp + 8); 356 | const args = loadSliceOfValues(sp + 16); 357 | const result = Reflect.construct(v, args); 358 | sp = this._inst.exports.getsp(); // see comment above 359 | storeValue(sp + 40, result); 360 | mem().setUint8(sp + 48, 1); 361 | } catch (err) { 362 | storeValue(sp + 40, err); 363 | mem().setUint8(sp + 48, 0); 364 | } 365 | }, 366 | 367 | // func valueLength(v ref) int 368 | "syscall/js.valueLength": (sp) => { 369 | setInt64(sp + 16, parseInt(loadValue(sp + 8).length)); 370 | }, 371 | 372 | // valuePrepareString(v ref) (ref, int) 373 | "syscall/js.valuePrepareString": (sp) => { 374 | const str = encoder.encode(String(loadValue(sp + 8))); 375 | storeValue(sp + 16, str); 376 | setInt64(sp + 24, str.length); 377 | }, 378 | 379 | // valueLoadString(v ref, b []byte) 380 | "syscall/js.valueLoadString": (sp) => { 381 | const str = loadValue(sp + 8); 382 | loadSlice(sp + 16).set(str); 383 | }, 384 | 385 | // func valueInstanceOf(v ref, t ref) bool 386 | "syscall/js.valueInstanceOf": (sp) => { 387 | mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); 388 | }, 389 | 390 | "debug": (value) => { 391 | console.log(value); 392 | }, 393 | } 394 | }; 395 | } 396 | 397 | async run(instance) { 398 | this._inst = instance; 399 | this._values = [ // TODO: garbage collection 400 | NaN, 401 | 0, 402 | null, 403 | true, 404 | false, 405 | global, 406 | this._inst.exports.mem, 407 | this, 408 | ]; 409 | this._refs = new Map(); 410 | this.exited = false; 411 | 412 | const mem = new DataView(this._inst.exports.mem.buffer) 413 | 414 | // Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory. 415 | let offset = 4096; 416 | 417 | const strPtr = (str) => { 418 | let ptr = offset; 419 | new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); 420 | offset += str.length + (8 - (str.length % 8)); 421 | return ptr; 422 | }; 423 | 424 | const argc = this.argv.length; 425 | 426 | const argvPtrs = []; 427 | this.argv.forEach((arg) => { 428 | argvPtrs.push(strPtr(arg)); 429 | }); 430 | 431 | const keys = Object.keys(this.env).sort(); 432 | argvPtrs.push(keys.length); 433 | keys.forEach((key) => { 434 | argvPtrs.push(strPtr(`${key}=${this.env[key]}`)); 435 | }); 436 | 437 | const argv = offset; 438 | argvPtrs.forEach((ptr) => { 439 | mem.setUint32(offset, ptr, true); 440 | mem.setUint32(offset + 4, 0, true); 441 | offset += 8; 442 | }); 443 | 444 | this._inst.exports.run(argc, argv); 445 | if (this.exited) { 446 | this._resolveExitPromise(); 447 | } 448 | await this._exitPromise; 449 | } 450 | 451 | _resume() { 452 | if (this.exited) { 453 | throw new Error("Go program has already exited"); 454 | } 455 | this._inst.exports.resume(); 456 | if (this.exited) { 457 | this._resolveExitPromise(); 458 | } 459 | } 460 | 461 | _makeFuncWrapper(id) { 462 | const go = this; 463 | return function () { 464 | const event = { id: id, this: this, args: arguments }; 465 | go._pendingEvent = event; 466 | go._resume(); 467 | return event.result; 468 | }; 469 | } 470 | } 471 | 472 | if ( 473 | global.require && 474 | global.require.main === module && 475 | global.process && 476 | global.process.versions && 477 | !global.process.versions.electron 478 | ) { 479 | if (process.argv.length < 3) { 480 | console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); 481 | process.exit(1); 482 | } 483 | 484 | const go = new Go(); 485 | go.argv = process.argv.slice(2); 486 | go.env = Object.assign({ TMPDIR: require("os").tmpdir() }, process.env); 487 | go.exit = process.exit; 488 | WebAssembly.instantiate(fs.readFileSync(process.argv[2]), go.importObject).then((result) => { 489 | process.on("exit", (code) => { // Node.js exits if no event handler is pending 490 | if (code === 0 && !go.exited) { 491 | // deadlock, make Go print error and stack traces 492 | go._pendingEvent = { id: 0 }; 493 | go._resume(); 494 | } 495 | }); 496 | return go.run(result.instance); 497 | }).catch((err) => { 498 | console.error(err); 499 | process.exit(1); 500 | }); 501 | } 502 | })(); -------------------------------------------------------------------------------- /cmd/bench/cmd.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/antonito/gfile/internal/utils" 5 | "github.com/antonito/gfile/pkg/session/bench" 6 | "github.com/antonito/gfile/pkg/session/common" 7 | log "github.com/sirupsen/logrus" 8 | "gopkg.in/urfave/cli.v1" 9 | ) 10 | 11 | func handler(c *cli.Context) error { 12 | isMaster := c.Bool("master") 13 | 14 | conf := bench.Config{ 15 | Master: isMaster, 16 | Configuration: common.Configuration{ 17 | OnCompletion: func() { 18 | }, 19 | }, 20 | } 21 | 22 | customSTUN := c.String("stun") 23 | if customSTUN != "" { 24 | if err := utils.ParseSTUN(customSTUN); err != nil { 25 | return err 26 | } 27 | conf.STUN = customSTUN 28 | } 29 | 30 | sess := bench.NewWith(conf) 31 | return sess.Start() 32 | } 33 | 34 | // New creates the command 35 | func New() cli.Command { 36 | log.Traceln("Installing 'bench' command") 37 | return cli.Command{ 38 | Name: "bench", 39 | Aliases: []string{"b"}, 40 | Usage: "Benchmark the connexion", 41 | Action: handler, 42 | Flags: []cli.Flag{ 43 | cli.BoolFlag{ 44 | Name: "master, m", 45 | Usage: "Is creating the SDP offer?", 46 | }, 47 | cli.StringFlag{ 48 | Name: "stun", 49 | Usage: "Use a specific STUN server (ex: --stun stun.l.google.com:19302)", 50 | }, 51 | }, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cmd/install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "sort" 5 | 6 | "github.com/antonito/gfile/cmd/bench" 7 | "github.com/antonito/gfile/cmd/receive" 8 | "github.com/antonito/gfile/cmd/send" 9 | log "github.com/sirupsen/logrus" 10 | "gopkg.in/urfave/cli.v1" 11 | ) 12 | 13 | // Install all the commands 14 | func Install(app *cli.App) { 15 | app.Commands = []cli.Command{ 16 | send.New(), 17 | receive.New(), 18 | bench.New(), 19 | } 20 | log.Trace("Installed commands") 21 | 22 | sort.Sort(cli.CommandsByName(app.Commands)) 23 | } 24 | -------------------------------------------------------------------------------- /cmd/receive/cmd.go: -------------------------------------------------------------------------------- 1 | package receive 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | log "github.com/sirupsen/logrus" 8 | 9 | "github.com/antonito/gfile/internal/utils" 10 | "github.com/antonito/gfile/pkg/session/receiver" 11 | "gopkg.in/urfave/cli.v1" 12 | ) 13 | 14 | func handler(c *cli.Context) error { 15 | output := c.String("output") 16 | if output == "" { 17 | return fmt.Errorf("output parameter missing") 18 | } 19 | f, err := os.OpenFile(output, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) 20 | if err != nil { 21 | return err 22 | } 23 | defer f.Close() 24 | 25 | conf := receiver.Config{ 26 | Stream: f, 27 | } 28 | 29 | customSTUN := c.String("stun") 30 | if customSTUN != "" { 31 | if err := utils.ParseSTUN(customSTUN); err != nil { 32 | return err 33 | } 34 | conf.STUN = customSTUN 35 | } 36 | 37 | sess := receiver.NewWith(conf) 38 | return sess.Start() 39 | } 40 | 41 | // New creates the command 42 | func New() cli.Command { 43 | log.Traceln("Installing 'receive' command") 44 | return cli.Command{ 45 | Name: "receive", 46 | Aliases: []string{"r"}, 47 | Usage: "Receive a file", 48 | Action: handler, 49 | Flags: []cli.Flag{ 50 | cli.StringFlag{ 51 | Name: "output, o", 52 | Usage: "Output", 53 | }, 54 | cli.StringFlag{ 55 | Name: "stun", 56 | Usage: "Use a specific STUN server (ex: --stun stun.l.google.com:19302)", 57 | }, 58 | }, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/send/cmd.go: -------------------------------------------------------------------------------- 1 | package send 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/antonito/gfile/internal/utils" 8 | "github.com/antonito/gfile/pkg/session/common" 9 | "github.com/antonito/gfile/pkg/session/sender" 10 | log "github.com/sirupsen/logrus" 11 | "gopkg.in/urfave/cli.v1" 12 | ) 13 | 14 | func handler(c *cli.Context) error { 15 | fileToSend := c.String("file") 16 | if fileToSend == "" { 17 | return fmt.Errorf("file parameter missing") 18 | } 19 | f, err := os.Open(fileToSend) 20 | if err != nil { 21 | return err 22 | } 23 | defer f.Close() 24 | conf := sender.Config{ 25 | Stream: f, 26 | Configuration: common.Configuration{ 27 | OnCompletion: func() { 28 | }, 29 | }, 30 | } 31 | 32 | customSTUN := c.String("stun") 33 | if customSTUN != "" { 34 | if err := utils.ParseSTUN(customSTUN); err != nil { 35 | return err 36 | } 37 | conf.STUN = customSTUN 38 | } 39 | 40 | sess := sender.NewWith(conf) 41 | return sess.Start() 42 | } 43 | 44 | // New creates the command 45 | func New() cli.Command { 46 | log.Traceln("Installing 'send' command") 47 | return cli.Command{ 48 | Name: "send", 49 | Aliases: []string{"s"}, 50 | Usage: "Sends a file", 51 | Action: handler, 52 | Flags: []cli.Flag{ 53 | cli.StringFlag{ 54 | Name: "file, f", 55 | Usage: "Send content of file `FILE`", 56 | }, 57 | cli.StringFlag{ 58 | Name: "stun", 59 | Usage: "Use a specific STUN server (ex: --stun stun.l.google.com:19302)", 60 | }, 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/antonito/gfile 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/golang/protobuf v1.3.1 // indirect 7 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 8 | github.com/kr/pretty v0.1.0 // indirect 9 | github.com/lucas-clemente/quic-go v0.11.0 // indirect 10 | github.com/onsi/ginkgo v1.8.0 // indirect 11 | github.com/onsi/gomega v1.5.0 // indirect 12 | github.com/pion/webrtc/v2 v2.0.1 13 | github.com/sirupsen/logrus v1.4.1 14 | github.com/stretchr/testify v1.3.0 15 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect 16 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 // indirect 17 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 18 | gopkg.in/urfave/cli.v1 v1.20.0 19 | gopkg.in/yaml.v2 v2.2.2 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= 2 | github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 7 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 8 | github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= 9 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 10 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 11 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 12 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 14 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 15 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 16 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 17 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 18 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 19 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 20 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 21 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 22 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 23 | github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= 24 | github.com/lucas-clemente/quic-go v0.11.0 h1:R7uxGrBWWSp817cdhkrunFsOA26vadf4EI9slWzkjlQ= 25 | github.com/lucas-clemente/quic-go v0.11.0/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= 26 | github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA= 27 | github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= 28 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 29 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 30 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 31 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 32 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 33 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 34 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 35 | github.com/pion/datachannel v1.3.0 h1:gxt/xGufDn8Yylk0uJB231xbGQVlFjVps+KdUAUl5Ls= 36 | github.com/pion/datachannel v1.3.0/go.mod h1:lxFbZLIT+EBPmy5AiCv8M0CXkcuTL53A4cyagZiRrDo= 37 | github.com/pion/dtls v1.3.0 h1:5jcC5bBzRcLfxmUH60zp/slIe/tjCLmz6AUZagPYmhA= 38 | github.com/pion/dtls v1.3.0/go.mod h1:CjlPLfQdsTg3G4AEXjJp8FY5bRweBlxHrgoFrN+fQsk= 39 | github.com/pion/ice v0.2.1 h1:DhYn8s52H54SBbS5qu3XoGvTfseU47pe15yV3udNpww= 40 | github.com/pion/ice v0.2.1/go.mod h1:igvbO76UeYthbSu0UsUTqjyWpFT3diUmM+x2vt4p4fw= 41 | github.com/pion/logging v0.2.1-0.20190404202522-3c79a8accd0a/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 42 | github.com/pion/logging v0.2.1 h1:LwASkBKZ+2ysGJ+jLv1E/9H1ge0k1nTfi1X+5zirkDk= 43 | github.com/pion/logging v0.2.1/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 44 | github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA= 45 | github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= 46 | github.com/pion/rtcp v1.1.5 h1:UO4u+U3IYVzA1tWCNrR+hUo02tpOrn4elwZ9pQzBVKo= 47 | github.com/pion/rtcp v1.1.5/go.mod h1:a5dj2d6BKIKHl43EnAOIrCczcjESrtPuMgfmL6/K6QM= 48 | github.com/pion/rtp v1.1.1 h1:lag+9/lSOLBEYeYB/28KXm/ka1H++4wkmSj/WkttV6Y= 49 | github.com/pion/rtp v1.1.1/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE= 50 | github.com/pion/sctp v1.5.0 h1:VcixluIP/XBKL3wRRYIzpvbkFQFVs2yUWJo1NUivy7k= 51 | github.com/pion/sctp v1.5.0/go.mod h1:btfZTRxsoVwp7PfvorgOKqkxV/BKHGGrNf1YUKnMGRQ= 52 | github.com/pion/sdp/v2 v2.1.1 h1:i3fAyjiLuQseYNo0BtCOPfzp91Ppb7vasRGmUUTog28= 53 | github.com/pion/sdp/v2 v2.1.1/go.mod h1:idSlWxhfWQDtTy9J05cgxpHBu/POwXN2VDRGYxT/EjU= 54 | github.com/pion/srtp v1.2.1 h1:t31SdcMM22MI1Slu591uhX/aVrvNSPpO0XnR62v9x7k= 55 | github.com/pion/srtp v1.2.1/go.mod h1:clAbcxURqAYE9KrsByaBCPK7vUC553yKJ99oHnso5YY= 56 | github.com/pion/stun v0.2.1 h1:rSKJ0ynYkRalRD8BifmkaGLeepCFuGTwG6FxPsrPK8o= 57 | github.com/pion/stun v0.2.1/go.mod h1:TChCNKgwnFiFG/c9K+zqEdd6pO6tlODb9yN1W/zVfsE= 58 | github.com/pion/transport v0.6.0 h1:WAoyJg/6OI8dhCVFl/0JHTMd1iu2iHgGUXevptMtJ3U= 59 | github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= 60 | github.com/pion/webrtc/v2 v2.0.1 h1:aBfUI9WRCsJWd0eZXEWVWvmIBmSuJup2rAM4V6RHAY4= 61 | github.com/pion/webrtc/v2 v2.0.1/go.mod h1:k5JH7wA2/QjMTRb4/zxsC9psvHHVh/snXTmCrLuPRu0= 62 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 63 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 64 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= 68 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 72 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 75 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 76 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= 77 | golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 78 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 79 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= 80 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 81 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 82 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 83 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 84 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 85 | golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 86 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 87 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 h1:1Fzlr8kkDLQwqMP8GxrhptBLqZG/EDpiATneiZHY998= 88 | golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 89 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 90 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 91 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 92 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 93 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 94 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 95 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 96 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 97 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 98 | gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= 99 | gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= 100 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 101 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 102 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 103 | -------------------------------------------------------------------------------- /internal/buffer/buffer.go: -------------------------------------------------------------------------------- 1 | package buffer 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | ) 7 | 8 | // Buffer is a threadsafe buffer 9 | type Buffer struct { 10 | b bytes.Buffer 11 | m sync.Mutex 12 | } 13 | 14 | // Read in a thread-safe way 15 | func (b *Buffer) Read(p []byte) (n int, err error) { 16 | b.m.Lock() 17 | defer b.m.Unlock() 18 | return b.b.Read(p) 19 | } 20 | 21 | // ReadString in a thread-safe way 22 | func (b *Buffer) ReadString(delim byte) (line string, err error) { 23 | b.m.Lock() 24 | defer b.m.Unlock() 25 | return b.b.ReadString(delim) 26 | } 27 | 28 | // Write in a thread-safe way 29 | func (b *Buffer) Write(p []byte) (n int, err error) { 30 | b.m.Lock() 31 | defer b.m.Unlock() 32 | return b.b.Write(p) 33 | } 34 | 35 | // WriteString in a thread-safe way 36 | func (b *Buffer) WriteString(s string) (n int, err error) { 37 | b.m.Lock() 38 | defer b.m.Unlock() 39 | return b.b.WriteString(s) 40 | } 41 | 42 | // String in a thread-safe way 43 | func (b *Buffer) String() string { 44 | b.m.Lock() 45 | defer b.m.Unlock() 46 | return b.b.String() 47 | } 48 | -------------------------------------------------------------------------------- /internal/session/getters.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import "io" 4 | 5 | // SDPProvider returns the SDP input 6 | func (s *Session) SDPProvider() io.Reader { 7 | return s.sdpInput 8 | } 9 | -------------------------------------------------------------------------------- /internal/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/antonito/gfile/pkg/stats" 9 | "github.com/antonito/gfile/pkg/utils" 10 | "github.com/pion/webrtc/v2" 11 | ) 12 | 13 | // CompletionHandler to be called when transfer is done 14 | type CompletionHandler func() 15 | 16 | // Session contains common elements to perform send/receive 17 | type Session struct { 18 | Done chan struct{} 19 | NetworkStats *stats.Stats 20 | sdpInput io.Reader 21 | sdpOutput io.Writer 22 | peerConnection *webrtc.PeerConnection 23 | onCompletion CompletionHandler 24 | stunServers []string 25 | } 26 | 27 | // New creates a new Session 28 | func New(sdpInput io.Reader, sdpOutput io.Writer, customSTUN string) Session { 29 | sess := Session{ 30 | sdpInput: sdpInput, 31 | sdpOutput: sdpOutput, 32 | Done: make(chan struct{}), 33 | NetworkStats: stats.New(), 34 | stunServers: []string{fmt.Sprintf("stun:%s", customSTUN)}, 35 | } 36 | 37 | if sdpInput == nil { 38 | sess.sdpInput = os.Stdin 39 | } 40 | if sdpOutput == nil { 41 | sess.sdpOutput = os.Stdout 42 | } 43 | if customSTUN == "" { 44 | sess.stunServers = []string{"stun:stun.l.google.com:19302"} 45 | } 46 | return sess 47 | } 48 | 49 | // CreateConnection prepares a WebRTC connection 50 | func (s *Session) CreateConnection(onConnectionStateChange func(connectionState webrtc.ICEConnectionState)) error { 51 | config := webrtc.Configuration{ 52 | ICEServers: []webrtc.ICEServer{ 53 | { 54 | URLs: s.stunServers, 55 | }, 56 | }, 57 | } 58 | 59 | // Create a new RTCPeerConnection 60 | peerConnection, err := webrtc.NewPeerConnection(config) 61 | if err != nil { 62 | return err 63 | } 64 | s.peerConnection = peerConnection 65 | peerConnection.OnICEConnectionStateChange(onConnectionStateChange) 66 | 67 | return nil 68 | } 69 | 70 | // ReadSDP from the SDP input stream 71 | func (s *Session) ReadSDP() error { 72 | var sdp webrtc.SessionDescription 73 | 74 | fmt.Println("Please, paste the remote SDP:") 75 | for { 76 | encoded, err := utils.MustReadStream(s.sdpInput) 77 | if err == nil { 78 | if err := utils.Decode(encoded, &sdp); err == nil { 79 | break 80 | } 81 | } 82 | fmt.Println("Invalid SDP, try again...") 83 | } 84 | return s.peerConnection.SetRemoteDescription(sdp) 85 | } 86 | 87 | // CreateDataChannel that will be used to send data 88 | func (s *Session) CreateDataChannel(c *webrtc.DataChannelInit) (*webrtc.DataChannel, error) { 89 | return s.peerConnection.CreateDataChannel("data", c) 90 | } 91 | 92 | // OnDataChannel sets an OnDataChannel handler 93 | func (s *Session) OnDataChannel(handler func(d *webrtc.DataChannel)) { 94 | s.peerConnection.OnDataChannel(handler) 95 | } 96 | 97 | // CreateAnswer set the local description and print the answer SDP 98 | func (s *Session) CreateAnswer() error { 99 | // Create an answer 100 | answer, err := s.peerConnection.CreateAnswer(nil) 101 | if err != nil { 102 | return err 103 | } 104 | return s.createSessionDescription(answer) 105 | } 106 | 107 | // CreateOffer set the local description and print the offer SDP 108 | func (s *Session) CreateOffer() error { 109 | // Create an offer 110 | answer, err := s.peerConnection.CreateOffer(nil) 111 | if err != nil { 112 | return err 113 | } 114 | return s.createSessionDescription(answer) 115 | } 116 | 117 | // createSessionDescription set the local description and print the SDP 118 | func (s *Session) createSessionDescription(desc webrtc.SessionDescription) error { 119 | // Sets the LocalDescription, and starts our UDP listeners 120 | if err := s.peerConnection.SetLocalDescription(desc); err != nil { 121 | return err 122 | } 123 | desc.SDP = utils.StripSDP(desc.SDP) 124 | 125 | // Output the SDP in base64 so we can paste it in browser 126 | resp, err := utils.Encode(desc) 127 | if err != nil { 128 | return err 129 | } 130 | fmt.Println("Send this SDP:") 131 | fmt.Fprintf(s.sdpOutput, "%s\n", resp) 132 | return nil 133 | } 134 | 135 | // OnCompletion is called when session ends 136 | func (s *Session) OnCompletion() { 137 | if s.onCompletion != nil { 138 | s.onCompletion() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /internal/session/session_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "os" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_New(t *testing.T) { 14 | assert := assert.New(t) 15 | input := bufio.NewReader(&bytes.Buffer{}) 16 | output := bufio.NewWriter(&bytes.Buffer{}) 17 | 18 | sess := New(nil, nil, "") 19 | assert.Equal(os.Stdin, sess.sdpInput) 20 | assert.Equal(os.Stdout, sess.sdpOutput) 21 | assert.Equal(1, len(sess.stunServers)) 22 | assert.Equal("stun:stun.l.google.com:19302", sess.stunServers[0]) 23 | 24 | sess = New(input, output, "test:123") 25 | assert.Equal(input, sess.sdpInput) 26 | assert.Equal(output, sess.sdpOutput) 27 | assert.Equal(1, len(sess.stunServers)) 28 | assert.Equal(true, strings.HasPrefix(sess.stunServers[0], "stun:")) 29 | arr := strings.Split(sess.stunServers[0], ":") 30 | assert.Equal(3, len(arr)) 31 | assert.Equal("stun", arr[0]) 32 | assert.Equal("test", arr[1]) 33 | assert.Equal("123", arr[2]) 34 | } 35 | -------------------------------------------------------------------------------- /internal/utils/stun_arg.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // ParseSTUN checks if a STUN addr is valid 10 | func ParseSTUN(stunAddr string) error { 11 | arr := strings.Split(stunAddr, ":") 12 | if len(arr) != 2 { 13 | return fmt.Errorf("invalid stun adress") 14 | } 15 | port, err := strconv.Atoi(arr[1]) 16 | if err != nil || (port <= 0 || port > 0xffff) { 17 | return fmt.Errorf("invalid port %v", port) 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/utils/stun_arg_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_STUN_Arg(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | tests := []struct { 14 | input string 15 | err error 16 | }{ 17 | { 18 | input: "", 19 | err: fmt.Errorf("invalid stun adress"), 20 | }, 21 | { 22 | input: "test", 23 | err: fmt.Errorf("invalid stun adress"), 24 | }, 25 | { 26 | input: "stun:lol:lol", 27 | err: fmt.Errorf("invalid stun adress"), 28 | }, 29 | { 30 | input: "test:wtf", 31 | err: fmt.Errorf("invalid port 0"), 32 | }, 33 | { 34 | input: "test:-2", 35 | err: fmt.Errorf("invalid port -2"), 36 | }, 37 | { 38 | // 0xffff + 1 39 | input: "test:65536", 40 | err: fmt.Errorf("invalid port 65536"), 41 | }, 42 | { 43 | input: "test:5432", 44 | err: nil, 45 | }, 46 | { 47 | input: "stun.l.google.com:19302", 48 | err: nil, 49 | }, 50 | } 51 | 52 | for _, cur := range tests { 53 | err := ParseSTUN(cur.input) 54 | assert.Equal(cur.err, err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/antonito/gfile/cmd" 7 | "gopkg.in/urfave/cli.v1" 8 | 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func setupLogger() { 13 | log.SetOutput(os.Stdout) 14 | 15 | logLevel := log.WarnLevel 16 | 17 | if lvl, ok := os.LookupEnv("GFILE_LOG"); ok { 18 | switch lvl { 19 | case "TRACE": 20 | logLevel = log.TraceLevel 21 | case "DEBUG": 22 | logLevel = log.DebugLevel 23 | case "INFO": 24 | logLevel = log.InfoLevel 25 | case "WARN": 26 | logLevel = log.WarnLevel 27 | case "PANIC": 28 | logLevel = log.PanicLevel 29 | case "ERROR": 30 | logLevel = log.ErrorLevel 31 | case "FATAL": 32 | logLevel = log.FatalLevel 33 | } 34 | } 35 | log.SetLevel(logLevel) 36 | } 37 | 38 | func init() { 39 | setupLogger() 40 | } 41 | 42 | func run(args []string) error { 43 | app := cli.NewApp() 44 | app.Name = "gfile" 45 | app.Version = "0.1" 46 | cli.VersionFlag = cli.BoolFlag{ 47 | Name: "version, V", 48 | Usage: "print only the version", 49 | } 50 | log.Tracef("Starting %s v%v\n", app.Name, app.Version) 51 | 52 | cmd.Install(app) 53 | return app.Run(args) 54 | } 55 | 56 | func main() { 57 | if err := run(os.Args); err != nil { 58 | log.Fatal(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_InitLogger(t *testing.T) { 12 | assert := assert.New(t) 13 | 14 | tests := []struct { 15 | level string 16 | expected log.Level 17 | }{ 18 | { 19 | level: "", 20 | expected: log.WarnLevel, 21 | }, 22 | { 23 | level: "InvalidValue", 24 | expected: log.WarnLevel, 25 | }, 26 | { 27 | level: "TRACE", 28 | expected: log.TraceLevel, 29 | }, 30 | { 31 | level: "DEBUG", 32 | expected: log.DebugLevel, 33 | }, 34 | { 35 | level: "INFO", 36 | expected: log.InfoLevel, 37 | }, 38 | { 39 | level: "WARN", 40 | expected: log.WarnLevel, 41 | }, 42 | { 43 | level: "PANIC", 44 | expected: log.PanicLevel, 45 | }, 46 | { 47 | level: "ERROR", 48 | expected: log.ErrorLevel, 49 | }, 50 | { 51 | level: "FATAL", 52 | expected: log.FatalLevel, 53 | }, 54 | } 55 | 56 | for _, cur := range tests { 57 | if cur.level == "" { 58 | os.Unsetenv("GFILE_LOG") 59 | } else { 60 | os.Setenv("GFILE_LOG", cur.level) 61 | } 62 | setupLogger() 63 | assert.Equal(cur.expected, log.GetLevel()) 64 | } 65 | } 66 | 67 | func Test_Run(t *testing.T) { 68 | assert := assert.New(t) 69 | 70 | args := os.Args[0:1] 71 | err := run(args) 72 | assert.Nil(err) 73 | } 74 | 75 | func Test_Help(t *testing.T) { 76 | assert := assert.New(t) 77 | 78 | args := os.Args[0:1] 79 | args = append(args, "help") 80 | err := run(args) 81 | assert.Nil(err) 82 | } 83 | 84 | func Test_RunReceive(t *testing.T) { 85 | assert := assert.New(t) 86 | 87 | args := os.Args[0:1] 88 | args = append(args, "r") 89 | err := run(args) 90 | assert.NotNil(err) 91 | 92 | args = os.Args[0:1] 93 | args = append(args, "receive") 94 | err = run(args) 95 | assert.NotNil(err) 96 | 97 | // TODO: Test correct start ? 98 | } 99 | 100 | func Test_RunSend(t *testing.T) { 101 | assert := assert.New(t) 102 | 103 | args := os.Args[0:1] 104 | args = append(args, "s") 105 | err := run(args) 106 | assert.NotNil(err) 107 | 108 | args = os.Args[0:1] 109 | args = append(args, "send") 110 | err = run(args) 111 | assert.NotNil(err) 112 | 113 | // TODO: Test correct start ? 114 | } 115 | -------------------------------------------------------------------------------- /pkg/session/bench/benchmark.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | internalSess "github.com/antonito/gfile/internal/session" 8 | "github.com/antonito/gfile/pkg/session/common" 9 | "github.com/antonito/gfile/pkg/stats" 10 | ) 11 | 12 | const ( 13 | bufferThresholdDefault = 64 * 1024 // 64kB 14 | testDurationDefault = 20 * time.Second 15 | testDurationErrorDefault = (testDurationDefault * 10) / 7 16 | ) 17 | 18 | // Session is a benchmark session 19 | type Session struct { 20 | sess internalSess.Session 21 | master bool 22 | wg sync.WaitGroup 23 | 24 | // Settings 25 | bufferThreshold uint64 26 | testDuration time.Duration 27 | testDurationError time.Duration 28 | 29 | startPhase2 chan struct{} 30 | uploadNetworkStats *stats.Stats 31 | downloadDone chan bool 32 | downloadNetworkStats *stats.Stats 33 | } 34 | 35 | // New creates a new sender session 36 | func new(s internalSess.Session, isMaster bool) *Session { 37 | return &Session{ 38 | sess: s, 39 | master: isMaster, 40 | 41 | bufferThreshold: bufferThresholdDefault, 42 | testDuration: testDurationDefault, 43 | testDurationError: testDurationErrorDefault, 44 | 45 | startPhase2: make(chan struct{}), 46 | downloadDone: make(chan bool), 47 | uploadNetworkStats: stats.New(), 48 | downloadNetworkStats: stats.New(), 49 | } 50 | } 51 | 52 | // Config contains custom configuration for a session 53 | type Config struct { 54 | common.Configuration 55 | Master bool // Will create the SDP offer ? 56 | } 57 | 58 | // NewWith createa a new benchmark Session with custom configuration 59 | func NewWith(c Config) *Session { 60 | return new(internalSess.New(c.SDPProvider, c.SDPOutput, c.STUN), c.Master) 61 | } 62 | -------------------------------------------------------------------------------- /pkg/session/bench/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/antonito/gfile/internal/buffer" 8 | "github.com/antonito/gfile/pkg/session/common" 9 | "github.com/antonito/gfile/pkg/utils" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func Test_New(t *testing.T) { 14 | assert := assert.New(t) 15 | 16 | sess := NewWith(Config{ 17 | Master: false, 18 | }) 19 | 20 | assert.NotNil(sess) 21 | assert.Equal(false, sess.master) 22 | } 23 | 24 | func Test_Bench(t *testing.T) { 25 | assert := assert.New(t) 26 | 27 | sessionSDPProvider := &buffer.Buffer{} 28 | sessionSDPOutput := &buffer.Buffer{} 29 | sessionMasterSDPProvider := &buffer.Buffer{} 30 | sessionMasterSDPOutput := &buffer.Buffer{} 31 | 32 | testDuration := 2 * time.Second 33 | 34 | sess := NewWith(Config{ 35 | Configuration: common.Configuration{ 36 | SDPProvider: sessionSDPProvider, 37 | SDPOutput: sessionSDPOutput, 38 | }, 39 | Master: false, 40 | }) 41 | assert.NotNil(sess) 42 | sess.testDuration = testDuration 43 | sess.testDurationError = (testDuration * 10) / 8 44 | 45 | sessMaster := NewWith(Config{ 46 | Configuration: common.Configuration{ 47 | SDPProvider: sessionMasterSDPProvider, 48 | SDPOutput: sessionMasterSDPOutput, 49 | }, 50 | Master: true, 51 | }) 52 | assert.NotNil(sessMaster) 53 | sessMaster.testDuration = testDuration 54 | sessMaster.testDurationError = (testDuration * 10) / 8 55 | 56 | masterDone := make(chan struct{}) 57 | go func() { 58 | defer close(masterDone) 59 | err := sessMaster.Start() 60 | assert.Nil(err) 61 | }() 62 | 63 | sdp, err := utils.MustReadStream(sessionMasterSDPOutput) 64 | assert.Nil(err) 65 | sdp += "\n" 66 | n, err := sessionSDPProvider.WriteString(sdp) 67 | assert.Nil(err) 68 | assert.Equal(len(sdp), n) 69 | 70 | slaveDone := make(chan struct{}) 71 | go func() { 72 | defer close(slaveDone) 73 | err := sess.Start() 74 | assert.Nil(err) 75 | }() 76 | 77 | // Get SDP from slave and send it to the master 78 | sdp, err = utils.MustReadStream(sessionSDPOutput) 79 | assert.Nil(err) 80 | n, err = sessionMasterSDPProvider.WriteString(sdp) 81 | assert.Nil(err) 82 | assert.Equal(len(sdp), n) 83 | 84 | <-masterDone 85 | <-slaveDone 86 | } 87 | -------------------------------------------------------------------------------- /pkg/session/bench/id.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | const ( 4 | // Used as upload channel for master (and download channel for non-master) 5 | // 43981 -> 0xABCD 6 | dataChannel1ID = uint16(43981) 7 | // Used as download channel for master (and upload channel for non-master) 8 | // 61185 -> 0xef01 9 | dataChannel2ID = uint16(61185) 10 | ) 11 | 12 | func (s *Session) uploadChannelID() uint16 { 13 | if s.master { 14 | return dataChannel1ID 15 | } 16 | return dataChannel2ID 17 | } 18 | 19 | func (s *Session) downloadChannelID() uint16 { 20 | if s.master { 21 | return dataChannel2ID 22 | } 23 | return dataChannel1ID 24 | } 25 | -------------------------------------------------------------------------------- /pkg/session/bench/id_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_IDs(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | sess := NewWith(Config{ 13 | Master: false, 14 | }) 15 | assert.NotNil(sess) 16 | assert.Equal(false, sess.master) 17 | 18 | sessMaster := NewWith(Config{ 19 | Master: true, 20 | }) 21 | assert.NotNil(sessMaster) 22 | assert.Equal(true, sessMaster.master) 23 | 24 | assert.Equal(sessMaster.downloadChannelID(), sess.uploadChannelID()) 25 | assert.Equal(sessMaster.uploadChannelID(), sess.downloadChannelID()) 26 | assert.NotEqual(sessMaster.downloadChannelID(), sess.downloadChannelID()) 27 | 28 | } 29 | -------------------------------------------------------------------------------- /pkg/session/bench/init.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pion/webrtc/v2" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Start initializes the connection and the benchmark 11 | func (s *Session) Start() error { 12 | if err := s.sess.CreateConnection(s.onConnectionStateChange()); err != nil { 13 | log.Errorln(err) 14 | return err 15 | } 16 | 17 | s.sess.OnDataChannel(s.onNewDataChannel()) 18 | if err := s.createUploadDataChannel(); err != nil { 19 | log.Errorln(err) 20 | return err 21 | } 22 | 23 | s.wg.Add(2) // Download + Upload 24 | if s.master { 25 | if err := s.createMasterSession(); err != nil { 26 | return err 27 | } 28 | } else { 29 | if err := s.createSlaveSession(); err != nil { 30 | return err 31 | } 32 | } 33 | // Wait for benchmarks to be done 34 | s.wg.Wait() 35 | 36 | fmt.Printf("Upload: %s\n", s.uploadNetworkStats.String()) 37 | fmt.Printf("Download: %s\n", s.downloadNetworkStats.String()) 38 | s.sess.OnCompletion() 39 | return nil 40 | } 41 | 42 | func (s *Session) initDataChannel(channelID *uint16) (*webrtc.DataChannel, error) { 43 | ordered := true 44 | maxPacketLifeTime := uint16(10000) 45 | return s.sess.CreateDataChannel(&webrtc.DataChannelInit{ 46 | Ordered: &ordered, 47 | MaxPacketLifeTime: &maxPacketLifeTime, 48 | ID: channelID, 49 | }) 50 | } 51 | 52 | func (s *Session) createUploadDataChannel() error { 53 | channelID := s.uploadChannelID() 54 | dataChannel, err := s.initDataChannel(&channelID) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | dataChannel.OnOpen(s.onOpenUploadHandler(dataChannel)) 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/session/bench/session.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/pion/webrtc/v2" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | // Useful for unit tests 9 | func (s *Session) onNewDataChannelHelper(name string, channelID uint16, d *webrtc.DataChannel) { 10 | log.Tracef("New DataChannel %s (id: %x)\n", name, channelID) 11 | 12 | switch channelID { 13 | case s.downloadChannelID(): 14 | log.Traceln("Created Download data channel") 15 | d.OnClose(s.onCloseHandlerDownload()) 16 | go s.onOpenHandlerDownload(d)() 17 | 18 | case s.uploadChannelID(): 19 | log.Traceln("Created Upload data channel") 20 | 21 | default: 22 | log.Warningln("Created unknown data channel") 23 | } 24 | } 25 | 26 | func (s *Session) onNewDataChannel() func(d *webrtc.DataChannel) { 27 | return func(d *webrtc.DataChannel) { 28 | if d == nil || d.ID() == nil { 29 | return 30 | } 31 | s.onNewDataChannelHelper(d.Label(), *d.ID(), d) 32 | } 33 | } 34 | 35 | func (s *Session) createMasterSession() error { 36 | if err := s.sess.CreateOffer(); err != nil { 37 | log.Errorln(err) 38 | return err 39 | } 40 | 41 | if err := s.sess.ReadSDP(); err != nil { 42 | log.Errorln(err) 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | func (s *Session) createSlaveSession() error { 49 | if err := s.sess.ReadSDP(); err != nil { 50 | log.Errorln(err) 51 | return err 52 | } 53 | 54 | if err := s.sess.CreateAnswer(); err != nil { 55 | log.Errorln(err) 56 | return err 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/session/bench/session_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_OnNewDataChannel(t *testing.T) { 11 | assert := assert.New(t) 12 | testDuration := 2 * time.Second 13 | 14 | sess := NewWith(Config{ 15 | Master: false, 16 | }) 17 | assert.NotNil(sess) 18 | sess.testDuration = testDuration 19 | sess.testDurationError = (testDuration * 10) / 8 20 | 21 | sess.onNewDataChannel()(nil) 22 | 23 | testID := sess.uploadChannelID() 24 | sess.onNewDataChannelHelper("", testID, nil) 25 | 26 | testID = sess.uploadChannelID() | sess.downloadChannelID() 27 | sess.onNewDataChannelHelper("", testID, nil) 28 | } 29 | -------------------------------------------------------------------------------- /pkg/session/bench/state.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "github.com/pion/webrtc/v2" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func (s *Session) onConnectionStateChange() func(connectionState webrtc.ICEConnectionState) { 9 | return func(connectionState webrtc.ICEConnectionState) { 10 | log.Infof("ICE Connection State has changed: %s\n", connectionState.String()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pkg/session/bench/state_download.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/pion/webrtc/v2" 8 | log "github.com/sirupsen/logrus" 9 | ) 10 | 11 | func (s *Session) onOpenHandlerDownload(dc *webrtc.DataChannel) func() { 12 | // If master, wait for the upload to complete 13 | // If not master, close the channel so the upload can start 14 | return func() { 15 | if s.master { 16 | <-s.startPhase2 17 | } 18 | 19 | log.Debugf("Starting to download data...") 20 | defer log.Debugf("Stopped downloading data...") 21 | 22 | s.downloadNetworkStats.Start() 23 | 24 | // Useful for unit tests 25 | if dc != nil { 26 | dc.OnMessage(func(msg webrtc.DataChannelMessage) { 27 | fmt.Printf("Downloading at %.2f MB/s\r", s.downloadNetworkStats.Bandwidth()) 28 | s.downloadNetworkStats.AddBytes(uint64(len(msg.Data))) 29 | }) 30 | } else { 31 | log.Warningln("No DataChannel provided") 32 | } 33 | 34 | timeoutErr := time.After(s.testDurationError) 35 | fmt.Printf("Downloading random datas ... (%d s)\n", int(s.testDuration.Seconds())) 36 | 37 | select { 38 | case <-s.downloadDone: 39 | case <-timeoutErr: 40 | log.Error("Time'd out") 41 | } 42 | 43 | log.Traceln("Done downloading") 44 | 45 | if !s.master { 46 | close(s.startPhase2) 47 | } 48 | 49 | fmt.Printf("\n") 50 | s.downloadNetworkStats.Stop() 51 | s.wg.Done() 52 | } 53 | } 54 | 55 | func (s *Session) onCloseHandlerDownload() func() { 56 | return func() { 57 | close(s.downloadDone) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/session/bench/state_upload.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/pion/webrtc/v2" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func (s *Session) onOpenUploadHandler(dc *webrtc.DataChannel) func() { 13 | return func() { 14 | if !s.master { 15 | <-s.startPhase2 16 | } 17 | 18 | log.Debugln("Starting to upload data...") 19 | defer log.Debugln("Stopped uploading data...") 20 | 21 | lenToken := uint64(4096) 22 | token := make([]byte, lenToken) 23 | if _, err := rand.Read(token); err != nil { 24 | log.Fatalln("Err: ", err) 25 | } 26 | 27 | s.uploadNetworkStats.Start() 28 | 29 | // Useful for unit tests 30 | if dc != nil { 31 | dc.SetBufferedAmountLowThreshold(s.bufferThreshold) 32 | dc.OnBufferedAmountLow(func() { 33 | if err := dc.Send(token); err == nil { 34 | fmt.Printf("Uploading at %.2f MB/s\r", s.uploadNetworkStats.Bandwidth()) 35 | s.uploadNetworkStats.AddBytes(lenToken) 36 | } 37 | }) 38 | } else { 39 | log.Warningln("No DataChannel provided") 40 | } 41 | 42 | fmt.Printf("Uploading random datas ... (%d s)\n", int(s.testDuration.Seconds())) 43 | timeout := time.After(s.testDuration) 44 | timeoutErr := time.After(s.testDurationError) 45 | 46 | if dc != nil { 47 | // Ignore potential error 48 | _ = dc.Send(token) 49 | } 50 | SENDING_LOOP: 51 | for { 52 | select { 53 | case <-timeoutErr: 54 | log.Error("Time'd out") 55 | break SENDING_LOOP 56 | 57 | case <-timeout: 58 | log.Traceln("Done uploading") 59 | break SENDING_LOOP 60 | } 61 | } 62 | fmt.Printf("\n") 63 | s.uploadNetworkStats.Stop() 64 | 65 | if dc != nil { 66 | dc.Close() 67 | } 68 | 69 | if s.master { 70 | close(s.startPhase2) 71 | } 72 | 73 | s.wg.Done() 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/session/bench/timeout_test.go: -------------------------------------------------------------------------------- 1 | package bench 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_TimeoutDownload(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | sess := NewWith(Config{ 14 | Master: false, 15 | }) 16 | 17 | assert.NotNil(sess) 18 | assert.Equal(false, sess.master) 19 | sess.testDurationError = 2 * time.Millisecond 20 | 21 | sess.wg.Add(1) 22 | sess.onOpenHandlerDownload(nil)() 23 | } 24 | 25 | func Test_TimeoutUpload(t *testing.T) { 26 | assert := assert.New(t) 27 | 28 | sess := NewWith(Config{ 29 | Master: true, 30 | }) 31 | 32 | assert.NotNil(sess) 33 | assert.Equal(true, sess.master) 34 | sess.testDurationError = 2 * time.Millisecond 35 | 36 | sess.wg.Add(1) 37 | sess.onOpenUploadHandler(nil)() 38 | } 39 | -------------------------------------------------------------------------------- /pkg/session/common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/antonito/gfile/internal/session" 7 | ) 8 | 9 | // Configuration common to both Sender and Receiver session 10 | type Configuration struct { 11 | SDPProvider io.Reader // The SDP reader 12 | SDPOutput io.Writer // The SDP writer 13 | OnCompletion session.CompletionHandler // Handler to call on session completion 14 | STUN string // Custom STUN server 15 | } 16 | -------------------------------------------------------------------------------- /pkg/session/receiver/init.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pion/webrtc/v2" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | // Initialize creates the connection, the datachannel and creates the offer 11 | func (s *Session) Initialize() error { 12 | if s.initialized { 13 | return nil 14 | } 15 | if err := s.sess.CreateConnection(s.onConnectionStateChange()); err != nil { 16 | log.Errorln(err) 17 | return err 18 | } 19 | s.createDataHandler() 20 | if err := s.sess.ReadSDP(); err != nil { 21 | log.Errorln(err) 22 | return err 23 | } 24 | if err := s.sess.CreateAnswer(); err != nil { 25 | log.Errorln(err) 26 | return err 27 | } 28 | 29 | s.initialized = true 30 | return nil 31 | } 32 | 33 | // Start initializes the connection and the file transfer 34 | func (s *Session) Start() error { 35 | if err := s.Initialize(); err != nil { 36 | return err 37 | } 38 | 39 | // Handle data 40 | s.receiveData() 41 | s.sess.OnCompletion() 42 | return nil 43 | } 44 | 45 | func (s *Session) createDataHandler() { 46 | s.sess.OnDataChannel(func(d *webrtc.DataChannel) { 47 | log.Debugf("New DataChannel %s %d\n", d.Label(), d.ID()) 48 | s.sess.NetworkStats.Start() 49 | d.OnMessage(s.onMessage()) 50 | d.OnClose(s.onClose()) 51 | }) 52 | } 53 | 54 | func (s *Session) receiveData() { 55 | log.Infoln("Starting to receive data...") 56 | defer log.Infoln("Stopped receiving data...") 57 | 58 | // Consume the message channel, until done 59 | // Does not stop on error 60 | for { 61 | select { 62 | case <-s.sess.Done: 63 | s.sess.NetworkStats.Stop() 64 | fmt.Printf("\nNetwork: %s\n", s.sess.NetworkStats.String()) 65 | return 66 | case msg := <-s.msgChannel: 67 | n, err := s.stream.Write(msg.Data) 68 | 69 | if err != nil { 70 | log.Errorln(err) 71 | } else { 72 | currentSpeed := s.sess.NetworkStats.Bandwidth() 73 | fmt.Printf("Transferring at %.2f MB/s\r", currentSpeed) 74 | s.sess.NetworkStats.AddBytes(uint64(n)) 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pkg/session/receiver/receiver.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "io" 5 | 6 | internalSess "github.com/antonito/gfile/internal/session" 7 | "github.com/antonito/gfile/pkg/session/common" 8 | "github.com/pion/webrtc/v2" 9 | ) 10 | 11 | // Session is a receiver session 12 | type Session struct { 13 | sess internalSess.Session 14 | stream io.Writer 15 | msgChannel chan webrtc.DataChannelMessage 16 | initialized bool 17 | } 18 | 19 | func new(s internalSess.Session, f io.Writer) *Session { 20 | return &Session{ 21 | sess: s, 22 | stream: f, 23 | msgChannel: make(chan webrtc.DataChannelMessage, 4096*2), 24 | initialized: false, 25 | } 26 | } 27 | 28 | // New creates a new receiver session 29 | func New(f io.Writer) *Session { 30 | return new(internalSess.New(nil, nil, ""), f) 31 | } 32 | 33 | // Config contains custom configuration for a session 34 | type Config struct { 35 | common.Configuration 36 | Stream io.Writer // The Stream to write to 37 | } 38 | 39 | // NewWith createa a new receiver Session with custom configuration 40 | func NewWith(c Config) *Session { 41 | return new(internalSess.New(c.SDPProvider, c.SDPOutput, c.STUN), c.Stream) 42 | } 43 | 44 | // SetStream changes the stream, useful for WASM integration 45 | func (s *Session) SetStream(stream io.Writer) { 46 | s.stream = stream 47 | } 48 | -------------------------------------------------------------------------------- /pkg/session/receiver/receiver_test.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_New(t *testing.T) { 12 | assert := assert.New(t) 13 | output := bufio.NewWriter(&bytes.Buffer{}) 14 | 15 | sess := New(output) 16 | 17 | assert.NotNil(sess) 18 | assert.Equal(output, sess.stream) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/session/receiver/state.go: -------------------------------------------------------------------------------- 1 | package receiver 2 | 3 | import ( 4 | "github.com/pion/webrtc/v2" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | func (s *Session) onConnectionStateChange() func(connectionState webrtc.ICEConnectionState) { 9 | return func(connectionState webrtc.ICEConnectionState) { 10 | log.Infof("ICE Connection State has changed: %s\n", connectionState.String()) 11 | } 12 | } 13 | 14 | func (s *Session) onMessage() func(msg webrtc.DataChannelMessage) { 15 | return func(msg webrtc.DataChannelMessage) { 16 | // Store each message in the message channel 17 | s.msgChannel <- msg 18 | } 19 | } 20 | 21 | func (s *Session) onClose() func() { 22 | return func() { 23 | close(s.sess.Done) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/session/sender/getters.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import "io" 4 | 5 | // SDPProvider returns the underlying SDPProvider 6 | func (s *Session) SDPProvider() io.Reader { 7 | return s.sess.SDPProvider() 8 | } 9 | -------------------------------------------------------------------------------- /pkg/session/sender/init.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "github.com/pion/webrtc/v2" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | const ( 9 | bufferThreshold = 512 * 1024 // 512kB 10 | ) 11 | 12 | // Initialize creates the connection, the datachannel and creates the offer 13 | func (s *Session) Initialize() error { 14 | if s.initialized { 15 | return nil 16 | } 17 | 18 | if err := s.sess.CreateConnection(s.onConnectionStateChange()); err != nil { 19 | log.Errorln(err) 20 | return err 21 | } 22 | if err := s.createDataChannel(); err != nil { 23 | log.Errorln(err) 24 | return err 25 | } 26 | if err := s.sess.CreateOffer(); err != nil { 27 | log.Errorln(err) 28 | return err 29 | } 30 | 31 | s.initialized = true 32 | return nil 33 | } 34 | 35 | // Start the connection and the file transfer 36 | func (s *Session) Start() error { 37 | if err := s.Initialize(); err != nil { 38 | return err 39 | } 40 | go s.readFile() 41 | if err := s.sess.ReadSDP(); err != nil { 42 | log.Errorln(err) 43 | return err 44 | } 45 | <-s.sess.Done 46 | s.sess.OnCompletion() 47 | return nil 48 | } 49 | 50 | func (s *Session) createDataChannel() error { 51 | ordered := true 52 | maxPacketLifeTime := uint16(10000) 53 | dataChannel, err := s.sess.CreateDataChannel(&webrtc.DataChannelInit{ 54 | Ordered: &ordered, 55 | MaxPacketLifeTime: &maxPacketLifeTime, 56 | }) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | s.dataChannel = dataChannel 62 | s.dataChannel.OnBufferedAmountLow(s.onBufferedAmountLow()) 63 | s.dataChannel.SetBufferedAmountLowThreshold(bufferThreshold) 64 | s.dataChannel.OnOpen(s.onOpenHandler()) 65 | s.dataChannel.OnClose(s.onCloseHandler()) 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /pkg/session/sender/io.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (s *Session) readFile() { 11 | log.Infof("Starting to read data...") 12 | s.readingStats.Start() 13 | defer func() { 14 | s.readingStats.Pause() 15 | log.Infof("Stopped reading data...") 16 | close(s.output) 17 | }() 18 | 19 | for { 20 | // Read file 21 | s.dataBuff = s.dataBuff[:cap(s.dataBuff)] 22 | n, err := s.stream.Read(s.dataBuff) 23 | if err != nil { 24 | if err == io.EOF { 25 | s.readingStats.Stop() 26 | log.Debugf("Got EOF after %v bytes!\n", s.readingStats.Bytes()) 27 | return 28 | } 29 | log.Errorf("Read Error: %v\n", err) 30 | return 31 | } 32 | s.dataBuff = s.dataBuff[:n] 33 | s.readingStats.AddBytes(uint64(n)) 34 | 35 | s.output <- outputMsg{ 36 | n: n, 37 | // Make a copy of the buffer 38 | buff: append([]byte(nil), s.dataBuff...), 39 | } 40 | } 41 | } 42 | 43 | func (s *Session) onBufferedAmountLow() func() { 44 | return func() { 45 | data := <-s.output 46 | if data.n != 0 { 47 | s.msgToBeSent = append(s.msgToBeSent, data) 48 | } else if len(s.msgToBeSent) == 0 && s.dataChannel.BufferedAmount() == 0 { 49 | s.sess.NetworkStats.Stop() 50 | s.close(false) 51 | return 52 | } 53 | 54 | currentSpeed := s.sess.NetworkStats.Bandwidth() 55 | fmt.Printf("Transferring at %.2f MB/s\r", currentSpeed) 56 | 57 | for len(s.msgToBeSent) != 0 { 58 | cur := s.msgToBeSent[0] 59 | 60 | if err := s.dataChannel.Send(cur.buff); err != nil { 61 | log.Errorf("Error, cannot send to client: %v\n", err) 62 | return 63 | } 64 | s.sess.NetworkStats.AddBytes(uint64(cur.n)) 65 | s.msgToBeSent = s.msgToBeSent[1:] 66 | } 67 | } 68 | } 69 | 70 | func (s *Session) writeToNetwork() { 71 | // Set callback, as transfer may be paused 72 | s.dataChannel.OnBufferedAmountLow(s.onBufferedAmountLow()) 73 | 74 | <-s.stopSending 75 | s.dataChannel.OnBufferedAmountLow(nil) 76 | s.sess.NetworkStats.Pause() 77 | log.Infof("Pausing network I/O... (remaining at least %v packets)\n", len(s.output)) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/session/sender/sender.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | 7 | internalSess "github.com/antonito/gfile/internal/session" 8 | "github.com/antonito/gfile/pkg/session/common" 9 | "github.com/antonito/gfile/pkg/stats" 10 | "github.com/pion/webrtc/v2" 11 | ) 12 | 13 | const ( 14 | // Must be <= 16384 15 | senderBuffSize = 16384 16 | ) 17 | 18 | type outputMsg struct { 19 | n int 20 | buff []byte 21 | } 22 | 23 | // Session is a sender session 24 | type Session struct { 25 | sess internalSess.Session 26 | stream io.Reader 27 | initialized bool 28 | 29 | dataChannel *webrtc.DataChannel 30 | dataBuff []byte 31 | msgToBeSent []outputMsg 32 | stopSending chan struct{} 33 | output chan outputMsg 34 | 35 | doneCheckLock sync.Mutex 36 | doneCheck bool 37 | 38 | // Stats/infos 39 | readingStats *stats.Stats 40 | } 41 | 42 | // New creates a new sender session 43 | func new(s internalSess.Session, f io.Reader) *Session { 44 | return &Session{ 45 | sess: s, 46 | stream: f, 47 | initialized: false, 48 | dataBuff: make([]byte, senderBuffSize), 49 | stopSending: make(chan struct{}, 1), 50 | output: make(chan outputMsg, senderBuffSize*10), 51 | doneCheck: false, 52 | readingStats: stats.New(), 53 | } 54 | } 55 | 56 | // New creates a new receiver session 57 | func New(f io.Reader) *Session { 58 | return new(internalSess.New(nil, nil, ""), f) 59 | } 60 | 61 | // Config contains custom configuration for a session 62 | type Config struct { 63 | common.Configuration 64 | Stream io.Reader // The Stream to read from 65 | } 66 | 67 | // NewWith createa a new sender Session with custom configuration 68 | func NewWith(c Config) *Session { 69 | return new(internalSess.New(c.SDPProvider, c.SDPOutput, c.STUN), c.Stream) 70 | } 71 | 72 | // SetStream changes the stream, useful for WASM integration 73 | func (s *Session) SetStream(stream io.Reader) { 74 | s.stream = stream 75 | } 76 | -------------------------------------------------------------------------------- /pkg/session/sender/sender_test.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_New(t *testing.T) { 12 | assert := assert.New(t) 13 | input := bufio.NewReader(&bytes.Buffer{}) 14 | 15 | sess := New(input) 16 | 17 | assert.NotNil(sess) 18 | assert.Equal(input, sess.stream) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/session/sender/state.go: -------------------------------------------------------------------------------- 1 | package sender 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pion/webrtc/v2" 7 | log "github.com/sirupsen/logrus" 8 | ) 9 | 10 | func (s *Session) onConnectionStateChange() func(connectionState webrtc.ICEConnectionState) { 11 | return func(connectionState webrtc.ICEConnectionState) { 12 | log.Infof("ICE Connection State has changed: %s\n", connectionState.String()) 13 | if connectionState == webrtc.ICEConnectionStateDisconnected { 14 | s.stopSending <- struct{}{} 15 | } 16 | } 17 | } 18 | 19 | func (s *Session) onOpenHandler() func() { 20 | return func() { 21 | s.sess.NetworkStats.Start() 22 | 23 | log.Infof("Starting to send data...") 24 | defer log.Infof("Stopped sending data...") 25 | 26 | s.writeToNetwork() 27 | } 28 | } 29 | 30 | func (s *Session) onCloseHandler() func() { 31 | return func() { 32 | s.close(true) 33 | } 34 | } 35 | 36 | func (s *Session) close(calledFromCloseHandler bool) { 37 | if !calledFromCloseHandler { 38 | s.dataChannel.Close() 39 | } 40 | 41 | // Sometime, onCloseHandler is not invoked, so it's a work-around 42 | s.doneCheckLock.Lock() 43 | if s.doneCheck { 44 | s.doneCheckLock.Unlock() 45 | return 46 | } 47 | s.doneCheck = true 48 | s.doneCheckLock.Unlock() 49 | s.dumpStats() 50 | close(s.sess.Done) 51 | } 52 | 53 | func (s *Session) dumpStats() { 54 | fmt.Printf(` 55 | Disk : %s 56 | Network: %s 57 | `, s.readingStats.String(), s.sess.NetworkStats.String()) 58 | } 59 | -------------------------------------------------------------------------------- /pkg/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | // Session defines a common interface for sender and receiver sessions 4 | type Session interface { 5 | // Start a connection and starts the file transfer 6 | Start() error 7 | } 8 | -------------------------------------------------------------------------------- /pkg/session/session_test.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/antonito/gfile/internal/buffer" 9 | "github.com/antonito/gfile/pkg/session/common" 10 | "github.com/antonito/gfile/pkg/session/receiver" 11 | "github.com/antonito/gfile/pkg/session/sender" 12 | "github.com/antonito/gfile/pkg/utils" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | // Tests 17 | 18 | func Test_CreateReceiverSession(t *testing.T) { 19 | assert := assert.New(t) 20 | stream := &bytes.Buffer{} 21 | 22 | sess := receiver.NewWith(receiver.Config{ 23 | Stream: stream, 24 | }) 25 | assert.NotNil(sess) 26 | } 27 | 28 | func Test_TransferSmallMessage(t *testing.T) { 29 | assert := assert.New(t) 30 | 31 | // Create client receiver 32 | clientStream := &buffer.Buffer{} 33 | clientSDPProvider := &buffer.Buffer{} 34 | clientSDPOutput := &buffer.Buffer{} 35 | clientConfig := receiver.Config{ 36 | Stream: clientStream, 37 | Configuration: common.Configuration{ 38 | SDPProvider: clientSDPProvider, 39 | SDPOutput: clientSDPOutput, 40 | }, 41 | } 42 | clientSession := receiver.NewWith(clientConfig) 43 | assert.NotNil(clientSession) 44 | 45 | // Create sender 46 | senderStream := &buffer.Buffer{} 47 | senderSDPProvider := &buffer.Buffer{} 48 | senderSDPOutput := &buffer.Buffer{} 49 | n, err := senderStream.WriteString("Hello World!\n") 50 | assert.Nil(err) 51 | assert.Equal(13, n) // Len "Hello World\n" 52 | senderConfig := sender.Config{ 53 | Stream: senderStream, 54 | Configuration: common.Configuration{ 55 | SDPProvider: senderSDPProvider, 56 | SDPOutput: senderSDPOutput, 57 | }, 58 | } 59 | senderSession := sender.NewWith(senderConfig) 60 | assert.NotNil(senderSession) 61 | 62 | senderDone := make(chan struct{}) 63 | go func() { 64 | defer close(senderDone) 65 | err := senderSession.Start() 66 | assert.Nil(err) 67 | }() 68 | 69 | // Get SDP from sender and send it to the client 70 | sdp, err := utils.MustReadStream(senderSDPOutput) 71 | assert.Nil(err) 72 | fmt.Printf("READ SDP -> %s\n", sdp) 73 | sdp += "\n" 74 | n, err = clientSDPProvider.WriteString(sdp) 75 | assert.Nil(err) 76 | assert.Equal(len(sdp), n) 77 | 78 | clientDone := make(chan struct{}) 79 | go func() { 80 | defer close(clientDone) 81 | err := clientSession.Start() 82 | assert.Nil(err) 83 | }() 84 | 85 | // Get SDP from client and send it to the sender 86 | sdp, err = utils.MustReadStream(clientSDPOutput) 87 | assert.Nil(err) 88 | n, err = senderSDPProvider.WriteString(sdp) 89 | assert.Nil(err) 90 | assert.Equal(len(sdp), n) 91 | 92 | fmt.Println("Waiting for everyone to be done...") 93 | <-senderDone 94 | <-clientDone 95 | 96 | msg, err := clientStream.ReadString('\n') 97 | assert.Nil(err) 98 | assert.Equal("Hello World!\n", msg) 99 | } 100 | -------------------------------------------------------------------------------- /pkg/stats/bytes.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | // Bytes returns the stored number of bytes 4 | func (s *Stats) Bytes() uint64 { 5 | s.lock.RLock() 6 | defer s.lock.RUnlock() 7 | 8 | return s.nbBytes 9 | } 10 | 11 | // AddBytes increase the nbBytes counter 12 | func (s *Stats) AddBytes(c uint64) { 13 | s.lock.Lock() 14 | defer s.lock.Unlock() 15 | 16 | s.nbBytes += c 17 | } 18 | -------------------------------------------------------------------------------- /pkg/stats/bytes_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func Test_Bytes(t *testing.T) { 10 | assert := assert.New(t) 11 | 12 | tests := []struct { 13 | before uint64 14 | add uint64 15 | after uint64 16 | }{ 17 | { 18 | before: 0, 19 | add: 0, 20 | after: 0, 21 | }, 22 | { 23 | before: 0, 24 | add: 1, 25 | after: 1, 26 | }, 27 | { 28 | before: 1, 29 | add: 10, 30 | after: 11, 31 | }, 32 | } 33 | 34 | s := New() 35 | for _, cur := range tests { 36 | assert.Equal(cur.before, s.Bytes()) 37 | s.AddBytes(cur.add) 38 | assert.Equal(cur.after, s.Bytes()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/stats/ctrl.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "time" 4 | 5 | // Start stores the "start" timestamp 6 | func (s *Stats) Start() { 7 | s.lock.Lock() 8 | defer s.lock.Unlock() 9 | 10 | if s.timeStart.IsZero() { 11 | s.timeStart = time.Now() 12 | } else if !s.timePause.IsZero() { 13 | s.timePaused += time.Since(s.timePause) 14 | // Reset 15 | s.timePause = time.Time{} 16 | } 17 | } 18 | 19 | // Pause stores an interruption timestamp 20 | func (s *Stats) Pause() { 21 | s.lock.RLock() 22 | 23 | if s.timeStart.IsZero() || !s.timeStop.IsZero() { 24 | // Can't stop if not started, or if stopped 25 | s.lock.RUnlock() 26 | return 27 | } 28 | s.lock.RUnlock() 29 | 30 | s.lock.Lock() 31 | defer s.lock.Unlock() 32 | 33 | if s.timePause.IsZero() { 34 | s.timePause = time.Now() 35 | } 36 | } 37 | 38 | // Stop stores the "stop" timestamp 39 | func (s *Stats) Stop() { 40 | s.lock.RLock() 41 | 42 | if s.timeStart.IsZero() { 43 | // Can't stop if not started 44 | s.lock.RUnlock() 45 | return 46 | } 47 | s.lock.RUnlock() 48 | 49 | s.lock.Lock() 50 | defer s.lock.Unlock() 51 | 52 | if s.timeStop.IsZero() { 53 | s.timeStop = time.Now() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/stats/ctrl_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_ControlFlow(t *testing.T) { 11 | assert := assert.New(t) 12 | s := New() 13 | 14 | // Everything should be 0 at the beginning 15 | assert.Equal(true, s.timeStart.IsZero()) 16 | assert.Equal(true, s.timeStop.IsZero()) 17 | assert.Equal(true, s.timePause.IsZero()) 18 | 19 | // Should not do anything 20 | s.Stop() 21 | assert.Equal(true, s.timeStop.IsZero()) 22 | 23 | // Should not do anything 24 | s.Pause() 25 | assert.Equal(true, s.timePause.IsZero()) 26 | 27 | // Should start 28 | s.Start() 29 | originalStart := s.timeStart 30 | assert.Equal(false, s.timeStart.IsZero()) 31 | 32 | // Should pause 33 | s.Pause() 34 | assert.Equal(false, s.timePause.IsZero()) 35 | originalPause := s.timePause 36 | // Should not modify 37 | s.Pause() 38 | assert.Equal(originalPause, s.timePause) 39 | 40 | // Should release 41 | assert.Equal(int64(0), s.timePaused.Nanoseconds()) 42 | s.Start() 43 | assert.NotEqual(0, s.timePaused.Nanoseconds()) 44 | originalPausedDuration := s.timePaused 45 | assert.Equal(true, s.timePause.IsZero()) 46 | assert.Equal(originalStart, s.timeStart) 47 | 48 | s.Pause() 49 | time.Sleep(10 * time.Nanosecond) 50 | s.Start() 51 | assert.Equal(true, s.timePaused > originalPausedDuration) 52 | 53 | s.Stop() 54 | assert.Equal(false, s.timeStop.IsZero()) 55 | } 56 | -------------------------------------------------------------------------------- /pkg/stats/data.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import "time" 4 | 5 | // Duration returns the 'stop - start' duration, if stopped 6 | // Returns 0 if not started 7 | // Returns time.Since(s.timeStart) if not stopped 8 | func (s *Stats) Duration() time.Duration { 9 | s.lock.RLock() 10 | defer s.lock.RUnlock() 11 | 12 | if s.timeStart.IsZero() { 13 | return 0 14 | } else if s.timeStop.IsZero() { 15 | return time.Since(s.timeStart) - s.timePaused 16 | } 17 | return s.timeStop.Sub(s.timeStart) - s.timePaused 18 | } 19 | 20 | // Bandwidth returns the IO speed in MB/s 21 | func (s *Stats) Bandwidth() float64 { 22 | s.lock.RLock() 23 | defer s.lock.RUnlock() 24 | 25 | return (float64(s.nbBytes) / 1024 / 1024) / s.Duration().Seconds() 26 | } 27 | -------------------------------------------------------------------------------- /pkg/stats/data_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func Test_Bandwidth(t *testing.T) { 12 | assert := assert.New(t) 13 | s := New() 14 | 15 | now := time.Now() 16 | tests := []struct { 17 | startTime time.Time 18 | stopTime time.Time 19 | pauseDuration time.Duration 20 | bytesCount uint64 21 | expectedBandwidth float64 22 | }{ 23 | { 24 | startTime: time.Time{}, 25 | stopTime: time.Time{}, 26 | pauseDuration: 0, 27 | bytesCount: 0, 28 | expectedBandwidth: math.NaN(), 29 | }, 30 | { 31 | startTime: now, 32 | stopTime: time.Time{}, 33 | pauseDuration: 0, 34 | bytesCount: 0, 35 | expectedBandwidth: 0, 36 | }, 37 | { 38 | startTime: now, 39 | stopTime: now.Add(time.Duration(1 * 1000000000)), 40 | pauseDuration: 0, 41 | bytesCount: 1024 * 1024, 42 | expectedBandwidth: 1, 43 | }, 44 | { 45 | startTime: now, 46 | stopTime: now.Add(time.Duration(2 * 1000000000)), 47 | pauseDuration: time.Duration(1 * 1000000000), 48 | bytesCount: 1024 * 1024, 49 | expectedBandwidth: 1, 50 | }, 51 | } 52 | 53 | for _, cur := range tests { 54 | s.timeStart = cur.startTime 55 | s.timeStop = cur.stopTime 56 | s.timePaused = cur.pauseDuration 57 | s.nbBytes = cur.bytesCount 58 | 59 | if math.IsNaN(cur.expectedBandwidth) { 60 | assert.Equal(true, math.IsNaN(s.Bandwidth())) 61 | } else { 62 | assert.Equal(cur.expectedBandwidth, s.Bandwidth()) 63 | } 64 | } 65 | } 66 | 67 | func Test_Duration(t *testing.T) { 68 | assert := assert.New(t) 69 | s := New() 70 | 71 | // Should be 0 72 | assert.Equal(time.Duration(0), s.Duration()) 73 | 74 | // Should return time.Since() 75 | s.Start() 76 | durationTmp := s.Duration() 77 | time.Sleep(10 * time.Nanosecond) 78 | assert.Equal(true, s.Duration() > durationTmp) 79 | } 80 | -------------------------------------------------------------------------------- /pkg/stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Stats provide a way to track statistics infos 10 | type Stats struct { 11 | lock *sync.RWMutex 12 | nbBytes uint64 13 | timeStart time.Time 14 | timeStop time.Time 15 | 16 | timePause time.Time 17 | timePaused time.Duration 18 | } 19 | 20 | // New creates a new Stats 21 | func New() *Stats { 22 | return &Stats{ 23 | lock: &sync.RWMutex{}, 24 | } 25 | } 26 | 27 | func (s *Stats) String() string { 28 | s.lock.RLock() 29 | defer s.lock.RUnlock() 30 | return fmt.Sprintf("%v bytes | %-v | %0.4f MB/s", s.Bytes(), s.Duration(), s.Bandwidth()) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "strings" 13 | ) 14 | 15 | // MustReadStream blocks until input is received from the stream 16 | func MustReadStream(stream io.Reader) (string, error) { 17 | r := bufio.NewReader(stream) 18 | 19 | var in string 20 | for { 21 | var err error 22 | in, err = r.ReadString('\n') 23 | if err != io.EOF { 24 | if err != nil { 25 | return "", err 26 | } 27 | } 28 | in = strings.TrimSpace(in) 29 | if len(in) > 0 { 30 | break 31 | } 32 | } 33 | 34 | fmt.Println("") 35 | return in, nil 36 | } 37 | 38 | // StripSDP remove useless elements from an SDP 39 | func StripSDP(originalSDP string) string { 40 | finalSDP := strings.Replace(originalSDP, "a=group:BUNDLE audio video data", "a=group:BUNDLE data", -1) 41 | tmp := strings.Split(finalSDP, "m=audio") 42 | beginningSdp := tmp[0] 43 | 44 | var endSdp string 45 | if len(tmp) > 1 { 46 | tmp = strings.Split(tmp[1], "a=end-of-candidates") 47 | endSdp = strings.Join(tmp[2:], "a=end-of-candidates") 48 | } else { 49 | endSdp = strings.Join(tmp[1:], "a=end-of-candidates") 50 | } 51 | 52 | finalSDP = beginningSdp + endSdp 53 | finalSDP = strings.Replace(finalSDP, "\r\n\r\n", "\r\n", -1) 54 | finalSDP = strings.Replace(finalSDP, "\n\n", "\n", -1) 55 | return finalSDP 56 | } 57 | 58 | // Encode encodes the input in base64 59 | // It can optionally zip the input before encoding 60 | func Encode(obj interface{}) (string, error) { 61 | b, err := json.Marshal(obj) 62 | if err != nil { 63 | return "", err 64 | } 65 | var gzbuff bytes.Buffer 66 | gz, err := gzip.NewWriterLevel(&gzbuff, gzip.BestCompression) 67 | if err != nil { 68 | return "", err 69 | } 70 | if _, err := gz.Write(b); err != nil { 71 | return "", err 72 | } 73 | if err := gz.Flush(); err != nil { 74 | return "", err 75 | } 76 | if err := gz.Close(); err != nil { 77 | return "", err 78 | } 79 | 80 | return base64.StdEncoding.EncodeToString(gzbuff.Bytes()), nil 81 | } 82 | 83 | // Decode decodes the input from base64 84 | // It can optionally unzip the input after decoding 85 | func Decode(in string, obj interface{}) error { 86 | b, err := base64.StdEncoding.DecodeString(in) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | gz, err := gzip.NewReader(bytes.NewReader(b)) 92 | if err != nil { 93 | return err 94 | } 95 | defer gz.Close() 96 | s, err := ioutil.ReadAll(gz) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | return json.Unmarshal(s, obj) 102 | } 103 | -------------------------------------------------------------------------------- /pkg/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func Test_ReadStream(t *testing.T) { 11 | assert := assert.New(t) 12 | stream := &bytes.Buffer{} 13 | 14 | _, err := stream.WriteString("Hello\n") 15 | assert.Nil(err) 16 | 17 | str, err := MustReadStream(stream) 18 | assert.Equal("Hello", str) 19 | assert.Nil(err) 20 | } 21 | 22 | func Test_StripSDP(t *testing.T) { 23 | assert := assert.New(t) 24 | 25 | tests := []struct { 26 | sdp string 27 | expected string 28 | }{ 29 | { 30 | sdp: "", 31 | expected: "", 32 | }, 33 | { 34 | sdp: `v=0 35 | o=- 297292268 1552262038 IN IP4 0.0.0.0 36 | s=- 37 | t=0 0 38 | a=fingerprint:sha-256 70:E0:B2:DA:F8:04:D6:0C:32:03:DF:CD:A8:70:EC:45:10:FF:66:6F:3D:72:B1:BA:4C:AF:FB:5E:BE:F9:CF:6A 39 | a=group:BUNDLE audio video data 40 | m=audio 9 UDP/TLS/RTP/SAVPF 111 9 41 | c=IN IP4 0.0.0.0 42 | a=setup:actpass 43 | a=mid:audio 44 | a=ice-ufrag:SNxNaqIiaNoDiCNM 45 | a=ice-pwd:dSZlwOEOKEmBfNiXCtpmPTOVJlwUCaFX 46 | a=rtcp-mux 47 | a=rtcp-rsize 48 | a=rtpmap:111 opus/48000/2 49 | a=fmtp:111 minptime=10;useinbandfec=1 50 | a=rtpmap:9 G722/8000 51 | a=recvonly 52 | a=candidate:foundation 1 udp 3776 192.168.100.207 61879 typ host generation 0 53 | a=candidate:foundation 2 udp 3776 192.168.100.207 61879 typ host generation 0 54 | a=end-of-candidates 55 | a=setup:actpass 56 | m=video 9 UDP/TLS/RTP/SAVPF 96 100 98 57 | c=IN IP4 0.0.0.0 58 | a=setup:actpass 59 | a=mid:video 60 | a=ice-ufrag:SNxNaqIiaNoDiCNM 61 | a=ice-pwd:dSZlwOEOKEmBfNiXCtpmPTOVJlwUCaFX 62 | a=rtcp-mux 63 | a=rtcp-rsize 64 | a=rtpmap:96 VP8/90000 65 | a=rtpmap:100 H264/90000 66 | a=fmtp:100 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f 67 | a=rtpmap:98 VP9/90000 68 | a=recvonly 69 | a=candidate:foundation 1 udp 3776 192.168.100.207 61879 typ host generation 0 70 | a=candidate:foundation 2 udp 3776 192.168.100.207 61879 typ host generation 0 71 | a=end-of-candidates 72 | a=setup:actpass 73 | m=application 9 DTLS/SCTP 5000 74 | c=IN IP4 0.0.0.0 75 | a=setup:actpass 76 | a=mid:data 77 | a=sendrecv 78 | a=sctpmap:5000 webrtc-datachannel 1024 79 | a=ice-ufrag:SNxNaqIiaNoDiCNM 80 | a=ice-pwd:dSZlwOEOKEmBfNiXCtpmPTOVJlwUCaFX 81 | a=candidate:foundation 1 udp 3776 192.168.100.207 61879 typ host generation 0 82 | a=candidate:foundation 2 udp 3776 192.168.100.207 61879 typ host generation 0 83 | a=end-of-candidates 84 | a=setup:actpass 85 | `, 86 | expected: `v=0 87 | o=- 297292268 1552262038 IN IP4 0.0.0.0 88 | s=- 89 | t=0 0 90 | a=fingerprint:sha-256 70:E0:B2:DA:F8:04:D6:0C:32:03:DF:CD:A8:70:EC:45:10:FF:66:6F:3D:72:B1:BA:4C:AF:FB:5E:BE:F9:CF:6A 91 | a=group:BUNDLE data 92 | a=setup:actpass 93 | m=application 9 DTLS/SCTP 5000 94 | c=IN IP4 0.0.0.0 95 | a=setup:actpass 96 | a=mid:data 97 | a=sendrecv 98 | a=sctpmap:5000 webrtc-datachannel 1024 99 | a=ice-ufrag:SNxNaqIiaNoDiCNM 100 | a=ice-pwd:dSZlwOEOKEmBfNiXCtpmPTOVJlwUCaFX 101 | a=candidate:foundation 1 udp 3776 192.168.100.207 61879 typ host generation 0 102 | a=candidate:foundation 2 udp 3776 192.168.100.207 61879 typ host generation 0 103 | a=end-of-candidates 104 | a=setup:actpass 105 | `, 106 | }, 107 | } 108 | 109 | for _, cur := range tests { 110 | assert.Equal(cur.expected, StripSDP(cur.sdp)) 111 | } 112 | } 113 | 114 | func Test_Encode(t *testing.T) { 115 | assert := assert.New(t) 116 | 117 | tests := []struct { 118 | input interface{} 119 | shouldErr bool 120 | expected string 121 | }{ 122 | // Invalid object 123 | { 124 | input: make(chan int), 125 | shouldErr: true, 126 | }, 127 | // Empty input 128 | { 129 | input: nil, 130 | shouldErr: false, 131 | expected: "H4sIAAAAAAAC/8orzckBAAAA//8BAAD//0/8yyUEAAAA", 132 | }, 133 | // Not JSON 134 | { 135 | input: "ThisTestIsNotInB64", 136 | shouldErr: false, 137 | expected: "H4sIAAAAAAAC/1IKycgsDkktLvEs9ssv8cxzMjNRAgAAAP//AQAA//8+sWiWFAAAAA==", 138 | }, 139 | // JSON 140 | { 141 | input: struct { 142 | Name string `json:"name"` 143 | }{ 144 | Name: "TestJson", 145 | }, 146 | shouldErr: false, 147 | expected: "H4sIAAAAAAAC/6pWykvMTVWyUgpJLS7xKs7PU6oFAAAA//8BAAD//3cqgZQTAAAA", 148 | }, 149 | } 150 | 151 | for _, cur := range tests { 152 | res, err := Encode(cur.input) 153 | 154 | if cur.shouldErr { 155 | assert.NotNil(err) 156 | } else { 157 | assert.Nil(err) 158 | assert.Equal(cur.expected, res) 159 | } 160 | } 161 | } 162 | 163 | func Test_Decode(t *testing.T) { 164 | assert := assert.New(t) 165 | 166 | tests := []struct { 167 | input string 168 | shouldErr bool 169 | }{ 170 | // Empty string 171 | { 172 | input: "", 173 | shouldErr: true, 174 | }, 175 | // Not base64 176 | { 177 | input: "ThisTestIsNotInB64", 178 | shouldErr: true, 179 | }, 180 | // Not base64 JSON 181 | { 182 | input: "aGVsbG8gd29ybGQ=", 183 | shouldErr: true, 184 | }, 185 | // Base64 JSON 186 | { 187 | input: "H4sIAAAAAAAC/+xVTY/bNhD9KwOdK5ukqK8JdFh77XabNPXGXqcJcmFEystGogiKsuMU/e+FLG/qtEWBBRo0h4UgCTPz+OaRegP9FvijVQEGwnQH5YLvgk7aAIN9Qd65d6YtQojzmCRpmmZA45ixhFNO4eYl3Kw4kMnpGqBdEQ4vXxA4xaKotNkpZ502Hrt7EbI4gYjiLMKE4oIhYZgskWXIyfCcLZFdY5ZgxnEeYTRDHmGeYsyQ57icIWdI5ji/wnmClCBLMUowjjBLcXGFEUHOx8Y71/YWZ3cvr18sQPRSt7DXUrUghRcDpCnGbA5316vp5sV6+mqzmq6vtqslUEohH0Bl8fdNiqJTvrcoSq/3asw0WuKJbgx1qcK+cmKH5avl6zflzeGu8z9vd4df/qzbg8TXerH7cfnrQj9/u/a36+2b55ubulosDx9u5eb2p7cj2vnShk3/8SJynf6kHmLbCIuD5Nb23ZRnhJApOx9/48dSo431ulEFJc/6TmnzXhhZqbKgX7Dk8H3K2HSgOCs1l9sshZFaCq+wansjhdetAQq9tBClaQI0ZxOaZBMyoVkOcRQnCfijhfu287BTRrlxCfkXOvbf0p3VUUKApxM63CyfRAxinhJ6outcVX8EJ6R0f2npbOv8Gfk4+V+jnzIybKvwc9tutPFo63+ycZ7AoCPPHmvlE+X/ZuU8ge0qm+bkswsfPE4I/MASflkaHU4I1Gqv6lB0x6ZR3h1DUdftQcmCPrOi/KC8/nQ6zLBppRqSrq10rcJxmZYFZ4TQ6kshGWxX+WW3p3H45sdBWFvrckTmcD1MxHq+WUF8/oiPmYOHf8VQN9Kpcn+OytEgAycc1Hvny3DAlvfCGFUDJYx/jfF5Mtw3Z7jg9z8AAAD//wEAAP//RjpVQj8JAAA=", 188 | shouldErr: false, 189 | }, 190 | } 191 | 192 | var obj interface{} 193 | for _, cur := range tests { 194 | err := Decode(cur.input, &obj) 195 | 196 | if cur.shouldErr { 197 | assert.NotNil(err) 198 | } else { 199 | assert.Nil(err) 200 | } 201 | } 202 | } 203 | 204 | func Test_EncodeDecode(t *testing.T) { 205 | assert := assert.New(t) 206 | 207 | input := struct { 208 | Name string `json:"name"` 209 | }{ 210 | Name: "TestJson", 211 | } 212 | 213 | encoded, err := Encode(input) 214 | assert.Nil(err) 215 | assert.Equal("H4sIAAAAAAAC/6pWykvMTVWyUgpJLS7xKs7PU6oFAAAA//8BAAD//3cqgZQTAAAA", encoded) 216 | 217 | var obj struct { 218 | Name string `json:"name"` 219 | } 220 | err = Decode(encoded, &obj) 221 | assert.Nil(err) 222 | assert.Equal(input, obj) 223 | } 224 | --------------------------------------------------------------------------------