├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── documentation.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .devcontainer │ └── devcontainer.json ├── .gitignore ├── .vscode │ └── launch.json ├── Dockerfile ├── config.yml ├── entrypoint-dev.sh ├── entrypoint.sh ├── go.mod ├── go.sum └── src │ ├── agent │ ├── generators.go │ ├── serveragent.go │ └── udpclientsocket.go │ ├── common │ ├── networkutils.go │ ├── stringutils.go │ └── utils.go │ ├── conference │ ├── conference.go │ └── conferencemanager.go │ ├── config │ └── config.go │ ├── dtls │ ├── alert.go │ ├── algopair.go │ ├── certificate.go │ ├── certificaterequest.go │ ├── certificateverify.go │ ├── changecipherspec.go │ ├── ciphersuites.go │ ├── clienthello.go │ ├── clientkeyexchange.go │ ├── crypto.go │ ├── cryptogcm.go │ ├── dtlsmessage.go │ ├── dtlsstate.go │ ├── extensions.go │ ├── finished.go │ ├── handshakecontext.go │ ├── handshakeheader.go │ ├── handshakemanager.go │ ├── helloverifyrequest.go │ ├── init.go │ ├── random.go │ ├── recordheader.go │ ├── serverhello.go │ ├── serverhellodone.go │ ├── serverkeyexchange.go │ └── simpleextensions.go │ ├── logging │ └── logging.go │ ├── main.go │ ├── rtcp │ ├── header.go │ └── packet.go │ ├── rtp │ ├── header.go │ └── packet.go │ ├── sdp │ └── sdp.go │ ├── signaling │ ├── httpserver.go │ ├── joinconferencedata.go │ ├── wsclient.go │ └── wshub.go │ ├── srtp │ ├── cryptogcm.go │ ├── protectionprofiles.go │ ├── srtpcontext.go │ └── srtpmanager.go │ ├── stun │ ├── attribute.go │ ├── attributes.go │ ├── atttributetype.go │ ├── message.go │ ├── messageclass.go │ ├── messagemethod.go │ ├── messagetype.go │ └── stunclient.go │ ├── transcoding │ └── vp8.go │ └── udp │ └── udpListener.go ├── docker-compose.dev.yml ├── docker-compose.yml ├── docs ├── 00-INFRASTRUCTURE.md ├── 01-RUNNING-IN-DEV-MODE.md ├── 02-BACKEND-INITIALIZATION.md ├── 03-FIRST-CLIENT-COMES-IN.md ├── 04-STUN-BINDING-REQUEST-FROM-CLIENT.md ├── 05-DTLS-HANDSHAKE.md ├── 06-SRTP-INITIALIZATION.md ├── 07-SRTP-PACKETS-COME.md ├── 08-VP8-PACKET-DECODE.md ├── 09-CONCLUSION.md ├── README.md ├── images │ ├── 01-01-open-folder-in-container.png │ ├── 01-02-select-folder.png │ ├── 01-03-starting-dev-container-small.png │ ├── 01-04-starting-dev-container-log.png │ ├── 01-05-install-go-deps-small.png │ ├── 01-06-install-go-deps-log.png │ ├── 01-07-backend-initial-output.png │ ├── 03-01-browser-first-visit.png │ ├── 03-02-browser-ask-permissions.png │ ├── 03-03-browser-onnegotiationneeded.png │ ├── 03-04-browser-received-welcome-message.png │ ├── 03-05-server-a-new-client-connected.png │ ├── 03-06-browser-received-sdpoffer-message-json.png │ ├── 03-07-browser-received-sdpoffer.png │ ├── 03-08-browser-onsignalingstatechange.png │ ├── 03-09-browser-generate-sdpanswer.png │ ├── 03-10-browser-ice-events.png │ ├── 03-11-browser-send-sdp-answer-signaling.png │ ├── 03-12-server-receive-sdpanswer.png │ ├── 04-01-server-received-stun-binding-request.png │ ├── 05-01-received-first-clienthello.png │ ├── 05-02-sent-helloverifyrequest.png │ ├── 05-03-received-second-clienthello.png │ ├── 05-04-processed-second-clienthello.png │ ├── 05-05-sent-serverhello.png │ ├── 05-06-sent-certificate.png │ ├── 05-07-sent-serverkeyexchange.png │ ├── 05-08-sent-certificaterequest.png │ ├── 05-09-sent-serverhellodone.png │ ├── 05-10-received-certificate.png │ ├── 05-11-received-clientkeyexchange.png │ ├── 05-12-message-concatenation-result.png │ ├── 05-13-init-gcm.png │ ├── 05-14-received-certificateverify.png │ ├── 05-15-received-changecipherspec.png │ ├── 05-16-received-finished.png │ ├── 05-17-sent-changecipherspec.png │ ├── 05-18-sent-finished.png │ ├── 06-01-srtp-initialization.png │ ├── 07-01-received-rtp-packet.png │ ├── 08-01-image-saved.png │ └── icon.svg └── mkdocs │ ├── .gitignore │ ├── assets │ ├── icon.png │ └── icon.svg │ ├── execute-mkdocs.sh │ ├── extra-content │ └── index.md │ ├── javascripts │ └── mathjax.js │ ├── mkdocs.yml.template │ ├── prepare-mkdocs.sh │ └── stylesheets │ └── custom.css └── ui ├── .devcontainer └── devcontainer.json ├── .gitignore ├── Dockerfile ├── package.json ├── src ├── app.ts ├── index.html ├── sdp.ts └── style │ └── main.css ├── tsconfig.json └── webpack.config.js /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh text eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [adalkiran] 4 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Release Documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - 'docs-mkdocs' 8 | - 'main' 9 | paths: 10 | - 'docs/**' 11 | - '.github/workflows/**' 12 | pull_request: 13 | branches: 14 | - 'main' 15 | 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | deploy: 21 | name: Deploy documentation 22 | runs-on: ubuntu-latest 23 | steps: 24 | 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | with: 28 | sparse-checkout: | 29 | .github 30 | docs 31 | 32 | - name: Prepare files 33 | run: chmod +x docs/mkdocs/prepare-mkdocs.sh && cd docs && ./mkdocs/prepare-mkdocs.sh 34 | 35 | - name: Set up Python 36 | uses: actions/setup-python@v5 37 | with: 38 | python-version: '3.12.2' 39 | 40 | - name: Install mkdocs 41 | run: pip install mkdocs-material mkdocs-material[imaging] 42 | 43 | - name: Perform deployment 44 | run: cd docs/mkdocs && mkdocs gh-deploy --force 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build and Test 5 | 6 | # This workflow only runs linter, builds and runs unit tests for development branches. 7 | 8 | on: 9 | workflow_dispatch: 10 | push: 11 | branches: 12 | - '**' 13 | - '!main' 14 | paths-ignore: 15 | - 'docs/**' 16 | pull_request: 17 | branches: 18 | - '**' 19 | - '!main' 20 | 21 | jobs: 22 | 23 | build: 24 | name: Build and test project 25 | runs-on: ubuntu-latest 26 | 27 | steps: 28 | - name: Checkout code 29 | uses: actions/checkout@v3 30 | 31 | - name: Set up Go 32 | uses: actions/setup-go@v4 33 | with: 34 | go-version: '1.23' 35 | 36 | - name: Preparing 37 | run: sudo apt-get -y install libvpx-dev 38 | 39 | - name: Linting 40 | run: | 41 | cd backend 42 | go fmt ./... 43 | go vet ./... 44 | 45 | - name: Test 46 | run: cd backend && go test -v ./... 47 | 48 | - name: Build 49 | run: cd backend && go build -v ./... 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store -------------------------------------------------------------------------------- /backend/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See: https://code.visualstudio.com/docs/remote/containers-advanced#_connecting-to-multiple-containers-at-once 2 | 3 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 4 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/javascript-node 5 | { 6 | "name": "WebRTC Nuts and Bolts Backend Container", 7 | 8 | "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose.dev.yml"], 9 | "service": "backend", 10 | "shutdownAction": "none", 11 | 12 | 13 | "workspaceFolder": "/workspace", 14 | 15 | // Set *default* container specific settings.json values on container create. 16 | "settings": { 17 | "go.useLanguageServer": true 18 | }, 19 | 20 | // Add the IDs of extensions you want installed when the container is created. 21 | "extensions": [ 22 | "golang.go" 23 | ] 24 | } -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | /output -------------------------------------------------------------------------------- /backend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch file", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "debug", 12 | "program": "src/" 13 | } 14 | 15 | ] 16 | } -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | # See: https://levelup.gitconnected.com/debugging-go-inside-docker-using-visual-studio-code-and-remote-containers-5c3724fe87b9 2 | # See for available variants: https://hub.docker.com/_/golang?tab=tags 3 | ARG VARIANT=1.23.0-bookworm 4 | FROM golang:${VARIANT} 5 | 6 | COPY entrypoint.sh entrypoint-dev.sh / 7 | 8 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 9 | && apt-get -y install libvpx-dev libogg-dev libvorbis-dev libopus-dev portaudio19-dev \ 10 | && chmod +x /entrypoint*.sh 11 | 12 | WORKDIR /workspace 13 | 14 | ENTRYPOINT "/entrypoint.sh" -------------------------------------------------------------------------------- /backend/config.yml: -------------------------------------------------------------------------------- 1 | server: 2 | softwareName: "WebRTCNutsBolts" 3 | udp: 4 | singlePort: 15000 # If you need to change, you should change corresponding port config in docker-compose.yml 5 | dockerHostIp: "" # e.g. 192.168.0.11 6 | signaling: 7 | wsPort: 8081 # If you need to change, you should change corresponding port config in docker-compose.yml 8 | domainName: "webrtcnutsbolts-notexists.com" 9 | stunServerAddr: "stun.l.google.com:19302" 10 | maskIpOnConsole: true 11 | requestAudio: false -------------------------------------------------------------------------------- /backend/entrypoint-dev.sh: -------------------------------------------------------------------------------- 1 | echo "Downloading dependent Go modules..." 2 | go mod download -x 3 | echo "Running into Waiting loop..." 4 | tail -f /dev/null -------------------------------------------------------------------------------- /backend/entrypoint.sh: -------------------------------------------------------------------------------- 1 | echo "Downloading dependent Go modules..." 2 | go mod download -x 3 | echo "Running application..." 4 | cd src 5 | go run . -------------------------------------------------------------------------------- /backend/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adalkiran/webrtc-nuts-and-bolts 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/fatih/color v1.17.0 7 | github.com/gorilla/websocket v1.5.3 8 | github.com/spf13/viper v1.19.0 9 | github.com/xlab/libvpx-go v0.0.0-20220203233824-652b2616315c 10 | golang.org/x/crypto v0.27.0 11 | gopkg.in/yaml.v3 v3.0.1 12 | ) 13 | 14 | require ( 15 | github.com/fsnotify/fsnotify v1.7.0 // indirect 16 | github.com/hashicorp/hcl v1.0.0 // indirect 17 | github.com/magiconair/properties v1.8.7 // indirect 18 | github.com/mattn/go-colorable v0.1.13 // indirect 19 | github.com/mattn/go-isatty v0.0.20 // indirect 20 | github.com/mitchellh/mapstructure v1.5.0 // indirect 21 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 22 | github.com/sagikazarmark/locafero v0.6.0 // indirect 23 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 24 | github.com/sourcegraph/conc v0.3.0 // indirect 25 | github.com/spf13/afero v1.11.0 // indirect 26 | github.com/spf13/cast v1.7.0 // indirect 27 | github.com/spf13/pflag v1.0.5 // indirect 28 | github.com/subosito/gotenv v1.6.0 // indirect 29 | go.uber.org/multierr v1.11.0 // indirect 30 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect 31 | golang.org/x/sys v0.25.0 // indirect 32 | golang.org/x/text v0.18.0 // indirect 33 | gopkg.in/ini.v1 v1.67.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /backend/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 3 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 5 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 6 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 7 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 8 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 9 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 10 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 11 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 12 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 13 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 14 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 15 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 16 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 17 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 18 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 19 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 20 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 21 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 22 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 23 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 24 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 25 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 26 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 27 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 28 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 29 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 30 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 31 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 32 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 33 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 34 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 35 | github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= 36 | github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= 37 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 38 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 39 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 40 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 41 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 42 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 43 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 44 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 45 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 46 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 47 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 48 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 49 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 50 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 51 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 52 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 53 | github.com/xlab/libvpx-go v0.0.0-20220203233824-652b2616315c h1:dYh8PXMQ2Ibn0EpOHJEUyaWlcZ1egvB3elvzPzC7JZ8= 54 | github.com/xlab/libvpx-go v0.0.0-20220203233824-652b2616315c/go.mod h1:aDpRjomFsJw5z7oxScCKeB5NNGqibqdOgmpnOaEVMQs= 55 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 56 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 57 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 58 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 59 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 60 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 61 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 64 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 65 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 66 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 68 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 69 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 70 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 71 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 72 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 73 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 74 | -------------------------------------------------------------------------------- /backend/src/agent/generators.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import "math/rand" 4 | 5 | // See: https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go 6 | 7 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 8 | 9 | func randStringRunes(n int) string { 10 | b := make([]rune, n) 11 | for i := range b { 12 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 13 | } 14 | return string(b) 15 | } 16 | 17 | func GenerateICEUfrag() string { 18 | return randStringRunes(16)[0:13] + "wnb" // It will be better to generate fully random, but we want to see "our sign" in the traffic :) 19 | } 20 | 21 | func GenerateICEPwd() string { 22 | return randStringRunes(32) 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/agent/serveragent.go: -------------------------------------------------------------------------------- 1 | package agent 2 | 3 | import ( 4 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/dtls" 5 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 6 | ) 7 | 8 | type ServerAgent struct { 9 | ConferenceName string 10 | Ufrag string 11 | Pwd string 12 | FingerprintHash string 13 | IceCandidates []*IceCandidate 14 | SignalingMediaComponents map[string]*SignalingMediaComponent 15 | Sockets map[string]UDPClientSocket 16 | } 17 | 18 | type SignalingMediaComponent struct { 19 | Agent *ServerAgent 20 | Ufrag string 21 | Pwd string 22 | FingerprintHash string 23 | } 24 | 25 | type IceCandidate struct { 26 | Ip string 27 | Port int 28 | } 29 | 30 | func NewServerAgent(candidateIPs []string, udpPort int, conferenceName string) *ServerAgent { 31 | result := &ServerAgent{ 32 | ConferenceName: conferenceName, 33 | Ufrag: GenerateICEUfrag(), 34 | Pwd: GenerateICEPwd(), 35 | FingerprintHash: dtls.ServerCertificateFingerprint, 36 | IceCandidates: []*IceCandidate{}, 37 | SignalingMediaComponents: map[string]*SignalingMediaComponent{}, 38 | Sockets: map[string]UDPClientSocket{}, 39 | } 40 | for _, candidateIP := range candidateIPs { 41 | result.IceCandidates = append(result.IceCandidates, &IceCandidate{ 42 | Ip: candidateIP, 43 | Port: udpPort, 44 | }) 45 | } 46 | logging.Descf(logging.ProtoAPP, "A new server ICE Agent was created (for a new conference) with Ufrag: %s, Pwd: %s, FingerprintHash: %s", result.Ufrag, result.Pwd, result.FingerprintHash) 47 | return result 48 | } 49 | 50 | func (a *ServerAgent) EnsureSignalingMediaComponent(iceUfrag string, icePwd string, fingerprintHash string) *SignalingMediaComponent { 51 | result, ok := a.SignalingMediaComponents[iceUfrag] 52 | if ok { 53 | return result 54 | } 55 | result = &SignalingMediaComponent{ 56 | Agent: a, 57 | Ufrag: iceUfrag, 58 | Pwd: icePwd, 59 | FingerprintHash: fingerprintHash, 60 | } 61 | a.SignalingMediaComponents[iceUfrag] = result 62 | return result 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/common/networkutils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | func GetLocalIPs() []string { 8 | result := make([]string, 0) 9 | addrs, err := net.InterfaceAddrs() 10 | if err != nil { 11 | return result 12 | } 13 | for _, addr := range addrs { 14 | // check the address type and if it is not a loopback the display it 15 | if ipnet, ok := addr.(*net.IPNet); ok { 16 | if ipnet.IP.To4() != nil && !ipnet.IP.IsLoopback() { 17 | result = append(result, ipnet.IP.To4().String()) 18 | } 19 | } 20 | } 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/common/stringutils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | func JoinSlice(separator string, indent bool, lines ...string) string { 8 | result := "" 9 | for i, line := range lines { 10 | if indent { 11 | result += "\t" 12 | } 13 | result += line 14 | if i < len(lines)-1 { 15 | result += separator 16 | } 17 | } 18 | return result 19 | } 20 | 21 | func ProcessIndent(title string, bullet string, lines []string) string { 22 | result := title 23 | if result != "" { 24 | result += "\n" 25 | } 26 | for i, line := range lines { 27 | result += "\t" 28 | if bullet != "" { 29 | result += bullet + " " 30 | } 31 | if strings.Contains(line, "\n") { 32 | parts := strings.Split(line, "\n") 33 | result += ProcessIndent(parts[0], "", parts[1:]) 34 | } else { 35 | result += line 36 | } 37 | if i < len(lines)-1 { 38 | result += "\n" 39 | } 40 | } 41 | return result 42 | } 43 | 44 | func ToStrSlice(v ...interface{}) []string { 45 | result := make([]string, len(v)) 46 | for i, item := range v { 47 | result[i] = item.(string) 48 | } 49 | return result 50 | } 51 | 52 | func MaskIPString(ip string) string { 53 | parts := strings.Split(ip, ".") 54 | result := "" 55 | for i, part := range parts { 56 | if i > 0 { 57 | result += "." 58 | } 59 | if i < 2 { 60 | result += part 61 | } else { 62 | result += "***" 63 | } 64 | } 65 | return result 66 | } 67 | -------------------------------------------------------------------------------- /backend/src/common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "fmt" 4 | 5 | func Uint8SliceToHexStr(slice []uint8) string { 6 | if len(slice) == 0 { 7 | return "" 8 | } 9 | result := make([]string, len(slice)) 10 | for i, item := range slice { 11 | result[i] = fmt.Sprintf("0x%02x", item) 12 | } 13 | return fmt.Sprintf("%s", result) 14 | } 15 | 16 | func Uint16SliceToHexStr(slice []uint16) string { 17 | if len(slice) == 0 { 18 | return "" 19 | } 20 | result := make([]string, len(slice)) 21 | for i, item := range slice { 22 | result[i] = fmt.Sprintf("0x%04x", item) 23 | } 24 | return fmt.Sprintf("%s", result) 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/conference/conference.go: -------------------------------------------------------------------------------- 1 | package conference 2 | 3 | import ( 4 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/agent" 5 | ) 6 | 7 | type Conference struct { 8 | ConferenceName string 9 | IceAgent *agent.ServerAgent 10 | } 11 | 12 | func NewConference(conferenceName string, candidateIPs []string, udpPort int) *Conference { 13 | result := &Conference{ 14 | ConferenceName: conferenceName, 15 | IceAgent: agent.NewServerAgent(candidateIPs, udpPort, conferenceName), 16 | } 17 | return result 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/conference/conferencemanager.go: -------------------------------------------------------------------------------- 1 | package conference 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/agent" 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/sdp" 9 | ) 10 | 11 | type ConferenceManager struct { 12 | CandidateIPs []string 13 | UDPPort int 14 | Conferences map[string]*Conference //key: ConferenceName 15 | Agents map[string]*agent.ServerAgent //key: Ufrag 16 | 17 | ChanSdpOffer chan *sdp.SdpMessage 18 | } 19 | 20 | func NewConferenceManager(candidateIPs []string, udpPort int) *ConferenceManager { 21 | result := &ConferenceManager{ 22 | CandidateIPs: candidateIPs, 23 | UDPPort: udpPort, 24 | Conferences: map[string]*Conference{}, 25 | Agents: map[string]*agent.ServerAgent{}, 26 | ChanSdpOffer: make(chan *sdp.SdpMessage, 1), 27 | } 28 | return result 29 | } 30 | 31 | func (m *ConferenceManager) EnsureConference(conferenceName string) *Conference { 32 | conference, ok := m.Conferences[conferenceName] 33 | if !ok { 34 | newConference := NewConference(conferenceName, m.CandidateIPs, m.UDPPort) 35 | m.Conferences[conferenceName] = newConference 36 | m.Agents[newConference.IceAgent.Ufrag] = newConference.IceAgent 37 | return newConference 38 | } 39 | return conference 40 | } 41 | 42 | func (m *ConferenceManager) GetAgent(ufrag string) (*agent.ServerAgent, bool) { 43 | result, ok := m.Agents[ufrag] 44 | return result, ok 45 | } 46 | 47 | func (m *ConferenceManager) Run(waitGroup *sync.WaitGroup) { 48 | defer waitGroup.Done() 49 | for { 50 | select { 51 | case sdpOffer := <-m.ChanSdpOffer: 52 | conference, ok := m.Conferences[sdpOffer.ConferenceName] 53 | if !ok { 54 | logging.Warningf(logging.ProtoSDP, "Conference not found: %s, ignoring SdpOffer\n", sdpOffer.ConferenceName) 55 | continue 56 | } 57 | for _, sdpMediaItem := range sdpOffer.MediaItems { 58 | conference.IceAgent.EnsureSignalingMediaComponent(sdpMediaItem.Ufrag, sdpMediaItem.Pwd, sdpMediaItem.FingerprintHash) 59 | } 60 | logging.Descf(logging.ProtoSDP, "We processed incoming SDP, notified the conference's ICE Agent object (SignalingMediaComponents) about client (media) components' ufrag, pwd and fingerprint hash in the SDP. The server knows some metadata about the UDP packets will come in future. Now we are waiting for a STUN Binding Request packet via UDP, with server Ufrag %s from the client!", sdpOffer.MediaItems[0].Ufrag) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/viper" 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | // See: https://medium.com/@bnprashanth256/reading-configuration-files-and-environment-variables-in-go-golang-c2607f912b63 11 | 12 | type Configurations struct { 13 | Server ServerConfigurations 14 | } 15 | 16 | type ServerConfigurations struct { 17 | SoftwareName string 18 | UDP UDPConfigurations 19 | Signaling SignalingConfigurations 20 | DomainName string 21 | StunServerAddr string 22 | MaskIpOnConsole bool 23 | RequestAudio bool 24 | } 25 | 26 | type UDPConfigurations struct { 27 | SinglePort int 28 | DockerHostIp string 29 | } 30 | 31 | type SignalingConfigurations struct { 32 | WsPort int 33 | } 34 | 35 | var Val Configurations 36 | 37 | func Load() { 38 | // Set the file name of the configurations file 39 | viper.SetConfigName("config") 40 | 41 | // Set the path to look for the configurations file 42 | viper.AddConfigPath(".") 43 | viper.AddConfigPath("../") 44 | 45 | // Enable VIPER to read Environment Variables 46 | viper.AutomaticEnv() 47 | 48 | viper.SetConfigType("yml") 49 | 50 | if err := viper.ReadInConfig(); err != nil { 51 | fmt.Printf("Error reading config file, %s", err) 52 | } 53 | 54 | // Set undefined variables 55 | viper.SetDefault("database.dbname", "test_db") 56 | 57 | err := viper.Unmarshal(&Val) 58 | if err != nil { 59 | fmt.Printf("Unable to decode into struct, %v", err) 60 | } 61 | } 62 | 63 | func ToString() string { 64 | result, err := yaml.Marshal(Val) 65 | if err != nil { 66 | return fmt.Sprintf("Error: %s", err) 67 | } 68 | return string(result) 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/dtls/alert.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "fmt" 4 | 5 | type AlertLevel byte 6 | 7 | const ( 8 | AlertLevelWarning AlertLevel = 1 9 | AlertLevelFatal AlertLevel = 2 10 | ) 11 | 12 | func (al *AlertLevel) String() string { 13 | var result string 14 | switch *al { 15 | case AlertLevelWarning: 16 | result = "Warning" 17 | case AlertLevelFatal: 18 | result = "Fatal" 19 | default: 20 | result = "Unknown Alert Type" 21 | } 22 | return fmt.Sprintf("%s (%v)", result, *al) 23 | } 24 | 25 | type AlertDescription byte 26 | 27 | const ( 28 | AlertDescriptionCloseNotify AlertDescription = 0 29 | AlertDescriptionUnexpectedMessage AlertDescription = 10 30 | AlertDescriptionBadRecordMac AlertDescription = 20 31 | AlertDescriptionDecryptionFailed AlertDescription = 21 32 | AlertDescriptionRecordOverflow AlertDescription = 22 33 | AlertDescriptionDecompressionFailure AlertDescription = 30 34 | AlertDescriptionHandshakeFailure AlertDescription = 40 35 | AlertDescriptionNoCertificate AlertDescription = 41 36 | AlertDescriptionBadCertificate AlertDescription = 42 37 | AlertDescriptionUnsupportedCertificate AlertDescription = 43 38 | AlertDescriptionCertificateRevoked AlertDescription = 44 39 | AlertDescriptionCertificateExpired AlertDescription = 45 40 | AlertDescriptionCertificateUnknown AlertDescription = 46 41 | AlertDescriptionIllegalParameter AlertDescription = 47 42 | AlertDescriptionUnknownCA AlertDescription = 48 43 | AlertDescriptionAccessDenied AlertDescription = 49 44 | AlertDescriptionDecodeError AlertDescription = 50 45 | AlertDescriptionDecryptError AlertDescription = 51 46 | AlertDescriptionExportRestriction AlertDescription = 60 47 | AlertDescriptionProtocolVersion AlertDescription = 70 48 | AlertDescriptionInsufficientSecurity AlertDescription = 71 49 | AlertDescriptionInternalError AlertDescription = 80 50 | AlertDescriptionUserCanceled AlertDescription = 90 51 | AlertDescriptionNoRenegotiation AlertDescription = 100 52 | AlertDescriptionUnsupportedExtension AlertDescription = 110 53 | ) 54 | 55 | func (ad *AlertDescription) String() string { 56 | var result string 57 | switch *ad { 58 | case AlertDescriptionCloseNotify: 59 | result = "CloseNotify" 60 | case AlertDescriptionUnexpectedMessage: 61 | result = "UnexpectedMessage" 62 | case AlertDescriptionBadRecordMac: 63 | result = "BadRecordMac" 64 | case AlertDescriptionDecryptionFailed: 65 | result = "DecryptionFailed" 66 | case AlertDescriptionRecordOverflow: 67 | result = "RecordOverflow" 68 | case AlertDescriptionDecompressionFailure: 69 | result = "DecompressionFailure" 70 | case AlertDescriptionHandshakeFailure: 71 | result = "HandshakeFailure" 72 | case AlertDescriptionNoCertificate: 73 | result = "NoCertificate" 74 | case AlertDescriptionBadCertificate: 75 | result = "BadCertificate" 76 | case AlertDescriptionUnsupportedCertificate: 77 | result = "UnsupportedCertificate" 78 | case AlertDescriptionCertificateRevoked: 79 | result = "CertificateRevoked" 80 | case AlertDescriptionCertificateExpired: 81 | result = "CertificateExpired" 82 | case AlertDescriptionCertificateUnknown: 83 | result = "CertificateUnknown" 84 | case AlertDescriptionIllegalParameter: 85 | result = "IllegalParameter" 86 | case AlertDescriptionUnknownCA: 87 | result = "UnknownCA" 88 | case AlertDescriptionAccessDenied: 89 | result = "AccessDenied" 90 | case AlertDescriptionDecodeError: 91 | result = "DecodeError" 92 | case AlertDescriptionDecryptError: 93 | result = "DecryptError" 94 | case AlertDescriptionExportRestriction: 95 | result = "ExportRestriction" 96 | case AlertDescriptionProtocolVersion: 97 | result = "ProtocolVersion" 98 | case AlertDescriptionInsufficientSecurity: 99 | result = "InsufficientSecurity" 100 | case AlertDescriptionInternalError: 101 | result = "InternalError" 102 | case AlertDescriptionUserCanceled: 103 | result = "UserCanceled" 104 | case AlertDescriptionNoRenegotiation: 105 | result = "NoRenegotiation" 106 | case AlertDescriptionUnsupportedExtension: 107 | result = "UnsupportedExtension" 108 | 109 | default: 110 | result = "Unknown Alert Description" 111 | } 112 | return fmt.Sprintf("%s (%v)", result, *ad) 113 | } 114 | 115 | type Alert struct { 116 | Level AlertLevel 117 | Description AlertDescription 118 | } 119 | 120 | func (m *Alert) GetContentType() ContentType { 121 | return ContentTypeAlert 122 | } 123 | 124 | func (m *Alert) String() string { 125 | return fmt.Sprintf("Alert %s %s", &m.Level, &m.Description) 126 | } 127 | 128 | func (m *Alert) Decode(buf []byte, offset int, arrayLen int) (int, error) { 129 | m.Level = AlertLevel(buf[offset]) 130 | offset++ 131 | m.Description = AlertDescription(buf[offset]) 132 | offset++ 133 | return offset, nil 134 | } 135 | 136 | func (m *Alert) Encode() []byte { 137 | result := []byte{byte(m.Level), byte(m.Description)} 138 | return result 139 | } 140 | -------------------------------------------------------------------------------- /backend/src/dtls/algopair.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "fmt" 4 | 5 | type AlgoPair struct { 6 | HashAlgorithm HashAlgorithm 7 | SignatureAlgorithm SignatureAlgorithm 8 | } 9 | 10 | func (m AlgoPair) String() string { 11 | return fmt.Sprintf("{HashAlg: %s Signature Alg: %s}", m.HashAlgorithm, m.SignatureAlgorithm) 12 | } 13 | 14 | func (m *AlgoPair) Decode(buf []byte, offset int, arrayLen int) (int, error) { 15 | m.HashAlgorithm = HashAlgorithm(buf[offset]) 16 | offset += 1 17 | m.SignatureAlgorithm = SignatureAlgorithm(buf[offset]) 18 | offset += 1 19 | return offset, nil 20 | } 21 | 22 | func (m *AlgoPair) Encode() []byte { 23 | result := []byte{ 24 | byte(m.HashAlgorithm), 25 | byte(m.SignatureAlgorithm), 26 | } 27 | return result 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/dtls/certificate.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "fmt" 4 | 5 | type Certificate struct { 6 | Certificates [][]byte 7 | } 8 | 9 | func (m *Certificate) String() string { 10 | return fmt.Sprintf("[Certificate] Certificates: %d bytes", len(m.Certificates[0])) 11 | } 12 | 13 | func (m *Certificate) GetContentType() ContentType { 14 | return ContentTypeHandshake 15 | } 16 | 17 | func (m *Certificate) GetHandshakeType() HandshakeType { 18 | return HandshakeTypeCertificate 19 | } 20 | 21 | func (m *Certificate) Decode(buf []byte, offset int, arrayLen int) (int, error) { 22 | m.Certificates = make([][]byte, 0) 23 | length := NewUint24FromBytes(buf[offset : offset+3]) 24 | lengthInt := int(length.ToUint32()) 25 | offset += 3 26 | offsetBackup := offset 27 | for offset < offsetBackup+int(lengthInt) { 28 | certificateLength := NewUint24FromBytes(buf[offset : offset+3]) 29 | certificateLengthInt := int(certificateLength.ToUint32()) 30 | offset += 3 31 | 32 | certificateBytes := make([]byte, certificateLengthInt) 33 | copy(certificateBytes, buf[offset:offset+certificateLengthInt]) 34 | offset += certificateLengthInt 35 | m.Certificates = append(m.Certificates, certificateBytes) 36 | } 37 | return offset, nil 38 | } 39 | 40 | func (m *Certificate) Encode() []byte { 41 | encodedCertificates := make([]byte, 0) 42 | for _, certificate := range m.Certificates { 43 | certificateLength := NewUint24FromUInt32(uint32(len(certificate))) 44 | encodedCertificates = append(encodedCertificates, certificateLength[:]...) 45 | encodedCertificates = append(encodedCertificates, certificate...) 46 | } 47 | length := NewUint24FromUInt32(uint32(len(encodedCertificates))) 48 | result := append(length[:], encodedCertificates...) 49 | return result 50 | } 51 | -------------------------------------------------------------------------------- /backend/src/dtls/certificaterequest.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type CertificateRequest struct { 9 | CertificateTypes []CertificateType 10 | AlgoPairs []AlgoPair 11 | } 12 | 13 | func (m *CertificateRequest) String() string { 14 | return fmt.Sprintf("[CertificateRequest] CertificateTypes: %s, AlgoPair: %s", m.CertificateTypes, m.AlgoPairs) 15 | } 16 | 17 | func (m *CertificateRequest) GetContentType() ContentType { 18 | return ContentTypeHandshake 19 | } 20 | 21 | func (m *CertificateRequest) GetHandshakeType() HandshakeType { 22 | return HandshakeTypeCertificateRequest 23 | } 24 | 25 | func (m *CertificateRequest) Decode(buf []byte, offset int, arrayLen int) (int, error) { 26 | certificateTypeCount := buf[offset] 27 | offset++ 28 | m.CertificateTypes = make([]CertificateType, int(certificateTypeCount)) 29 | for i := 0; i < int(certificateTypeCount); i++ { 30 | m.CertificateTypes[i] = CertificateType(buf[offset+i]) 31 | } 32 | offset += int(certificateTypeCount) 33 | algoPairLength := binary.BigEndian.Uint16(buf[offset : offset+2]) 34 | offset += 2 35 | algoPairCount := algoPairLength / 2 36 | m.AlgoPairs = make([]AlgoPair, algoPairCount) 37 | for i := 0; i < int(algoPairCount); i++ { 38 | m.AlgoPairs[i] = AlgoPair{} 39 | lastOffset, err := m.AlgoPairs[i].Decode(buf, offset, arrayLen) 40 | if err != nil { 41 | return offset, err 42 | } 43 | offset = lastOffset 44 | } 45 | offset += 2 // Distinguished Names Length 46 | 47 | return offset, nil 48 | } 49 | 50 | func (m *CertificateRequest) Encode() []byte { 51 | result := make([]byte, 0) 52 | result = append(result, byte(len(m.CertificateTypes))) 53 | encodedCertificateTypes := make([]byte, 0) 54 | for i := 0; i < len(m.CertificateTypes); i++ { 55 | encodedCertificateTypes = append(encodedCertificateTypes, byte(m.CertificateTypes[i])) 56 | } 57 | result = append(result, encodedCertificateTypes...) 58 | 59 | encodedAlgoPairs := make([]byte, 0) 60 | for i := 0; i < len(m.AlgoPairs); i++ { 61 | encodedAlgoPairs = append(encodedAlgoPairs, m.AlgoPairs[i].Encode()...) 62 | } 63 | b := make([]byte, 2) 64 | binary.BigEndian.PutUint16(b, uint16(len(encodedAlgoPairs))) 65 | result = append(result, b...) 66 | result = append(result, encodedAlgoPairs...) 67 | result = append(result, []byte{0x00, 0x00}...) // Distinguished Names Length 68 | 69 | return result 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/dtls/certificateverify.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type CertificateVerify struct { 9 | AlgoPair AlgoPair 10 | Signature []byte 11 | } 12 | 13 | func (m *CertificateVerify) String() string { 14 | return fmt.Sprintf("[CertificateVerify] AlgoPair: %s, Signature: 0x%x", m.AlgoPair, m.Signature) 15 | } 16 | 17 | func (m *CertificateVerify) GetContentType() ContentType { 18 | return ContentTypeHandshake 19 | } 20 | 21 | func (m *CertificateVerify) GetHandshakeType() HandshakeType { 22 | return HandshakeTypeCertificateVerify 23 | } 24 | 25 | func (m *CertificateVerify) Decode(buf []byte, offset int, arrayLen int) (int, error) { 26 | m.AlgoPair = AlgoPair{} 27 | offset, err := m.AlgoPair.Decode(buf, offset, arrayLen) 28 | if err != nil { 29 | return offset, err 30 | } 31 | signatureLength := binary.BigEndian.Uint16(buf[offset : offset+2]) 32 | offset += 2 33 | m.Signature = make([]byte, signatureLength) 34 | copy(m.Signature, buf[offset:offset+int(signatureLength)]) 35 | offset += int(signatureLength) 36 | return offset, nil 37 | } 38 | 39 | func (m *CertificateVerify) Encode() []byte { 40 | result := make([]byte, 0) 41 | result = append(result, m.AlgoPair.Encode()...) 42 | b := make([]byte, 2) 43 | binary.BigEndian.PutUint16(b, uint16(len(m.Signature))) 44 | result = append(result, b...) 45 | result = append(result, m.Signature...) 46 | return result 47 | } 48 | -------------------------------------------------------------------------------- /backend/src/dtls/changecipherspec.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type ChangeCipherSpec struct { 8 | } 9 | 10 | func (m *ChangeCipherSpec) String() string { 11 | return "[ChangeCipherSpec] Data: 1" 12 | } 13 | 14 | func (m *ChangeCipherSpec) GetContentType() ContentType { 15 | return ContentTypeChangeCipherSpec 16 | } 17 | 18 | func (m *ChangeCipherSpec) Decode(buf []byte, offset int, arrayLen int) (int, error) { 19 | if arrayLen < 1 || buf[offset] != 1 { 20 | offset++ 21 | return offset, errors.New("invalid cipher spec") 22 | } 23 | offset++ 24 | return offset, nil 25 | } 26 | 27 | func (m *ChangeCipherSpec) Encode() []byte { 28 | result := []byte{0x01} 29 | return result 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/dtls/clienthello.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/common" 8 | ) 9 | 10 | type ClientHello struct { 11 | Version DtlsVersion 12 | Random Random 13 | Cookie []byte 14 | SessionID []byte 15 | CipherSuiteIDs []CipherSuiteID 16 | CompressionMethodIDs []byte 17 | Extensions map[ExtensionType]Extension 18 | } 19 | 20 | func (m *ClientHello) String() string { 21 | extensionsStr := make([]string, len(m.Extensions)) 22 | i := 0 23 | for _, ext := range m.Extensions { 24 | extensionsStr[i] = ext.String() 25 | i++ 26 | } 27 | cipherSuiteIDsStr := make([]string, len(m.CipherSuiteIDs)) 28 | for i, cs := range m.CipherSuiteIDs { 29 | cipherSuiteIDsStr[i] = cs.String() 30 | } 31 | cookieStr := fmt.Sprintf("%x", m.Cookie) 32 | if len(cookieStr) == 0 { 33 | cookieStr = "" 34 | } else { 35 | cookieStr = "0x" + cookieStr 36 | } 37 | 38 | return common.JoinSlice("\n", false, 39 | fmt.Sprintf("[ClientHello] Ver: %s, Cookie: %s, SessionID: %d", m.Version, cookieStr, m.SessionID), 40 | common.ProcessIndent("Cipher Suite IDs:", "+", cipherSuiteIDsStr), 41 | common.ProcessIndent("Extensions:", "+", extensionsStr), 42 | ) 43 | } 44 | 45 | func (m *ClientHello) GetContentType() ContentType { 46 | return ContentTypeHandshake 47 | } 48 | 49 | func (m *ClientHello) GetHandshakeType() HandshakeType { 50 | return HandshakeTypeClientHello 51 | } 52 | 53 | func (m *ClientHello) Encode() []byte { 54 | return []byte{} 55 | } 56 | 57 | func (m *ClientHello) Decode(buf []byte, offset int, arrayLen int) (int, error) { 58 | // https://github.com/pion/dtls/blob/680c851ed9efc926757f7df6858c82ac63f03a5d/pkg/protocol/handshake/message_client_hello.go#L66 59 | m.Version = DtlsVersion(binary.BigEndian.Uint16(buf[offset : offset+2])) 60 | offset += 2 61 | 62 | decodedRandom, offset, err := DecodeRandom(buf, offset, arrayLen) 63 | if err != nil { 64 | return offset, err 65 | } 66 | m.Random = *decodedRandom 67 | 68 | sessionIDLength := buf[offset] 69 | offset++ 70 | m.SessionID = make([]byte, sessionIDLength) 71 | copy(m.SessionID, buf[offset:offset+int(sessionIDLength)]) 72 | offset += int(sessionIDLength) 73 | 74 | cookieLength := buf[offset] 75 | offset++ 76 | m.Cookie = make([]byte, cookieLength) 77 | copy(m.Cookie, buf[offset:offset+int(cookieLength)]) 78 | offset += int(cookieLength) 79 | 80 | cipherSuiteIDs, offset, err := decodeCipherSuiteIDs(buf, offset, arrayLen) 81 | if err != nil { 82 | return offset, err 83 | } 84 | m.CipherSuiteIDs = cipherSuiteIDs 85 | 86 | compressionMethodIDs, offset, err := decodeCompressionMethodIDs(buf, offset, arrayLen) 87 | if err != nil { 88 | return offset, err 89 | } 90 | m.CompressionMethodIDs = compressionMethodIDs 91 | 92 | exts, offset, err := DecodeExtensionMap(buf, offset, arrayLen) 93 | if err != nil { 94 | return offset, err 95 | } 96 | m.Extensions = exts 97 | 98 | return offset, nil 99 | } 100 | 101 | func decodeCipherSuiteIDs(buf []byte, offset int, arrayLen int) ([]CipherSuiteID, int, error) { 102 | length := binary.BigEndian.Uint16(buf[offset : offset+2]) 103 | count := length / 2 104 | offset += 2 105 | result := make([]CipherSuiteID, count) 106 | for i := 0; i < int(count); i++ { 107 | result[i] = CipherSuiteID(binary.BigEndian.Uint16(buf[offset : offset+2])) 108 | offset += 2 109 | } 110 | return result, offset, nil 111 | } 112 | 113 | func decodeCompressionMethodIDs(buf []byte, offset int, arrayLen int) ([]byte, int, error) { 114 | count := buf[offset] 115 | offset += 1 116 | result := make([]byte, count) 117 | for i := 0; i < int(count); i++ { 118 | result[i] = buf[offset] 119 | offset += 1 120 | } 121 | return result, offset, nil 122 | } 123 | -------------------------------------------------------------------------------- /backend/src/dtls/clientkeyexchange.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "fmt" 4 | 5 | type ClientKeyExchange struct { 6 | PublicKey []byte 7 | } 8 | 9 | func (m *ClientKeyExchange) String() string { 10 | return fmt.Sprintf("[ClientKeyExchange] PublicKey: 0x%x", m.PublicKey) 11 | } 12 | 13 | func (m *ClientKeyExchange) GetContentType() ContentType { 14 | return ContentTypeHandshake 15 | } 16 | 17 | func (m *ClientKeyExchange) GetHandshakeType() HandshakeType { 18 | return HandshakeTypeClientKeyExchange 19 | } 20 | 21 | func (m *ClientKeyExchange) Decode(buf []byte, offset int, arrayLen int) (int, error) { 22 | publicKeyLength := buf[offset] 23 | offset++ 24 | m.PublicKey = make([]byte, publicKeyLength) 25 | copy(m.PublicKey, buf[offset:offset+int(publicKeyLength)]) 26 | offset += int(publicKeyLength) 27 | return offset, nil 28 | } 29 | 30 | func (m *ClientKeyExchange) Encode() []byte { 31 | result := make([]byte, 1) 32 | result[0] = byte(len(m.PublicKey)) 33 | result = append(result, m.PublicKey...) 34 | return result 35 | } 36 | -------------------------------------------------------------------------------- /backend/src/dtls/cryptogcm.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | "fmt" 8 | ) 9 | 10 | const ( 11 | gcmTagLength = 16 12 | gcmNonceLength = 12 13 | headerSize = 13 14 | ) 15 | 16 | type GCM struct { 17 | localGCM, remoteGCM cipher.AEAD 18 | localWriteIV, remoteWriteIV []byte 19 | } 20 | 21 | // NewGCM creates a DTLS GCM Cipher 22 | func NewGCM(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*GCM, error) { 23 | localBlock, err := aes.NewCipher(localKey) 24 | if err != nil { 25 | return nil, err 26 | } 27 | localGCM, err := cipher.NewGCM(localBlock) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | remoteBlock, err := aes.NewCipher(remoteKey) 33 | if err != nil { 34 | return nil, err 35 | } 36 | remoteGCM, err := cipher.NewGCM(remoteBlock) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | return &GCM{ 42 | localGCM: localGCM, 43 | localWriteIV: localWriteIV, 44 | remoteGCM: remoteGCM, 45 | remoteWriteIV: remoteWriteIV, 46 | }, nil 47 | } 48 | 49 | // Encrypts a DTLS RecordLayer message 50 | func (g *GCM) Encrypt(header *RecordHeader, raw []byte) ([]byte, error) { 51 | nonce := make([]byte, gcmNonceLength) 52 | copy(nonce, g.localWriteIV[:4]) 53 | if _, err := rand.Read(nonce[4:]); err != nil { 54 | return nil, err 55 | } 56 | 57 | additionalData := generateAEADAdditionalData(header, len(raw)) 58 | encryptedPayload := g.localGCM.Seal(nil, nonce, raw, additionalData) 59 | r := make([]byte, len(nonce[4:])+len(encryptedPayload)) 60 | copy(r, nonce[4:]) 61 | copy(r[len(nonce[4:]):], encryptedPayload) 62 | return r, nil 63 | } 64 | 65 | // Decrypts a DTLS RecordLayer message 66 | func (g *GCM) Decrypt(h *RecordHeader, in []byte) ([]byte, error) { 67 | switch { 68 | case h.ContentType == ContentTypeChangeCipherSpec: 69 | // Nothing to encrypt with ChangeCipherSpec 70 | return in, nil 71 | } 72 | 73 | nonce := make([]byte, 0, gcmNonceLength) 74 | nonce = append(append(nonce, g.remoteWriteIV[:4]...), in[0:8]...) 75 | out := in[8:] 76 | 77 | additionalData := generateAEADAdditionalData(h, len(out)-gcmTagLength) 78 | var err error 79 | out, err = g.remoteGCM.Open(out[:0], nonce, out, additionalData) 80 | if err != nil { 81 | return nil, fmt.Errorf("error on decrypting packet: %v", err) 82 | } 83 | return out, nil 84 | } 85 | -------------------------------------------------------------------------------- /backend/src/dtls/dtlsmessage.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 8 | ) 9 | 10 | type uint24 [3]byte 11 | 12 | func (b *uint24) ToUint32() uint32 { 13 | // https://stackoverflow.com/questions/45000982/convert-3-bytes-to-int-in-go 14 | return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 15 | } 16 | 17 | func NewUint24FromUInt32(i uint32) uint24 { 18 | buf := make([]byte, 4) 19 | binary.BigEndian.PutUint32(buf, uint32(i)) 20 | result := new(uint24) 21 | copy(result[:], buf[1:4]) 22 | return *result 23 | } 24 | 25 | func NewUint24FromBytes(buf []byte) uint24 { 26 | result := new(uint24) 27 | copy(result[:], buf[0:3]) 28 | return *result 29 | } 30 | 31 | type BaseDtlsMessage interface { 32 | GetContentType() ContentType 33 | Encode() []byte 34 | Decode(buf []byte, offset int, arrayLen int) (int, error) 35 | String() string 36 | } 37 | 38 | type BaseDtlsHandshakeMessage interface { 39 | GetContentType() ContentType 40 | GetHandshakeType() HandshakeType 41 | Encode() []byte 42 | Decode(buf []byte, offset int, arrayLen int) (int, error) 43 | } 44 | 45 | var ( 46 | errIncompleteDtlsMessage = errors.New("data contains incomplete DTLS message") 47 | errUnknownDtlsContentType = errors.New("data contains unkown DTLS content type") 48 | errUnknownDtlsHandshakeType = errors.New("data contains unkown DTLS handshake type") 49 | ) 50 | 51 | func IsDtlsPacket(buf []byte, offset int, arrayLen int) bool { 52 | return arrayLen > 0 && buf[offset] >= 20 && buf[offset] <= 63 53 | } 54 | 55 | func DecodeDtlsMessage(context *HandshakeContext, buf []byte, offset int, arrayLen int) (*RecordHeader, *HandshakeHeader, BaseDtlsMessage, int, error) { 56 | if arrayLen < 1 { 57 | return nil, nil, nil, offset, errIncompleteDtlsMessage 58 | } 59 | header, offset, err := DecodeRecordHeader(buf, offset, arrayLen) 60 | if err != nil { 61 | return nil, nil, nil, offset, err 62 | } 63 | 64 | if header.Epoch < context.ClientEpoch { 65 | // Ignore incoming message 66 | offset += int(header.Length) 67 | return nil, nil, nil, offset, nil 68 | } 69 | 70 | context.ClientEpoch = header.Epoch 71 | 72 | var decryptedBytes []byte 73 | var encryptedBytes []byte 74 | if header.Epoch > 0 { 75 | // Data arrives encrypted, we should decrypt it before. 76 | if context.IsCipherSuiteInitialized { 77 | encryptedBytes = buf[offset : offset+int(header.Length)] 78 | offset += int(header.Length) 79 | decryptedBytes, err = context.GCM.Decrypt(header, encryptedBytes) 80 | if err != nil { 81 | return nil, nil, nil, offset, err 82 | } 83 | } 84 | } 85 | 86 | switch header.ContentType { 87 | case ContentTypeHandshake: 88 | if decryptedBytes == nil { 89 | offsetBackup := offset 90 | handshakeHeader, offset, err := DecodeHandshakeHeader(buf, offset, arrayLen) 91 | if err != nil { 92 | return nil, nil, nil, offset, err 93 | } 94 | if handshakeHeader.Length.ToUint32() != handshakeHeader.FragmentLength.ToUint32() { 95 | // Ignore fragmented packets 96 | logging.Warningf(logging.ProtoDTLS, "Ignore fragmented packets: %s", header.ContentType) 97 | return nil, nil, nil, offset + int(handshakeHeader.FragmentLength.ToUint32()), nil 98 | } 99 | result, offset, err := decodeHandshake(header, handshakeHeader, buf, offset, arrayLen) 100 | if err != nil { 101 | return nil, nil, nil, offset, err 102 | } 103 | copyArray := make([]byte, offset-offsetBackup) 104 | copy(copyArray, buf[offsetBackup:offset]) 105 | context.HandshakeMessagesReceived[handshakeHeader.HandshakeType] = copyArray 106 | 107 | return header, handshakeHeader, result, offset, err 108 | } else { 109 | handshakeHeader, decryptedOffset, err := DecodeHandshakeHeader(decryptedBytes, 0, len(decryptedBytes)) 110 | if err != nil { 111 | return nil, nil, nil, offset, err 112 | } 113 | 114 | result, _, err := decodeHandshake(header, handshakeHeader, decryptedBytes, decryptedOffset, len(decryptedBytes)-decryptedOffset) 115 | 116 | copyArray := make([]byte, len(decryptedBytes)) 117 | copy(copyArray, decryptedBytes) 118 | context.HandshakeMessagesReceived[handshakeHeader.HandshakeType] = copyArray 119 | 120 | return header, handshakeHeader, result, offset, err 121 | } 122 | case ContentTypeChangeCipherSpec: 123 | changeCipherSpec := &ChangeCipherSpec{} 124 | offset, err := changeCipherSpec.Decode(buf, offset, arrayLen) 125 | if err != nil { 126 | return nil, nil, nil, offset, err 127 | } 128 | return header, nil, changeCipherSpec, offset, nil 129 | case ContentTypeAlert: 130 | alert := &Alert{} 131 | if decryptedBytes == nil { 132 | offset, err = alert.Decode(buf, offset, arrayLen) 133 | } else { 134 | _, err = alert.Decode(decryptedBytes, 0, len(decryptedBytes)) 135 | } 136 | if err != nil { 137 | return nil, nil, nil, offset, err 138 | } 139 | return header, nil, alert, offset, nil 140 | 141 | default: 142 | return nil, nil, nil, offset, errUnknownDtlsContentType 143 | } 144 | } 145 | 146 | func decodeHandshake(header *RecordHeader, handshakeHeader *HandshakeHeader, buf []byte, offset int, arrayLen int) (BaseDtlsMessage, int, error) { 147 | var result BaseDtlsMessage 148 | switch handshakeHeader.HandshakeType { 149 | case HandshakeTypeClientHello: 150 | result = new(ClientHello) 151 | case HandshakeTypeServerHello: 152 | result = new(ServerHello) 153 | case HandshakeTypeCertificate: 154 | result = new(Certificate) 155 | case HandshakeTypeServerKeyExchange: 156 | result = new(ServerKeyExchange) 157 | case HandshakeTypeCertificateRequest: 158 | result = new(CertificateRequest) 159 | case HandshakeTypeServerHelloDone: 160 | result = new(ServerHelloDone) 161 | case HandshakeTypeClientKeyExchange: 162 | result = new(ClientKeyExchange) 163 | case HandshakeTypeCertificateVerify: 164 | result = new(CertificateVerify) 165 | case HandshakeTypeFinished: 166 | result = new(Finished) 167 | default: 168 | return nil, offset, errUnknownDtlsHandshakeType 169 | } 170 | offset, err := result.Decode(buf, offset, arrayLen) 171 | return result, offset, err 172 | 173 | } 174 | -------------------------------------------------------------------------------- /backend/src/dtls/dtlsstate.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | type DTLSState byte 4 | 5 | const ( 6 | DTLSStateNew DTLSState = 1 7 | DTLSStateConnecting DTLSState = 2 8 | DTLSStateConnected DTLSState = 3 9 | DTLSStateFailed DTLSState = 4 10 | ) 11 | 12 | func (s DTLSState) String() string { 13 | switch s { 14 | case DTLSStateNew: 15 | return "New" 16 | case DTLSStateConnecting: 17 | return "Connecting" 18 | case DTLSStateConnected: 19 | return "Connected" 20 | case DTLSStateFailed: 21 | return "Failed" 22 | default: 23 | return "Unknown" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/dtls/extensions.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "encoding/binary" 4 | 5 | type ExtensionType uint16 6 | 7 | type Extension interface { 8 | ExtensionType() ExtensionType 9 | Encode() []byte 10 | Decode(extensionLength int, buf []byte, offset int, arrayLen int) error 11 | String() string 12 | } 13 | 14 | const ( 15 | ExtensionTypeServerName ExtensionType = 0 16 | ExtensionTypeSupportedEllipticCurves ExtensionType = 10 17 | ExtensionTypeSupportedPointFormats ExtensionType = 11 18 | ExtensionTypeSupportedSignatureAlgorithms ExtensionType = 13 19 | ExtensionTypeUseSRTP ExtensionType = 14 20 | ExtensionTypeALPN ExtensionType = 16 21 | ExtensionTypeUseExtendedMasterSecret ExtensionType = 23 22 | ExtensionTypeRenegotiationInfo ExtensionType = 65281 23 | 24 | ExtensionTypeUnknown ExtensionType = 65535 //Not a valid value 25 | ) 26 | 27 | func DecodeExtensionMap(buf []byte, offset int, arrayLen int) (map[ExtensionType]Extension, int, error) { 28 | result := map[ExtensionType]Extension{} 29 | length := binary.BigEndian.Uint16(buf[offset : offset+2]) 30 | offset += 2 31 | offsetBackup := offset 32 | for offset < offsetBackup+int(length) { 33 | extensionType := ExtensionType(binary.BigEndian.Uint16(buf[offset : offset+2])) 34 | offset += 2 35 | extensionLength := binary.BigEndian.Uint16(buf[offset : offset+2]) 36 | offset += 2 37 | var extension Extension = nil 38 | switch extensionType { 39 | case ExtensionTypeUseExtendedMasterSecret: 40 | extension = new(ExtUseExtendedMasterSecret) 41 | case ExtensionTypeUseSRTP: 42 | extension = new(ExtUseSRTP) 43 | case ExtensionTypeSupportedPointFormats: 44 | extension = new(ExtSupportedPointFormats) 45 | case ExtensionTypeSupportedEllipticCurves: 46 | extension = new(ExtSupportedEllipticCurves) 47 | default: 48 | extension = &ExtUnknown{ 49 | Type: extensionType, 50 | DataLength: extensionLength, 51 | } 52 | } 53 | if extension != nil { 54 | err := extension.Decode(int(extensionLength), buf, offset, arrayLen) 55 | 56 | if err != nil { 57 | return nil, offset, err 58 | } 59 | AddExtension(result, extension) 60 | } 61 | offset += int(extensionLength) 62 | } 63 | return result, offset, nil 64 | } 65 | 66 | func EncodeExtensionMap(extensionMap map[ExtensionType]Extension) []byte { 67 | result := make([]byte, 2) 68 | encodedBody := make([]byte, 0) 69 | for _, extension := range extensionMap { 70 | encodedExtension := extension.Encode() 71 | encodedExtType := make([]byte, 2) 72 | binary.BigEndian.PutUint16(encodedExtType, uint16(extension.ExtensionType())) 73 | encodedBody = append(encodedBody, encodedExtType...) 74 | 75 | encodedExtLen := make([]byte, 2) 76 | binary.BigEndian.PutUint16(encodedExtLen, uint16(len(encodedExtension))) 77 | encodedBody = append(encodedBody, encodedExtLen...) 78 | encodedBody = append(encodedBody, encodedExtension...) 79 | } 80 | binary.BigEndian.PutUint16(result[0:], uint16(len(encodedBody))) 81 | result = append(result, encodedBody...) 82 | return result 83 | } 84 | 85 | func AddExtension(extensionMap map[ExtensionType]Extension, extension Extension) { 86 | extType := extension.ExtensionType() 87 | // This is only for debugging purposes to assign unique map key value for unknown types 88 | if extType == ExtensionTypeUnknown { 89 | for { 90 | _, ok := extensionMap[extType] 91 | if !ok { 92 | break 93 | } 94 | extType-- 95 | } 96 | } 97 | extensionMap[extType] = extension 98 | } 99 | -------------------------------------------------------------------------------- /backend/src/dtls/finished.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import "fmt" 4 | 5 | type Finished struct { 6 | VerifyData []byte 7 | } 8 | 9 | func (m *Finished) String() string { 10 | return fmt.Sprintf("[Finished] VerifyData: 0x%x (%d bytes)", m.VerifyData, len(m.VerifyData)) 11 | } 12 | 13 | func (m *Finished) GetContentType() ContentType { 14 | return ContentTypeHandshake 15 | } 16 | 17 | func (m *Finished) GetHandshakeType() HandshakeType { 18 | return HandshakeTypeFinished 19 | } 20 | 21 | func (m *Finished) Decode(buf []byte, offset int, arrayLen int) (int, error) { 22 | m.VerifyData = make([]byte, arrayLen) 23 | copy(m.VerifyData, buf[offset:offset+arrayLen]) 24 | offset += len(m.VerifyData) 25 | return offset, nil 26 | } 27 | 28 | func (m *Finished) Encode() []byte { 29 | result := make([]byte, len(m.VerifyData)) 30 | copy(result, m.VerifyData) 31 | return result 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/dtls/handshakecontext.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 7 | ) 8 | 9 | type HandshakeContext struct { 10 | //Client IP and Port 11 | Addr *net.UDPAddr 12 | //Server UDP listener connection 13 | Conn *net.UDPConn 14 | ClientUfrag string 15 | ExpectedFingerprintHash string 16 | 17 | DTLSState DTLSState 18 | OnDTLSStateChangeHandler func(DTLSState) 19 | 20 | ProtocolVersion DtlsVersion 21 | CipherSuite *CipherSuite 22 | CurveType CurveType 23 | Curve Curve 24 | SRTPProtectionProfile SRTPProtectionProfile 25 | ClientRandom *Random 26 | ClientKeyExchangePublic []byte 27 | 28 | ServerRandom *Random 29 | ServerMasterSecret []byte 30 | ServerPublicKey []byte 31 | ServerPrivateKey []byte 32 | ServerKeySignature []byte 33 | ClientCertificates [][]byte 34 | 35 | IsCipherSuiteInitialized bool 36 | GCM *GCM 37 | 38 | UseExtendedMasterSecret bool 39 | 40 | HandshakeMessagesReceived map[HandshakeType][]byte 41 | HandshakeMessagesSent map[HandshakeType][]byte 42 | 43 | ClientEpoch uint16 44 | ClientSequenceNumber uint16 45 | ServerEpoch uint16 46 | ServerSequenceNumber uint16 47 | ServerHandshakeSequenceNumber uint16 48 | 49 | Cookie []byte 50 | Flight Flight 51 | 52 | KeyingMaterialCache []byte 53 | } 54 | 55 | func (c *HandshakeContext) IncreaseServerEpoch() { 56 | c.ServerEpoch++ 57 | c.ServerSequenceNumber = 0 58 | } 59 | 60 | func (c *HandshakeContext) IncreaseServerSequence() { 61 | c.ServerSequenceNumber++ 62 | } 63 | 64 | func (c *HandshakeContext) IncreaseServerHandshakeSequence() { 65 | c.ServerHandshakeSequenceNumber++ 66 | } 67 | 68 | type Flight byte 69 | 70 | const ( 71 | Flight0 Flight = 0 72 | Flight2 Flight = 2 73 | Flight4 Flight = 4 74 | Flight6 Flight = 6 75 | ) 76 | 77 | // https://github.com/pion/dtls/blob/bee42643f57a7f9c85ee3aa6a45a4fa9811ed122/state.go#L182 78 | func (c *HandshakeContext) ExportKeyingMaterial(length int) ([]byte, error) { 79 | if c.KeyingMaterialCache != nil { 80 | return c.KeyingMaterialCache, nil 81 | } 82 | encodedClientRandom := c.ClientRandom.Encode() 83 | encodedServerRandom := c.ServerRandom.Encode() 84 | var err error 85 | logging.Descf(logging.ProtoDTLS, "Exporting keying material from DTLS context (expected length: %d)...", length) 86 | c.KeyingMaterialCache, err = GenerateKeyingMaterial(c.ServerMasterSecret, encodedClientRandom, encodedServerRandom, c.CipherSuite.HashAlgorithm, length) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return c.KeyingMaterialCache, nil 91 | } 92 | 93 | func (c *HandshakeContext) SetDTLSState(dtlsState DTLSState) { 94 | if c.DTLSState == dtlsState { 95 | return 96 | } 97 | c.DTLSState = dtlsState 98 | if c.OnDTLSStateChangeHandler != nil { 99 | c.OnDTLSStateChangeHandler(dtlsState) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /backend/src/dtls/handshakeheader.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type HandshakeType uint8 9 | 10 | const ( 11 | // https://github.com/eclipse/tinydtls/blob/706888256c3e03d9fcf1ec37bb1dd6499213be3c/dtls.h#L344 12 | HandshakeTypeHelloRequest HandshakeType = 0 13 | HandshakeTypeClientHello HandshakeType = 1 14 | HandshakeTypeServerHello HandshakeType = 2 15 | HandshakeTypeHelloVerifyRequest HandshakeType = 3 16 | HandshakeTypeCertificate HandshakeType = 11 17 | HandshakeTypeServerKeyExchange HandshakeType = 12 18 | HandshakeTypeCertificateRequest HandshakeType = 13 19 | HandshakeTypeServerHelloDone HandshakeType = 14 20 | HandshakeTypeCertificateVerify HandshakeType = 15 21 | HandshakeTypeClientKeyExchange HandshakeType = 16 22 | HandshakeTypeFinished HandshakeType = 20 23 | ) 24 | 25 | type HandshakeHeader struct { 26 | //https://github.com/eclipse/tinydtls/blob/706888256c3e03d9fcf1ec37bb1dd6499213be3c/dtls.h#L344 27 | HandshakeType HandshakeType 28 | Length uint24 29 | MessageSequence uint16 30 | FragmentOffset uint24 31 | FragmentLength uint24 32 | } 33 | 34 | func (ht HandshakeType) String() string { 35 | var result string 36 | switch ht { 37 | case HandshakeTypeHelloRequest: 38 | result = "HelloRequest" 39 | case HandshakeTypeClientHello: 40 | result = "ClientHello" 41 | case HandshakeTypeServerHello: 42 | result = "ServerHello" 43 | case HandshakeTypeHelloVerifyRequest: 44 | result = "VerifyRequest" 45 | case HandshakeTypeCertificate: 46 | result = "Certificate" 47 | case HandshakeTypeServerKeyExchange: 48 | result = "ServerKeyExchange" 49 | case HandshakeTypeCertificateRequest: 50 | result = "CertificateRequest" 51 | case HandshakeTypeServerHelloDone: 52 | result = "ServerHelloDone" 53 | case HandshakeTypeCertificateVerify: 54 | result = "CertificateVerify" 55 | case HandshakeTypeClientKeyExchange: 56 | result = "ClientKeyExchange" 57 | case HandshakeTypeFinished: 58 | result = "Finished" 59 | default: 60 | result = "Unknown type" 61 | } 62 | return fmt.Sprintf("%s (%d)", result, uint8(ht)) 63 | } 64 | 65 | func (h *HandshakeHeader) String() string { 66 | return fmt.Sprintf("[Handshake Header] Handshake Type: %s, Message Seq: %d", h.HandshakeType, h.MessageSequence) 67 | } 68 | 69 | func (h *HandshakeHeader) Encode() []byte { 70 | result := make([]byte, 12) 71 | result[0] = byte(h.HandshakeType) 72 | copy(result[1:], h.Length[:]) 73 | binary.BigEndian.PutUint16(result[4:], h.MessageSequence) 74 | copy(result[6:], h.FragmentOffset[:]) 75 | copy(result[9:], h.FragmentLength[:]) 76 | return result 77 | } 78 | 79 | func DecodeHandshakeHeader(buf []byte, offset int, arrayLen int) (*HandshakeHeader, int, error) { 80 | result := new(HandshakeHeader) 81 | 82 | result.HandshakeType = HandshakeType(buf[offset]) 83 | offset++ 84 | result.Length = NewUint24FromBytes(buf[offset : offset+3]) 85 | offset += 3 86 | result.MessageSequence = binary.BigEndian.Uint16(buf[offset : offset+2]) 87 | offset += 2 88 | result.FragmentOffset = NewUint24FromBytes(buf[offset : offset+3]) 89 | offset += 3 90 | result.FragmentLength = NewUint24FromBytes(buf[offset : offset+3]) 91 | offset += 3 92 | return result, offset, nil 93 | } 94 | -------------------------------------------------------------------------------- /backend/src/dtls/helloverifyrequest.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type HelloVerifyRequest struct { 9 | Version DtlsVersion 10 | Cookie []byte 11 | } 12 | 13 | func (m *HelloVerifyRequest) String() string { 14 | cookieStr := fmt.Sprintf("%x", m.Cookie) 15 | if len(cookieStr) == 0 { 16 | cookieStr = "" 17 | } else { 18 | cookieStr = "0x" + cookieStr 19 | } 20 | return fmt.Sprintf("[HelloVerifyRequest] Ver: %s, Cookie: %s", m.Version, cookieStr) 21 | } 22 | 23 | func (m *HelloVerifyRequest) GetContentType() ContentType { 24 | return ContentTypeHandshake 25 | } 26 | 27 | func (m *HelloVerifyRequest) GetHandshakeType() HandshakeType { 28 | return HandshakeTypeHelloVerifyRequest 29 | } 30 | 31 | func (m *HelloVerifyRequest) Decode(buf []byte, offset int, arrayLen int) (int, error) { 32 | // https://github.com/pion/dtls/blob/680c851ed9efc926757f7df6858c82ac63f03a5d/pkg/protocol/handshake/message_client_hello.go#L66 33 | m.Version = DtlsVersion(binary.BigEndian.Uint16(buf[offset : offset+2])) 34 | offset += 2 35 | 36 | cookieLength := buf[offset] 37 | offset++ 38 | m.Cookie = make([]byte, cookieLength) 39 | copy(m.Cookie, buf[offset:offset+int(cookieLength)]) 40 | offset += int(cookieLength) 41 | 42 | return offset, nil 43 | } 44 | 45 | func (m *HelloVerifyRequest) Encode() []byte { 46 | result := make([]byte, 3) 47 | binary.BigEndian.PutUint16(result[0:2], uint16(m.Version)) 48 | result[2] = byte(len(m.Cookie)) 49 | result = append(result, m.Cookie...) 50 | 51 | return result 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/dtls/init.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "crypto/tls" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/config" 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 8 | ) 9 | 10 | var ( 11 | ServerCertificate *tls.Certificate 12 | ServerCertificateFingerprint string 13 | ) 14 | 15 | func Init() { 16 | logging.Infof(logging.ProtoDTLS, "Initializing self signed certificate for server...") 17 | serverCertificate, err := GenerateServerCertificate(config.Val.Server.DomainName) 18 | if err != nil { 19 | panic(err) 20 | } 21 | ServerCertificate = serverCertificate 22 | ServerCertificateFingerprint = GetCertificateFingerprint(serverCertificate) 23 | logging.Infof(logging.ProtoDTLS, "Self signed certificate created with fingerprint %s", ServerCertificateFingerprint) 24 | logging.Descf(logging.ProtoDTLS, "This certificate is stored in dtls.ServerCertificate variable globally, it will be used while DTLS handshake, sending SDP, SRTP, SRTCP packets, etc...") 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/dtls/random.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | "time" 7 | ) 8 | 9 | const ( 10 | RandomBytesLength = 28 11 | ) 12 | 13 | // https://github.com/pion/dtls/blob/b3e235f54b60ccc31aa10193807b5e8e394f17ff/pkg/protocol/handshake/random.go 14 | type Random struct { 15 | GMTUnixTime time.Time 16 | RandomBytes [RandomBytesLength]byte 17 | } 18 | 19 | func (r *Random) Encode() []byte { 20 | result := make([]byte, 4+RandomBytesLength) 21 | 22 | binary.BigEndian.PutUint32(result[0:4], uint32(r.GMTUnixTime.Unix())) 23 | copy(result[4:], r.RandomBytes[:]) 24 | return result 25 | } 26 | 27 | func (r *Random) Generate() error { 28 | r.GMTUnixTime = time.Now() 29 | tmp := make([]byte, RandomBytesLength) 30 | _, err := rand.Read(tmp) 31 | copy(r.RandomBytes[:], tmp) 32 | return err 33 | } 34 | 35 | func DecodeRandom(buf []byte, offset int, arrayLen int) (*Random, int, error) { 36 | result := new(Random) 37 | result.GMTUnixTime = time.Unix(int64(binary.BigEndian.Uint32(buf[offset:offset+4])), 0) 38 | offset += 4 39 | copy(result.RandomBytes[:], buf[offset:offset+RandomBytesLength]) 40 | offset += RandomBytesLength 41 | 42 | return result, offset, nil 43 | } 44 | -------------------------------------------------------------------------------- /backend/src/dtls/recordheader.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type ContentType uint8 9 | 10 | type DtlsVersion uint16 11 | 12 | const ( 13 | SequenceNumberSize = 6 // 48 bit 14 | 15 | // https://github.com/eclipse/tinydtls/blob/706888256c3e03d9fcf1ec37bb1dd6499213be3c/dtls.h#L314 16 | ContentTypeChangeCipherSpec ContentType = 20 17 | ContentTypeAlert ContentType = 21 18 | ContentTypeHandshake ContentType = 22 19 | ContentTypeApplicationData ContentType = 23 20 | 21 | DtlsVersion1_0 DtlsVersion = 0xfeff 22 | DtlsVersion1_2 DtlsVersion = 0xfefd 23 | ) 24 | 25 | type RecordHeader struct { 26 | //https://github.com/eclipse/tinydtls/blob/706888256c3e03d9fcf1ec37bb1dd6499213be3c/dtls.h#L320 27 | ContentType ContentType 28 | Version DtlsVersion 29 | Epoch uint16 30 | SequenceNumber [SequenceNumberSize]byte 31 | Length uint16 32 | } 33 | 34 | func (t ContentType) String() string { 35 | var result string 36 | switch t { 37 | case ContentTypeChangeCipherSpec: 38 | result = "ChangeCipherSpec" 39 | case ContentTypeAlert: 40 | result = "Alert" 41 | case ContentTypeHandshake: 42 | result = "Handshake" 43 | case ContentTypeApplicationData: 44 | result = "ApplicationData" 45 | 46 | default: 47 | result = "Unknown Content Type" 48 | } 49 | return fmt.Sprintf("%s (%d)", result, uint8(t)) 50 | } 51 | 52 | func (v DtlsVersion) String() string { 53 | var result string 54 | switch v { 55 | case DtlsVersion1_0: 56 | result = "1.0" 57 | case DtlsVersion1_2: 58 | result = "1.2" 59 | default: 60 | result = "Unknown Version" 61 | } 62 | return fmt.Sprintf("%s (0x%x)", result, uint16(v)) 63 | } 64 | 65 | func (h *RecordHeader) String() string { 66 | seqNum := binary.BigEndian.Uint64(append([]byte{0, 0}, h.SequenceNumber[:]...)) 67 | return fmt.Sprintf("[Record Header] Content Type: %s, Ver: %s, Epoch: %d, SeqNum: %d", h.ContentType, h.Version, h.Epoch, seqNum) 68 | } 69 | 70 | func (h *RecordHeader) Encode() []byte { 71 | result := make([]byte, 7+SequenceNumberSize) 72 | result[0] = byte(h.ContentType) 73 | binary.BigEndian.PutUint16(result[1:], uint16(h.Version)) 74 | binary.BigEndian.PutUint16(result[3:], uint16(h.Epoch)) 75 | copy(result[5:], h.SequenceNumber[:]) 76 | binary.BigEndian.PutUint16(result[5+SequenceNumberSize:], uint16(h.Length)) 77 | return result 78 | } 79 | 80 | func DecodeRecordHeader(buf []byte, offset int, arrayLen int) (*RecordHeader, int, error) { 81 | result := new(RecordHeader) 82 | 83 | result.ContentType = ContentType(buf[offset]) 84 | offset++ 85 | result.Version = DtlsVersion(binary.BigEndian.Uint16(buf[offset : offset+2])) 86 | offset += 2 87 | result.Epoch = binary.BigEndian.Uint16(buf[offset : offset+2]) 88 | offset += 2 89 | copy(result.SequenceNumber[:], buf[offset:offset+SequenceNumberSize]) 90 | offset += SequenceNumberSize 91 | result.Length = binary.BigEndian.Uint16(buf[offset : offset+2]) 92 | offset += 2 93 | return result, offset, nil 94 | } 95 | -------------------------------------------------------------------------------- /backend/src/dtls/serverhello.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/common" 8 | ) 9 | 10 | type ServerHello struct { 11 | Version DtlsVersion 12 | Random Random 13 | SessionID []byte 14 | 15 | CipherSuiteID CipherSuiteID 16 | CompressionMethodID byte 17 | Extensions map[ExtensionType]Extension 18 | } 19 | 20 | func (m *ServerHello) String() string { 21 | extensionsStr := make([]string, len(m.Extensions)) 22 | i := 0 23 | for _, ext := range m.Extensions { 24 | extensionsStr[i] = ext.String() 25 | i++ 26 | } 27 | return common.JoinSlice("\n", false, 28 | fmt.Sprintf("[ServerHello] Ver: %s, SessionID: %d", m.Version, m.SessionID), 29 | fmt.Sprintf("Cipher Suite ID: 0x%x", m.CipherSuiteID), 30 | common.ProcessIndent("Extensions:", "+", extensionsStr), 31 | ) 32 | } 33 | 34 | func (m *ServerHello) GetContentType() ContentType { 35 | return ContentTypeHandshake 36 | } 37 | 38 | func (m *ServerHello) GetHandshakeType() HandshakeType { 39 | return HandshakeTypeServerHello 40 | } 41 | 42 | func (m *ServerHello) Decode(buf []byte, offset int, arrayLen int) (int, error) { 43 | // https://github.com/pion/dtls/blob/680c851ed9efc926757f7df6858c82ac63f03a5d/pkg/protocol/handshake/message_client_hello.go#L66 44 | m.Version = DtlsVersion(binary.BigEndian.Uint16(buf[offset : offset+2])) 45 | offset += 2 46 | 47 | decodedRandom, offset, err := DecodeRandom(buf, offset, arrayLen) 48 | if err != nil { 49 | return offset, err 50 | } 51 | m.Random = *decodedRandom 52 | 53 | sessionIDLength := buf[offset] 54 | offset++ 55 | m.SessionID = make([]byte, sessionIDLength) 56 | copy(m.SessionID, buf[offset:offset+int(sessionIDLength)]) 57 | offset += int(sessionIDLength) 58 | 59 | m.CipherSuiteID = CipherSuiteID(binary.BigEndian.Uint16(buf[offset : offset+2])) 60 | offset += 2 61 | 62 | m.CompressionMethodID = buf[offset] 63 | offset++ 64 | 65 | extensionsMap, offset, err := DecodeExtensionMap(buf, offset, arrayLen) 66 | if err != nil { 67 | return offset, err 68 | } 69 | m.Extensions = extensionsMap 70 | return offset, nil 71 | } 72 | 73 | func (m *ServerHello) Encode() []byte { 74 | result := make([]byte, 2) 75 | binary.BigEndian.PutUint16(result[0:2], uint16(m.Version)) 76 | result = append(result, m.Random.Encode()...) 77 | 78 | result = append(result, byte(len(m.SessionID))) 79 | result = append(result, m.SessionID...) 80 | 81 | result = append(result, []byte{0x00, 0x00}...) 82 | binary.BigEndian.PutUint16(result[len(result)-2:], uint16(m.CipherSuiteID)) 83 | 84 | result = append(result, m.CompressionMethodID) 85 | 86 | encodedExtensions := EncodeExtensionMap(m.Extensions) 87 | result = append(result, encodedExtensions...) 88 | 89 | return result 90 | } 91 | -------------------------------------------------------------------------------- /backend/src/dtls/serverhellodone.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | type ServerHelloDone struct { 4 | } 5 | 6 | func (m *ServerHelloDone) String() string { 7 | return "[ServerHelloDone]" 8 | } 9 | 10 | func (m *ServerHelloDone) GetContentType() ContentType { 11 | return ContentTypeHandshake 12 | } 13 | 14 | func (m *ServerHelloDone) GetHandshakeType() HandshakeType { 15 | return HandshakeTypeServerHelloDone 16 | } 17 | 18 | func (m *ServerHelloDone) Decode(buf []byte, offset int, arrayLen int) (int, error) { 19 | return offset, nil 20 | } 21 | 22 | func (m *ServerHelloDone) Encode() []byte { 23 | result := make([]byte, 0) 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/dtls/serverkeyexchange.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type ServerKeyExchange struct { 9 | EllipticCurveType CurveType 10 | NamedCurve Curve 11 | PublicKey []byte 12 | AlgoPair AlgoPair 13 | Signature []byte 14 | } 15 | 16 | func (m *ServerKeyExchange) String() string { 17 | return fmt.Sprintf("[ServerKeyExchange] EllipticCurveType: %s, NamedCurve: %s, AlgoPair: %s, PublicKey: 0x%x", m.EllipticCurveType, m.NamedCurve, m.AlgoPair, m.PublicKey) 18 | } 19 | 20 | func (m *ServerKeyExchange) GetContentType() ContentType { 21 | return ContentTypeHandshake 22 | } 23 | 24 | func (m *ServerKeyExchange) GetHandshakeType() HandshakeType { 25 | return HandshakeTypeServerKeyExchange 26 | } 27 | 28 | func (m *ServerKeyExchange) Decode(buf []byte, offset int, arrayLen int) (int, error) { 29 | m.EllipticCurveType = CurveType(buf[offset]) 30 | offset++ 31 | m.NamedCurve = Curve(binary.BigEndian.Uint16(buf[offset : offset+2])) 32 | offset += 2 33 | publicKeyLength := buf[offset] 34 | offset++ 35 | m.PublicKey = make([]byte, publicKeyLength) 36 | copy(m.PublicKey, buf[offset:offset+int(publicKeyLength)]) 37 | m.AlgoPair = AlgoPair{} 38 | offset, err := m.AlgoPair.Decode(buf, offset, arrayLen) 39 | if err != nil { 40 | return offset, err 41 | } 42 | signatureLength := binary.BigEndian.Uint16(buf[offset : offset+2]) 43 | offset += 2 44 | m.Signature = make([]byte, signatureLength) 45 | copy(m.Signature, buf[offset:offset+int(signatureLength)]) 46 | offset += int(signatureLength) 47 | return offset, nil 48 | } 49 | 50 | func (m *ServerKeyExchange) Encode() []byte { 51 | result := make([]byte, 4) 52 | result[0] = byte(m.EllipticCurveType) 53 | binary.BigEndian.PutUint16(result[1:], uint16(m.NamedCurve)) 54 | result[3] = byte(len(m.PublicKey)) 55 | result = append(result, m.PublicKey...) 56 | result = append(result, m.AlgoPair.Encode()...) 57 | b := make([]byte, 2) 58 | binary.BigEndian.PutUint16(b, uint16(len(m.Signature))) 59 | result = append(result, b...) 60 | result = append(result, m.Signature...) 61 | return result 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/dtls/simpleextensions.go: -------------------------------------------------------------------------------- 1 | package dtls 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/common" 9 | ) 10 | 11 | type ExtUseExtendedMasterSecret struct { 12 | } 13 | 14 | func (e *ExtUseExtendedMasterSecret) String() string { 15 | return "[UseExtendedMasterSecret]" 16 | } 17 | 18 | func (e *ExtUseExtendedMasterSecret) ExtensionType() ExtensionType { 19 | return ExtensionTypeUseExtendedMasterSecret 20 | } 21 | 22 | func (e *ExtUseExtendedMasterSecret) Encode() []byte { 23 | return []byte{} 24 | } 25 | 26 | func (e *ExtUseExtendedMasterSecret) Decode(extensionLength int, buf []byte, offset int, arrayLen int) error { 27 | return nil 28 | } 29 | 30 | type ExtRenegotiationInfo struct { 31 | } 32 | 33 | func (e *ExtRenegotiationInfo) String() string { 34 | return "[RenegotiationInfo]" 35 | } 36 | 37 | func (e *ExtRenegotiationInfo) ExtensionType() ExtensionType { 38 | return ExtensionTypeRenegotiationInfo 39 | } 40 | 41 | func (e *ExtRenegotiationInfo) Encode() []byte { 42 | // Empty byte array length is zero 43 | return []byte{0} 44 | } 45 | 46 | func (e *ExtRenegotiationInfo) Decode(extensionLength int, buf []byte, offset int, arrayLen int) error { 47 | return nil 48 | } 49 | 50 | type ExtUseSRTP struct { 51 | ProtectionProfiles []SRTPProtectionProfile 52 | Mki []byte 53 | } 54 | 55 | func (e *ExtUseSRTP) String() string { 56 | protectionProfilesStr := make([]string, len(e.ProtectionProfiles)) 57 | for i, p := range e.ProtectionProfiles { 58 | protectionProfilesStr[i] = p.String() 59 | } 60 | return common.JoinSlice("\n", false, 61 | "[UseSRTP]", 62 | common.ProcessIndent("Protection Profiles:", "+", protectionProfilesStr), 63 | ) 64 | } 65 | 66 | func (e *ExtUseSRTP) ExtensionType() ExtensionType { 67 | return ExtensionTypeUseSRTP 68 | } 69 | 70 | func (e *ExtUseSRTP) Encode() []byte { 71 | result := make([]byte, 2+(len(e.ProtectionProfiles)*2)+1+len(e.Mki)) 72 | offset := 0 73 | binary.BigEndian.PutUint16(result[offset:], uint16(len(e.ProtectionProfiles)*2)) 74 | offset += 2 75 | for i := 0; i < len(e.ProtectionProfiles); i++ { 76 | binary.BigEndian.PutUint16(result[offset:], uint16(e.ProtectionProfiles[i])) 77 | offset += 2 78 | } 79 | result[offset] = byte(len(e.Mki)) 80 | offset++ 81 | copy(result[offset:], e.Mki) 82 | offset += len(e.Mki) 83 | return result 84 | } 85 | 86 | func (e *ExtUseSRTP) Decode(extensionLength int, buf []byte, offset int, arrayLen int) error { 87 | protectionProfilesLength := binary.BigEndian.Uint16(buf[offset : offset+2]) 88 | offset += 2 89 | protectionProfilesCount := protectionProfilesLength / 2 90 | e.ProtectionProfiles = make([]SRTPProtectionProfile, protectionProfilesCount) 91 | for i := 0; i < int(protectionProfilesCount); i++ { 92 | e.ProtectionProfiles[i] = SRTPProtectionProfile(binary.BigEndian.Uint16(buf[offset : offset+2])) 93 | offset += 2 94 | } 95 | mkiLength := buf[offset] 96 | offset++ 97 | 98 | e.Mki = make([]byte, mkiLength) 99 | copy(e.Mki, buf[offset:offset+int(mkiLength)]) 100 | offset += int(mkiLength) 101 | 102 | return nil 103 | } 104 | 105 | // Only Uncompressed was implemented. 106 | // See for further Elliptic Curve Point Format types: https://www.rfc-editor.org/rfc/rfc8422.html#section-5.1.2 107 | type ExtSupportedPointFormats struct { 108 | PointFormats []PointFormat 109 | } 110 | 111 | func (e *ExtSupportedPointFormats) String() string { 112 | return fmt.Sprintf("[SupportedPointFormats] Point Formats: %s", fmt.Sprint(e.PointFormats)) 113 | } 114 | 115 | func (e *ExtSupportedPointFormats) ExtensionType() ExtensionType { 116 | return ExtensionTypeSupportedPointFormats 117 | } 118 | 119 | func (e *ExtSupportedPointFormats) Encode() []byte { 120 | result := make([]byte, 1+(len(e.PointFormats))) 121 | offset := 0 122 | result[offset] = byte(len(e.PointFormats)) 123 | offset++ 124 | for i := 0; i < len(e.PointFormats); i++ { 125 | result[offset] = byte(e.PointFormats[i]) 126 | offset++ 127 | } 128 | return result 129 | } 130 | 131 | func (e *ExtSupportedPointFormats) Decode(extensionLength int, buf []byte, offset int, arrayLen int) error { 132 | pointFormatsCount := buf[offset] 133 | offset++ 134 | e.PointFormats = make([]PointFormat, pointFormatsCount) 135 | for i := 0; i < int(pointFormatsCount); i++ { 136 | e.PointFormats[i] = PointFormat(buf[offset]) 137 | offset++ 138 | } 139 | 140 | return nil 141 | } 142 | 143 | // Only X25519 was implemented. 144 | // See for further NamedCurve types: https://www.rfc-editor.org/rfc/rfc8422.html#section-5.1.1 145 | type ExtSupportedEllipticCurves struct { 146 | Curves []Curve 147 | } 148 | 149 | func (e *ExtSupportedEllipticCurves) String() string { 150 | curvesStr := make([]string, len(e.Curves)) 151 | for i, c := range e.Curves { 152 | curvesStr[i] = c.String() 153 | } 154 | return common.JoinSlice("\n", false, 155 | "[SupportedEllipticCurves]", 156 | common.ProcessIndent("Curves:", "+", curvesStr), 157 | ) 158 | } 159 | 160 | func (e *ExtSupportedEllipticCurves) ExtensionType() ExtensionType { 161 | return ExtensionTypeSupportedEllipticCurves 162 | } 163 | 164 | func (e *ExtSupportedEllipticCurves) Encode() []byte { 165 | result := make([]byte, 1+(len(e.Curves)*2)) 166 | offset := 0 167 | binary.BigEndian.PutUint16(result[offset:], uint16(len(e.Curves))) 168 | offset += 2 169 | for i := 0; i < len(e.Curves); i++ { 170 | binary.BigEndian.PutUint16(result[offset:], uint16(e.Curves[i])) 171 | offset += 2 172 | } 173 | return result 174 | } 175 | 176 | func (e *ExtSupportedEllipticCurves) Decode(extensionLength int, buf []byte, offset int, arrayLen int) error { 177 | curvesLength := binary.BigEndian.Uint16(buf[offset:]) 178 | offset += 2 179 | curvesCount := curvesLength / 2 180 | e.Curves = make([]Curve, curvesCount) 181 | for i := 0; i < int(curvesCount); i++ { 182 | e.Curves[i] = Curve(binary.BigEndian.Uint16(buf[offset:])) 183 | offset += 2 184 | } 185 | 186 | return nil 187 | } 188 | 189 | // ExtUnknown is not for processing. It is only for debugging purposes. 190 | type ExtUnknown struct { 191 | Type ExtensionType 192 | DataLength uint16 193 | } 194 | 195 | func (e *ExtUnknown) String() string { 196 | return fmt.Sprintf("[Unknown Extension Type] Ext Type: %d, Data: %d bytes", e.Type, e.DataLength) 197 | } 198 | 199 | func (e *ExtUnknown) ExtensionType() ExtensionType { 200 | return 65535 // An invalid value 201 | } 202 | 203 | func (e *ExtUnknown) Encode() []byte { 204 | panic(errors.New("ExtUnknown cannot be encoded, it's readonly")) 205 | } 206 | 207 | func (e *ExtUnknown) Decode(extensionLength int, buf []byte, offset int, arrayLen int) error { 208 | return nil 209 | } 210 | -------------------------------------------------------------------------------- /backend/src/logging/logging.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var ( 12 | blacklist = map[string]string{} 13 | 14 | protocolPrefixColor = color.New(color.FgWhite, color.BgBlue).SprintfFunc() 15 | underlinePrefixColor = color.New(color.Underline).SprintFunc() 16 | 17 | freeLevel = NewLoggerLevel("") 18 | descLevel = NewLoggerLevel("DESCRIPTION", color.FgGreen) 19 | infoLevel = NewLoggerLevel("INFO", color.FgHiBlue) 20 | warningLevel = NewLoggerLevel("WARNING", color.FgYellow) 21 | errorLevel = NewLoggerLevel("ERROR", color.FgRed) 22 | 23 | Freef = freeLevel.Printf 24 | Descf = descLevel.Printf 25 | Infof = infoLevel.Printf 26 | Warningf = warningLevel.Printf 27 | Errorf = errorLevel.Printf 28 | ) 29 | 30 | const ( 31 | ProtoAPP = "APP" 32 | ProtoHTTP = "HTTP" 33 | ProtoWS = "WS" 34 | ProtoSDP = "SDP" 35 | ProtoCRYPTO = "CRYPTO" 36 | ProtoUDP = "UDP" 37 | ProtoSTUN = "STUN" 38 | ProtoDTLS = "DTLS" 39 | ProtoRTP = "RTP" 40 | ProtoSRTP = "SRTP" 41 | ProtoRTCP = "RTCP" 42 | ProtoVP8 = "VP8" 43 | ) 44 | 45 | type ColorFunc func(format string, v ...interface{}) string 46 | 47 | type LoggerLevel struct { 48 | logLevelPrefix string 49 | colorFunc ColorFunc 50 | } 51 | 52 | func NewLoggerLevel(logLevelPrefix string, colorAttributes ...color.Attribute) *LoggerLevel { 53 | //Color module should be enabled, if you don't this, color module doesn't act as expected. 54 | color.NoColor = false 55 | return &LoggerLevel{ 56 | logLevelPrefix: logLevelPrefix, 57 | colorFunc: color.New(colorAttributes...).SprintfFunc(), 58 | } 59 | } 60 | 61 | func (l *LoggerLevel) processString(s string, colorFunc ColorFunc) string { 62 | startTag := "" 63 | endTag := "" 64 | for startIdx := strings.Index(s, startTag); startIdx > -1; startIdx = strings.Index(s, startTag) { 65 | endIdx := strings.Index(s, endTag) 66 | if endIdx < startIdx { 67 | //format = format[:startIdx] + format[startIdx+len(startTag):] 68 | panic(fmt.Errorf("format string is invalid: proper %s not found in: %s", endTag, s)) 69 | } else { 70 | tagBody := s[startIdx+len(startTag) : endIdx] 71 | //We should recall colorFunc after application of another color function. Because underlinePrefixColor resets all formatting syntax. 72 | s = s[:startIdx] + underlinePrefixColor(tagBody) + colorFunc("%s", s[endIdx+len(endTag):]) 73 | } 74 | } 75 | return s 76 | } 77 | 78 | func (l *LoggerLevel) printNow() string { 79 | now := time.Now() // get this early. 80 | year, month, day := now.Date() 81 | hour, min, sec := now.Clock() 82 | return fmt.Sprintf("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, min, sec) 83 | } 84 | 85 | func (l *LoggerLevel) Printf(protocolPrefix string, format string, v ...interface{}) { 86 | timeText := l.printNow() 87 | protocolText := "" 88 | if protocolPrefix != "" { 89 | protocolText = protocolPrefixColor("[%s]", protocolPrefix) 90 | for i := len(protocolPrefix); i < 6; i++ { 91 | protocolText = protocolText + " " 92 | } 93 | } 94 | bodyText := fmt.Sprintf(format, v...) 95 | 96 | for searchFor, replaceWith := range blacklist { 97 | bodyText = strings.ReplaceAll(bodyText, searchFor, replaceWith) 98 | } 99 | 100 | if l.logLevelPrefix != "" { 101 | bodyText = l.colorFunc("[%s] %s\n", l.logLevelPrefix, bodyText) 102 | } else { 103 | bodyText = l.colorFunc("%s\n", bodyText) 104 | } 105 | bodyText = l.processString(bodyText, l.colorFunc) 106 | fmt.Printf("%s %s %s", timeText, protocolText, bodyText) 107 | } 108 | 109 | func LineSpacer(lineCount int) { 110 | s := "" 111 | for i := 0; i < lineCount; i++ { 112 | s = s + "\n" 113 | } 114 | fmt.Print(s) 115 | } 116 | 117 | func AddToBlacklist(searchFor string, replaceWith string) { 118 | blacklist[searchFor] = replaceWith 119 | } 120 | -------------------------------------------------------------------------------- /backend/src/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Gorilla WebSocket 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 | package main 6 | 7 | import ( 8 | "fmt" 9 | "sync" 10 | "time" 11 | 12 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/agent" 13 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/common" 14 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference" 15 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/config" 16 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/dtls" 17 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 18 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/signaling" 19 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/stun" 20 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/udp" 21 | ) 22 | 23 | var ( 24 | conferenceManager *conference.ConferenceManager 25 | ) 26 | 27 | func main() { 28 | //See: https://codewithyury.com/golang-wait-for-all-goroutines-to-finish/ 29 | //See: https://www.geeksforgeeks.org/using-waitgroup-in-golang/ 30 | waitGroup := new(sync.WaitGroup) 31 | 32 | logging.Freef("", "Welcome to WebRTC Nuts and Bolts!") 33 | logging.Freef("", "=================================") 34 | logging.Freef("", "You can trace these logs to understand the WebRTC processes and flows.") 35 | logging.LineSpacer(3) 36 | 37 | logging.Infof(logging.ProtoAPP, "Reading configuration file...") 38 | config.Load() 39 | 40 | if config.Val.Server.UDP.DockerHostIp != "" && config.Val.Server.MaskIpOnConsole { 41 | logging.AddToBlacklist(config.Val.Server.UDP.DockerHostIp, common.MaskIPString(config.Val.Server.UDP.DockerHostIp)) 42 | } 43 | 44 | logging.Descf(logging.ProtoAPP, "Configuration content:\n%s", config.ToString()) 45 | 46 | dtls.Init() 47 | 48 | discoveredServerIPs := discoverServerIPs() 49 | 50 | if config.Val.Server.MaskIpOnConsole { 51 | for _, ip := range discoveredServerIPs { 52 | logging.AddToBlacklist(ip, common.MaskIPString(ip)) 53 | } 54 | } 55 | 56 | logging.Infof(logging.ProtoAPP, "Discovered IPs: [%s]", common.JoinSlice(", ", false, discoveredServerIPs...)) 57 | logging.Descf(logging.ProtoAPP, "We looked to network device interfaces for IP addresses, and also asked \"what is my WAN IP?\" to the specified STUN server, via STUN protocol. Additionally, if defined, we add statically configured IP to the list. We use these IPs to create local ICE candidates (to say remote peers \"hey, I'm open to the network by these addresses and ports, maybe you can contact me by one of these IP-port pairs, I hope you can achieve with one of them.\").") 58 | 59 | conferenceManager = conference.NewConferenceManager(discoveredServerIPs, config.Val.Server.UDP.SinglePort) 60 | waitGroup.Add(1) 61 | go conferenceManager.Run(waitGroup) 62 | 63 | var udpListener = udp.NewUdpListener("0.0.0.0", config.Val.Server.UDP.SinglePort, conferenceManager) 64 | waitGroup.Add(1) 65 | go udpListener.Run(waitGroup) 66 | 67 | httpServer, err := signaling.NewHttpServer(fmt.Sprintf(":%d", config.Val.Server.Signaling.WsPort), conferenceManager) 68 | if err != nil { 69 | logging.Errorf(logging.ProtoAPP, "Http Server error: %s", err) 70 | } 71 | waitGroup.Add(1) 72 | go httpServer.Run(waitGroup) 73 | 74 | //We can run in an idle loop with calling last (httpServer's) Run function without go routine, but we want to see the sync.WaitGroup in action. 75 | time.Sleep(1 * time.Second) 76 | logging.Infof(logging.ProtoAPP, "Server components started...") 77 | logging.LineSpacer(2) 78 | waitGroup.Wait() 79 | } 80 | 81 | func discoverServerIPs() []string { 82 | localIPs := common.GetLocalIPs() 83 | result := []string{} 84 | result = append(result, localIPs...) 85 | 86 | if config.Val.Server.MaskIpOnConsole { 87 | for _, ip := range result { 88 | logging.AddToBlacklist(ip, common.MaskIPString(ip)) 89 | } 90 | } 91 | 92 | logging.Infof(logging.ProtoAPP, "Discovered Local IPs: [%s]", common.JoinSlice(", ", false, result...)) 93 | 94 | if config.Val.Server.UDP.DockerHostIp != "" { 95 | result = append(result, config.Val.Server.UDP.DockerHostIp) 96 | logging.Infof(logging.ProtoAPP, "Added configured IP statically (not discovered): %s", config.Val.Server.UDP.DockerHostIp) 97 | 98 | } 99 | 100 | logging.Infof(logging.ProtoAPP, "Creating STUN Client...") 101 | 102 | stunClientUfrag := agent.GenerateICEUfrag() 103 | stunClientPwd := agent.GenerateICEPwd() 104 | stunClient := stun.NewStunClient(config.Val.Server.StunServerAddr, stunClientUfrag, stunClientPwd) 105 | mappedAddress, err := stunClient.Discover() 106 | if err != nil { 107 | logging.Errorf(logging.ProtoAPP, "[STUN] Discovery error: %s", err) 108 | return result 109 | } 110 | externalIP := mappedAddress.IP.To4().String() 111 | if config.Val.Server.MaskIpOnConsole { 112 | logging.AddToBlacklist(externalIP, common.MaskIPString(externalIP)) 113 | } 114 | logging.Infof(logging.ProtoAPP, "Discovered external IP from STUN server (%s) as %s", stunClient.ServerAddr, externalIP) 115 | 116 | result = append(result, externalIP) 117 | return result 118 | } 119 | -------------------------------------------------------------------------------- /backend/src/rtcp/header.go: -------------------------------------------------------------------------------- 1 | package rtcp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type PacketType byte 9 | 10 | const ( 11 | // https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml 12 | PayloadTypeFIR PacketType = 192 13 | PayloadTypeNACK PacketType = 193 14 | PayloadTypeSenderReport PacketType = 200 15 | PayloadTypeReceiverReport PacketType = 201 16 | PayloadTypeSourceDescription PacketType = 202 17 | PayloadTypeGoodbye PacketType = 203 18 | PayloadTypeApplicationDefined PacketType = 204 19 | PayloadTypeGenericRTPFeedback PacketType = 205 20 | PayloadTypePayloadSpecific PacketType = 206 21 | PayloadTypeExtendedReport PacketType = 207 22 | PayloadTypeAVBRTCPPacket PacketType = 208 23 | ) 24 | 25 | type Header struct { 26 | Version byte 27 | Padding bool 28 | ReceptionReportCount byte 29 | PacketType PacketType 30 | Length uint16 31 | } 32 | 33 | /* 34 | 0 1 2 3 35 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 36 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 37 | |V=2|P| RC | PT | length | 38 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 39 | | Payload | 40 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 41 | */ 42 | 43 | func IsRtcpPacket(buf []byte, offset int, arrayLen int) bool { 44 | // https://csperkins.org/standards/ietf-67/2006-11-07-IETF67-AVT-rtp-rtcp-mux.pdf 45 | // Initial segment of RTCP header; 8 bit packet 46 | // type; values 192, 193, 200...208 used 47 | payloadType := buf[offset+1] 48 | return (payloadType >= 192 && payloadType <= 193) || (payloadType >= 200 && payloadType <= 208) 49 | } 50 | 51 | func DecodeHeader(buf []byte, offset int, arrayLen int) (*Header, int, error) { 52 | result := new(Header) 53 | firstByte := buf[offset] 54 | offset++ 55 | result.Version = firstByte & 0b11000000 >> 6 56 | result.Padding = (firstByte & 0b00100000 >> 5) == 1 57 | result.ReceptionReportCount = firstByte & 0b00011111 58 | 59 | result.PacketType = PacketType(buf[offset]) 60 | offset++ 61 | 62 | result.Length = binary.BigEndian.Uint16(buf[offset : offset+2]) 63 | offset += 2 64 | 65 | return result, offset, nil 66 | } 67 | 68 | func (pt PacketType) String() string { 69 | var result string 70 | switch pt { 71 | case PayloadTypeFIR: 72 | result = "FIR" 73 | case PayloadTypeNACK: 74 | result = "NACK" 75 | case PayloadTypeSenderReport: 76 | result = "SenderReport" 77 | case PayloadTypeReceiverReport: 78 | result = "ReceiverReport" 79 | case PayloadTypeSourceDescription: 80 | result = "SourceDescription" 81 | case PayloadTypeGoodbye: 82 | result = "Goodbye" 83 | case PayloadTypeApplicationDefined: 84 | result = "ApplicationDefined" 85 | case PayloadTypeGenericRTPFeedback: 86 | result = "GenericRTPFeedback" 87 | case PayloadTypePayloadSpecific: 88 | result = "PayloadSpecific" 89 | case PayloadTypeExtendedReport: 90 | result = "ExtendedReport" 91 | case PayloadTypeAVBRTCPPacket: 92 | result = "AVBRTCPPacket" 93 | default: 94 | result = "Unknown" 95 | } 96 | return fmt.Sprintf("%s (%d)", result, pt) 97 | } 98 | -------------------------------------------------------------------------------- /backend/src/rtcp/packet.go: -------------------------------------------------------------------------------- 1 | package rtcp 2 | 3 | import "fmt" 4 | 5 | type Packet struct { 6 | Header *Header 7 | Payload []byte 8 | } 9 | 10 | func DecodePacket(buf []byte, offset int, arrayLen int) (*Packet, int, error) { 11 | result := new(Packet) 12 | var err error 13 | result.Header, offset, err = DecodeHeader(buf, offset, arrayLen) 14 | if err != nil { 15 | return nil, offset, err 16 | } 17 | // Passed decoding payload 18 | offset += int(result.Header.Length) 19 | return result, offset, nil 20 | } 21 | 22 | func (p *Packet) String() string { 23 | return fmt.Sprintf("Version: %d, Packet Type: %s, ReceptionReportCount: %d, Payload Length: %d", 24 | p.Header.Version, p.Header.PacketType, p.Header.ReceptionReportCount, p.Header.Length) 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/rtp/header.go: -------------------------------------------------------------------------------- 1 | package rtp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | type PayloadType byte 9 | 10 | const ( 11 | PayloadTypeVP8 PayloadType = 96 12 | PayloadTypeOpus PayloadType = 109 13 | ) 14 | 15 | type Header struct { 16 | Version byte 17 | Padding bool 18 | Extension bool 19 | Marker bool 20 | PayloadType PayloadType 21 | SequenceNumber uint16 22 | Timestamp uint32 23 | SSRC uint32 24 | CSRC []uint32 25 | ExtensionProfile uint16 26 | Extensions []Extension 27 | 28 | RawData []byte 29 | } 30 | 31 | type Extension struct { 32 | Id byte 33 | Payload []byte 34 | } 35 | 36 | /* 37 | 0 1 2 3 38 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 39 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 40 | |V=2|P|X| CC |M| PT | Sequence Number | 41 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 42 | | Timestamp | 43 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 44 | | Synchronization Source (SSRC) identifier | 45 | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ 46 | | Contributing Source (CSRC) identifiers | 47 | | .... | 48 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 49 | | Payload | 50 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 51 | */ 52 | 53 | func IsRtpPacket(buf []byte, offset int, arrayLen int) bool { 54 | // https://csperkins.org/standards/ietf-67/2006-11-07-IETF67-AVT-rtp-rtcp-mux.pdf 55 | // Initial segment of RTP header; 7 bit payload 56 | // type; values 0...35 and 96...127 usually used 57 | payloadType := buf[offset+1] & 0b01111111 58 | return (payloadType <= 35) || (payloadType >= 96 && payloadType <= 127) 59 | } 60 | 61 | func DecodeHeader(buf []byte, offset int, arrayLen int) (*Header, int, error) { 62 | result := new(Header) 63 | offsetBackup := offset 64 | firstByte := buf[offset] 65 | offset++ 66 | result.Version = firstByte & 0b11000000 >> 6 67 | result.Padding = (firstByte & 0b00100000 >> 5) == 1 68 | result.Extension = (firstByte & 0b00010000 >> 4) == 1 69 | csrcCount := firstByte & 0b00001111 70 | 71 | secondByte := buf[offset] 72 | offset++ 73 | result.Marker = (secondByte & 0b10000000 >> 7) == 1 74 | result.PayloadType = PayloadType(secondByte & 0b01111111) 75 | 76 | result.SequenceNumber = binary.BigEndian.Uint16(buf[offset : offset+2]) 77 | offset += 2 78 | result.Timestamp = binary.BigEndian.Uint32(buf[offset : offset+4]) 79 | offset += 4 80 | result.SSRC = binary.BigEndian.Uint32(buf[offset : offset+4]) 81 | offset += 4 82 | 83 | result.CSRC = make([]uint32, csrcCount) 84 | for i := 0; i < int(csrcCount); i++ { 85 | result.CSRC[i] = binary.BigEndian.Uint32(buf[offset : offset+4]) 86 | offset += 4 87 | } 88 | result.RawData = buf[offsetBackup:offset] 89 | return result, offset, nil 90 | } 91 | 92 | func (pt PayloadType) String() string { 93 | result := pt.CodecName() 94 | return fmt.Sprintf("%s (%d)", result, pt) 95 | } 96 | 97 | func (pt PayloadType) CodecCodeNumber() string { 98 | return fmt.Sprintf("%d", int(pt)) 99 | } 100 | 101 | func (pt PayloadType) CodecName() string { 102 | var result string 103 | switch pt { 104 | case PayloadTypeVP8: 105 | result = "VP8/90000" 106 | case PayloadTypeOpus: 107 | result = "OPUS/48000/2" 108 | default: 109 | result = "Unknown" 110 | } 111 | return result 112 | } 113 | -------------------------------------------------------------------------------- /backend/src/rtp/packet.go: -------------------------------------------------------------------------------- 1 | package rtp 2 | 3 | import "fmt" 4 | 5 | type Packet struct { 6 | Header *Header 7 | HeaderSize int 8 | Payload []byte 9 | RawData []byte 10 | } 11 | 12 | func DecodePacket(buf []byte, offset int, arrayLen int) (*Packet, int, error) { 13 | result := new(Packet) 14 | result.RawData = append([]byte{}, buf[offset:offset+arrayLen]...) 15 | var err error 16 | offsetBackup := offset 17 | result.Header, offset, err = DecodeHeader(buf, offset, arrayLen) 18 | if err != nil { 19 | return nil, offset, err 20 | } 21 | result.HeaderSize = offset - offsetBackup 22 | lastPosition := arrayLen - 1 23 | if result.Header.Padding { 24 | paddingSize := buf[arrayLen-1] 25 | lastPosition = arrayLen - 1 - int(paddingSize) 26 | } 27 | result.Payload = buf[offset:lastPosition] 28 | return result, offset, nil 29 | } 30 | 31 | func (p *Packet) String() string { 32 | return fmt.Sprintf("RTP Version: %d, SSRC: %d, Payload Type: %s, Seq Number: %d, CSRC Count: %d, Payload Length: %d Marker: %v", 33 | p.Header.Version, p.Header.SSRC, p.Header.PayloadType, p.Header.SequenceNumber, len(p.Header.CSRC), len(p.Payload), p.Header.Marker) 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/sdp/sdp.go: -------------------------------------------------------------------------------- 1 | package sdp 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/agent" 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/common" 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/config" 9 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 10 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp" 11 | ) 12 | 13 | type MediaType string 14 | 15 | const ( 16 | MediaTypeVideo MediaType = "video" 17 | MediaTypeAudio MediaType = "audio" 18 | ) 19 | 20 | type CandidateType string 21 | 22 | const ( 23 | CandidateTypeHost CandidateType = "host" 24 | ) 25 | 26 | type TransportType string 27 | 28 | const ( 29 | TransportTypeUdp TransportType = "udp" 30 | TransportTypeTcp TransportType = "tcp" 31 | ) 32 | 33 | type FingerprintType string 34 | 35 | const ( 36 | FingerprintTypeSHA256 FingerprintType = "sha-256" 37 | ) 38 | 39 | type SdpMessage struct { 40 | ConferenceName string 41 | SessionID string `json:"sessionId"` 42 | MediaItems []SdpMedia `json:"mediaItems"` 43 | } 44 | 45 | type SdpMedia struct { 46 | MediaId int `json:"mediaId"` 47 | Type MediaType `json:"type"` 48 | Ufrag string `json:"ufrag"` 49 | Pwd string `json:"pwd"` 50 | FingerprintType FingerprintType `json:"fingerprintType"` 51 | FingerprintHash string `json:"fingerprintHash"` 52 | Candidates []SdpMediaCandidate `json:"candidates"` 53 | Payloads string `json:"payloads"` 54 | RTPCodec string `json:"rtpCodec"` 55 | } 56 | 57 | type SdpMediaCandidate struct { 58 | Ip string `json:"ip"` 59 | Port int `json:"port"` 60 | Type CandidateType `json:"type"` 61 | Transport TransportType `json:"transport"` 62 | } 63 | 64 | func ParseSdpOfferAnswer(offer map[string]interface{}) *SdpMessage { 65 | sdpMessage := &SdpMessage{} 66 | sdpMessage.SessionID = offer["origin"].(map[string]interface{})["sessionId"].(string) 67 | 68 | mediaItems := offer["media"].([]interface{}) 69 | 70 | for _, mediaItemObj := range mediaItems { 71 | sdpMedia := SdpMedia{} 72 | mediaItem := mediaItemObj.(map[string]interface{}) 73 | //mediaId := mediaItem["mid"].(float64) 74 | sdpMedia.Type = MediaType(mediaItem["type"].(string)) 75 | candidates := make([]interface{}, 0) 76 | if mediaItem["candidates"] != nil { 77 | candidates = mediaItem["candidates"].([]interface{}) 78 | } 79 | sdpMedia.Ufrag = mediaItem["iceUfrag"].(string) 80 | sdpMedia.Pwd = mediaItem["icePwd"].(string) 81 | //iceOptions := mediaItem["iceOptions"].(string) 82 | fingerprintRaw, ok := mediaItem["fingerprint"] 83 | if !ok { 84 | fingerprintRaw = offer["fingerprint"] 85 | } 86 | fingerprint := fingerprintRaw.(map[string]interface{}) 87 | sdpMedia.FingerprintType = FingerprintType(fingerprint["type"].(string)) 88 | sdpMedia.FingerprintHash = fingerprint["hash"].(string) 89 | //direction := mediaItem["direction"].(string) 90 | for _, candidateObj := range candidates { 91 | sdpMediaCandidate := SdpMediaCandidate{} 92 | candidate := candidateObj.(map[string]interface{}) 93 | //foundation := candidate["foundation"].(float64) 94 | sdpMediaCandidate.Type = CandidateType(candidate["type"].(string)) 95 | sdpMediaCandidate.Transport = TransportType(candidate["transport"].(string)) 96 | sdpMediaCandidate.Ip = candidate["ip"].(string) 97 | sdpMediaCandidate.Port = int(candidate["port"].(float64)) 98 | sdpMedia.Candidates = append(sdpMedia.Candidates, sdpMediaCandidate) 99 | } 100 | sdpMessage.MediaItems = append(sdpMessage.MediaItems, sdpMedia) 101 | } 102 | logging.Descf(logging.ProtoSDP, "It seems the client has received our SDP Offer, processed it, accepted it, initialized it's media devices (webcam, microphone, etc...), started it's UDP listener, and sent us this SDP Answer. In this project, we don't use the client's candidates, because we has implemented only receiver functionalities, so we don't have any media stream to send :)") 103 | logging.Infof(logging.ProtoSDP, "Processing Incoming SDP: %s", sdpMessage) 104 | logging.LineSpacer(2) 105 | return sdpMessage 106 | } 107 | 108 | func GenerateSdpOffer(iceAgent *agent.ServerAgent) *SdpMessage { 109 | candidates := []SdpMediaCandidate{} 110 | for _, agentCandidate := range iceAgent.IceCandidates { 111 | candidates = append(candidates, SdpMediaCandidate{ 112 | Ip: agentCandidate.Ip, 113 | Port: agentCandidate.Port, 114 | Type: "host", 115 | Transport: TransportTypeUdp, 116 | }) 117 | } 118 | offer := &SdpMessage{ 119 | SessionID: "1234", 120 | MediaItems: []SdpMedia{ 121 | { 122 | MediaId: 0, 123 | Type: MediaTypeVideo, 124 | Payloads: rtp.PayloadTypeVP8.CodecCodeNumber(), //96 125 | RTPCodec: rtp.PayloadTypeVP8.CodecName(), //VP8/90000 126 | Ufrag: iceAgent.Ufrag, 127 | Pwd: iceAgent.Pwd, 128 | /* 129 | https://webrtcforthecurious.com/docs/04-securing/ 130 | Certificate # 131 | Certificate contains the certificate for the Client or Server. 132 | This is used to uniquely identify who we were communicating with. 133 | After the handshake is over we will make sure this certificate 134 | when hashed matches the fingerprint in the SessionDescription. 135 | */ 136 | FingerprintType: FingerprintTypeSHA256, 137 | FingerprintHash: iceAgent.FingerprintHash, 138 | Candidates: candidates, 139 | }, 140 | }, 141 | } 142 | if config.Val.Server.RequestAudio { 143 | offer.MediaItems = append(offer.MediaItems, SdpMedia{ 144 | MediaId: 1, 145 | Type: MediaTypeAudio, 146 | Payloads: rtp.PayloadTypeOpus.CodecCodeNumber(), //109 147 | RTPCodec: rtp.PayloadTypeOpus.CodecName(), //OPUS/48000/2 148 | Ufrag: iceAgent.Ufrag, 149 | Pwd: iceAgent.Pwd, 150 | /* 151 | https://webrtcforthecurious.com/docs/04-securing/ 152 | Certificate # 153 | Certificate contains the certificate for the Client or Server. 154 | This is used to uniquely identify who we were communicating with. 155 | After the handshake is over we will make sure this certificate 156 | when hashed matches the fingerprint in the SessionDescription. 157 | */ 158 | FingerprintType: FingerprintTypeSHA256, 159 | FingerprintHash: iceAgent.FingerprintHash, 160 | Candidates: candidates, 161 | }) 162 | } 163 | return offer 164 | } 165 | 166 | func (m *SdpMessage) String() string { 167 | mediaItemsStr := make([]string, len(m.MediaItems)) 168 | i := 0 169 | for _, media := range m.MediaItems { 170 | mediaItemsStr[i] = fmt.Sprintf("[SdpMedia] %s", media) 171 | i++ 172 | } 173 | 174 | return common.JoinSlice("\n", false, 175 | fmt.Sprintf("SessionID: %s", m.SessionID), 176 | common.ProcessIndent("MediaItems:", "+", mediaItemsStr), 177 | ) 178 | } 179 | 180 | func (m SdpMedia) String() string { 181 | candidatesStr := make([]string, len(m.Candidates)) 182 | i := 0 183 | for _, candidate := range m.Candidates { 184 | candidatesStr[i] = fmt.Sprintf("[SdpCandidate] %s", candidate) 185 | i++ 186 | } 187 | 188 | return common.JoinSlice("\n", false, 189 | common.ProcessIndent(fmt.Sprintf("MediaId: %d, Type: %s, Ufrag: %s, Pwd: %s", m.MediaId, m.Type, m.Ufrag, m.Pwd), "", []string{ 190 | fmt.Sprintf("FingerprintType: %s, FingerprintHash: %s", m.FingerprintType, m.FingerprintHash), 191 | common.ProcessIndent("Candidates:", "+", candidatesStr), 192 | })) 193 | } 194 | 195 | func (m SdpMediaCandidate) String() string { 196 | return fmt.Sprintf("Type: %s, Transport: %s, Ip: %s, Port: %d", m.Type, m.Transport, m.Ip, m.Port) 197 | } 198 | -------------------------------------------------------------------------------- /backend/src/signaling/httpserver.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference" 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 9 | ) 10 | 11 | type HttpServer struct { 12 | HttpServerAddr string 13 | WsHub *WsHub 14 | } 15 | 16 | func NewHttpServer(httpServerAddr string, conferenceManager *conference.ConferenceManager) (*HttpServer, error) { 17 | wsHub := newWsHub(conferenceManager) 18 | 19 | httpServer := &HttpServer{ 20 | HttpServerAddr: httpServerAddr, 21 | WsHub: wsHub, 22 | } 23 | http.HandleFunc("/", httpServer.serveHome) 24 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 25 | httpServer.serveWs(w, r) 26 | }) 27 | 28 | return httpServer, nil 29 | } 30 | 31 | func (s *HttpServer) Run(waitGroup *sync.WaitGroup) { 32 | defer waitGroup.Done() 33 | go s.WsHub.run() 34 | logging.Infof(logging.ProtoWS, "WebSocket Server started on %s", s.HttpServerAddr) 35 | logging.Descf(logging.ProtoWS, "Clients should make first contact with this WebSocket (the Signaling part)") 36 | err := http.ListenAndServe(s.HttpServerAddr, nil) 37 | if err != nil { 38 | panic(err) 39 | } 40 | } 41 | 42 | func (s *HttpServer) serveHome(w http.ResponseWriter, r *http.Request) { 43 | logging.Infof(logging.ProtoHTTP, "Request: %s", r.URL) 44 | if r.URL.Path != "/" { 45 | http.Error(w, "Not found", http.StatusNotFound) 46 | return 47 | } 48 | if r.Method != "GET" { 49 | http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 50 | return 51 | } 52 | http.ServeFile(w, r, "home.html") 53 | } 54 | 55 | // serveWs handles websocket requests from the peer. 56 | func (s *HttpServer) serveWs(w http.ResponseWriter, r *http.Request) { 57 | upgrader.CheckOrigin = func(r *http.Request) bool { return true } 58 | conn, err := upgrader.Upgrade(w, r, nil) 59 | if err != nil { 60 | logging.Errorf(logging.ProtoHTTP, "Error: %s", err) 61 | return 62 | } 63 | client := &WsClient{wsHub: s.WsHub, conn: conn, send: make(chan []byte, 256)} 64 | client.wsHub.register <- client 65 | 66 | // Allow collection of memory referenced by the caller by doing all work in 67 | // new goroutines. 68 | go client.writePump() 69 | go client.readPump() 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/signaling/joinconferencedata.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | type JoinConferenceData struct { 4 | ConferenceName string 5 | WsClient WsClient 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/signaling/wsclient.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "bytes" 5 | "time" 6 | 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference" 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | // See: https://github.com/gorilla/websocket/tree/master/examples/chat 13 | 14 | const ( 15 | // Time allowed to write a message to the peer. 16 | writeWait = 10 * time.Second 17 | 18 | // Time allowed to read the next pong message from the peer. 19 | pongWait = 60 * time.Second 20 | 21 | // Send pings to peer with this period. Must be less than pongWait. 22 | pingPeriod = (pongWait * 9) / 10 23 | 24 | // Maximum message size allowed from peer. 25 | maxMessageSize = 81920 26 | ) 27 | 28 | var ( 29 | newline = []byte{'\n'} 30 | space = []byte{' '} 31 | ) 32 | 33 | var upgrader = websocket.Upgrader{ 34 | ReadBufferSize: 1024, 35 | WriteBufferSize: 1024, 36 | } 37 | 38 | // Client is a middleman between the websocket connection and the hub. 39 | type WsClient struct { 40 | id int 41 | 42 | wsHub *WsHub 43 | 44 | // The websocket connection. 45 | conn *websocket.Conn 46 | 47 | // Buffered channel of outbound messages. 48 | send chan []byte 49 | 50 | conference *conference.Conference 51 | } 52 | 53 | type ReceivedMessage struct { 54 | Sender *WsClient 55 | Message []byte 56 | } 57 | 58 | type ClientWelcomeMessage struct { 59 | Id int `json:"id"` 60 | Message string `json:"message"` 61 | } 62 | 63 | type ClientErrorMessage struct { 64 | ErrorCode string `json:"errorCode"` 65 | Message string `json:"message"` 66 | } 67 | 68 | // readPump pumps messages from the websocket connection to the hub. 69 | // 70 | // The application runs readPump in a per-connection goroutine. The application 71 | // ensures that there is at most one reader on a connection by executing all 72 | // reads from this goroutine. 73 | func (c *WsClient) readPump() { 74 | defer func() { 75 | c.wsHub.unregister <- c 76 | c.conn.Close() 77 | }() 78 | c.conn.SetReadLimit(maxMessageSize) 79 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 80 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 81 | for { 82 | _, message, err := c.conn.ReadMessage() 83 | if err != nil { 84 | logging.Errorf(logging.ProtoWS, "Receive error: %s", err) 85 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 86 | logging.Errorf(logging.ProtoWS, "Unexpected error: %v", err) 87 | } else { 88 | c.conn.WriteJSON(ClientErrorMessage{ 89 | ErrorCode: "receive-error", 90 | Message: err.Error(), 91 | }) 92 | } 93 | break 94 | } 95 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 96 | 97 | //c.hub.broadcast <- message 98 | c.wsHub.messageReceived <- &ReceivedMessage{ 99 | Sender: c, 100 | Message: message, 101 | } 102 | } 103 | } 104 | 105 | // writePump pumps messages from the hub to the websocket connection. 106 | // 107 | // A goroutine running writePump is started for each connection. The 108 | // application ensures that there is at most one writer to a connection by 109 | // executing all writes from this goroutine. 110 | func (c *WsClient) writePump() { 111 | ticker := time.NewTicker(pingPeriod) 112 | defer func() { 113 | ticker.Stop() 114 | c.conn.Close() 115 | }() 116 | for { 117 | select { 118 | case message, ok := <-c.send: 119 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 120 | if !ok { 121 | // The hub closed the channel. 122 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 123 | return 124 | } 125 | 126 | w, err := c.conn.NextWriter(websocket.TextMessage) 127 | if err != nil { 128 | return 129 | } 130 | w.Write(message) 131 | 132 | // Add queued chat messages to the current websocket message. 133 | n := len(c.send) 134 | for i := 0; i < n; i++ { 135 | w.Write(newline) 136 | w.Write(<-c.send) 137 | } 138 | 139 | if err := w.Close(); err != nil { 140 | return 141 | } 142 | case <-ticker.C: 143 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 144 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 145 | return 146 | } 147 | } 148 | } 149 | } 150 | 151 | func (c *WsClient) RemoteAddrStr() string { 152 | if c.conn == nil { 153 | return "" 154 | } 155 | return c.conn.RemoteAddr().String() 156 | } 157 | -------------------------------------------------------------------------------- /backend/src/signaling/wshub.go: -------------------------------------------------------------------------------- 1 | package signaling 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference" 7 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/sdp" 9 | ) 10 | 11 | // See: https://github.com/gorilla/websocket/tree/master/examples/chat 12 | 13 | // Hub maintains the set of active clients and broadcasts messages to the 14 | // clients. 15 | type WsHub struct { 16 | maxClientId int 17 | 18 | // Registered clients. 19 | clients map[*WsClient]bool 20 | 21 | // Inbound messages from the clients. 22 | messageReceived chan *ReceivedMessage 23 | 24 | broadcast chan BroadcastMessage 25 | 26 | // Register requests from the clients. 27 | register chan *WsClient 28 | 29 | // Unregister requests from clients. 30 | unregister chan *WsClient 31 | 32 | ConferenceManager *conference.ConferenceManager 33 | } 34 | 35 | type BroadcastMessage struct { 36 | Message []byte 37 | ExcludeClients []int 38 | } 39 | 40 | type MessageContainer struct { 41 | Type string `json:"type"` 42 | Data interface{} `json:"data"` 43 | } 44 | 45 | func newWsHub(conferenceManager *conference.ConferenceManager) *WsHub { 46 | return &WsHub{ 47 | messageReceived: make(chan *ReceivedMessage), 48 | broadcast: make(chan BroadcastMessage), 49 | register: make(chan *WsClient), 50 | unregister: make(chan *WsClient), 51 | clients: make(map[*WsClient]bool), 52 | ConferenceManager: conferenceManager, 53 | } 54 | } 55 | 56 | func processBroadcastMessage(h *WsHub, broadcastMessage BroadcastMessage) { 57 | for client := range h.clients { 58 | if broadcastMessage.ExcludeClients != nil { 59 | var found = false 60 | for _, item := range broadcastMessage.ExcludeClients { 61 | if item == client.id { 62 | found = true 63 | break 64 | } 65 | } 66 | if found { 67 | continue 68 | } 69 | } 70 | select { 71 | case client.send <- broadcastMessage.Message: 72 | default: 73 | close(client.send) 74 | delete(h.clients, client) 75 | } 76 | } 77 | } 78 | 79 | func writeContainerJSON(client *WsClient, messageType string, messageData interface{}) { 80 | client.conn.WriteJSON(MessageContainer{ 81 | Type: messageType, 82 | Data: messageData, 83 | }) 84 | } 85 | 86 | func (h *WsHub) run() { 87 | for { 88 | select { 89 | case client := <-h.register: 90 | h.maxClientId++ 91 | client.id = h.maxClientId 92 | h.clients[client] = true 93 | logging.Infof(logging.ProtoWS, "A new client connected: client %d (from %s)", client.id, client.conn.RemoteAddr()) 94 | logging.Descf(logging.ProtoWS, "Sending welcome message via WebSocket. The client is informed with client ID given by the signaling server.") 95 | writeContainerJSON(client, "Welcome", ClientWelcomeMessage{ 96 | Id: client.id, 97 | Message: "Welcome!", 98 | }) 99 | case client := <-h.unregister: 100 | if _, ok := h.clients[client]; ok { 101 | delete(h.clients, client) 102 | close(client.send) 103 | logging.Infof(logging.ProtoWS, "Client disconnected: client %d (from %s)", client.id, client.conn.RemoteAddr()) 104 | } 105 | case broadcastMessage := <-h.broadcast: 106 | processBroadcastMessage(h, broadcastMessage) 107 | case receivedMessage := <-h.messageReceived: 108 | var messageObj map[string]interface{} 109 | json.Unmarshal(receivedMessage.Message, &messageObj) 110 | 111 | logging.Infof(logging.ProtoWS, "Message received from client %d type %s", receivedMessage.Sender.id, messageObj["type"]) 112 | switch messageObj["type"] { 113 | case "JoinConference": 114 | h.processJoinConference(messageObj["data"].(map[string]interface{}), receivedMessage.Sender) 115 | case "SdpOfferAnswer": 116 | incomingSdpOfferAnswerMessage := sdp.ParseSdpOfferAnswer(messageObj["data"].(map[string]interface{})) 117 | incomingSdpOfferAnswerMessage.ConferenceName = receivedMessage.Sender.conference.ConferenceName 118 | h.ConferenceManager.ChanSdpOffer <- incomingSdpOfferAnswerMessage 119 | /* 120 | processBroadcastMessage(h, BroadcastMessage{ 121 | ExcludeClients: []int{receivedMessage.Sender.id}, 122 | Message: receivedMessage.Message, 123 | }) 124 | */ 125 | default: 126 | h.broadcast <- BroadcastMessage{ 127 | Message: receivedMessage.Message, 128 | } 129 | } 130 | } 131 | } 132 | } 133 | 134 | func (h *WsHub) processJoinConference(messageData map[string]interface{}, wsClient *WsClient) { 135 | conferenceName := messageData["conferenceName"].(string) 136 | logging.Descf(logging.ProtoWS, "The client %d wanted to join the conference %s.", wsClient.id, conferenceName) 137 | wsClient.conference = h.ConferenceManager.EnsureConference(conferenceName) 138 | logging.Descf(logging.ProtoWS, "The client was joined the conference. Now we should generate an SDP Offer including our UDP candidates (IP-port pairs) and send to the client via Signaling/WebSocket.") 139 | sdpMessage := sdp.GenerateSdpOffer(wsClient.conference.IceAgent) 140 | logging.Infof(logging.ProtoSDP, "Sending SDP Offer to client %d (%s) for conference %s: %s", wsClient.id, wsClient.RemoteAddrStr(), conferenceName, sdpMessage) 141 | logging.LineSpacer(2) 142 | writeContainerJSON(wsClient, "SdpOffer", sdpMessage) 143 | } 144 | -------------------------------------------------------------------------------- /backend/src/srtp/cryptogcm.go: -------------------------------------------------------------------------------- 1 | package srtp 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/binary" 7 | "errors" 8 | 9 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp" 10 | ) 11 | 12 | // https://github.com/pion/srtp/blob/e338637eb5c459e0e43daf9c88cf28dd441eeb7c/context.go#L9 13 | const ( 14 | labelSRTPEncryption = 0x00 15 | labelSRTPAuthenticationTag = 0x01 16 | labelSRTPSalt = 0x02 17 | 18 | labelSRTCPEncryption = 0x03 19 | labelSRTCPAuthenticationTag = 0x04 20 | labelSRTCPSalt = 0x05 21 | 22 | seqNumMedian = 1 << 15 23 | seqNumMax = 1 << 16 24 | ) 25 | 26 | type GCM struct { 27 | srtpGCM, srtcpGCM cipher.AEAD 28 | srtpSalt, srtcpSalt []byte 29 | } 30 | 31 | // NewGCM creates an SRTP GCM Cipher 32 | func NewGCM(masterKey, masterSalt []byte) (*GCM, error) { 33 | srtpSessionKey, err := aesCmKeyDerivation(labelSRTPEncryption, masterKey, masterSalt, 0, len(masterKey)) 34 | if err != nil { 35 | return nil, err 36 | } 37 | srtpBlock, err := aes.NewCipher(srtpSessionKey) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | srtpGCM, err := cipher.NewGCM(srtpBlock) 43 | if err != nil { 44 | return nil, err 45 | } 46 | srtcpSessionKey, err := aesCmKeyDerivation(labelSRTCPEncryption, masterKey, masterSalt, 0, len(masterKey)) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | srtcpBlock, err := aes.NewCipher(srtcpSessionKey) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | srtcpGCM, err := cipher.NewGCM(srtcpBlock) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | srtpSalt, err := aesCmKeyDerivation(labelSRTPSalt, masterKey, masterSalt, 0, len(masterSalt)) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | srtcpSalt, err := aesCmKeyDerivation(labelSRTCPSalt, masterKey, masterSalt, 0, len(masterSalt)) 67 | 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return &GCM{ 73 | srtpGCM: srtpGCM, 74 | srtpSalt: srtpSalt, 75 | srtcpGCM: srtcpGCM, 76 | srtcpSalt: srtcpSalt, 77 | }, nil 78 | } 79 | 80 | func (g *GCM) rtpInitializationVector(header *rtp.Header, roc uint32) []byte { 81 | iv := make([]byte, 12) 82 | binary.BigEndian.PutUint32(iv[2:], header.SSRC) 83 | binary.BigEndian.PutUint32(iv[6:], roc) 84 | binary.BigEndian.PutUint16(iv[10:], header.SequenceNumber) 85 | 86 | for i := range iv { 87 | iv[i] ^= g.srtpSalt[i] 88 | } 89 | return iv 90 | } 91 | 92 | func (g *GCM) Decrypt(packet *rtp.Packet, roc uint32) ([]byte, error) { 93 | ciphertext := packet.RawData 94 | 95 | dst := make([]byte, len(ciphertext)) 96 | copy(dst, ciphertext) 97 | aeadAuthTagLen := 16 98 | resultLength := len(ciphertext) - aeadAuthTagLen 99 | if resultLength < len(dst) { 100 | dst = dst[:resultLength] 101 | } 102 | 103 | iv := g.rtpInitializationVector(packet.Header, roc) 104 | 105 | if _, err := g.srtpGCM.Open( 106 | dst[packet.HeaderSize:packet.HeaderSize], iv, ciphertext[packet.HeaderSize:], ciphertext[:packet.HeaderSize], 107 | ); err != nil { 108 | return nil, err 109 | } 110 | 111 | copy(dst[:packet.HeaderSize], ciphertext[:packet.HeaderSize]) 112 | return dst, nil 113 | } 114 | 115 | // https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/key_derivation.go#L8 116 | func aesCmKeyDerivation(label byte, masterKey, masterSalt []byte, indexOverKdr int, outLen int) ([]byte, error) { 117 | if indexOverKdr != 0 { 118 | // 24-bit "index DIV kdr" must be xored to prf input. 119 | return nil, errors.New("non-zero kdr not supported") 120 | } 121 | 122 | // https://tools.ietf.org/html/rfc3711#appendix-B.3 123 | // The input block for AES-CM is generated by exclusive-oring the master salt with the 124 | // concatenation of the encryption key label 0x00 with (index DIV kdr), 125 | // - index is 'rollover count' and DIV is 'divided by' 126 | 127 | nMasterKey := len(masterKey) 128 | nMasterSalt := len(masterSalt) 129 | 130 | prfIn := make([]byte, nMasterKey) 131 | copy(prfIn[:nMasterSalt], masterSalt) 132 | 133 | prfIn[7] ^= label 134 | 135 | // The resulting value is then AES encrypted using the master key to get the cipher key. 136 | block, err := aes.NewCipher(masterKey) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | out := make([]byte, ((outLen+nMasterKey)/nMasterKey)*nMasterKey) 142 | var i uint16 143 | for n := 0; n < outLen; n += nMasterKey { 144 | binary.BigEndian.PutUint16(prfIn[nMasterKey-2:], i) 145 | block.Encrypt(out[n:n+nMasterKey], prfIn) 146 | i++ 147 | } 148 | return out[:outLen], nil 149 | } 150 | -------------------------------------------------------------------------------- /backend/src/srtp/protectionprofiles.go: -------------------------------------------------------------------------------- 1 | package srtp 2 | 3 | import "fmt" 4 | 5 | type ProtectionProfile uint16 6 | 7 | const ( 8 | ProtectionProfile_AEAD_AES_128_GCM ProtectionProfile = ProtectionProfile(0x0007) 9 | ) 10 | 11 | type EncryptionKeys struct { 12 | ServerMasterKey []byte 13 | ServerMasterSalt []byte 14 | ClientMasterKey []byte 15 | ClientMasterSalt []byte 16 | } 17 | 18 | func (p ProtectionProfile) String() string { 19 | var result string 20 | switch p { 21 | case ProtectionProfile_AEAD_AES_128_GCM: 22 | result = "SRTP_AEAD_AES_128_GCM" 23 | default: 24 | result = "Unknown SRTP Protection Profile" 25 | } 26 | return fmt.Sprintf("%s (0x%04x)", result, uint16(p)) 27 | } 28 | 29 | func (p ProtectionProfile) KeyLength() (int, error) { 30 | switch p { 31 | case ProtectionProfile_AEAD_AES_128_GCM: 32 | return 16, nil 33 | } 34 | return 0, fmt.Errorf("unknown protection profile: %d", p) 35 | } 36 | 37 | func (p ProtectionProfile) SaltLength() (int, error) { 38 | switch p { 39 | case ProtectionProfile_AEAD_AES_128_GCM: 40 | return 12, nil 41 | } 42 | return 0, fmt.Errorf("unknown protection profile: %d", p) 43 | } 44 | 45 | func (p ProtectionProfile) AeadAuthTagLength() (int, error) { 46 | switch p { 47 | case ProtectionProfile_AEAD_AES_128_GCM: 48 | return 16, nil 49 | } 50 | return 0, fmt.Errorf("unknown protection profile: %d", p) 51 | } 52 | 53 | func InitGCM(masterKey, masterSalt []byte) (*GCM, error) { 54 | gcm, err := NewGCM(masterKey, masterSalt) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return gcm, nil 59 | } 60 | -------------------------------------------------------------------------------- /backend/src/srtp/srtpcontext.go: -------------------------------------------------------------------------------- 1 | package srtp 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp" 7 | ) 8 | 9 | type SRTPContext struct { 10 | Addr *net.UDPAddr 11 | Conn *net.UDPConn 12 | ProtectionProfile ProtectionProfile 13 | GCM *GCM 14 | srtpSSRCStates map[uint32]*srtpSSRCState 15 | } 16 | 17 | type srtpSSRCState struct { 18 | ssrc uint32 19 | index uint64 20 | rolloverHasProcessed bool 21 | } 22 | 23 | // https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/context.go#L159 24 | func (c *SRTPContext) getSRTPSSRCState(ssrc uint32) *srtpSSRCState { 25 | s, ok := c.srtpSSRCStates[ssrc] 26 | if ok { 27 | return s 28 | } 29 | 30 | s = &srtpSSRCState{ 31 | ssrc: ssrc, 32 | } 33 | c.srtpSSRCStates[ssrc] = s 34 | return s 35 | } 36 | 37 | func (s *srtpSSRCState) nextRolloverCount(sequenceNumber uint16) (uint32, func()) { 38 | seq := int32(sequenceNumber) 39 | localRoc := uint32(s.index >> 16) 40 | localSeq := int32(s.index & (seqNumMax - 1)) 41 | 42 | guessRoc := localRoc 43 | var difference int32 = 0 44 | 45 | if s.rolloverHasProcessed { 46 | // When localROC is equal to 0, and entering seq-localSeq > seqNumMedian 47 | // judgment, it will cause guessRoc calculation error 48 | if s.index > seqNumMedian { 49 | if localSeq < seqNumMedian { 50 | if seq-localSeq > seqNumMedian { 51 | guessRoc = localRoc - 1 52 | difference = seq - localSeq - seqNumMax 53 | } else { 54 | guessRoc = localRoc 55 | difference = seq - localSeq 56 | } 57 | } else { 58 | if localSeq-seqNumMedian > seq { 59 | guessRoc = localRoc + 1 60 | difference = seq - localSeq + seqNumMax 61 | } else { 62 | guessRoc = localRoc 63 | difference = seq - localSeq 64 | } 65 | } 66 | } else { 67 | // localRoc is equal to 0 68 | difference = seq - localSeq 69 | } 70 | } 71 | 72 | return guessRoc, func() { 73 | if !s.rolloverHasProcessed { 74 | s.index |= uint64(sequenceNumber) 75 | s.rolloverHasProcessed = true 76 | return 77 | } 78 | if difference > 0 { 79 | s.index += uint64(difference) 80 | } 81 | } 82 | } 83 | 84 | // https://github.com/pion/srtp/blob/3c34651fa0c6de900bdc91062e7ccb5992409643/srtp.go#L8 85 | func (c *SRTPContext) DecryptRTPPacket(packet *rtp.Packet) ([]byte, error) { 86 | s := c.getSRTPSSRCState(packet.Header.SSRC) 87 | roc, updateROC := s.nextRolloverCount(packet.Header.SequenceNumber) 88 | result, err := c.GCM.Decrypt(packet, roc) 89 | if err != nil { 90 | return nil, err 91 | } 92 | updateROC() 93 | return result[packet.HeaderSize:], nil 94 | } 95 | -------------------------------------------------------------------------------- /backend/src/srtp/srtpmanager.go: -------------------------------------------------------------------------------- 1 | package srtp 2 | 3 | import ( 4 | "net" 5 | 6 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 7 | ) 8 | 9 | type SRTPManager struct { 10 | } 11 | 12 | func NewSRTPManager() *SRTPManager { 13 | return &SRTPManager{} 14 | } 15 | 16 | func (m *SRTPManager) NewContext(addr *net.UDPAddr, conn *net.UDPConn, protectionProfile ProtectionProfile) *SRTPContext { 17 | result := &SRTPContext{ 18 | Addr: addr, 19 | Conn: conn, 20 | ProtectionProfile: protectionProfile, 21 | srtpSSRCStates: map[uint32]*srtpSSRCState{}, 22 | } 23 | return result 24 | } 25 | 26 | func (m *SRTPManager) extractEncryptionKeys(protectionProfile ProtectionProfile, keyingMaterial []byte) (*EncryptionKeys, error) { 27 | // https://github.com/pion/srtp/blob/82008b58b1e7be7a0cb834270caafacc7ba53509/keying.go#L14 28 | keyLength, err := protectionProfile.KeyLength() 29 | if err != nil { 30 | return nil, err 31 | } 32 | saltLength, err := protectionProfile.SaltLength() 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | offset := 0 38 | clientMasterKey := keyingMaterial[offset : offset+keyLength] 39 | offset += keyLength 40 | serverMasterKey := keyingMaterial[offset : offset+keyLength] 41 | offset += keyLength 42 | clientMasterSalt := keyingMaterial[offset : offset+saltLength] 43 | offset += saltLength 44 | serverMasterSalt := keyingMaterial[offset : offset+saltLength] 45 | 46 | result := &EncryptionKeys{ 47 | ClientMasterKey: clientMasterKey, 48 | ClientMasterSalt: clientMasterSalt, 49 | ServerMasterKey: serverMasterKey, 50 | ServerMasterSalt: serverMasterSalt, 51 | } 52 | return result, nil 53 | } 54 | 55 | func (m *SRTPManager) InitCipherSuite(context *SRTPContext, keyingMaterial []byte) error { 56 | logging.Descf(logging.ProtoSRTP, "Initializing SRTP Cipher Suite...") 57 | keys, err := m.extractEncryptionKeys(context.ProtectionProfile, keyingMaterial) 58 | if err != nil { 59 | return err 60 | } 61 | logging.Descf(logging.ProtoSRTP, "Extracted encryption keys from keying material (%d bytes) [protection profile %s]\n\tClientMasterKey: 0x%x (%d bytes)\n\tClientMasterSalt: 0x%x (%d bytes)\n\tServerMasterKey: 0x%x (%d bytes)\n\tServerMasterSalt: 0x%x (%d bytes)", 62 | len(keyingMaterial), context.ProtectionProfile, 63 | keys.ClientMasterKey, len(keys.ClientMasterKey), 64 | keys.ClientMasterSalt, len(keys.ClientMasterSalt), 65 | keys.ServerMasterKey, len(keys.ServerMasterKey), 66 | keys.ServerMasterSalt, len(keys.ServerMasterSalt)) 67 | logging.Descf(logging.ProtoSRTP, "Initializing GCM using ClientMasterKey and ClientMasterSalt") 68 | gcm, err := InitGCM(keys.ClientMasterKey, keys.ClientMasterSalt) 69 | if err != nil { 70 | return err 71 | } 72 | context.GCM = gcm 73 | return nil 74 | 75 | } 76 | -------------------------------------------------------------------------------- /backend/src/stun/attribute.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | ) 7 | 8 | const ( 9 | attributeHeaderSize = 4 10 | ) 11 | 12 | type Attribute struct { 13 | AttributeType AttributeType 14 | Value []byte 15 | OffsetInMessage int 16 | } 17 | 18 | func (a *Attribute) GetRawDataLength() int { 19 | return len(a.Value) 20 | } 21 | 22 | func (a *Attribute) GetRawFullLength() int { 23 | return attributeHeaderSize + len(a.Value) 24 | } 25 | 26 | func (a Attribute) String() string { 27 | return fmt.Sprintf("%s: [%s]", a.AttributeType, a.Value) 28 | } 29 | 30 | func DecodeAttribute(buf []byte, offset int, arrayLen int) (*Attribute, error) { 31 | if arrayLen < attributeHeaderSize { 32 | return nil, errIncompleteTURNFrame 33 | } 34 | offsetBackup := offset 35 | attrType := binary.BigEndian.Uint16(buf[offset : offset+2]) 36 | 37 | offset += 2 38 | 39 | attrLength := int(binary.BigEndian.Uint16(buf[offset : offset+2])) 40 | 41 | offset += 2 42 | 43 | result := new(Attribute) 44 | 45 | result.OffsetInMessage = offsetBackup 46 | result.AttributeType = AttributeType(attrType) 47 | 48 | result.Value = buf[offset : offset+attrLength] 49 | 50 | return result, nil 51 | } 52 | 53 | func (a *Attribute) Encode() []byte { 54 | attrLen := 4 + len(a.Value) 55 | attrLen += (4 - (attrLen % 4)) % 4 56 | result := make([]byte, attrLen) 57 | binary.BigEndian.PutUint16(result[0:2], uint16(a.AttributeType)) 58 | binary.BigEndian.PutUint16(result[2:4], uint16(len(a.Value))) 59 | copy(result[4:], a.Value) 60 | return result 61 | } 62 | -------------------------------------------------------------------------------- /backend/src/stun/attributes.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import ( 4 | "encoding/binary" 5 | "net" 6 | ) 7 | 8 | type IPFamily byte 9 | 10 | const ( 11 | IPFamilyIPv4 IPFamily = 0x01 12 | IPFamilyIPV6 IPFamily = 0x02 13 | ) 14 | 15 | type MappedAddress struct { 16 | IPFamily IPFamily 17 | IP net.IP 18 | Port uint16 19 | } 20 | 21 | func CreateAttrXorMappedAddress(transactionID []byte, addr *net.UDPAddr) *Attribute { 22 | // https://github.com/jitsi/ice4j/blob/311a495b21f38cc2dfcc4f7118dab96b8134aed6/src/main/java/org/ice4j/attribute/XorMappedAddressAttribute.java#L131 23 | xorMask := make([]byte, 16) 24 | binary.BigEndian.PutUint32(xorMask[0:4], magicCookie) 25 | copy(xorMask[4:], transactionID) 26 | //addressBytes := ms.Addr.IP 27 | portModifier := ((uint16(xorMask[0]) << 8) & 0x0000FF00) | (uint16(xorMask[1]) & 0x000000FF) 28 | addressBytes := make([]byte, len(addr.IP.To4())) 29 | copy(addressBytes, addr.IP.To4()) 30 | port := uint16(addr.Port) ^ portModifier 31 | for i := range addressBytes { 32 | addressBytes[i] ^= xorMask[i] 33 | } 34 | 35 | value := make([]byte, 8) 36 | 37 | value[1] = byte(IPFamilyIPv4) 38 | binary.BigEndian.PutUint16(value[2:4], port) 39 | copy(value[4:8], addressBytes) 40 | return &Attribute{ 41 | AttributeType: AttrXorMappedAddress, 42 | Value: value, 43 | } 44 | } 45 | 46 | func DecodeAttrXorMappedAddress(attr Attribute, transactionID [12]byte) *MappedAddress { 47 | xorMask := make([]byte, 16) 48 | binary.BigEndian.PutUint32(xorMask[0:4], magicCookie) 49 | copy(xorMask[4:], transactionID[:]) 50 | 51 | xorIP := make([]byte, 16) 52 | for i := 0; i < len(attr.Value)-4; i++ { 53 | xorIP[i] = attr.Value[i+4] ^ xorMask[i] 54 | } 55 | family := IPFamily(attr.Value[1]) 56 | port := binary.BigEndian.Uint16(attr.Value[2:4]) 57 | // Truncate if IPv4, otherwise net.IP sometimes renders it as an IPv6 address. 58 | if family == IPFamilyIPv4 { 59 | xorIP = xorIP[:4] 60 | } 61 | x := binary.BigEndian.Uint16(xorMask[:2]) 62 | return &MappedAddress{ 63 | IPFamily: family, 64 | IP: net.IP(xorIP), 65 | Port: port ^ x, 66 | } 67 | } 68 | 69 | func CreateAttrUserName(userName string) *Attribute { 70 | return &Attribute{ 71 | AttributeType: AttrUserName, 72 | Value: []byte(userName), 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /backend/src/stun/atttributetype.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import "fmt" 4 | 5 | type AttributeType uint16 6 | 7 | type attributeTypeDef struct { 8 | Name string 9 | } 10 | 11 | func (at AttributeType) String() string { 12 | attributeTypeDef, ok := attributeTypeMap[at] 13 | if !ok { 14 | // Just return hex representation of unknown attribute type. 15 | return fmt.Sprintf("0x%x", uint16(at)) 16 | } 17 | return attributeTypeDef.Name 18 | } 19 | 20 | const ( 21 | // STUN attributes: 22 | 23 | AttrMappedAddress AttributeType = 0x0001 24 | AttrResponseAddress AttributeType = 0x0002 25 | AttrChangeRequest AttributeType = 0x0003 26 | AttrSourceAddress AttributeType = 0x0004 27 | AttrChangedAddress AttributeType = 0x0005 28 | AttrUserName AttributeType = 0x0006 29 | AttrPassword AttributeType = 0x0007 30 | AttrMessageIntegrity AttributeType = 0x0008 31 | AttrErrorCode AttributeType = 0x0009 32 | AttrUnknownAttributes AttributeType = 0x000a 33 | AttrReflectedFrom AttributeType = 0x000b 34 | AttrRealm AttributeType = 0x0014 35 | AttrNonce AttributeType = 0x0015 36 | AttrXorMappedAddress AttributeType = 0x0020 37 | AttrSoftware AttributeType = 0x8022 38 | AttrAlternameServer AttributeType = 0x8023 39 | AttrFingerprint AttributeType = 0x8028 40 | 41 | // TURN attributes: 42 | AttrChannelNumber AttributeType = 0x000C 43 | AttrLifetime AttributeType = 0x000D 44 | AttrXorPeerAdddress AttributeType = 0x0012 45 | AttrData AttributeType = 0x0013 46 | AttrXorRelayedAddress AttributeType = 0x0016 47 | AttrEvenPort AttributeType = 0x0018 48 | AttrRequestedPort AttributeType = 0x0019 49 | AttrDontFragment AttributeType = 0x001A 50 | AttrReservationRequest AttributeType = 0x0022 51 | 52 | // ICE attributes: 53 | AttrPriority AttributeType = 0x0024 54 | AttrUseCandidate AttributeType = 0x0025 55 | AttrIceControlled AttributeType = 0x8029 56 | AttrIceControlling AttributeType = 0x802A 57 | ) 58 | 59 | var attributeTypeMap = map[AttributeType]attributeTypeDef{ 60 | // STUN attributes: 61 | AttrMappedAddress: {"MAPPED-ADDRESS"}, 62 | AttrResponseAddress: {"RESPONSE-ADDRESS"}, 63 | AttrChangeRequest: {"CHANGE-REQUEST"}, 64 | AttrSourceAddress: {"SOURCE-ADDRESS"}, 65 | AttrChangedAddress: {"CHANGED-ADDRESS"}, 66 | AttrUserName: {"USERNAME"}, 67 | AttrPassword: {"PASSWORD"}, 68 | AttrMessageIntegrity: {"MESSAGE-INTEGRITY"}, 69 | AttrErrorCode: {"ERROR-CODE"}, 70 | AttrUnknownAttributes: {"UNKNOWN-ATTRIBUTE"}, 71 | AttrReflectedFrom: {"REFLECTED-FROM"}, 72 | AttrRealm: {"REALM"}, 73 | AttrNonce: {"NONCE"}, 74 | AttrXorMappedAddress: {"XOR-MAPPED-ADDRES"}, 75 | AttrSoftware: {"SOFTWARE"}, 76 | AttrAlternameServer: {"ALTERNATE-SERVER"}, 77 | AttrFingerprint: {"FINGERPRINT"}, 78 | 79 | // TURN attributes: 80 | AttrChannelNumber: {"CHANNEL-NUMBER"}, 81 | AttrLifetime: {"LIFETIME"}, 82 | AttrXorPeerAdddress: {"XOR-PEER-ADDRESS"}, 83 | AttrData: {"DATA"}, 84 | AttrXorRelayedAddress: {"XOR-RELAYED-ADDRESS"}, 85 | AttrEvenPort: {"EVEN-PORT"}, 86 | AttrRequestedPort: {"REQUESTED-TRANSPORT"}, 87 | AttrDontFragment: {"DONT-FRAGMENT"}, 88 | AttrReservationRequest: {"RESERVATION-TOKEN"}, 89 | 90 | // ICE attributes: 91 | AttrPriority: {"PRIORITY"}, 92 | AttrUseCandidate: {"USE-CANDIDATE"}, 93 | AttrIceControlled: {"ICE-CONTROLLED"}, 94 | AttrIceControlling: {"ICE-CONTROLLING"}, 95 | } 96 | -------------------------------------------------------------------------------- /backend/src/stun/message.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha1" 7 | "encoding/base64" 8 | "encoding/binary" 9 | "errors" 10 | "fmt" 11 | "hash/crc32" 12 | "strings" 13 | 14 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/config" 15 | ) 16 | 17 | var ( 18 | //errInvalidTURNFrame = errors.New("data is not a valid TURN frame, no STUN or ChannelData found") 19 | errIncompleteTURNFrame = errors.New("data contains incomplete STUN or TURN frame") 20 | ) 21 | 22 | type Message struct { 23 | MessageType MessageType 24 | TransactionID [TransactionIDSize]byte 25 | Attributes map[AttributeType]Attribute 26 | RawMessage []byte 27 | } 28 | 29 | const ( 30 | magicCookie = 0x2112A442 31 | messageHeaderSize = 20 32 | 33 | TransactionIDSize = 12 // 96 bit 34 | 35 | stunHeaderSize = 20 36 | 37 | hmacSignatureSize = 20 38 | 39 | fingerprintSize = 4 40 | 41 | fingerprintXorMask = 0x5354554e 42 | ) 43 | 44 | func (m Message) String() string { 45 | transactionIDStr := base64.StdEncoding.EncodeToString(m.TransactionID[:]) 46 | attrsStr := "" 47 | for _, a := range m.Attributes { 48 | attrsStr += fmt.Sprintf("%s ", strings.ReplaceAll(a.String(), "\r", " ")) 49 | } 50 | return fmt.Sprintf("%s id=%s attrs=%s", m.MessageType, transactionIDStr, attrsStr) 51 | } 52 | 53 | func IsMessage(buf []byte, offset int, arrayLen int) bool { 54 | return arrayLen >= messageHeaderSize && binary.BigEndian.Uint32(buf[offset+4:offset+8]) == magicCookie 55 | } 56 | 57 | func DecodeMessage(buf []byte, offset int, arrayLen int) (*Message, error) { 58 | if arrayLen < stunHeaderSize { 59 | return nil, errIncompleteTURNFrame 60 | } 61 | 62 | offsetBackup := offset 63 | 64 | messageType := binary.BigEndian.Uint16(buf[offset : offset+2]) 65 | 66 | offset += 2 67 | 68 | messageLength := int(binary.BigEndian.Uint16(buf[offset : offset+2])) 69 | 70 | offset += 2 71 | 72 | // Adding message cookie length 73 | offset += 4 74 | 75 | result := new(Message) 76 | 77 | result.RawMessage = buf[offsetBackup : offsetBackup+arrayLen] 78 | 79 | result.MessageType = decodeMessageType(messageType) 80 | 81 | copy(result.TransactionID[:], buf[offset:offset+TransactionIDSize]) 82 | 83 | offset += TransactionIDSize 84 | result.Attributes = map[AttributeType]Attribute{} 85 | for offset-stunHeaderSize < messageLength { 86 | decodedAttr, err := DecodeAttribute(buf, offset, arrayLen) 87 | if err != nil { 88 | return nil, err 89 | } 90 | result.SetAttribute(*decodedAttr) 91 | offset += decodedAttr.GetRawFullLength() 92 | 93 | if decodedAttr.GetRawDataLength()%4 > 0 { 94 | offset += 4 - decodedAttr.GetRawDataLength()%4 95 | } 96 | } 97 | return result, nil 98 | } 99 | 100 | func calculateHmac(binMsg []byte, pwd string) []byte { 101 | key := []byte(pwd) 102 | messageLength := uint16(len(binMsg) + attributeHeaderSize + hmacSignatureSize - messageHeaderSize) 103 | binary.BigEndian.PutUint16(binMsg[2:4], messageLength) 104 | mac := hmac.New(sha1.New, key) 105 | mac.Write(binMsg) 106 | return mac.Sum(nil) 107 | } 108 | 109 | func calculateFingerprint(binMsg []byte) []byte { 110 | result := make([]byte, 4) 111 | messageLength := uint16(len(binMsg) + attributeHeaderSize + fingerprintSize - messageHeaderSize) 112 | binary.BigEndian.PutUint16(binMsg[2:4], messageLength) 113 | 114 | binary.BigEndian.PutUint32(result, crc32.ChecksumIEEE(binMsg)^fingerprintXorMask) 115 | return result 116 | } 117 | 118 | func (m *Message) preEncode() { 119 | // https://github.com/jitsi/ice4j/blob/32a8aadae8fde9b94081f8d002b6fda3490c20dc/src/main/java/org/ice4j/message/Message.java#L1015 120 | delete(m.Attributes, AttrMessageIntegrity) 121 | delete(m.Attributes, AttrFingerprint) 122 | m.Attributes[AttrSoftware] = *createAttrSoftware(config.Val.Server.SoftwareName) 123 | } 124 | func (m *Message) postEncode(encodedMessage []byte, dataLength int, pwd string) []byte { 125 | // https://github.com/jitsi/ice4j/blob/32a8aadae8fde9b94081f8d002b6fda3490c20dc/src/main/java/org/ice4j/message/Message.java#L1015 126 | messageIntegrityAttr := &Attribute{ 127 | AttributeType: AttrMessageIntegrity, 128 | Value: calculateHmac(encodedMessage, pwd), 129 | } 130 | encodedMessageIntegrity := messageIntegrityAttr.Encode() 131 | encodedMessage = append(encodedMessage, encodedMessageIntegrity...) 132 | 133 | messageFingerprint := &Attribute{ 134 | AttributeType: AttrFingerprint, 135 | Value: calculateFingerprint(encodedMessage), 136 | } 137 | encodedFingerprint := messageFingerprint.Encode() 138 | 139 | encodedMessage = append(encodedMessage, encodedFingerprint...) 140 | 141 | binary.BigEndian.PutUint16(encodedMessage[2:4], uint16(dataLength+len(encodedMessageIntegrity)+len(encodedFingerprint))) 142 | 143 | return encodedMessage 144 | } 145 | 146 | func (m *Message) Encode(pwd string) []byte { 147 | m.preEncode() 148 | // https://github.com/jitsi/ice4j/blob/311a495b21f38cc2dfcc4f7118dab96b8134aed6/src/main/java/org/ice4j/message/Message.java#L907 149 | var encodedAttrs []byte 150 | for _, attr := range m.Attributes { 151 | encodedAttr := attr.Encode() 152 | encodedAttrs = append(encodedAttrs, encodedAttr...) 153 | } 154 | 155 | result := make([]byte, messageHeaderSize+len(encodedAttrs)) 156 | 157 | binary.BigEndian.PutUint16(result[0:2], m.MessageType.Encode()) 158 | binary.BigEndian.PutUint16(result[2:4], uint16(len(encodedAttrs))) 159 | binary.BigEndian.PutUint32(result[4:8], magicCookie) 160 | copy(result[8:20], m.TransactionID[:]) 161 | copy(result[20:], encodedAttrs) 162 | result = m.postEncode(result, len(encodedAttrs), pwd) 163 | 164 | return result 165 | } 166 | 167 | func (m *Message) Validate(ufrag string, pwd string) { 168 | // https://github.com/jitsi/ice4j/blob/311a495b21f38cc2dfcc4f7118dab96b8134aed6/src/main/java/org/ice4j/stack/StunStack.java#L1254 169 | userNameAttr, okUserName := m.Attributes[AttrUserName] 170 | if okUserName { 171 | userName := strings.Split(string(userNameAttr.Value), ":")[0] 172 | if userName != ufrag { 173 | panic("Message not valid: UserName!") 174 | } 175 | } 176 | if messageIntegrityAttr, ok := m.Attributes[AttrMessageIntegrity]; ok { 177 | if !okUserName { 178 | panic("Message not valid: missing username!") 179 | } 180 | binMsg := make([]byte, messageIntegrityAttr.OffsetInMessage) 181 | copy(binMsg, m.RawMessage[0:messageIntegrityAttr.OffsetInMessage]) 182 | 183 | calculatedHmac := calculateHmac(binMsg, pwd) 184 | if !bytes.Equal(calculatedHmac, messageIntegrityAttr.Value) { 185 | panic(fmt.Sprintf("Message not valid: MESSAGE-INTEGRITY not valid expected: %v , received: %v not compatible!", calculatedHmac, messageIntegrityAttr.Value)) 186 | } 187 | } 188 | 189 | if fingerprintAttr, ok := m.Attributes[AttrFingerprint]; ok { 190 | binMsg := make([]byte, fingerprintAttr.OffsetInMessage) 191 | copy(binMsg, m.RawMessage[0:fingerprintAttr.OffsetInMessage]) 192 | 193 | calculatedFingerprint := calculateFingerprint(binMsg) 194 | if !bytes.Equal(calculatedFingerprint, fingerprintAttr.Value) { 195 | panic(fmt.Sprintf("Message not valid: FINGERPRINT not valid expected: %v , received: %v not compatible!", calculatedFingerprint, fingerprintAttr.Value)) 196 | } 197 | } 198 | } 199 | 200 | func (m *Message) SetAttribute(attr Attribute) { 201 | m.Attributes[attr.AttributeType] = attr 202 | } 203 | 204 | func createAttrSoftware(software string) *Attribute { 205 | return &Attribute{ 206 | AttributeType: AttrSoftware, 207 | Value: []byte(software), 208 | } 209 | } 210 | 211 | func NewMessage(messageType MessageType, transactionID [12]byte) *Message { 212 | result := &Message{ 213 | MessageType: messageType, 214 | TransactionID: transactionID, 215 | Attributes: map[AttributeType]Attribute{}, 216 | } 217 | return result 218 | } 219 | -------------------------------------------------------------------------------- /backend/src/stun/messageclass.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import "fmt" 4 | 5 | type MessageClass byte 6 | 7 | type messageClassDef struct { 8 | Name string 9 | } 10 | 11 | const ( 12 | MessageClassRequest MessageClass = 0x00 13 | MessageClassIndication MessageClass = 0x01 14 | MessageClassSuccessResponse MessageClass = 0x02 15 | MessageClassErrorResponse MessageClass = 0x03 16 | ) 17 | 18 | var messageClassMap = map[MessageClass]messageClassDef{ 19 | MessageClassRequest: {"request"}, 20 | MessageClassIndication: {"indication"}, 21 | MessageClassSuccessResponse: {"success response"}, 22 | MessageClassErrorResponse: {"error response"}, 23 | } 24 | 25 | func (mc MessageClass) String() string { 26 | messageClassDef, ok := messageClassMap[mc] 27 | if !ok { 28 | // Just return hex representation of unknown class. 29 | return fmt.Sprintf("0x%x", uint16(mc)) 30 | } 31 | return messageClassDef.Name 32 | } 33 | -------------------------------------------------------------------------------- /backend/src/stun/messagemethod.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import "fmt" 4 | 5 | type MessageMethod uint16 6 | 7 | type messageMethodDef struct { 8 | Name string 9 | } 10 | 11 | const ( 12 | MessageMethodStunBinding MessageMethod = 0x0001 13 | MessageMethodTurnAllocate MessageMethod = 0x0003 14 | MessageMethodTurnRefresh MessageMethod = 0x0004 15 | MessageMethodTurnSend MessageMethod = 0x0006 16 | MessageMethodTurnData MessageMethod = 0x0007 17 | MessageMethodTurnCreatePermission MessageMethod = 0x0008 18 | MessageMethodTurnChannelBind MessageMethod = 0x0009 19 | MessageMethodTurnConnect MessageMethod = 0x000a 20 | MessageMethodTurnConnectionBind MessageMethod = 0x000b 21 | MessageMethodTurnConnectionAttempt MessageMethod = 0x000c 22 | ) 23 | 24 | var messageMethodMap = map[MessageMethod]messageMethodDef{ 25 | MessageMethodStunBinding: {"STUN Binding"}, 26 | MessageMethodTurnAllocate: {"TURN Allocate"}, 27 | MessageMethodTurnRefresh: {"TURN Refresh"}, 28 | MessageMethodTurnSend: {"TURN Send"}, 29 | MessageMethodTurnData: {"TURN Data"}, 30 | MessageMethodTurnCreatePermission: {"TURN CreatePermission"}, 31 | MessageMethodTurnChannelBind: {"TURN ChannelBind"}, 32 | MessageMethodTurnConnect: {"TURN Connect"}, 33 | MessageMethodTurnConnectionBind: {"TURN ConnectionBind"}, 34 | MessageMethodTurnConnectionAttempt: {"TURN ConnectionAttempt"}, 35 | } 36 | 37 | func (mm MessageMethod) String() string { 38 | messageMethodDef, ok := messageMethodMap[mm] 39 | if !ok { 40 | // Just return hex representation of unknown method. 41 | return fmt.Sprintf("0x%x", uint16(mm)) 42 | } 43 | return messageMethodDef.Name 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/stun/messagetype.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import "fmt" 4 | 5 | type MessageType struct { 6 | MessageMethod MessageMethod 7 | MessageClass MessageClass 8 | } 9 | 10 | func (mt MessageType) String() string { 11 | return fmt.Sprintf("%s %s", mt.MessageMethod, mt.MessageClass) 12 | } 13 | 14 | const ( 15 | methodABits = 0xf // 0b0000000000001111 16 | methodBBits = 0x70 // 0b0000000001110000 17 | methodDBits = 0xf80 // 0b0000111110000000 18 | 19 | methodBShift = 1 20 | methodDShift = 2 21 | 22 | firstBit = 0x1 23 | secondBit = 0x2 24 | 25 | c0Bit = firstBit 26 | c1Bit = secondBit 27 | 28 | classC0Shift = 4 29 | classC1Shift = 7 30 | ) 31 | 32 | func decodeMessageType(mt uint16) MessageType { 33 | // Decoding class. 34 | // We are taking first bit from v >> 4 and second from v >> 7. 35 | c0 := (mt >> classC0Shift) & c0Bit 36 | c1 := (mt >> classC1Shift) & c1Bit 37 | class := c0 + c1 38 | 39 | // Decoding method. 40 | a := mt & methodABits // A(M0-M3) 41 | b := (mt >> methodBShift) & methodBBits // B(M4-M6) 42 | d := (mt >> methodDShift) & methodDBits // D(M7-M11) 43 | m := a + b + d 44 | 45 | return MessageType{ 46 | MessageClass: MessageClass(class), 47 | MessageMethod: MessageMethod(m), 48 | } 49 | } 50 | 51 | func (mt *MessageType) Encode() uint16 { 52 | m := uint16(mt.MessageMethod) 53 | a := m & methodABits // A = M * 0b0000000000001111 (right 4 bits) 54 | b := m & methodBBits // B = M * 0b0000000001110000 (3 bits after A) 55 | d := m & methodDBits // D = M * 0b0000111110000000 (5 bits after B) 56 | 57 | // Shifting to add "holes" for C0 (at 4 bit) and C1 (8 bit). 58 | m = a + (b << methodBShift) + (d << methodDShift) 59 | 60 | // C0 is zero bit of C, C1 is first bit. 61 | // C0 = C * 0b01, C1 = (C * 0b10) >> 1 62 | // Ct = C0 << 4 + C1 << 8. 63 | // Optimizations: "((C * 0b10) >> 1) << 8" as "(C * 0b10) << 7" 64 | // We need C0 shifted by 4, and C1 by 8 to fit "11" and "7" positions 65 | // (see figure 3). 66 | c := uint16(mt.MessageClass) 67 | c0 := (c & c0Bit) << classC0Shift 68 | c1 := (c & c1Bit) << classC1Shift 69 | class := c0 + c1 70 | 71 | return m + class 72 | } 73 | 74 | var ( 75 | MessageTypeBindingRequest = MessageType{ 76 | MessageMethod: MessageMethodStunBinding, 77 | MessageClass: MessageClassRequest, 78 | } 79 | MessageTypeBindingSuccessResponse = MessageType{ 80 | MessageMethod: MessageMethodStunBinding, 81 | MessageClass: MessageClassSuccessResponse, 82 | } 83 | MessageTypeBindingErrorResponse = MessageType{ 84 | MessageMethod: MessageMethodStunBinding, 85 | MessageClass: MessageClassErrorResponse, 86 | } 87 | MessageTypeBindingIndication = MessageType{ 88 | MessageMethod: MessageMethodStunBinding, 89 | MessageClass: MessageClassIndication, 90 | } 91 | ) 92 | -------------------------------------------------------------------------------- /backend/src/stun/stunclient.go: -------------------------------------------------------------------------------- 1 | package stun 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "net" 7 | ) 8 | 9 | type StunClient struct { 10 | ServerAddr string 11 | Ufrag string 12 | Pwd string 13 | } 14 | 15 | func NewStunClient(serverAddr string, ufrag string, pwd string) *StunClient { 16 | return &StunClient{ 17 | ServerAddr: serverAddr, 18 | Ufrag: ufrag, 19 | Pwd: pwd, 20 | } 21 | } 22 | 23 | // https://github.com/ccding/go-stun 24 | func (c *StunClient) Discover() (*MappedAddress, error) { 25 | transactionID, err := generateTransactionID() 26 | if err != nil { 27 | return nil, err 28 | } 29 | serverUDPAddr, err := net.ResolveUDPAddr("udp", c.ServerAddr) 30 | if err != nil { 31 | return nil, err 32 | //return NATError, nil, err 33 | } 34 | bindingRequest := createBindingRequest(transactionID) 35 | encodedBindingRequest := bindingRequest.Encode(c.Pwd) 36 | conn, err := net.ListenUDP("udp", nil) 37 | if err != nil { 38 | return nil, err 39 | } 40 | defer conn.Close() 41 | conn.WriteToUDP(encodedBindingRequest, serverUDPAddr) 42 | buf := make([]byte, 1024) 43 | 44 | for { 45 | bufLen, addr, err := conn.ReadFromUDP(buf) 46 | if err != nil { 47 | return nil, err 48 | } 49 | // If requested target server address and responder address not fit, ignore the packet 50 | if !addr.IP.Equal(serverUDPAddr.IP) || addr.Port != serverUDPAddr.Port { 51 | continue 52 | } 53 | stunMessage, stunErr := DecodeMessage(buf, 0, bufLen) 54 | if stunErr != nil { 55 | panic(stunErr) 56 | } 57 | stunMessage.Validate(c.Ufrag, c.Pwd) 58 | if !bytes.Equal(stunMessage.TransactionID[:], transactionID[:]) { 59 | continue 60 | } 61 | xorMappedAddressAttr, ok := stunMessage.Attributes[AttrXorMappedAddress] 62 | if !ok { 63 | continue 64 | } 65 | mappedAddress := DecodeAttrXorMappedAddress(xorMappedAddressAttr, stunMessage.TransactionID) 66 | return mappedAddress, nil 67 | } 68 | } 69 | 70 | func generateTransactionID() ([12]byte, error) { 71 | result := [12]byte{} 72 | _, err := rand.Read(result[:]) 73 | if err != nil { 74 | return result, err 75 | } 76 | return result, nil 77 | } 78 | 79 | func createBindingRequest(transactionID [12]byte) *Message { 80 | responseMessage := NewMessage(MessageTypeBindingRequest, transactionID) 81 | return responseMessage 82 | } 83 | -------------------------------------------------------------------------------- /backend/src/transcoding/vp8.go: -------------------------------------------------------------------------------- 1 | package transcoding 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "image/jpeg" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 12 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/rtp" 13 | "github.com/xlab/libvpx-go/vpx" 14 | ) 15 | 16 | var ( 17 | currentFrame []byte 18 | seenKeyFrame = false 19 | ) 20 | 21 | // https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc 22 | type VP8Decoder struct { 23 | src <-chan *rtp.Packet 24 | context *vpx.CodecCtx 25 | iface *vpx.CodecIface 26 | } 27 | 28 | func NewVP8Decoder(src <-chan *rtp.Packet) (*VP8Decoder, error) { 29 | result := &VP8Decoder{ 30 | src: src, 31 | context: vpx.NewCodecCtx(), 32 | iface: vpx.DecoderIfaceVP8(), 33 | } 34 | err := vpx.Error(vpx.CodecDecInitVer(result.context, result.iface, nil, 0, vpx.DecoderABIVersion)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | return result, nil 39 | } 40 | 41 | var ( 42 | fileCount = 0 43 | saveDir = "../output" 44 | saveFilePrefix = "shoot" 45 | ) 46 | 47 | func (d *VP8Decoder) Run() { 48 | 49 | newpath := filepath.Join(".", saveDir) 50 | err := os.MkdirAll(newpath, os.ModePerm) 51 | 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | packetCounter := 0 57 | //https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc 58 | for rtpPacket := range d.src { 59 | packetCounter++ 60 | 61 | vp8Packet := &VP8Packet{} 62 | vp8Packet.Unmarshal(rtpPacket.Payload) 63 | isKeyFrame := vp8Packet.Payload[0] & 0x01 64 | 65 | switch { 66 | case !seenKeyFrame && isKeyFrame == 1: 67 | continue 68 | case currentFrame == nil && vp8Packet.S != 1: 69 | continue 70 | } 71 | 72 | seenKeyFrame = true 73 | currentFrame = append(currentFrame, vp8Packet.Payload[0:]...) 74 | 75 | if !rtpPacket.Header.Marker { 76 | continue 77 | } else if len(currentFrame) == 0 { 78 | continue 79 | } 80 | 81 | err := vpx.Error(vpx.CodecDecode(d.context, string(currentFrame), uint32(len(currentFrame)), nil, 0)) 82 | if err != nil { 83 | logging.Errorf(logging.ProtoVP8, "Error while decoding packet: %s", err) 84 | currentFrame = nil 85 | seenKeyFrame = false 86 | continue 87 | } 88 | 89 | var iter vpx.CodecIter 90 | img := vpx.CodecGetFrame(d.context, &iter) 91 | if img != nil { 92 | img.Deref() 93 | 94 | outputImageFilePath, err := d.saveImageFile(img) 95 | if err != nil { 96 | logging.Errorf(logging.ProtoVP8, "Error while image saving: %s", err) 97 | } else { 98 | logging.Infof(logging.ProtoVP8, "Image file saved: %s\n", outputImageFilePath) 99 | } 100 | 101 | } 102 | currentFrame = nil 103 | seenKeyFrame = false 104 | 105 | } 106 | } 107 | 108 | func (d *VP8Decoder) safeEncodeJpeg(buffer *bytes.Buffer, img *vpx.Image) (err error) { 109 | defer func() { 110 | if r := recover(); r != nil { 111 | err = fmt.Errorf("error: %v, image width: %d, height: %d", r, img.W, img.H) 112 | } 113 | }() 114 | return jpeg.Encode(buffer, img.ImageYCbCr(), nil) 115 | } 116 | 117 | func (d *VP8Decoder) saveImageFile(img *vpx.Image) (string, error) { 118 | fileCount++ 119 | buffer := new(bytes.Buffer) 120 | if err := d.safeEncodeJpeg(buffer, img); err != nil { 121 | return "", fmt.Errorf("jpeg Encode Error: %s", err) 122 | } 123 | 124 | outputImageFilePath := fmt.Sprintf("%s%d%s", filepath.Join(saveDir, saveFilePrefix), fileCount, ".jpg") 125 | fo, err := os.Create(outputImageFilePath) 126 | 127 | if err != nil { 128 | return "", fmt.Errorf("image create Error: %s", err) 129 | } 130 | // close fo on exit and check for its returned error 131 | defer func() { 132 | if err := fo.Close(); err != nil { 133 | panic(err) 134 | } 135 | }() 136 | 137 | if _, err := fo.Write(buffer.Bytes()); err != nil { 138 | return "", fmt.Errorf("image write Error: %s", err) 139 | } 140 | return outputImageFilePath, nil 141 | } 142 | 143 | /* 144 | 0 1 2 3 4 5 6 7 145 | +-+-+-+-+-+-+-+-+ 146 | |X|R|N|S|PartID | (REQUIRED) 147 | +-+-+-+-+-+-+-+-+ 148 | X: |I|L|T|K| RSV | (OPTIONAL) 149 | +-+-+-+-+-+-+-+-+ 150 | I: |M| PictureID | (OPTIONAL) 151 | +-+-+-+-+-+-+-+-+ 152 | L: | TL0PICIDX | (OPTIONAL) 153 | +-+-+-+-+-+-+-+-+ 154 | T/K: |TID|Y| KEYIDX | (OPTIONAL) 155 | +-+-+-+-+-+-+-+-+ 156 | */ 157 | 158 | // https://tools.ietf.org/id/draft-ietf-payload-vp8-05.html 159 | type VP8Packet struct { 160 | // Required Header 161 | X uint8 /* extended control bits present */ 162 | N uint8 /* when set to 1 this frame can be discarded */ 163 | S uint8 /* start of VP8 partition */ 164 | PID uint8 /* partition index */ 165 | 166 | // Extended control bits 167 | I uint8 /* 1 if PictureID is present */ 168 | L uint8 /* 1 if TL0PICIDX is present */ 169 | T uint8 /* 1 if TID is present */ 170 | K uint8 /* 1 if KEYIDX is present */ 171 | 172 | // Optional extension 173 | PictureID uint16 /* 8 or 16 bits, picture ID */ 174 | TL0PICIDX uint8 /* 8 bits temporal level zero index */ 175 | TID uint8 /* 2 bits temporal layer index */ 176 | Y uint8 /* 1 bit layer sync bit */ 177 | KEYIDX uint8 /* 5 bits temporal key frame index */ 178 | 179 | Payload []byte 180 | } 181 | 182 | func (p *VP8Packet) Unmarshal(payload []byte) ([]byte, error) { 183 | if payload == nil { 184 | return nil, errors.New("errNilPacket") 185 | } 186 | 187 | payloadLen := len(payload) 188 | 189 | if payloadLen < 4 { 190 | return nil, errors.New("errShortPacket") 191 | } 192 | 193 | payloadIndex := 0 194 | 195 | p.X = (payload[payloadIndex] & 0x80) >> 7 196 | p.N = (payload[payloadIndex] & 0x20) >> 5 197 | p.S = (payload[payloadIndex] & 0x10) >> 4 198 | p.PID = payload[payloadIndex] & 0x07 199 | 200 | payloadIndex++ 201 | 202 | if p.X == 1 { 203 | p.I = (payload[payloadIndex] & 0x80) >> 7 204 | p.L = (payload[payloadIndex] & 0x40) >> 6 205 | p.T = (payload[payloadIndex] & 0x20) >> 5 206 | p.K = (payload[payloadIndex] & 0x10) >> 4 207 | payloadIndex++ 208 | } 209 | 210 | if p.I == 1 { // PID present? 211 | if payload[payloadIndex]&0x80 > 0 { // M == 1, PID is 16bit 212 | p.PictureID = (uint16(payload[payloadIndex]&0x7F) << 8) | uint16(payload[payloadIndex+1]) 213 | payloadIndex += 2 214 | } else { 215 | p.PictureID = uint16(payload[payloadIndex]) 216 | payloadIndex++ 217 | } 218 | } 219 | 220 | if payloadIndex >= payloadLen { 221 | return nil, errors.New("errShortPacket") 222 | } 223 | 224 | if p.L == 1 { 225 | p.TL0PICIDX = payload[payloadIndex] 226 | payloadIndex++ 227 | } 228 | 229 | if payloadIndex >= payloadLen { 230 | return nil, errors.New("errShortPacket") 231 | } 232 | 233 | if p.T == 1 || p.K == 1 { 234 | if p.T == 1 { 235 | p.TID = payload[payloadIndex] >> 6 236 | p.Y = (payload[payloadIndex] >> 5) & 0x1 237 | } 238 | if p.K == 1 { 239 | p.KEYIDX = payload[payloadIndex] & 0x1F 240 | } 241 | payloadIndex++ 242 | } 243 | 244 | if payloadIndex >= payloadLen { 245 | return nil, errors.New("errShortPacket") 246 | } 247 | p.Payload = payload[payloadIndex:] 248 | return p.Payload, nil 249 | } 250 | -------------------------------------------------------------------------------- /backend/src/udp/udpListener.go: -------------------------------------------------------------------------------- 1 | package udp 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/agent" 9 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/conference" 10 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/logging" 11 | "github.com/adalkiran/webrtc-nuts-and-bolts/src/stun" 12 | ) 13 | 14 | type UdpListener struct { 15 | Ip string 16 | Port int 17 | 18 | conn *net.UDPConn 19 | 20 | ConferenceManager *conference.ConferenceManager 21 | 22 | Sockets map[string]*agent.UDPClientSocket 23 | } 24 | 25 | func NewUdpListener(ip string, port int, conferenceManager *conference.ConferenceManager) *UdpListener { 26 | return &UdpListener{ 27 | Ip: ip, 28 | Port: port, 29 | ConferenceManager: conferenceManager, 30 | Sockets: map[string]*agent.UDPClientSocket{}, 31 | } 32 | } 33 | 34 | func readUfrag(buf []byte, offset int, arrayLen int) (string, string, bool) { 35 | if !stun.IsMessage(buf, offset, arrayLen) { 36 | return "", "", false 37 | } 38 | stunMessage, stunErr := stun.DecodeMessage(buf, 0, arrayLen) 39 | if stunErr != nil { 40 | panic(stunErr) 41 | } 42 | if stunMessage.MessageType != stun.MessageTypeBindingRequest { 43 | return "", "", false 44 | } 45 | userNameAttr, userNameExists := stunMessage.Attributes[stun.AttrUserName] 46 | 47 | if !userNameExists { 48 | return "", "", false 49 | } 50 | 51 | userNameParts := strings.Split(string(userNameAttr.Value), ":") 52 | serverUserName := userNameParts[0] 53 | clientUserName := userNameParts[1] 54 | return serverUserName, clientUserName, true 55 | } 56 | 57 | func (udpListener *UdpListener) Run(waitGroup *sync.WaitGroup) { 58 | defer waitGroup.Done() 59 | conn, err := net.ListenUDP("udp", &net.UDPAddr{ 60 | IP: net.IP{0, 0, 0, 0}, 61 | Port: udpListener.Port, 62 | }) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | udpListener.conn = conn 68 | 69 | defer conn.Close() 70 | 71 | logging.Infof(logging.ProtoUDP, "Listen on %v %d/UDP", "single-port", udpListener.Port) 72 | logging.Descf(logging.ProtoUDP, "Clients will do media streaming via this connection. This UDP listener acts as demultiplexer, in other words, it can speak in STUN, DTLS, SRTP, SRTCP, etc... protocols in single port. It differentiates protocols by looking shape of packet headers. Clients don't know this listener's IP and port now, they will learn via signaling the SDP offer/answer data when they join a conference via signaling subsystem/WebSocket.") 73 | 74 | buf := make([]byte, 2048) 75 | 76 | // We run into an infinite loop for any incoming UDP packet from specified port. 77 | for { 78 | bufLen, addr, err := conn.ReadFromUDP(buf) 79 | if err == nil { 80 | // Is the client socket known and authenticated by server before? 81 | destinationSocket, ok := udpListener.Sockets[string(addr.IP)+":"+string(rune(addr.Port))] 82 | 83 | if !ok { 84 | logging.Descf(logging.ProtoUDP, "An UDP packet received from %s:%d first time. This client is not known already by UDP server. Is it a STUN binding request?", addr.IP, addr.Port) 85 | // If client socket is not known by server, it can be a STUN binding request. 86 | // Read the server and client ufrag (user fragment) string concatenated via ":" and split it. 87 | serverUfrag, clientUfrag, ok := readUfrag(buf, 0, bufLen) 88 | 89 | if !ok { 90 | logging.Descf(logging.ProtoUDP, "This packet is not a valid STUN binding request, ignore it.") 91 | // If this is not a valid STUN binding request, ignore it. 92 | continue 93 | } 94 | logging.Descf(logging.ProtoUDP, "It is a valid STUN binding request with Server Ufrag: %s, Client Ufrag: %s", serverUfrag, clientUfrag) 95 | logging.Descf(logging.ProtoUDP, "Looking for valid server agent related with an existing conference, with Ufrag: %s", serverUfrag) 96 | // It seems a valid STUN binding request, does serverUfrag point 97 | // a server agent (of a defined conference) which is already listening? 98 | agent, ok := udpListener.ConferenceManager.GetAgent(serverUfrag) 99 | if !ok { 100 | logging.Descf(logging.ProtoUDP, "Any server agent couldn't be found that serverUfrag %s points, ignore it.", serverUfrag) 101 | // Any server agent couldn't be found that serverUfrag points, ignore it. 102 | continue 103 | } 104 | logging.Descf(logging.ProtoUDP, "Found server ICE Agent related with conference %s.", agent.ConferenceName) 105 | signalingMediaComponent, ok := agent.SignalingMediaComponents[clientUfrag] 106 | if !ok { 107 | logging.Descf(logging.ProtoUDP, "Client Ufrag %s is not known by server agent %s. SDP Offer/Answer should be processed before UDP STUN binding request. Ignore it.", clientUfrag, serverUfrag) 108 | // Any server agent couldn't be found that serverUfrag points, ignore it. 109 | continue 110 | } 111 | logging.Descf(logging.ProtoUDP, "Found SignalingMediaComponent for client Ufrag %s. It seems we can define a client socket to the server agent(%s). Creating a new UDPClientSocket object for this UDP client.", signalingMediaComponent.Ufrag, agent.ConferenceName) 112 | // It seems we can define a client socket to the server agent. 113 | destinationSocket = udpListener.addNewUDPClientSocket(agent, addr, clientUfrag) 114 | 115 | // If this STUN binding request's client ufrag is not known by our agent, ignore the request. 116 | // Agent should know the ufrag by SDP offer coming from WebSocket, before coming STUN binding request from UDP. 117 | if destinationSocket == nil { 118 | continue 119 | } 120 | } 121 | 122 | // Now the client socket is known by server, we forward incoming byte array to our socket object dedicated for the client. 123 | destinationSocket.AddBuffer(buf, 0, bufLen) 124 | } else { 125 | logging.Errorf(logging.ProtoUDP, "Some error: %s", err) 126 | } 127 | } 128 | 129 | } 130 | 131 | func (udpListener *UdpListener) addNewUDPClientSocket(serverAgent *agent.ServerAgent, addr *net.UDPAddr, clientUfrag string) *agent.UDPClientSocket { 132 | clientComponent, ok := serverAgent.SignalingMediaComponents[clientUfrag] 133 | if !ok { 134 | // It seems clientUfrag is not known by the server agent, ignore it. 135 | return nil 136 | } 137 | udpClientSocket, err := agent.NewUDPClientSocket(addr, serverAgent.Ufrag, serverAgent.Pwd, clientUfrag, udpListener.conn, clientComponent.FingerprintHash) 138 | if err != nil { 139 | panic(err) 140 | } 141 | serverAgent.Sockets[clientUfrag] = *udpClientSocket 142 | udpListener.Sockets[string(addr.IP)+":"+string(rune(addr.Port))] = udpClientSocket 143 | return udpClientSocket 144 | } 145 | -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | services: 2 | backend: 3 | entrypoint: /entrypoint-dev.sh 4 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | ui: 3 | image: webrtc-nuts-and-bolts/ui 4 | container_name: webrtcnb-ui 5 | build: 6 | context: ui # Dockerfile location 7 | args: 8 | # See for available variants: https://github.com/devcontainers/images/tree/main/src/typescript-node 9 | - VARIANT:22-bookworm 10 | volumes: 11 | # Mount the root folder that contains .git 12 | - "./ui:/workspace:cached" 13 | ports: 14 | - "8080:8080" # Port expose for UI Webpack Dev Server 15 | 16 | backend: 17 | image: webrtc-nuts-and-bolts/backend 18 | container_name: webrtcnb-backend 19 | build: 20 | context: backend # Dockerfile location 21 | args: 22 | # See for available variants: https://hub.docker.com/_/golang?tab=tags 23 | - VARIANT:1.23.0-bookworm 24 | # See: https://code.visualstudio.com/docs/remote/create-dev-container#_set-up-a-folder-to-run-in-a-container 25 | # [Optional] Required for ptrace-based debuggers like C++, Go, and Rust 26 | cap_add: 27 | - SYS_PTRACE 28 | security_opt: 29 | - seccomp:unconfined 30 | volumes: 31 | # Mount the root folder that contains .git 32 | - "./backend:/workspace:cached" 33 | ports: 34 | - "8081:8081" # Port expose for backend WebSocket 35 | - "15000:15000/udp" # Port expose for backend UDP end -------------------------------------------------------------------------------- /docs/01-RUNNING-IN-DEV-MODE.md: -------------------------------------------------------------------------------- 1 | # **1. RUNNING IN DEVELOPMENT MODE** 2 | 3 | * Clone this repo. 4 | 5 | * Learn your host machine's LAN IP address: 6 | 7 | ```console 8 | $ ifconfig | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1' 9 | 10 | 192.168.***.*** 11 | ``` 12 | 13 | * Open [backend/config.yml](../backend/config.yml) file, write your LAN IP address into server/udp/dockerHostIp section. 14 | 15 | * If you don't have VS Code and [Remote Development extension pack](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remote-extensionpack) installed, install them. [This link](https://code.visualstudio.com/docs/remote/containers) can be helpful. 16 | 17 | * Start VS Code, open the cloned folder of "webrtc-nuts-and-bolts". 18 | 19 | * Press F1 and select **"Remote Containers: Open Folder in Container..."** then select "backend" folder in "webrtc-nuts-and-bolts". 20 | 21 | Open folder in container 22 | 23 | Select "backend" folder 24 | 25 | * This command creates (if don't exist) required containers in Docker, then connects inside of webrtcnb-backend container for development and debugging purposes. 26 | 27 | * You will see this notification while building image and starting container. If you click on this notification, you will see a log similar to image below. 28 | 29 | Starting Dev Container small 30 | 31 | ![Starting Dev Container log](images/01-04-starting-dev-container-log.png) 32 | 33 | * When webrtcnb-backend container has been built and started, VS Code will ask you for some required installations related to Go language, click "Install All" for these prompts. 34 | 35 | Install Go Dependencies small 36 | 37 | * After clicking "Install All", you will see installation logs similar to image below. 38 | 39 | Install Go Dependencies log 40 | 41 | * When you see "You are ready to Go. :)" message in the log, you can press F5 to run and debug our server inside the container. VS Code can ask for installing other dependencies (like "dlv"), click on "Install" again. If VS Code asked for some extra installations, after installation you may need to press F5 again. 42 | 43 | * You can switch to **"DEBUG CONSOLE"** tab at bottom, you will be able to see the output of running server application: 44 | 45 | ![Backend initial output](images/01-07-backend-initial-output.png) 46 | 47 | * Now your backend server is ready to accept requests from browser! 48 | 49 |
50 | 51 | --- 52 | 53 |
54 | 55 | [<  Previous chapter: INFRASTRUCTURE](./00-INFRASTRUCTURE.md)      |      [Next chapter: BACKEND INITIALIZATION  >](./02-BACKEND-INITIALIZATION.md) 56 | 57 |
58 | -------------------------------------------------------------------------------- /docs/08-VP8-PACKET-DECODE.md: -------------------------------------------------------------------------------- 1 | # **8. VP8 PACKET DECODE** 2 | 3 | In previous chapter, we decoded the SRTP packet and decrypted it successfully, also determined that this packet contains a VP8 video data part. 4 | 5 | * If the packet's PayloadType is in PayloadTypeVP8 type, it forwards to the UDPClientSocket's "vp8Depacketizer" [channel](https://go.dev/tour/concurrency/2). This channel is listened by Run() function of VP8Decoder object in [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go), shown below 6 | 7 | * Our VP8Decoder aims to process incoming VP8 RTP packets, catch and concatenate keyframe packets then convert the result to an image object and save it as a JPEG image file. 8 | 9 | VP8 Payload Descriptor 10 | 11 | ```console 12 | 0 1 2 3 4 5 6 7 13 | +-+-+-+-+-+-+-+-+ 14 | |X|R|N|S|PartID | (REQUIRED) 15 | +-+-+-+-+-+-+-+-+ 16 | X: |I|L|T|K| RSV | (OPTIONAL) 17 | +-+-+-+-+-+-+-+-+ 18 | I: |M| PictureID | (OPTIONAL) 19 | +-+-+-+-+-+-+-+-+ 20 | L: | TL0PICIDX | (OPTIONAL) 21 | +-+-+-+-+-+-+-+-+ 22 | T/K: |TID|Y| KEYIDX | (OPTIONAL) 23 | +-+-+-+-+-+-+-+-+ 24 | ``` 25 | 26 | Our VP8 packet struct in [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go) 27 | 28 | from [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go) 29 | 30 | ```go 31 | type VP8Packet struct { 32 | // Required Header 33 | X uint8 /* extended control bits present */ 34 | N uint8 /* when set to 1 this frame can be discarded */ 35 | S uint8 /* start of VP8 partition */ 36 | PID uint8 /* partition index */ 37 | 38 | // Extended control bits 39 | I uint8 /* 1 if PictureID is present */ 40 | L uint8 /* 1 if TL0PICIDX is present */ 41 | T uint8 /* 1 if TID is present */ 42 | K uint8 /* 1 if KEYIDX is present */ 43 | 44 | // Optional extension 45 | PictureID uint16 /* 8 or 16 bits, picture ID */ 46 | TL0PICIDX uint8 /* 8 bits temporal level zero index */ 47 | TID uint8 /* 2 bits temporal layer index */ 48 | Y uint8 /* 1 bit layer sync bit */ 49 | KEYIDX uint8 /* 5 bits temporal key frame index */ 50 | 51 | Payload []byte 52 | } 53 | ``` 54 | 55 | * We used [libvpx-go](https://github.com/xlab/libvpx-go) as codec, we didn't implement the VP8 codec. 56 | 57 | from [backend/src/transcoding/vp8.go](../backend/src/transcoding/vp8.go) 58 | 59 | ```go 60 | func (d *VP8Decoder) Run() { 61 | 62 | newpath := filepath.Join(".", saveDir) 63 | err := os.MkdirAll(newpath, os.ModePerm) 64 | 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | packetCounter := 0 70 | for rtpPacket := range d.src { 71 | packetCounter++ 72 | 73 | vp8Packet := &VP8Packet{} 74 | vp8Packet.Unmarshal(rtpPacket.Payload) 75 | isKeyFrame := vp8Packet.Payload[0] & 0x01 76 | 77 | switch { 78 | case !seenKeyFrame && isKeyFrame == 1: 79 | continue 80 | case currentFrame == nil && vp8Packet.S != 1: 81 | continue 82 | } 83 | 84 | seenKeyFrame = true 85 | currentFrame = append(currentFrame, vp8Packet.Payload[0:]...) 86 | 87 | if !rtpPacket.Header.Marker { 88 | continue 89 | } else if len(currentFrame) == 0 { 90 | continue 91 | } 92 | 93 | err := vpx.Error(vpx.CodecDecode(d.context, string(currentFrame), uint32(len(currentFrame)), nil, 0)) 94 | if err != nil { 95 | logging.Errorf(logging.ProtoVP8, "Error while decoding packet: %s", err) 96 | currentFrame = nil 97 | seenKeyFrame = false 98 | continue 99 | } 100 | 101 | var iter vpx.CodecIter 102 | img := vpx.CodecGetFrame(d.context, &iter) 103 | if img != nil { 104 | img.Deref() 105 | 106 | outputImageFilePath, err := d.saveImageFile(img) 107 | if err != nil { 108 | logging.Errorf(logging.ProtoVP8, "Error while image saving: %s", err) 109 | } else { 110 | logging.Infof(logging.ProtoVP8, "Image file saved: %s\n", outputImageFilePath) 111 | } 112 | 113 | } 114 | currentFrame = nil 115 | seenKeyFrame = false 116 | 117 | } 118 | } 119 | ``` 120 | 121 | * If the function call succeeds, 122 | 123 | ```go 124 | img := vpx.CodecGetFrame(d.context, &iter) 125 | ``` 126 | 127 | * We call img.Deref() to convert decoded C structures as Go wrapper variables 128 | 129 | ```go 130 | img.Deref() 131 | ``` 132 | 133 | * If everything has gone OK, save the image as a JPEG file by [jpeg.Encode](https://pkg.go.dev/image/jpeg#Encode) 134 | * You can see your caught keyframes at /backend/output/ folder as shoot1.jpg, shoot2.jpg, etc... if multiple keyframes were caught. 135 | * As you can see below, we received multiple RTP packets containing VP8 video data, in different packet lengths, then we caught a keyframe from these packets, concatenate them, then created and saved image. 136 | * At the end of our journey, we saw the "[INFO] Image file saved: ../output/shoot1.jpg" log line! All of our these efforts are to achieve this.... 137 | 138 | ![Image saved](images/08-01-image-saved.png) 139 | 140 | Sources: 141 | 142 | * [How to convert VP8 interframe into image with Pion/Webrtc? (Stackoverflow)](https://stackoverflow.com/questions/68859120/how-to-convert-vp8-interframe-into-image-with-pion-webrtc) 143 | * [RFC: RTP Payload Format for VP8 Video](https://tools.ietf.org/id/draft-ietf-payload-vp8-05.html) 144 | 145 |
146 | 147 | --- 148 | 149 |
150 | 151 | [<  Previous chapter: SRTP PACKETS COME](./07-SRTP-PACKETS-COME.md)      |      [Next chapter: CONCLUSION  >](./09-CONCLUSION.md) 152 | 153 |
154 | -------------------------------------------------------------------------------- /docs/09-CONCLUSION.md: -------------------------------------------------------------------------------- 1 | # **9. CONCLUSION** 2 | 3 | If you really read all of my journey in this walkthrough documentation, I want to congratulate you for your perseverance, patience, and durability :blush: Also, I want to thank you for your interest. 4 | 5 | If you liked this repo, you can give a star :star: on GitHub. Your support and feedback not only help the project improve and grow but also contribute to reaching a wider audience within the community. Additionally, it motivates me to create even more innovative projects in the future. 6 | 7 | Also thanks to contributors of the awesome sources which were referred during development of this project and writing this documentation. You can find these sources in [README](../README.md), also in between the lines. 8 | 9 | You can find me on   [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/alper-dalkiran/)   [![Twitter](https://img.shields.io/badge/Twitter-1DA1F2?style=for-the-badge&logo=twitter&logoColor=white&style=flat-square)](https://twitter.com/aalperdalkiran) 10 | 11 |
12 |
13 | Thanks sincerely, 14 |
15 | Adil Alper DALKIRAN 16 | 17 |
18 |
19 |
20 | 21 | --- 22 | 23 |
24 | 25 | [<  Previous chapter: VP8 PACKET DECODE](./08-VP8-PACKET-DECODE.md)      |      [Home  >>](../README.md) 26 | 27 |
28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # **WebRTC Nuts and Bolts: Documentation** 2 | 3 | Here is the adventure of a WebRTC stream from start to finish documented as step by step: 4 | 5 | [0. INFRASTRUCTURE](./00-INFRASTRUCTURE.md) 6 |
7 | [1. RUNNING IN DEVELOPMENT MODE](./01-RUNNING-IN-DEV-MODE.md) 8 |
9 | [2. BACKEND INITIALIZATION](./02-BACKEND-INITIALIZATION.md) 10 |
11 | [3. FIRST CLIENT COMES IN](./03-FIRST-CLIENT-COMES-IN.md) 12 |
13 | [4. STUN BINDING REQUEST FROM CLIENT](./04-STUN-BINDING-REQUEST-FROM-CLIENT.md) 14 |
15 | [5. DTLS HANDSHAKE](./05-DTLS-HANDSHAKE.md) 16 |
17 | [6. SRTP INITIALIZATION](./06-SRTP-INITIALIZATION.md) 18 |
19 | [7. SRTP PACKETS COME](./07-SRTP-PACKETS-COME.md) 20 |
21 | [8. VP8 PACKET DECODE](./08-VP8-PACKET-DECODE.md) 22 |
23 | [9. CONCLUSION](./09-CONCLUSION.md)
24 | 25 | --- 26 | 27 |
28 | 29 | [<<  Home: README](../README.md)      |      [Next chapter: INFRASTRUCTURE  >](./00-INFRASTRUCTURE.md) 30 | 31 |
32 | -------------------------------------------------------------------------------- /docs/images/01-01-open-folder-in-container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-01-open-folder-in-container.png -------------------------------------------------------------------------------- /docs/images/01-02-select-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-02-select-folder.png -------------------------------------------------------------------------------- /docs/images/01-03-starting-dev-container-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-03-starting-dev-container-small.png -------------------------------------------------------------------------------- /docs/images/01-04-starting-dev-container-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-04-starting-dev-container-log.png -------------------------------------------------------------------------------- /docs/images/01-05-install-go-deps-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-05-install-go-deps-small.png -------------------------------------------------------------------------------- /docs/images/01-06-install-go-deps-log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-06-install-go-deps-log.png -------------------------------------------------------------------------------- /docs/images/01-07-backend-initial-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/01-07-backend-initial-output.png -------------------------------------------------------------------------------- /docs/images/03-01-browser-first-visit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-01-browser-first-visit.png -------------------------------------------------------------------------------- /docs/images/03-02-browser-ask-permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-02-browser-ask-permissions.png -------------------------------------------------------------------------------- /docs/images/03-03-browser-onnegotiationneeded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-03-browser-onnegotiationneeded.png -------------------------------------------------------------------------------- /docs/images/03-04-browser-received-welcome-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-04-browser-received-welcome-message.png -------------------------------------------------------------------------------- /docs/images/03-05-server-a-new-client-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-05-server-a-new-client-connected.png -------------------------------------------------------------------------------- /docs/images/03-06-browser-received-sdpoffer-message-json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-06-browser-received-sdpoffer-message-json.png -------------------------------------------------------------------------------- /docs/images/03-07-browser-received-sdpoffer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-07-browser-received-sdpoffer.png -------------------------------------------------------------------------------- /docs/images/03-08-browser-onsignalingstatechange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-08-browser-onsignalingstatechange.png -------------------------------------------------------------------------------- /docs/images/03-09-browser-generate-sdpanswer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-09-browser-generate-sdpanswer.png -------------------------------------------------------------------------------- /docs/images/03-10-browser-ice-events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-10-browser-ice-events.png -------------------------------------------------------------------------------- /docs/images/03-11-browser-send-sdp-answer-signaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-11-browser-send-sdp-answer-signaling.png -------------------------------------------------------------------------------- /docs/images/03-12-server-receive-sdpanswer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/03-12-server-receive-sdpanswer.png -------------------------------------------------------------------------------- /docs/images/04-01-server-received-stun-binding-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/04-01-server-received-stun-binding-request.png -------------------------------------------------------------------------------- /docs/images/05-01-received-first-clienthello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-01-received-first-clienthello.png -------------------------------------------------------------------------------- /docs/images/05-02-sent-helloverifyrequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-02-sent-helloverifyrequest.png -------------------------------------------------------------------------------- /docs/images/05-03-received-second-clienthello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-03-received-second-clienthello.png -------------------------------------------------------------------------------- /docs/images/05-04-processed-second-clienthello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-04-processed-second-clienthello.png -------------------------------------------------------------------------------- /docs/images/05-05-sent-serverhello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-05-sent-serverhello.png -------------------------------------------------------------------------------- /docs/images/05-06-sent-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-06-sent-certificate.png -------------------------------------------------------------------------------- /docs/images/05-07-sent-serverkeyexchange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-07-sent-serverkeyexchange.png -------------------------------------------------------------------------------- /docs/images/05-08-sent-certificaterequest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-08-sent-certificaterequest.png -------------------------------------------------------------------------------- /docs/images/05-09-sent-serverhellodone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-09-sent-serverhellodone.png -------------------------------------------------------------------------------- /docs/images/05-10-received-certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-10-received-certificate.png -------------------------------------------------------------------------------- /docs/images/05-11-received-clientkeyexchange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-11-received-clientkeyexchange.png -------------------------------------------------------------------------------- /docs/images/05-12-message-concatenation-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-12-message-concatenation-result.png -------------------------------------------------------------------------------- /docs/images/05-13-init-gcm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-13-init-gcm.png -------------------------------------------------------------------------------- /docs/images/05-14-received-certificateverify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-14-received-certificateverify.png -------------------------------------------------------------------------------- /docs/images/05-15-received-changecipherspec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-15-received-changecipherspec.png -------------------------------------------------------------------------------- /docs/images/05-16-received-finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-16-received-finished.png -------------------------------------------------------------------------------- /docs/images/05-17-sent-changecipherspec.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-17-sent-changecipherspec.png -------------------------------------------------------------------------------- /docs/images/05-18-sent-finished.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/05-18-sent-finished.png -------------------------------------------------------------------------------- /docs/images/06-01-srtp-initialization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/06-01-srtp-initialization.png -------------------------------------------------------------------------------- /docs/images/07-01-received-rtp-packet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/07-01-received-rtp-packet.png -------------------------------------------------------------------------------- /docs/images/08-01-image-saved.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/images/08-01-image-saved.png -------------------------------------------------------------------------------- /docs/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/mkdocs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !mkdocs.yml.template 4 | !*.sh 5 | !stylesheets 6 | !stylesheets/**/* 7 | !javascripts 8 | !javascripts/**/* 9 | !assets 10 | !assets/**/* 11 | !extra-content 12 | !extra-content/**/* 13 | -------------------------------------------------------------------------------- /docs/mkdocs/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/docs/mkdocs/assets/icon.png -------------------------------------------------------------------------------- /docs/mkdocs/assets/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/mkdocs/execute-mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker run --rm -it -v ${PWD}/docs:/docs --entrypoint /docs/mkdocs/prepare-mkdocs.sh squidfunk/mkdocs-material 4 | docker run --rm -it -p 8000:8000 -v ${PWD}/docs/mkdocs:/docs squidfunk/mkdocs-material -------------------------------------------------------------------------------- /docs/mkdocs/javascripts/mathjax.js: -------------------------------------------------------------------------------- 1 | window.MathJax = { 2 | tex: { 3 | inlineMath: [["\\(", "\\)"]], 4 | displayMath: [["\\[", "\\]"]], 5 | processEscapes: true, 6 | processEnvironments: true 7 | }, 8 | options: { 9 | ignoreHtmlClass: ".*|", 10 | processHtmlClass: "arithmatex" 11 | } 12 | }; 13 | 14 | document$.subscribe(() => { 15 | MathJax.startup.output.clearCache() 16 | MathJax.typesetClear() 17 | MathJax.texReset() 18 | MathJax.typesetPromise() 19 | }) 20 | -------------------------------------------------------------------------------- /docs/mkdocs/mkdocs.yml.template: -------------------------------------------------------------------------------- 1 | site_name: WebRTC Nuts and Bolts 2 | site_url: https://adalkiran.github.io/webrtc-nuts-and-bolts/ 3 | site_author: Adil Alper DALKIRAN 4 | site_description: A holistic way of understanding how WebRTC and its protocols run in practice, with code and detailed documentation. 5 | copyright: Copyright © 2022 - present, Adil Alper DALKIRAN. All rights reserved. 6 | repo_url: https://github.com/adalkiran/webrtc-nuts-and-bolts 7 | repo_name: adalkiran/webrtc-nuts-and-bolts 8 | 9 | theme: 10 | name: 'material' 11 | logo: 'assets/icon.svg' 12 | favicon: 'assets/icon.png' 13 | features: 14 | - toc.follow 15 | - toc.integrate 16 | - navigation.tabs 17 | - navigation.tabs.sticky 18 | - navigation.top 19 | - navigation.tracking 20 | - navigation.footer 21 | - content.code.copy 22 | icon: 23 | repo: fontawesome/brands/github 24 | palette: 25 | # Palette toggle for automatic mode 26 | - media: "(prefers-color-scheme)" 27 | toggle: 28 | icon: material/brightness-auto 29 | name: Switch to light mode 30 | 31 | # Palette toggle for light mode 32 | - media: "(prefers-color-scheme: light)" 33 | scheme: default 34 | toggle: 35 | icon: material/weather-night 36 | name: Switch to dark mode 37 | 38 | # Palette toggle for dark mode 39 | - media: "(prefers-color-scheme: dark)" 40 | scheme: slate 41 | toggle: 42 | icon: material/weather-sunny 43 | name: Switch to system preference 44 | 45 | extra_css: 46 | - stylesheets/custom.css 47 | 48 | nav: 49 | - LLaMA Nuts and Bolts: '../llama-nuts-and-bolts' 50 | - 'WebRTC Nuts and Bolts': 51 | {{navigation_placeholder}} 52 | - 'Contact': 'https://www.linkedin.com/in/alper-dalkiran/' 53 | 54 | extra: 55 | social: 56 | - icon: fontawesome/brands/github 57 | name: 'adalkiran' 58 | link: https://github.com/adalkiran 59 | - icon: fontawesome/brands/x-twitter 60 | name: '@aadalkiran' 61 | link: https://www.linkedin.com/in/alper-dalkiran/ 62 | - icon: fontawesome/brands/linkedin 63 | name: 'in/alper-dalkiran' 64 | link: https://www.linkedin.com/in/alper-dalkiran/ 65 | analytics: 66 | provider: google 67 | property: G-05VMCF3NF0 68 | # consent: 69 | # title: Cookie consent 70 | # description: >- 71 | # We use cookies to recognize your repeated visits and preferences, as well 72 | # as to measure the effectiveness of our documentation and whether users 73 | # find what they're searching for. With your consent, you're helping us to 74 | # make our documentation better. 75 | 76 | markdown_extensions: 77 | - admonition 78 | - toc: 79 | permalink: true 80 | - md_in_html 81 | - pymdownx.arithmatex: 82 | generic: true 83 | inline_syntax: ['dollar'] 84 | - pymdownx.highlight: 85 | anchor_linenums: true 86 | line_spans: __span 87 | pygments_lang_class: true 88 | use_pygments: true 89 | - pymdownx.inlinehilite 90 | - pymdownx.snippets 91 | - pymdownx.superfences 92 | - pymdownx.emoji: 93 | emoji_index: !!python/name:material.extensions.emoji.twemoji 94 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 95 | 96 | plugins: 97 | - social 98 | 99 | extra_javascript: 100 | - javascripts/mathjax.js 101 | - https://polyfill.io/v3/polyfill.min.js?features=es6 102 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 103 | -------------------------------------------------------------------------------- /docs/mkdocs/prepare-mkdocs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | cd mkdocs 6 | rm -rf docs && mkdir -p docs 7 | cp -r ../*.md ../images ./stylesheets ./javascripts ./assets ./docs 8 | 9 | rm ./docs/README.md 10 | cp ./extra-content/*.md ./docs 11 | 12 | GITHUB_URL="https://github.com/adalkiran/webrtc-nuts-and-bolts" 13 | 14 | GITHUB_CONTENT_PREFIX="$GITHUB_URL/blob/main" 15 | 16 | nav_items="" 17 | 18 | for file in ./docs/*.md; do 19 | 20 | # Remove footer navigation 21 | sed -i -e ':a;N;$!ba; s/\s*
\s*---*.*//g; ta' "$file" 22 | 23 | # Edit same level paths 24 | sed -i -e 's/\](\.\//\](/g' "$file" 25 | 26 | # Edit upper level paths 27 | sed -i -e "s,\(\[[^\[]*\]\)(\.\.\/\([^)]*\)),\1($GITHUB_CONTENT_PREFIX\/\2),g" "$file" 28 | sed -i -e "s,\(\[.*\]\)(\.\.\/\([^)]*\)),\1($GITHUB_CONTENT_PREFIX\/\2),g" "$file" 29 | sed -i -e "s,\(\[.*\]\)(\([^)]*\.ipynb\)),\1($GITHUB_CONTENT_PREFIX\/docs\/\2),g" "$file" 30 | 31 | # Edit img tag paths 32 | sed -i -e 's/src="images\//src="..\/images\//g' "$file" 33 | 34 | # Edit external links 35 | sed -i -e "/\!\[/!s,\[\([^\[]*\)\](http\([^)]*\)),\\1\<\/a\>,g" "$file" 36 | sed -i -e "/\!\[/!s,\[\(.*\)\](http\([^)]*\)),\\1\<\/a\>,g" "$file" 37 | 38 | file_name=$(basename "$file") 39 | first_line=$(head -1 "$file") 40 | title=$(echo "$first_line" | sed -e 's/^[^\.]*\. \(.*\)\*\*/\1/g') 41 | chapter_num=$(echo "$first_line" | sed -e 's/^[^0-9]*\([0-9]*\)\..*/\1/g') 42 | case $first_line in 43 | "---"*) 44 | nav_items="\n - '$file_name'${nav_items}" 45 | ;; 46 | *) 47 | if [ ${#chapter_num} -lt 3 ]; then 48 | chapter_num=$(expr $chapter_num + 1) 49 | nav_items="${nav_items}\n - '$file_name'" 50 | else 51 | chapter_num=0 52 | nav_items="\n - '$file_name'${nav_items}" 53 | fi 54 | echo "--- 55 | title: $title 56 | type: docs 57 | menus: 58 | - main 59 | weight: $chapter_num 60 | --- 61 | " | cat - "$file" > temp && mv temp "$file" 62 | ;; 63 | esac 64 | done 65 | 66 | cat mkdocs.yml.template | sed -e "s/{{navigation_placeholder}}/${nav_items}/g" > mkdocs.yml 67 | -------------------------------------------------------------------------------- /docs/mkdocs/stylesheets/custom.css: -------------------------------------------------------------------------------- 1 | mjx-container { 2 | font-size: 16px!important; 3 | } -------------------------------------------------------------------------------- /ui/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // See: https://code.visualstudio.com/docs/remote/containers-advanced#_connecting-to-multiple-containers-at-once 2 | 3 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 4 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.195.0/containers/javascript-node 5 | { 6 | "name": "WebRTC Nuts and Bolts UI Container", 7 | 8 | "dockerComposeFile": ["../../docker-compose.yml", "../../docker-compose.dev.yml"], 9 | "service": "ui", 10 | "shutdownAction": "none", 11 | 12 | 13 | "workspaceFolder": "/workspace", 14 | 15 | 16 | // Set *default* container specific settings.json values on container create. 17 | "settings": { 18 | }, 19 | 20 | // Add the IDs of extensions you want installed when the container is created. 21 | "extensions": [ 22 | "dbaeumer.vscode-eslint" 23 | ], 24 | 25 | 26 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 27 | "remoteUser": "node" 28 | } -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | yarn.lock -------------------------------------------------------------------------------- /ui/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/devcontainers/images/blob/main/src/typescript-node/.devcontainer/Dockerfile 2 | # See for available variants: https://github.com/devcontainers/images/tree/main/src/typescript-node 3 | ARG VARIANT=22-bookworm 4 | FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:${VARIANT} 5 | 6 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive 7 | # && apt-get -y install --no-install-recommends 8 | 9 | # [Optional] Uncomment if you want to install an additional version of node using nvm 10 | # ARG EXTRA_NODE_VERSION=10 11 | # RUN su node -c "umask 0002 && ./usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 12 | 13 | # [Optional] Uncomment if you want to install more global node modules 14 | # RUN su node -c "npm install -g " 15 | 16 | WORKDIR /workspace 17 | 18 | ENTRYPOINT yarn install && npm run start -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webrtc-nuts-and-bolts-ui", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "watch": "webpack --watch", 9 | "start": "webpack serve", 10 | "build": "webpack" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "@types/sdp-transform": "^2.4.6", 17 | "css-loader": "^6.7.3", 18 | "html-webpack-plugin": "^5.5.0", 19 | "style-loader": "^3.3.2", 20 | "ts-loader": "^9.4.2", 21 | "typescript": "^5.0.2", 22 | "webpack": "^5.76.2", 23 | "webpack-cli": "^5.0.1", 24 | "webpack-dev-server": "^4.13.1" 25 | }, 26 | "dependencies": { 27 | "@types/jquery": "^3.5.16", 28 | "jquery": "^3.6.4", 29 | "sdp-transform": "^2.14.1", 30 | "socket.io-client": "^4.6.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WebRTC Nuts and Bolts Demo 6 | 7 | 16 | 17 | 18 | 19 |    20 | 21 |    22 | Connection status: not connected 23 | 24 | 25 | -------------------------------------------------------------------------------- /ui/src/sdp.ts: -------------------------------------------------------------------------------- 1 | type MediaType = 'audio' | 'video' 2 | type CandidateType = 'host' 3 | type TransportType = 'udp' | 'tcp' 4 | type FingerprintType = 'sha-256' 5 | 6 | 7 | class SdpMessage { 8 | sessionId: string 9 | mediaItems: SdpMedia[] 10 | } 11 | 12 | class SdpMedia { 13 | mediaId: number 14 | type: MediaType 15 | ufrag: string 16 | pwd: string 17 | fingerprintType: FingerprintType 18 | fingerprintHash: string 19 | candidates: SdpMediaCandidate[] 20 | payloads: string 21 | rtpCodec: string 22 | } 23 | 24 | class SdpMediaCandidate { 25 | ip: string 26 | port: number 27 | type: CandidateType 28 | transport: TransportType 29 | } 30 | 31 | export {SdpMessage} -------------------------------------------------------------------------------- /ui/src/style/main.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adalkiran/webrtc-nuts-and-bolts/7669f595f691b0ecab383e67fbfd67354894c225/ui/src/style/main.css -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "outDir": "./dist/", 5 | "noImplicitAny": true, 6 | "module": "es6", 7 | "target": "es5", 8 | "allowJs": true, 9 | "moduleResolution": "node" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: "development", 6 | entry: { 7 | index: "./src/app.ts", 8 | }, 9 | devtool: "inline-source-map", 10 | devServer: { 11 | host: "0.0.0.0", 12 | devMiddleware: { 13 | publicPath: "/" 14 | }, 15 | static: { 16 | directory: "./dist" 17 | }, 18 | hot: true 19 | }, 20 | output: { 21 | filename: "[name].bundle.js", 22 | path: path.resolve(__dirname, "dist"), 23 | clean: true, 24 | }, 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: 'ts-loader', 30 | exclude: /node_modules/, 31 | }, 32 | { 33 | test: /\.css$/, 34 | use: ["style-loader", "css-loader"] 35 | } 36 | ] 37 | }, 38 | plugins: [ 39 | new HtmlWebpackPlugin({ 40 | hash: true, 41 | template: path.resolve(__dirname, "src", "index.html"), 42 | filename: path.resolve(__dirname, "dist", "index.html") 43 | }), 44 | ], 45 | }; 46 | --------------------------------------------------------------------------------