From 9fe48bf78dfac66cc8524e55cacebeedad81fc2c Mon Sep 17 00:00:00 2001 From: Kostas Drakontidis Date: Sat, 26 Jul 2025 00:55:34 +0300 Subject: [PATCH] Upload server --- server/Makefile | 9 + server/config.yml | 86 ++++++++++ server/go.mod | 33 ++++ server/go.sum | 63 +++++++ server/main.go | 409 ++++++++++++++++++++++++++++++++++++++++++++++ server/types.go | 110 +++++++++++++ 6 files changed, 710 insertions(+) create mode 100644 server/Makefile create mode 100644 server/config.yml create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/main.go create mode 100644 server/types.go diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..2fb3825 --- /dev/null +++ b/server/Makefile @@ -0,0 +1,9 @@ +CC := go +NAME := server +OUT := bin/ + +all: main.go types.go + $(CC) build -o $(OUT)$(NAME) $^ + +clean: + $(RM) -r $(OUT) \ No newline at end of file diff --git a/server/config.yml b/server/config.yml new file mode 100644 index 0000000..7d2dfe6 --- /dev/null +++ b/server/config.yml @@ -0,0 +1,86 @@ +config: + SIGNALING_SERVER: "ws://" + PROXY_SERVER: "http://" + AUTHENTICATION: + USERNAME: "username" + PASSWORD: "password" + ICE_SERVERS: + - urls: + - "stun:stun.l.google.com:19302" + - urls: + - "turn:" + username: "turn_username" + credential: "turn_password" +messages: + START: "Loaded config. Starting...\n" + WS: + CONNECT: + SUCCESS: "Connected to WebSocket.\n" + ERROR: "Could not connect to WebSocket.\n %v\n\n" + CLOSE: + SUCCESS: "Disconnected from WebSocket.\n" + ERROR: "Could not close WebSocket connection.\n %v\n\n" + REGISTER: + SUCCESS: "Registered to WebSocket.\n" + ERROR: "Could not send register message.\n %v\n\n" + READ: + SUCCESS: "Received data from WebSocket.\n" + ERROR: "Could not read data from WebSocket.\n %v\n\n" + WRITE: + SUCCESS: "Sent data to WebSocket.\n" + ERROR: "Could not send data to WebSocket.\n %v\n\n" + JSON: + MARSHAL: + SUCCESS: "Converted struct to JSON string.\n" + ERROR: "Could not convert struct to JSON string.\n %v\n\n" + UNMARSHAL: + SUCCESS: "Converted JSON string to struct.\n" + ERROR: "Could not convert JSON string to struct.\n %v\n\n" + FORMAT: + SUCCESS: "Parsed JSON object type with value %s.\n" + ERROR: "Could not parse JSON object type.\n" + TYPE: + SUCCESS: "Handling JSON object as %s.\n" + ERROR: "Unknown JSON object %s.\n" + WEBRTC: + NEW: + SUCCESS: "Created WebRTC instance.\n" + ERROR: "Could not create WebRTC instance.\n %v\n\n" + CLOSE: + SUCCESS: "Closed P2P connection.\n" + ERROR: "Could not close P2P connection.\n %v\n\n" + ANSWER: + SUCCESS: "Created answer.\n" + ERROR: "Could not create answer.\n %v\n\n" + LOCAL_DESCRIPTION: + SUCCESS: "Set local description.\n" + ERROR: "Could not set local description.\n %v\n\n" + REMOTE_DESCRIPTION: + SUCCESS: "Set remote description.\n" + ERROR: "Could not set remote description.\n %v\n\n" + ADD_CANDIDATE: + SUCCESS: "Added ICE candidate.\n" + ERROR: "Could not add ICE candidate.\n %v\n\n" + SEND_DATA_CHANNEL: + SUCCESS: "Send data to data channel.\n" + ERROR: "Could not send data to data channel.\n %v\n\n" + ON_CANDIDATE: "Discovered ICE candidate.\n" + ON_DATA_CHANNEL: "Opened data channel.\n" + ON_MESSAGE: "Received data from data channel.\n" + HTTP: + READ: + SUCCESS: "Read HTTP response body.\n" + ERROR: "Could not read HTTP response body.\n %v\n\n" + REQUEST: + SUCCESS: "Created HTTP request.\n" + ERROR: "Could not create HTTP request.\n %v\n\n" + SEND_REQUEST: + SUCCESS: "Sent HTTP request.\n" + ERROR: "Could not send HTTP request.\n %v\n\n" + GOB: + ENCODE: + SUCCESS: "Encoded struct to buffer.\n" + ERROR: "Could not encode struct to buffer.\n %v\n\n" + DECODE: + SUCCESS: "Converted buffer to struct.\n" + ERROR: "Could not convert buffer to struct.\n %v\n\n" diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..2ab140f --- /dev/null +++ b/server/go.mod @@ -0,0 +1,33 @@ +module WebRTC + +go 1.24.5 + +require ( + github.com/go-yaml/yaml v2.1.0+incompatible + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.3 + github.com/pion/webrtc/v4 v4.1.3 +) + +require ( + github.com/pion/datachannel v1.5.10 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect + github.com/pion/randutil v0.1.0 // indirect + github.com/pion/rtcp v1.2.15 // indirect + github.com/pion/rtp v1.8.20 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.14 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn/v4 v4.0.0 // indirect + github.com/wlynxg/anet v0.0.5 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..28da2ea --- /dev/null +++ b/server/go.sum @@ -0,0 +1,63 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o= +github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= +github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= +github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= +github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= +github.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI= +github.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI= +github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM= +github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA= +github.com/pion/webrtc/v4 v4.1.3 h1:YZ67Boj9X/hk190jJZ8+HFGQ6DqSZ/fYP3sLAZv7c3c= +github.com/pion/webrtc/v4 v4.1.3/go.mod h1:rsq+zQ82ryfR9vbb0L1umPJ6Ogq7zm8mcn9fcGnxomM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..dcc9723 --- /dev/null +++ b/server/main.go @@ -0,0 +1,409 @@ +/* + * HTTP tunnel over WebRTC - Server implementation + * Author: Konstantinos Drakontidis + * Email: gedra100sh@gmail.com + */ +package main + +import ( + "bytes" + "encoding/gob" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + + "github.com/go-yaml/yaml" + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/pion/webrtc/v4" +) + +var config_t Config + +/* + * This function gets: + * dc: pointer to data channel + * raw: slice of bytes + * Receives data from dc, decodes it, creates and sends a HTTP request, + * encodes the HTTP response and sends it back to the dc. + */ +func channelHandler(dc *webrtc.DataChannel, raw []byte) { + // Creating a buffer and store the received + buf := bytes.NewBuffer(raw) + // Creating a decoder and decode the data + dec := gob.NewDecoder(buf) + var request_t Request + decode_error := dec.Decode(&request_t) + if decode_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.GOB.DECODE.ERROR, decode_error) + return + } else { + fmt.Print(config_t.Messages.GOB.DECODE.SUCCESS) + } + + // Constructing the HTTP request + req_body := bytes.NewBuffer(request_t.Body) + req, req_error := http.NewRequest(request_t.Method, config_t.Config.PROXY_SERVER+request_t.Path, req_body) + if req_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.HTTP.REQUEST.ERROR, req_error) + return + } else { + fmt.Print(config_t.Messages.HTTP.REQUEST.SUCCESS) + } + req.Header = request_t.Headers + + // Creating a HTTP client - handles redirect requests as normal requests + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + // Submitting the request + res, send_req_error := client.Do(req) + if send_req_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.HTTP.SEND_REQUEST.ERROR, send_req_error) + return + } else { + fmt.Print(config_t.Messages.HTTP.SEND_REQUEST.SUCCESS) + } + + // Constructing the response + res_body, read_body_error := io.ReadAll(res.Body) + if read_body_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.HTTP.READ.ERROR, read_body_error) + return + } else { + fmt.Print(config_t.Messages.HTTP.READ.SUCCESS) + } + response_t := Response{ + ID: request_t.ID, + Status: res.StatusCode, + Headers: res.Header, + Body: res_body, + } + + // Encoding and sending the response to the data channel + var enc_buf bytes.Buffer + enc := gob.NewEncoder(&enc_buf) + encode_error := enc.Encode(response_t) + if encode_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.GOB.ENCODE.ERROR, encode_error) + return + } else { + fmt.Print(config_t.Messages.GOB.ENCODE.SUCCESS) + } + + data_channel_send_error := dc.Send(enc_buf.Bytes()) + if data_channel_send_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.SEND_DATA_CHANNEL.ERROR, data_channel_send_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.SEND_DATA_CHANNEL.SUCCESS) + } +} + +/* + * This function gets: + * conn: pointer to the websocket connection + * peer_connections: pointer to map of tracked peer connections + * raw: slice of bytes + * Receives a peer connection offer, creates a new peer connection, sets the + * listeners for it. Sets the remote descriptions, generates, sets and sends the answer. + */ +func offer(conn *websocket.Conn, peer_connections *map[uuid.UUID]*webrtc.PeerConnection, raw []byte) { + // Constructing the offer + var offer_t Offer + message_unmarshal_error := json.Unmarshal(raw, &offer_t) + if message_unmarshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.UNMARSHAL.ERROR, message_unmarshal_error) + return + } else { + fmt.Print(config_t.Messages.JSON.UNMARSHAL.SUCCESS) + } + + // Creating the peer configuration + config := webrtc.Configuration{ + ICEServers: config_t.Config.ICE_SERVERS, + } + + // Creating peer connection + var new_peer_error error + p2p, new_peer_error := webrtc.NewPeerConnection(config) + if new_peer_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.NEW.ERROR, new_peer_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.NEW.SUCCESS) + } + + // Setting the peer listeners + p2p.OnICEConnectionStateChange(func(is webrtc.ICEConnectionState) { + fmt.Printf("ICE: %s\n", is.String()) + }) + p2p.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) { + fmt.Printf("WebRTC: %s\n", pcs.String()) + }) + p2p.OnICECandidate(func(candidate *webrtc.ICECandidate) { + // Checking if ICE gathering has finished + if candidate != nil { + fmt.Print(config_t.Messages.WEBRTC.ON_CANDIDATE) + // Constructing candidate, encoding to JSON and sending to websocket + candidate_t := Candidate{ + Type: "candidate", + Id: offer_t.Id, + Candidate: (*candidate).ToJSON(), + } + // Create JSON string for candidate and send it to the websocket + candidate_msg, message_marshal_error := json.Marshal(candidate_t) + if message_marshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.MARSHAL.ERROR, message_marshal_error) + return + } else { + fmt.Print(config_t.Messages.JSON.MARSHAL.SUCCESS) + } + ws_write_error := conn.WriteMessage(websocket.TextMessage, candidate_msg) + if ws_write_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WS.WRITE.ERROR, ws_write_error) + return + } else { + fmt.Print(config_t.Messages.WS.WRITE.SUCCESS) + } + } + }) + p2p.OnDataChannel(func(dc *webrtc.DataChannel) { + dc.OnOpen(func() { + fmt.Print(config_t.Messages.WEBRTC.ON_DATA_CHANNEL) + }) + dc.OnMessage(func(msg webrtc.DataChannelMessage) { + fmt.Print(config_t.Messages.WEBRTC.ON_MESSAGE) + channelHandler(dc, msg.Data) + }) + }) + + // Setting offer as remote description + set_remote_description_error := p2p.SetRemoteDescription(offer_t.Offer) + if set_remote_description_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.REMOTE_DESCRIPTION.ERROR, + set_remote_description_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.REMOTE_DESCRIPTION.SUCCESS) + } + + // Generating answer + answer_options := webrtc.AnswerOptions{ + OfferAnswerOptions: webrtc.OfferAnswerOptions{ + VoiceActivityDetection: false, + }, + } + answer, create_answer_error := p2p.CreateAnswer(&answer_options) + if create_answer_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.ANSWER.ERROR, create_answer_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.ANSWER.SUCCESS) + } + + // Constructing answer, creating JSON from answer and sending it to the websocket + answer_t := Answer{ + Type: "answer", + Id: offer_t.Id, + Answer: answer, + } + answer_msg, message_marshal_error := json.Marshal(answer_t) + if message_marshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.MARSHAL.ERROR, message_marshal_error) + return + } else { + fmt.Print(config_t.Messages.JSON.MARSHAL.SUCCESS) + } + ws_write_error := conn.WriteMessage(websocket.TextMessage, answer_msg) + if ws_write_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WS.WRITE.ERROR, ws_write_error) + return + } else { + fmt.Print(config_t.Messages.WS.WRITE.SUCCESS) + } + // Setting answer as local description + set_local_description_error := (*p2p).SetLocalDescription(answer) + if set_local_description_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.LOCAL_DESCRIPTION.ERROR, + set_local_description_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.LOCAL_DESCRIPTION.SUCCESS) + } + + // Storing the peer connection to the map + (*peer_connections)[offer_t.Id] = p2p +} + +/* + * This function gets: + * peer_connections: pointer to map of tracked peer connections + * raw: slice of bytes + * Adds ICE candidate received from the signaling server. + * The function does not return anything. + */ +func candidate(peer_connections *map[uuid.UUID]*webrtc.PeerConnection, raw []byte) { + // Constructing candidate from received data + var candidate_t Candidate + message_unmarshal_error := json.Unmarshal(raw, &candidate_t) + if message_unmarshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.UNMARSHAL.ERROR, message_unmarshal_error) + return + } else { + fmt.Print(config_t.Messages.JSON.UNMARSHAL.SUCCESS) + } + + // Getting peer instance from map + p2p := (*peer_connections)[candidate_t.Id] + // Adding candidate to peer connection + add_ice_error := p2p.AddICECandidate(candidate_t.Candidate) + if add_ice_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.ADD_CANDIDATE.ERROR, add_ice_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.ADD_CANDIDATE.SUCCESS) + } +} + +/* + * This function gets: + * peer_connections: pointer to map of tracked peer connections + * raw: slice of bytes + * Terminates a peer connection. + */ +func terminate(peer_connections *map[uuid.UUID]*webrtc.PeerConnection, raw []byte) { + var terminate_t Terminate + message_unmarshal_error := json.Unmarshal(raw, &terminate_t) + if message_unmarshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.UNMARSHAL.ERROR, message_unmarshal_error) + return + } else { + fmt.Print(config_t.Messages.JSON.UNMARSHAL.SUCCESS) + } + + // Getting peer instance from map and closing the connection + p2p := (*peer_connections)[terminate_t.Id] + p2p_close_error := p2p.Close() + if p2p_close_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WEBRTC.CLOSE.ERROR, p2p_close_error) + return + } else { + fmt.Print(config_t.Messages.WEBRTC.CLOSE.SUCCESS) + } + // Remove peer connection entry from map + delete(*peer_connections, terminate_t.Id) +} + +func main() { + // Attempting to read the config file + raw_config, config_error := os.ReadFile("config.yml") + if config_error != nil { + fmt.Fprintf(os.Stderr, "Could not read config file.\n%v\n", config_error) + os.Exit(-1) + } + + // Loading config file to config struct + config_error = yaml.Unmarshal(raw_config, &config_t) + if config_error != nil { + fmt.Fprintf(os.Stderr, "Could not load config file.\n%v\n", config_error) + os.Exit(-1) + } + + // Starting program + fmt.Print(config_t.Messages.START) + peer_connections := make(map[uuid.UUID]*webrtc.PeerConnection) + + // Attempting to connect to the signaling server via WebSocket + conn, _, dial_error := websocket.DefaultDialer.Dial(config_t.Config.SIGNALING_SERVER, nil) + if dial_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WS.CONNECT.ERROR, dial_error) + os.Exit(-1) + } else { + fmt.Print(config_t.Messages.WS.CONNECT.SUCCESS) + } + + defer func() { + ws_close_error := conn.Close() + if ws_close_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WS.CLOSE.ERROR, ws_close_error) + } else { + fmt.Print(config_t.Messages.WS.CLOSE.SUCCESS) + } + }() + + // Constructing the register message + register_t := Register{ + Type: "register", + Username: config_t.Config.AUTHENTICATION.USERNAME, + Password: config_t.Config.AUTHENTICATION.PASSWORD, + } + // Encoding register struct to JSON + register_msg, register_marshal_error := json.Marshal(register_t) + if register_marshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.MARSHAL.ERROR, register_marshal_error) + return + } else { + fmt.Print(config_t.Messages.JSON.MARSHAL.SUCCESS) + } + + // Registering to the signaling server as normal user + ws_write_error := conn.WriteMessage(websocket.TextMessage, register_msg) + if ws_write_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WS.REGISTER.ERROR, ws_write_error) + return + } else { + fmt.Print(config_t.Messages.WS.REGISTER.SUCCESS) + } + + for { + // Reading message + _, ws_msg, ws_read_error := conn.ReadMessage() + if ws_read_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.WS.READ.ERROR, ws_read_error) + os.Exit(-1) + } else { + fmt.Print(config_t.Messages.WS.READ.SUCCESS) + } + + // Decoding as any-format JSON + var parsed map[string]any + message_unmarshal_error := json.Unmarshal(ws_msg, &parsed) + if message_unmarshal_error != nil { + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.UNMARSHAL.ERROR, message_unmarshal_error) + continue + } else { + fmt.Print(config_t.Messages.JSON.UNMARSHAL.SUCCESS) + } + + // Parsing message type + msg_type, exists := parsed["type"].(string) + if !exists { + fmt.Fprint(os.Stderr, config_t.Messages.JSON.FORMAT.ERROR) + continue + } else { + fmt.Printf(config_t.Messages.JSON.FORMAT.SUCCESS, msg_type) + } + + // Setting handler function for each message type + switch msg_type { + case "offer": + fmt.Printf(config_t.Messages.JSON.TYPE.SUCCESS, msg_type) + offer(conn, &peer_connections, ws_msg) + case "candidate": + fmt.Printf(config_t.Messages.JSON.TYPE.SUCCESS, msg_type) + candidate(&peer_connections, ws_msg) + case "terminate": + fmt.Printf(config_t.Messages.JSON.TYPE.SUCCESS, msg_type) + terminate(&peer_connections, ws_msg) + default: + fmt.Fprintf(os.Stderr, config_t.Messages.JSON.TYPE.ERROR, msg_type) + continue + } + } +} diff --git a/server/types.go b/server/types.go new file mode 100644 index 0000000..a664b39 --- /dev/null +++ b/server/types.go @@ -0,0 +1,110 @@ +/* + * HTTP tunnel over WebRTC - Server types + * Author: Konstantinos Drakontidis + * Email: gedra100sh@gmail.com + */ +package main + +import ( + "github.com/google/uuid" + "github.com/pion/webrtc/v4" +) + +type Terminate struct { + Type string `json:"type"` + Id uuid.UUID `json:"id"` +} + +type Answer struct { + Type string `json:"type"` + Id uuid.UUID `json:"id"` + Answer webrtc.SessionDescription `json:"answer"` +} + +type Offer struct { + Type string `json:"type"` + Id uuid.UUID `json:"id"` + Offer webrtc.SessionDescription `json:"offer"` +} + +type Candidate struct { + Type string `json:"type"` + Id uuid.UUID `json:"id"` + Candidate webrtc.ICECandidateInit `json:"candidate"` +} + +type Register struct { + Type string `json:"type"` + Username string `json:"username"` + Password string `json:"password"` +} + +type Request struct { + ID string `json:"id"` + Method string `json:"method"` + Path string `json:"path"` + Headers map[string][]string `json:"headers"` + Body []byte `json:"body"` +} + +type Response struct { + ID string `json:"id"` + Status int `json:"status,omitempty"` + Headers map[string][]string `json:"headers,omitempty"` + Body []byte `json:"body,omitempty"` + Error string `json:"error,omitempty"` +} + +type Message struct { + SUCCESS string `yaml:"SUCCESS"` + ERROR string `yaml:"ERROR"` +} + +type Config struct { + Config struct { + SIGNALING_SERVER string `yaml:"SIGNALING_SERVER"` + PROXY_SERVER string `yaml:"PROXY_SERVER"` + AUTHENTICATION struct { + USERNAME string `yaml:"USERNAME"` + PASSWORD string `yaml:"PASSWORD"` + } `yaml:"AUTHENTICATION"` + ICE_SERVERS []webrtc.ICEServer `yaml:"ICE_SERVERS"` + } `yaml:"config"` + Messages struct { + START string `yaml:"START"` + WS struct { + CONNECT Message `yaml:"CONNECT"` + CLOSE Message `yaml:"CLOSE"` + REGISTER Message `yaml:"REGISTER"` + READ Message `yaml:"READ"` + WRITE Message `yaml:"WRITE"` + } `yaml:"WS"` + JSON struct { + MARSHAL Message `yaml:"MARSHAL"` + UNMARSHAL Message `yaml:"UNMARSHAL"` + FORMAT Message `yaml:"FORMAT"` + TYPE Message `yaml:"TYPE"` + } `yaml:"JSON"` + WEBRTC struct { + NEW Message `yaml:"NEW"` + CLOSE Message `yaml:"CLOSE"` + ANSWER Message `yaml:"ANSWER"` + LOCAL_DESCRIPTION Message `yaml:"LOCAL_DESCRIPTION"` + REMOTE_DESCRIPTION Message `yaml:"REMOTE_DESCRIPTION"` + ADD_CANDIDATE Message `yaml:"ADD_CANDIDATE"` + SEND_DATA_CHANNEL Message `yaml:"SEND_DATA_CHANNEL"` + ON_CANDIDATE string `yaml:"ON_CANDIDATE"` + ON_DATA_CHANNEL string `yaml:"ON_DATA_CHANNEL"` + ON_MESSAGE string `yaml:"ON_MESSAGE"` + } `yaml:"WEBRTC"` + HTTP struct { + READ Message `yaml:"READ"` + REQUEST Message `yaml:"REQUEST"` + SEND_REQUEST Message `yaml:"SEND_REQUEST"` + } `yaml:"HTTP"` + GOB struct { + ENCODE Message `yaml:"ENCODE"` + DECODE Message `yaml:"DECODE"` + } `yaml:"GOB"` + } `yaml:"messages"` +}