├── .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 | [](https://travis-ci.org/Antonito/gfile)
2 | [](https://goreportcard.com/report/github.com/Antonito/gfile)
3 | [](https://www.codacy.com/app/Antonito/gfile?utm_source=github.com&utm_medium=referral&utm_content=Antonito/gfile&utm_campaign=Badge_Grade)
4 | [](https://coveralls.io/github/Antonito/gfile?branch=master)
5 | [](https://opensource.org/licenses/MIT)
6 | [](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 | 
15 |
16 | ## Note
17 |
18 | This project is still in its early stage.
19 |
20 | ## How does it work ?
21 |
22 | 
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 |
--------------------------------------------------------------------------------