diff --git a/.github/workflows/golangci-lint.yaml b/.github/workflows/golangci-lint.yaml new file mode 100644 index 0000000..7c87b3c --- /dev/null +++ b/.github/workflows/golangci-lint.yaml @@ -0,0 +1,34 @@ +name: golangci-lint +on: [push, pull_request] + +permissions: + contents: read + pull-requests: read + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: WillAbides/setup-go-faster@v1 + with: + go-version: '1.21' + - name: golangci-lint-server + uses: golangci/golangci-lint-action@v3 + with: + version: latest + only-new-issues: true + working-directory: ./server + - name: golangci-lint-forwarder + uses: golangci/golangci-lint-action@v3 + with: + version: latest + only-new-issues: true + working-directory: ./forwarder + - name: golangci-lint-recorder + uses: golangci/golangci-lint-action@v3 + with: + version: latest + only-new-issues: true + working-directory: ./recorder \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..24e1c83 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,154 @@ +String registryEndpoint = 'registry.comp.ystv.co.uk' + +def branch = env.BRANCH_NAME.replaceAll("/", "_") +def image +String proceed = "yes" +String serverImageName = "ystv/streamer/server:${branch}-${env.BUILD_ID}" +String forwarderImageName = "ystv/streamer/forwarder:${branch}-${env.BUILD_ID}" +String recorderImageName = "ystv/streamer/recorder:${branch}-${env.BUILD_ID}" + +pipeline { + agent { + label 'docker' + } + + environment { + DOCKER_BUILDKIT = '1' + } + + stages { + stage('Build images') { + parallel { + stage('Build Server') { + steps { + script { + dir("server") { + docker.withRegistry('https://' + registryEndpoint, 'docker-registry') { + serverImage = docker.build(serverImageName, "--no-cache .") + } + } + } + } + } + stage('Build Forwarder') { + steps { + script { + dir("forwarder") { + docker.withRegistry('https://' + registryEndpoint, 'docker-registry') { + forwarderImage = docker.build(forwarderImageName, "--no-cache .") + } + } + } + } + } + stage('Build Recorder') { + steps { + script { + dir("recorder") { + docker.withRegistry('https://' + registryEndpoint, 'docker-registry') { + recorderImage = docker.build(recorderImageName, "--no-cache .") + } + } + } + } + } + } + } + + stage('Push images to registry') { + parallel { + stage('Push Server image to registry') { + steps { + script { + docker.withRegistry('https://' + registryEndpoint, 'docker-registry') { + serverImage.push() + if (env.BRANCH_IS_PRIMARY) { + serverImage.push('latest') + } + } + } + } + } + stage('Push Forwarder image to registry') { + steps { + script { + docker.withRegistry('https://' + registryEndpoint, 'docker-registry') { + forwarderImage.push() + if (env.BRANCH_IS_PRIMARY) { + forwarderImage.push('latest') + } + } + } + } + } + stage('Push Recorder image to registry') { + steps { + script { + docker.withRegistry('https://' + registryEndpoint, 'docker-registry') { + recorderImage.push() + if (env.BRANCH_IS_PRIMARY) { + recorderImage.push('latest') + } + } + } + } + } + } + } + + stage('Deploy') { + stages { + stage('Checking existing') { + steps { + script { + final String url = "https://streamer.dev.ystv.co.uk/activeStreams" + final def (String response, String tempCode) = + sh(script: "curl -s -w '~~~%{response_code}' $url", returnStdout: true) + .trim() + .tokenize("~~~") + int code = Integer.parseInt(tempCode) + + echo "HTTP response status code: $code" + if (response.contains("\"stream\":")) { + echo "HTTP response: $response" + + if (code == 200) { + def streams = sh(script: "echo '$response' | jq -M '.streams'", returnStdout: true) + if (streams > 0) { + proceed = "no" + } + } + } else { + echo "HTTP response not JSON, proceeding..." + } + } + } + } + stage('Development') { + when { + expression { env.BRANCH_IS_PRIMARY && proceed == "yes" } + } + steps { + build(job: 'Deploy Nomad Job', parameters: [ + string(name: 'JOB_FILE', value: 'streamer-dev.nomad'), + text(name: 'TAG_REPLACEMENTS', value: "${registryEndpoint}/${serverImageName} ${registryEndpoint}/${forwarderImageName} ${registryEndpoint}/${recorderImageName}") + ]) + } + } + + stage('Production') { + when { + // Checking if it is semantic version release. + expression { return env.TAG_NAME ==~ /v(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)/ && proceed == "yes" } + } + steps { + build(job: 'Deploy Nomad Job', parameters: [ + string(name: 'JOB_FILE', value: 'streamer-prod.nomad'), + text(name: 'TAG_REPLACEMENTS', value: "${registryEndpoint}/${serverImageName} ${registryEndpoint}/${forwarderImageName} ${registryEndpoint}/${recorderImageName}") + ]) + } + } + } + } + } +} \ No newline at end of file diff --git a/forwarder/Dockerfile b/forwarder/Dockerfile new file mode 100644 index 0000000..8e14935 --- /dev/null +++ b/forwarder/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.21.1-alpine3.18 AS build + +LABEL site="ystv-streamer-forwarder" + +VOLUME /logs + +WORKDIR /src/ + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/forwarder + +EXPOSE 1323 + +ENTRYPOINT ["/bin/forwarder"] \ No newline at end of file diff --git a/forwarder/example.env b/forwarder/example.env index e049061..467d55a 100644 --- a/forwarder/example.env +++ b/forwarder/example.env @@ -1 +1,3 @@ -STREAM_SERVER= \ No newline at end of file +STREAM_SERVER= +STREAMER_WEB_ADDRESS= +STREAMER_WEBSOCKET_PATH= \ No newline at end of file diff --git a/forwarder/forwarder_start.go b/forwarder/forwarder_start.go index 032f843..7344afb 100644 --- a/forwarder/forwarder_start.go +++ b/forwarder/forwarder_start.go @@ -1,70 +1,119 @@ package main import ( - "fmt" - "github.com/joho/godotenv" - "github.com/wricardo/gomux" + "github.com/patrickmn/go-cache" "log" - "os" + "os/exec" "strconv" - "strings" + "time" ) -func main() { - if strings.Contains(os.Args[0], "/var/folders") || strings.Contains(os.Args[0], "/tmp/go") || strings.Contains(os.Args[0], "./forwarder_start") { - if len(os.Args) < 5 { - log.Fatalf("echo Arguments error") - } - for i := 0; i < len(os.Args)-1; i++ { - os.Args[i] = os.Args[i+1] - } - } else { - if len(os.Args) < 4 { - log.Fatalf("echo Arguments error") - } - } - streamIn := os.Args[0] - websiteOut := os.Args[1] - unique := os.Args[2] - var serversKeys []string - for i := 3; i < len(os.Args)-1; i++ { - serversKeys = append(serversKeys, os.Args[i]) - } +func (v *Views) start(transporter Transporter) error { + streamIn := "rtmp://" + v.Config.StreamServer + transporter.Payload.(ForwarderStart).StreamIn + + if len(transporter.Payload.(ForwarderStart).WebsiteOut) > 0 { + finish := make(chan bool) - sessionName := "STREAM FORWARDER - " + unique + err := v.cache.Add(transporter.Unique+"_0Finish", finish, cache.NoExpiration) + if err != nil { + return err + } - s := gomux.NewSession(sessionName, os.Stdout) + go func() { + for { + v.cache.Delete(transporter.Unique + "_0") + switch { + case <-finish: + return + default: + c := exec.Command("ffmpeg", "-i", "\""+streamIn+"\"", "-c", "copy", "-f", "flv", "\""+v.Config.StreamServer+"live/"+transporter.Payload.(ForwarderStart).WebsiteOut+"\"", ">>", "\"/logs/"+transporter.Unique+"_0.txt\"", "2>&1") + err = v.cache.Add(transporter.Unique+"0", c, cache.NoExpiration) + if err != nil { + log.Println(err) + return + } + if err = c.Run(); err != nil { + log.Println("could not run command: ", err) + } + time.Sleep(500 * time.Millisecond) + } + } + }() - w1 := s.AddWindow("FORWARDING - 0") + go func() { + for { + switch { + case <-finish: + cmd, ok := v.cache.Get(transporter.Unique + "_0") + if !ok { + log.Println("unable to get cmd from cache") + } + c1 := cmd.(*exec.Cmd) + err = c1.Process.Kill() + if err != nil { + log.Println(err) + } + v.cache.Delete(transporter.Unique + "_0") + return + default: + time.Sleep(1 * time.Second) // This is so it doesn't spam constantly and take the entire CPU up + } + } + }() + } - var panes []*gomux.Pane + for i := 0; i < len(transporter.Payload.(ForwarderStart).Streams); i++ { + finish := make(chan bool) - err := godotenv.Load() - if err != nil { - fmt.Printf("echo Error loading .env file: %s", err) - } else { - streamServer := os.Getenv("STREAM_SERVER") - if websiteOut != "no" { - panes = append(panes, w1.Pane(0)) - panes[0].Exec("./forwarder_start.sh " + streamServer + streamIn + " " + streamServer + "live/" + websiteOut + " " + unique + " " + strconv.Itoa(0) + " | bash") - } else { - panes = append(panes, w1.Pane(0)) - panes[0].Exec("echo No website stream") + err := v.cache.Add(transporter.Unique+"_"+strconv.Itoa(i+1)+"Finish", finish, cache.NoExpiration) + if err != nil { + return err } - j := 1 - k := 0 - for i := 0; i < len(serversKeys); i = i + 2 { - if (i%8) == 0 && i != 0 { - k++ - w1 = s.AddWindow("FORWARDING - " + strconv.Itoa(k)) - panes = append(panes, w1.Pane(0)) + + k := i + go func() { + j := k + for { + v.cache.Delete(transporter.Unique + "_" + strconv.Itoa(j+1)) + switch { + case <-finish: + return + default: + c := exec.Command("ffmpeg", "-i", "\""+streamIn+"\"", "-c", "copy", "-f", "flv", "\""+transporter.Payload.(ForwarderStart).Streams[j]+"\"", ">>", "\"/logs/"+transporter.Unique+"_"+strconv.Itoa(j+1)+".txt\"", "2>&1") + err = v.cache.Add(transporter.Unique+"_"+strconv.Itoa(j+1), c, cache.NoExpiration) + if err != nil { + log.Println(err) + return + } + if err = c.Run(); err != nil { + log.Println("could not run command: ", err) + } + time.Sleep(500 * time.Millisecond) + } } - panes = append(panes, w1.Pane(0).Split()) - fmt.Println("echo", (i/2)+1) - panes[(i/2)+1].Exec("./forwarder_start.sh " + streamServer + streamIn + " " + serversKeys[i] + serversKeys[i+1] + " " + unique + " " + strconv.Itoa(j) + " | bash") - j++ - } + }() - fmt.Println("echo FORWARDER STARTED!") + go func() { + for { + switch { + case <-finish: + cmd, ok := v.cache.Get(transporter.Unique + "_" + strconv.Itoa(k)) + if !ok { + log.Println("unable to get cmd from cache") + } + c1 := cmd.(*exec.Cmd) + err = c1.Process.Kill() + if err != nil { + log.Println(err) + } + v.cache.Delete(transporter.Unique + "_" + strconv.Itoa(k)) + return + default: + time.Sleep(1 * time.Second) // This is so it doesn't spam constantly and take the entire CPU up + } + } + }() } + + return nil } diff --git a/forwarder/forwarder_start.sh b/forwarder/forwarder_start.sh deleted file mode 100644 index d08f569..0000000 --- a/forwarder/forwarder_start.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -if [ "$#" -ne 4 ]; then - raise error "Illegal number of arguments!" -else - for (( ; ; )) - do - ffmpeg -i "$1" -c copy -f flv "$2" >> "logs/$3_$4.txt" 2>&1 - sleep 1 - done -fi \ No newline at end of file diff --git a/forwarder/forwarder_status.go b/forwarder/forwarder_status.go index b1ba535..e1374ef 100644 --- a/forwarder/forwarder_status.go +++ b/forwarder/forwarder_status.go @@ -1,54 +1,51 @@ package main import ( + "bufio" + "bytes" "fmt" - "log" - "os" "os/exec" - "strconv" - "strings" ) -func main() { - if strings.Contains(os.Args[0], "/var/folders") || strings.Contains(os.Args[0], "/tmp/go") || strings.Contains(os.Args[0], "forwarder_status") { - if len(os.Args) != 4 { - log.Fatalf("Arguments error") - } - for i := 0; i < len(os.Args)-1; i++ { - os.Args[i] = os.Args[i+1] - } - } else { - if len(os.Args) != 3 { - log.Fatalf("Arguments error") - } - } - - website, _ := strconv.ParseBool(os.Args[0]) - streams, _ := strconv.Atoi(os.Args[1]) - unique := os.Args[2] - +func (v *Views) status(transporter Transporter) (ForwarderStatusResponse, error) { var start int - if website { + if transporter.Payload.(ForwarderStatus).Website { start = 0 } else { start = 1 } - m := make(map[string]string) + fStatusResponse := ForwarderStatusResponse{} + + for i := start; i <= transporter.Payload.(ForwarderStatus).Streams; i++ { + c := exec.Command("tail", "-n", "25", fmt.Sprintf("\"logs/%s_%d.txt\"", transporter.Unique, i)) + + var stdout bytes.Buffer + c.Stdout = &stdout + + var errOut string + + if err := c.Run(); err != nil { + errOut = fmt.Sprintf("could not run command: %+v", err) + } + + stderr, _ := c.StderrPipe() + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + errOut += "\n" + scanner.Text() + } + + if len(errOut) != 0 { + return ForwarderStatusResponse{}, fmt.Errorf(errOut) + } - for i := start; i <= streams; i++ { - out, err := exec.Command("./forwarder_status.sh", unique, strconv.Itoa(i)).Output() - if err != nil { - log.Fatal(err.Error()) + if i == 0 { + fStatusResponse.Website = stdout.String() } else { - if i == 0 { - m["website~"] = "~" + string(append(out, '\u0000')) - } else { - m[strconv.Itoa(i)+"~"] = "~" + string(append(out, '\u0000')) - } + fStatusResponse.Streams[uint64(i)] = stdout.String() } } - fmt.Println(m) + return fStatusResponse, nil } diff --git a/forwarder/forwarder_status.sh b/forwarder/forwarder_status.sh deleted file mode 100644 index 2acc630..0000000 --- a/forwarder/forwarder_status.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -if [ "$#" -ne 2 ]; then - raise error "Illegal number of arguments!" -fi -tail -n 25 "logs/$1_$2.txt" diff --git a/forwarder/forwarder_stop.go b/forwarder/forwarder_stop.go index d202b14..494b73f 100644 --- a/forwarder/forwarder_stop.go +++ b/forwarder/forwarder_stop.go @@ -2,38 +2,20 @@ package main import ( "fmt" - "github.com/wricardo/gomux" - "log" - "os" - "path/filepath" "strings" ) -func main() { - if strings.Contains(os.Args[0], "/var/folders") || strings.Contains(os.Args[0], "/tmp/go") || strings.Contains(os.Args[0], "./forwarder_stop") { - if len(os.Args) != 2 { - fmt.Println("echo", os.Args) - log.Fatalf("echo Arguments error") - } - for i := 0; i < len(os.Args)-1; i++ { - os.Args[i] = os.Args[i+1] - } - } else { - if len(os.Args) != 1 { - fmt.Println("echo", os.Args) - log.Fatalf("echo Arguments error") +func (v *Views) stop(transporter Transporter) error { + found := false + for k, item := range v.cache.Items() { + if strings.Contains(k, transporter.Unique) { + found = true + close(item.Object.(chan bool)) + v.cache.Delete(k) } } - unique := os.Args[0] - gomux.KillSession("STREAM FORWARDER - "+unique, os.Stdout) - files, err := filepath.Glob("logs/" + unique + "_*") - if err != nil { - panic(err) - } - for _, f := range files { - if err := os.Remove(f); err != nil { - panic(err) - } + if !found { + return fmt.Errorf("unable to find channels for: %s", transporter.Unique) } - fmt.Println("echo FORWARDER STOPPED!") + return nil } diff --git a/forwarder/go.mod b/forwarder/go.mod index 628dea3..08312ac 100644 --- a/forwarder/go.mod +++ b/forwarder/go.mod @@ -1,8 +1,24 @@ -module forwarder +module github.com/ystv/streamer/orwarder go 1.21 require ( + github.com/gorilla/websocket v1.5.1 github.com/joho/godotenv v1.5.1 - github.com/wricardo/gomux v0.0.0-20191125190231-fba15e6e61d9 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/labstack/echo/v4 v4.11.3 + github.com/mitchellh/mapstructure v1.5.0 + github.com/patrickmn/go-cache v2.1.0+incompatible +) + +require ( + github.com/labstack/gommon v0.4.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/forwarder/go.sum b/forwarder/go.sum index 90af874..c2e9a9e 100644 --- a/forwarder/go.sum +++ b/forwarder/go.sum @@ -1,6 +1,41 @@ -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/wricardo/gomux v0.0.0-20191125190231-fba15e6e61d9 h1:ImbOehYYe/nVj3j8Enov97Oewi6amG9IWJTL8NzskqI= -github.com/wricardo/gomux v0.0.0-20191125190231-fba15e6e61d9/go.mod h1:XY0ade0PJWj4LkuJPLU5wo3y+6B4w8C0goF6kTqtndY= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= +github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/forwarder/main.go b/forwarder/main.go new file mode 100644 index 0000000..7b3b5ed --- /dev/null +++ b/forwarder/main.go @@ -0,0 +1,356 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/gorilla/websocket" + "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" + "github.com/labstack/echo/v4" + "github.com/mitchellh/mapstructure" + "github.com/patrickmn/go-cache" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "time" +) + +type ( + Transporter struct { + Action string `json:"action"` + Unique string `json:"unique"` + Payload interface{} `json:"payload"` + } + + ForwarderStart struct { + StreamIn string `json:"streamIn"` + WebsiteOut string `json:"websiteOut"` + Streams []string `json:"streams"` + } + + ForwarderStatus struct { + Website bool `json:"website"` + Streams int `json:"streams"` + } + + ForwarderStatusResponse struct { + Website string `json:"website"` + Streams map[uint64]string `json:"streams"` + } + + Config struct { + StreamServer string `envconfig:"STREAM_SERVER"` + StreamerWebAddress string `envconfig:"STREAMER_WEB_ADDRESS"` + StreamerWebsocketPath string `envconfig:"STREAMER_WEBSOCKET_PATH"` + } + + Views struct { + Config Config + cache *cache.Cache + } +) + +func main() { + _ = godotenv.Load(".env") + + var config Config + err := envconfig.Process("", &config) + if err != nil { + log.Fatalf("failed to process env vars: %s", err) + } + + err = os.MkdirAll("/logs", 0777) + if err != nil { + log.Fatalf("error creating /logs: %+v", err) + } + + e := echo.New() + e.HideBanner = true + e.GET("/api/health", func(c echo.Context) error { + marshal, err := json.Marshal(struct { + Status int `json:"status"` + }{ + Status: http.StatusOK, + }) + if err != nil { + fmt.Println(err) + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: err.Error(), + Internal: err, + } + } + + c.Response().Header().Set("Content-Type", "application/json") + return c.JSON(http.StatusOK, marshal) + }) + + go func() { + if err = e.Start(":1323"); err != nil { + e.Logger.Fatal("shutting down the server") + } + }() + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + go func() { + for sig := range interrupt { + if err = e.Shutdown(context.Background()); err != nil { + e.Logger.Fatal(err) + } + fmt.Printf("signal: %s\n", sig) + os.Exit(0) + } + }() + + v := Views{ + Config: config, + cache: cache.New(cache.NoExpiration, 1*time.Hour), + } + + for { + v.run(config, interrupt) + } +} + +func (v *Views) run(config Config, interrupt chan os.Signal) { + messageOut := make(chan []byte) + errorChannel := make(chan error, 1) + done := make(chan struct{}) + u := url.URL{Scheme: "wss", Host: config.StreamerWebAddress, Path: "/" + config.StreamerWebsocketPath} + log.Printf("connecting to %s://%s", u.Scheme, u.Host) + c, resp, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + if resp != nil { + log.Printf("handshake failed with status %d", resp.StatusCode) + } + log.Printf("failed to dial url: %+v", err) + } + + defer func() { + if r := recover(); r != nil { + log.Printf("Restarting...") + select { + case <-messageOut: + break + default: + close(messageOut) + } + select { + case <-errorChannel: + break + default: + close(errorChannel) + } + select { + case <-done: + break + default: + close(done) + } + time.Sleep(5 * time.Second) + return + } + }() + + //When the program closes, close the connection + defer func(c *websocket.Conn) { + _ = c.Close() + }(c) + go func() { + defer close(done) + defer func() { + if r := recover(); r != nil { + close(errorChannel) + } + }() + err = c.WriteMessage(websocket.TextMessage, []byte("forwarder")) + if err != nil { + log.Printf("failed to write name: %+v", err) + close(errorChannel) + return + } + + var msg []byte + + _, msg, err = c.ReadMessage() + if err != nil { + log.Printf("failed to read acknowledgement: %+v", err) + close(errorChannel) + return + } + + if string(msg) != "ACKNOWLEDGED" { + log.Printf("failed to read acknowledgement: %s", string(msg)) + close(errorChannel) + return + } else { + log.Println("ACKNOWLEDGED") + log.Printf("connected to %s://%s", u.Scheme, u.Host) + } + + for { + var msgType int + var message []byte + msgType, message, err = c.ReadMessage() + if err != nil { + log.Printf("failed to read: %+v", err) + close(errorChannel) + return + } + if msgType == websocket.TextMessage && string(message) == "ping" { + err = c.WriteMessage(websocket.TextMessage, []byte("pong")) + if err != nil { + log.Printf("failed to write pong: %+v", err) + close(errorChannel) + return + } + continue + } + log.Printf("Received message: %s", message) + messageOut <- message + } + }() + + defer close(errorChannel) + for { + select { + case <-done: + case <-interrupt: + case <-errorChannel: + return + case m := <-messageOut: + log.Printf("Picked up message %s", m) + + var t Transporter + + err = json.Unmarshal(m, &t) + if err != nil { + log.Printf("failed to unmarshal data: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("failed to unmarshal data: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + return + } + continue + } + + if len(t.Unique) != 10 { + log.Printf("failed to get unique, length is not equal to 10: %d", len(t.Unique)) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("failed to get unique, length is not equal to 10: %d", len(t.Unique)))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + return + } + continue + } + + var out ForwarderStatusResponse + switch t.Action { + case "start": + var t1 ForwarderStart + + err = mapstructure.Decode(t.Payload, &t1) + if err != nil { + log.Printf("failed to decode: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to decode: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + + if len(t1.StreamIn) == 0 || len(t1.Streams) == 0 { + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to get payload for start: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + + t.Payload = t1 + + err = v.start(t) + if err != nil { + log.Printf("failed to start recorder: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to start recorder: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + case "status": + var t1 ForwarderStatus + + err = mapstructure.Decode(t.Payload, &t1) + if err != nil { + log.Printf("failed to decode: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to decode: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + + if t1.Streams == 0 { + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to get payload for start: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + + t.Payload = t1 + + out, err = v.status(t) + if err != nil { + log.Printf("failed to get status forwarder: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to get status forwarder: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + case "stop": + err = v.stop(t) + if err != nil { + log.Printf("failed to stop fprwarder: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to stop forwarder: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + default: + log.Printf("failed to get action: %s", t.Action) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("failed to get action: %s", t.Action))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + return + } + continue + } + + response := "OKAY" + + if len(out.Streams) > 0 { + var b []byte + b, err = json.Marshal(out) + if err != nil { + log.Printf("failed marshaling out: %+v", err) + return + } + response += "±~±" + string(b) // Some arbitrary connector string that is unlikely to be used ever by anything else + } + + err = c.WriteMessage(websocket.TextMessage, []byte(response)) + if err != nil { + log.Printf("failed to write okay response : %+v", err) + return + } + } + } +} diff --git a/gomux/cmd.go b/gomux/cmd.go deleted file mode 100644 index 1cca479..0000000 --- a/gomux/cmd.go +++ /dev/null @@ -1,110 +0,0 @@ -package gomux - -import "fmt" - -type killSession struct { - t string -} - -func (this killSession) String() string { - return fmt.Sprintf("tmux kill-Session -t \"%s\" > /dev/null 2>&1\n", this.t) -} - -type newSession struct { - d bool - s string - n string - c string -} - -func (this newSession) String() string { - cmd := "tmux new-Session" - if this.d == true { - cmd += " -d" - } - if this.s != "" { - cmd += " -s \"" + this.s + "\"" - } - if this.n != "" { - cmd += " -n " + this.n - } - if this.c != "" { - cmd += " -c " + this.c - } - return cmd + "\n" -} - -type splitWindow struct { - h bool - v bool - t string - c string -} - -func (this splitWindow) String() string { - cmd := "tmux split-Window" - if this.h == true { - cmd += " -h" - } - if this.v == true { - cmd += " -v" - } - if this.t != "" { - cmd += " -t \"" + this.t + "\"" - } - if this.c != "" { - cmd += " -c " + this.c - } - return cmd + "\n" -} - -type newWindow struct { - t string - n string - c string -} - -func (this newWindow) String() string { - cmd := "tmux new-Window" - if this.t != "" { - cmd += " " + this.t - } - if this.n != "" { - cmd += " -n \"" + this.n + "\"" - } - - if this.c != "" { - cmd += " -c " + this.c - } - return cmd + "\n" -} - -type renameWindow struct { - t string - n string -} - -func (this renameWindow) String() string { - cmd := "tmux rename-Window" - if this.t != "" { - cmd += " " + this.t - } - if this.n != "" { - cmd += " \"" + this.n + "\"" - } - - return cmd + "\n" -} - -type selectWindow struct { - t string -} - -func (this selectWindow) String() string { - cmd := "tmux select-Window" - if this.t != "" { - cmd += " -t \"" + this.t + "\"" - } - - return cmd + "\n" -} diff --git a/gomux/go.mod b/gomux/go.mod deleted file mode 100644 index ac6d0be..0000000 --- a/gomux/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/ystv/streamer/gomux - -go 1.21 diff --git a/gomux/go.sum b/gomux/go.sum deleted file mode 100644 index e69de29..0000000 diff --git a/gomux/gomux.go b/gomux/gomux.go deleted file mode 100644 index 661eed3..0000000 --- a/gomux/gomux.go +++ /dev/null @@ -1,212 +0,0 @@ -package gomux - -import ( - "fmt" - "io" - "strings" -) - -type Pane struct { - Number int - commands []string - Window *Window -} - -func NewPane(number int, window *Window) *Pane { - p := new(Pane) - p.Number = number - p.commands = make([]string, 0) - p.Window = window - return p -} - -type SplitAttr struct { - Directory string -} - -func (this *Pane) Exec(command string) { - fmt.Fprintf(this.Window.Session.Writer, "tmux send-keys -t \"%s\" \"%s\" %s\n", this.getTargetName(), strings.Replace(command, "\"", "\\\"", -1), "C-m") -} - -func (this *Pane) Vsplit() *Pane { - fmt.Fprint(this.Window.Session.Writer, splitWindow{h: true, t: this.getTargetName()}) - return this.Window.AddPane(this.Number + 1) -} - -func (this *Pane) VsplitWAttr(attr SplitAttr) *Pane { - var c string - if attr.Directory != "" { - c = attr.Directory - } else if this.Window.Directory != "" { - c = this.Window.Directory - } else if this.Window.Session.Directory != "" { - c = this.Window.Session.Directory - } - - fmt.Fprint(this.Window.Session.Writer, splitWindow{h: true, t: this.getTargetName(), c: c}) - return this.Window.AddPane(this.Number + 1) -} - -func (this *Pane) Split() *Pane { - fmt.Fprint(this.Window.Session.Writer, splitWindow{v: true, t: this.getTargetName()}) - return this.Window.AddPane(this.Number + 1) -} - -func (this *Pane) SplitWAttr(attr SplitAttr) *Pane { - var c string - if attr.Directory != "" { - c = attr.Directory - } else if this.Window.Directory != "" { - c = this.Window.Directory - } else if this.Window.Session.Directory != "" { - c = this.Window.Session.Directory - } - - fmt.Fprint(this.Window.Session.Writer, splitWindow{v: true, t: this.getTargetName(), c: c}) - return this.Window.AddPane(this.Number + 1) -} - -func (this *Pane) ResizeRight(num int) { - this.resize("R", num) -} - -func (this *Pane) ResizeLeft(num int) { - this.resize("L", num) -} - -func (this *Pane) ResizeUp(num int) { - this.resize("U", num) -} - -func (this *Pane) ResizeDown(num int) { - this.resize("U", num) -} - -func (this *Pane) resize(prefix string, num int) { - fmt.Fprintf(this.Window.Session.Writer, "tmux resize-pane -t \"%s\" -%s %v\n", this.getTargetName(), prefix, fmt.Sprint(num)) -} - -func (this *Pane) getTargetName() string { - return this.Window.Session.Name + ":" + fmt.Sprint(this.Window.Number) + "." + fmt.Sprint(this.Number) -} - -// Window Represent a tmux Window. You usually should not create an instance of Window directly. -type Window struct { - Number int - Name string - Directory string - Session *Session - panes []*Pane - split_commands []string -} - -type WindowAttr struct { - Name string - Directory string -} - -func createWindow(number int, attr WindowAttr, session *Session) *Window { - w := new(Window) - w.Name = attr.Name - w.Directory = attr.Directory - w.Number = number - w.Session = session - w.panes = make([]*Pane, 0) - w.split_commands = make([]string, 0) - w.AddPane(0) - - if number != 0 { - fmt.Fprint(session.Writer, newWindow{t: w.t(), n: w.Name, c: attr.Directory}) - } - - fmt.Fprint(session.Writer, renameWindow{t: w.t(), n: w.Name}) - return w -} - -func (this *Window) t() string { - return fmt.Sprintf("-t \"%s:%s\"", this.Session.Name, fmt.Sprint(this.Number)) -} - -// Create a new Pane and add to this Window -func (this *Window) AddPane(withNumber int) *Pane { - pane := NewPane(withNumber, this) - this.panes = append(this.panes, pane) - return pane -} - -// Find and return the Pane object by its index in the panes slice -func (this *Window) Pane(number int) *Pane { - return this.panes[number] -} - -// Executes a command on the first pane of this Window -// -// // example -// // example -func (this *Window) Exec(command string) { - this.Pane(0).Exec(command) -} - -func (this *Window) Select() { - fmt.Fprint(this.Session.Writer, selectWindow{t: this.Session.Name + ":" + fmt.Sprint(this.Number)}) -} - -// Session represents a tmux Session. -// -// Use the method NewSession to create a Session instance. -type Session struct { - Name string - Directory string - windows []*Window - directory string - next_window_number int - Writer io.Writer -} - -// Creates a new Tmux Session. It will kill any existing Session with the provided name. -func NewSession(name string, writer io.Writer) *Session { - p := SessionAttr{ - Name: name, - } - return NewSessionAttr(p, writer) -} - -type SessionAttr struct { - Name string - Directory string -} - -// Creates a new Tmux Session based on NewSessionAttr. It will kill any existing Session with the provided name. -func NewSessionAttr(p SessionAttr, writer io.Writer) *Session { - s := new(Session) - s.Writer = writer - s.Name = p.Name - s.Directory = p.Directory - s.windows = make([]*Window, 0) - - fmt.Fprint(writer, newSession{d: true, s: p.Name, c: p.Directory, n: "tmp"}) - return s -} - -// KillSession sends a command to kill the tmux Session -func KillSession(name string, writer io.Writer) { - fmt.Fprint(writer, killSession{t: name}) -} - -// Creates Window with provided name for this Session -func (this *Session) AddWindow(name string) *Window { - - attr := WindowAttr{ - Name: name, - } - - return this.AddWindowAttr(attr) -} - -// Creates Window with provided name for this Session -func (this *Session) AddWindowAttr(attr WindowAttr) *Window { - w := createWindow(this.next_window_number, attr, this) - this.windows = append(this.windows, w) - this.next_window_number = this.next_window_number + 1 - return w -} diff --git a/recorder/Dockerfile b/recorder/Dockerfile new file mode 100644 index 0000000..b081569 --- /dev/null +++ b/recorder/Dockerfile @@ -0,0 +1,20 @@ +FROM golang:1.21.1-alpine3.18 AS build + +LABEL site="ystv-streamer-recorder" + +VOLUME /logs + +WORKDIR /src/ + +COPY go.mod . +COPY go.sum . + +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /bin/recorder + +EXPOSE 1323 + +ENTRYPOINT ["/bin/recorder"] \ No newline at end of file diff --git a/recorder/example.env b/recorder/example.env index 77641e0..738dde4 100644 --- a/recorder/example.env +++ b/recorder/example.env @@ -1,2 +1,4 @@ RECORDING_LOCATION= -STREAM_SERVER= \ No newline at end of file +STREAM_SERVER= +STREAMER_WEB_ADDRESS= +STREAMER_WEBSOCKET_PATH= \ No newline at end of file diff --git a/recorder/go.mod b/recorder/go.mod index 2c280df..a0e0741 100644 --- a/recorder/go.mod +++ b/recorder/go.mod @@ -1,8 +1,24 @@ -module recorder +module github.com/ystv/streamer/recorder go 1.21 require ( + github.com/gorilla/websocket v1.5.1 github.com/joho/godotenv v1.5.1 - github.com/wricardo/gomux v0.0.0-20191125190231-fba15e6e61d9 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/labstack/echo/v4 v4.11.3 + github.com/mitchellh/mapstructure v1.5.0 + github.com/patrickmn/go-cache v2.1.0+incompatible +) + +require ( + github.com/labstack/gommon v0.4.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect ) diff --git a/recorder/go.sum b/recorder/go.sum index 90af874..c2e9a9e 100644 --- a/recorder/go.sum +++ b/recorder/go.sum @@ -1,6 +1,41 @@ -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/wricardo/gomux v0.0.0-20191125190231-fba15e6e61d9 h1:ImbOehYYe/nVj3j8Enov97Oewi6amG9IWJTL8NzskqI= -github.com/wricardo/gomux v0.0.0-20191125190231-fba15e6e61d9/go.mod h1:XY0ade0PJWj4LkuJPLU5wo3y+6B4w8C0goF6kTqtndY= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= +github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/recorder/main.go b/recorder/main.go new file mode 100644 index 0000000..6aa609f --- /dev/null +++ b/recorder/main.go @@ -0,0 +1,313 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/gorilla/websocket" + "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" + "github.com/labstack/echo/v4" + "github.com/mitchellh/mapstructure" + "github.com/patrickmn/go-cache" + "log" + "net/http" + "net/url" + "os" + "os/signal" + "time" +) + +type ( + Transporter struct { + Action string `json:"action"` + Unique string `json:"unique"` + Payload interface{} `json:"payload"` + } + + RecorderStart struct { + StreamIn string `json:"streamIn"` + PathOut string `json:"pathOut"` + } + + Config struct { + StreamServer string `envconfig:"STREAM_SERVER"` + RecordingLocation string `envconfig:"RECORDING_LOCATION"` + StreamerWebAddress string `envconfig:"STREAMER_WEB_ADDRESS"` + StreamerWebsocketPath string `envconfig:"STREAMER_WEBSOCKET_PATH"` + } + + Views struct { + Config Config + cache *cache.Cache + } +) + +func main() { + _ = godotenv.Load(".env") + + var config Config + err := envconfig.Process("", &config) + if err != nil { + log.Fatalf("failed to process env vars: %s", err) + } + + err = os.MkdirAll("/logs", 0777) + if err != nil { + log.Fatalf("error creating /logs: %+v", err) + } + + e := echo.New() + e.HideBanner = true + e.GET("/api/health", func(c echo.Context) error { + marshal, err := json.Marshal(struct { + Status int `json:"status"` + }{ + Status: http.StatusOK, + }) + if err != nil { + fmt.Println(err) + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: err.Error(), + Internal: err, + } + } + + c.Response().Header().Set("Content-Type", "application/json") + return c.JSON(http.StatusOK, marshal) + }) + + go func() { + if err = e.Start(":1323"); err != nil { + e.Logger.Fatal("shutting down the server") + } + }() + + interrupt := make(chan os.Signal, 1) + signal.Notify(interrupt, os.Interrupt) + go func() { + for sig := range interrupt { + if err = e.Shutdown(context.Background()); err != nil { + e.Logger.Fatal(err) + } + log.Printf("signal: %s\n", sig) + os.Exit(0) + } + }() + + v := Views{ + Config: config, + cache: cache.New(cache.NoExpiration, 1*time.Hour), + } + + for { + v.run(config, interrupt) + } +} + +func (v *Views) run(config Config, interrupt chan os.Signal) { + messageOut := make(chan []byte) + errorChannel := make(chan error, 1) + done := make(chan struct{}) + u := url.URL{Scheme: "wss", Host: config.StreamerWebAddress, Path: "/" + config.StreamerWebsocketPath} + log.Printf("connecting to %s://%s", u.Scheme, u.Host) + c, resp, err := websocket.DefaultDialer.Dial(u.String(), nil) + if err != nil { + if resp != nil { + log.Printf("handshake failed with status %d", resp.StatusCode) + } + log.Printf("failed to dial url: %+v", err) + } + + defer func() { + if r := recover(); r != nil { + log.Printf("Restarting...") + select { + case <-messageOut: + break + default: + close(messageOut) + } + select { + case <-errorChannel: + break + default: + close(errorChannel) + } + select { + case <-done: + break + default: + close(done) + } + time.Sleep(5 * time.Second) + return + } + }() + + defer func(c *websocket.Conn) { + _ = c.Close() + }(c) + go func() { + defer close(done) + defer func() { + if r := recover(); r != nil { + close(errorChannel) + } + }() + err = c.WriteMessage(websocket.TextMessage, []byte("recorder")) + if err != nil { + log.Printf("failed to write name: %+v", err) + close(errorChannel) + return + } + + _, msg, err := c.ReadMessage() + if err != nil { + log.Printf("failed to read acknowledgement: %+v", err) + close(errorChannel) + return + } + + if string(msg) != "ACKNOWLEDGED" { + log.Printf("failed to read acknowledgement: %s", string(msg)) + close(errorChannel) + return + } else { + log.Println("ACKNOWLEDGED") + log.Printf("connected to %s://%s", u.Scheme, u.Host) + } + + for { + msgType, message, err := c.ReadMessage() + if err != nil { + log.Printf("failed to read: %+v", err) + close(errorChannel) + return + } + if msgType == websocket.TextMessage && string(message) == "ping" { + err = c.WriteMessage(websocket.TextMessage, []byte("pong")) + if err != nil { + log.Printf("failed to write pong: %+v", err) + close(errorChannel) + return + } + continue + } + log.Printf("Received message: %s", message) + messageOut <- message + } + }() + + defer close(errorChannel) + for { + select { + case <-done: + case <-interrupt: + case <-errorChannel: + return + case m := <-messageOut: + log.Printf("Picked up message %s", m) + + var t Transporter + + err = json.Unmarshal(m, &t) + if err != nil { + log.Printf("failed to unmarshal data: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to unmarshal data: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + return + } + continue + } + + if len(t.Unique) != 10 { + log.Printf("failed to get unique, length is not equal to 10: %d", len(t.Unique)) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to get unique, length is not equal to 10: %d", len(t.Unique)))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + return + } + continue + } + + var out string + switch t.Action { + case "start": + var t1 RecorderStart + + err = mapstructure.Decode(t.Payload, &t1) + if err != nil { + log.Printf("failed to decode: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to decode: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + + if len(t1.StreamIn) == 0 || len(t1.PathOut) == 0 { + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to get payload for start: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + + t.Payload = t1 + + err = v.start(t) + if err != nil { + log.Printf("failed to start recorder: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to start recorder: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + case "status": + out, err = v.status(t) + if err != nil { + log.Printf("failed to get status recorder: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to get status recorder: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + case "stop": + err = v.stop(t) + if err != nil { + log.Printf("failed to stop recorder: %+v", err) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("ERROR: failed to stop recorder: %+v", err))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + } + return + } + default: + log.Printf("failed to get action: %s", t.Action) + err = c.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("failed to get action: %s", t.Action))) + if err != nil { + log.Printf("failed to write error response : %+v", err) + return + } + continue + } + + response := "OKAY" + + if len(out) > 0 { + response += "±~±" + out // Some arbitrary connector string that is unlikely to be used ever by anything else + } + + err = c.WriteMessage(websocket.TextMessage, []byte(response)) + if err != nil { + log.Printf("failed to write okay response : %+v", err) + return + } + } + } +} diff --git a/recorder/recorder_start.go b/recorder/recorder_start.go index 1f6e081..546350b 100644 --- a/recorder/recorder_start.go +++ b/recorder/recorder_start.go @@ -2,96 +2,111 @@ package main import ( "fmt" - "github.com/joho/godotenv" - "github.com/wricardo/gomux" + "github.com/patrickmn/go-cache" "log" "os" + "os/exec" + "strconv" "strings" + "time" ) -func main() { - fmt.Println("echo", os.Args) - if strings.Contains(os.Args[0], "/var/folders") || strings.Contains(os.Args[0], "/tmp/go") || strings.Contains(os.Args[0], "./recorder_start") { - if len(os.Args) != 4 { - fmt.Println("echo " + string(rune(len(os.Args)))) - log.Fatalf("echo Arguments error") - } - for i := 0; i < len(os.Args)-1; i++ { - os.Args[i] = os.Args[i+1] - } - } else { - if len(os.Args) != 3 { - fmt.Println("echo " + string(rune(len(os.Args)))) - log.Fatalf("echo Arguments error") - } - } - streamIn := os.Args[0] - pathOut := os.Args[1] - unique := os.Args[2] - array := strings.Split(pathOut, "/") +func (v *Views) start(transporter Transporter) error { + array := strings.Split(transporter.Payload.(RecorderStart).PathOut, "/") valid := false var path string - err := godotenv.Load() - if err != nil { - fmt.Printf("echo Error loading .env file: %s", err) + + if len(array) == 1 { + path = array[0] + valid = true } else { - recordingLocation := os.Getenv("RECORDING_LOCATION") - if len(array) == 1 { - path = array[0] + for i := 0; i < len(array)-1; i++ { + path += array[i] + "/" + } + err := os.MkdirAll(v.Config.RecordingLocation+path, 0777) + if err != nil { + return fmt.Errorf("error creating %s: %w", v.Config.RecordingLocation+path, err) + } + _, err1 := os.Stat(v.Config.RecordingLocation + path) + if os.IsNotExist(err1) { + return fmt.Errorf("unable to get path: %w", err1) + } + temp := array[len(array)-1] + _, err2 := os.Stat(v.Config.RecordingLocation + path + "/" + temp) + if os.IsNotExist(err2) { + path += array[len(array)-1] valid = true } else { - for i := 0; i < len(array)-1; i++ { - path += array[i] + "/" - } - err = os.MkdirAll(recordingLocation+path, 0777) - if err != nil { - fmt.Println("echo " + path) - fmt.Println("echo " + err.Error()) - log.Fatal("echo Error creating " + recordingLocation + path) - } - _, err1 := os.Stat(recordingLocation + path) - if os.IsNotExist(err1) { - fmt.Println(" echo RECORDER UNSUCCESSFUL!") - } else { - temp := array[len(array)-1] - _, err2 := os.Stat(recordingLocation + path + "/" + temp) - if os.IsNotExist(err2) { - path += array[len(array)-1] + split := strings.Split(temp, ".") + i := 0 + for { + _, err3 := os.Stat(v.Config.RecordingLocation + path + "/" + split[0] + "_" + string(rune(i)) + ".mkv") + if os.IsNotExist(err3) { + path += split[0] + string(rune(i)) + ".mkv" valid = true - } else { - split := strings.Split(temp, ".") - loop := true - i := 0 - for loop { - _, err3 := os.Stat(recordingLocation + path + "/" + split[0] + "_" + string(rune(i)) + ".mkv") - if os.IsNotExist(err3) { - path += split[0] + string(rune(i)) + ".mkv" - loop = false - valid = true - } else { - i++ - } - } + break } + i++ } } - if valid { - streamServer := os.Getenv("STREAM_SERVER") - - sessionName := "STREAM RECORDING - " + unique + } + if !valid { + return fmt.Errorf("invalid path") + } - s := gomux.NewSession(sessionName, os.Stdout) + streamIn := "rtmp://" + v.Config.StreamServer + transporter.Payload.(RecorderStart).StreamIn + path = v.Config.RecordingLocation + path - w1 := s.AddWindow("RECORDING") + finish := make(chan bool) - w1p0 := w1.Pane(0) + err := v.cache.Add(transporter.Unique+"Finish", finish, cache.NoExpiration) + if err != nil { + return err + } - path = strings.ReplaceAll(path, ".mkv", "") - w1p0.Exec("./recorder_start.sh " + streamServer + streamIn + " \"" + recordingLocation + path + "\" " + unique + " | bash") + go func() { + var i uint64 + for { + v.cache.Delete(transporter.Unique) + switch { + case <-finish: + return + default: + c := exec.Command("ffmpeg", "-i", "\""+streamIn+"\"", "-c", "copy", "\""+path+"_"+strconv.FormatUint(i, 10)+".mkv\"", ">>", "\"/logs/"+transporter.Unique+".txt\"", "2>&1") + err := v.cache.Add(transporter.Unique, c, cache.NoExpiration) + if err != nil { + log.Println(err) + return + } + if err = c.Run(); err != nil { + log.Println("could not run command: ", err) + } + time.Sleep(500 * time.Millisecond) + } + i++ + } + }() - fmt.Println("echo RECORDER STARTED!") - } else { - log.Fatal("echo Invalid string") + go func() { + for { + switch { + case <-finish: + cmd, ok := v.cache.Get(transporter.Unique) + if !ok { + log.Println("unable to get cmd from cache") + } + c1 := cmd.(*exec.Cmd) + err = c1.Process.Kill() + if err != nil { + log.Println(err) + } + v.cache.Delete(transporter.Unique) + return + default: + time.Sleep(1 * time.Second) // This is so it doesn't spam constantly and take the entire CPU up + } } - } + }() + + return nil } diff --git a/recorder/recorder_start.sh b/recorder/recorder_start.sh deleted file mode 100644 index deaf2a8..0000000 --- a/recorder/recorder_start.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -if [ "$#" -ne 3 ]; then - raise error "Illegal number of arguments!" -else - i=0 - for (( ; ; )) - do - ffmpeg -i "$1" -c copy "$2_$i.mkv" >> "logs/$3.txt" 2>&1 - ((i=i+1)) - sleep 1 - done -fi \ No newline at end of file diff --git a/recorder/recorder_status.go b/recorder/recorder_status.go new file mode 100644 index 0000000..618f329 --- /dev/null +++ b/recorder/recorder_status.go @@ -0,0 +1,33 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "os/exec" +) + +func (v *Views) status(transporter Transporter) (string, error) { + c := exec.Command("tail", "-n", "26", fmt.Sprintf("\"logs/%s.txt\"", transporter.Unique)) + + var stdout bytes.Buffer + c.Stdout = &stdout + + var errOut string + + if err := c.Run(); err != nil { + errOut = fmt.Sprintf("could not run command: %+v", err) + } + + stderr, _ := c.StderrPipe() + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + errOut += "\n" + scanner.Text() + } + + if len(errOut) != 0 { + return "", fmt.Errorf(errOut) + } + + return stdout.String(), nil +} diff --git a/recorder/recorder_status.sh b/recorder/recorder_status.sh deleted file mode 100644 index 735e001..0000000 --- a/recorder/recorder_status.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -if [ "$#" -ne 1 ]; then - raise error "Illegal number of arguments!" -fi -tail -n 26 "logs/$1.txt" diff --git a/recorder/recorder_stop.go b/recorder/recorder_stop.go index c3ee835..35d1b01 100644 --- a/recorder/recorder_stop.go +++ b/recorder/recorder_stop.go @@ -2,37 +2,14 @@ package main import ( "fmt" - "github.com/wricardo/gomux" - "log" - "os" - "os/exec" - "strings" ) -func main() { - if strings.Contains(os.Args[0], "/var/folders") || strings.Contains(os.Args[0], "/tmp/go") || strings.Contains(os.Args[0], "./recorder_stop") { - if len(os.Args) != 2 { - fmt.Println("echo", os.Args) - log.Fatalf("echo Arguments error") - } - for i := 0; i < len(os.Args)-1; i++ { - os.Args[i] = os.Args[i+1] - } - } else { - if len(os.Args) != 1 { - fmt.Println("echo", os.Args) - log.Fatalf("echo Arguments error") - } - } - unique := os.Args[0] - gomux.KillSession("STREAM RECORDING - "+unique, os.Stdout) - file := "logs/" + unique + ".txt" - cmd := exec.Command("/bin/rm", file) - stdout, err := cmd.Output() - if err != nil { - fmt.Println(err.Error()) - } else { - fmt.Println(string(stdout)) - fmt.Println("echo RECORDER STOPPED!") +func (v *Views) stop(transporter Transporter) error { + finish, ok := v.cache.Get(transporter.Unique + "Finish") + if !ok { + return fmt.Errorf("unable to find channel: %s", transporter.Unique) } + close(finish.(chan bool)) + v.cache.Delete(transporter.Unique + "Finish") + return nil } diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000..efbb4d6 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.21.0-alpine3.18 AS build + +LABEL stage="builder" + +VOLUME /db + +WORKDIR /src/ + +COPY . . + +RUN go mod download + +RUN apk update && apk add git && apk add make && apk add protoc && apk add protoc-gen-go --repository https://dl-cdn.alpinelinux.org/alpine/edge/testing/ --allow-untrusted + +RUN GOOS=linux GOARCH=amd64 make + +EXPOSE 8084 + +ENTRYPOINT ["/bin/streamer"] \ No newline at end of file diff --git a/server/Makefile b/server/Makefile new file mode 100644 index 0000000..b3f6462 --- /dev/null +++ b/server/Makefile @@ -0,0 +1,24 @@ +# parameters +GO_BUILD=CGO_ENABLED=0 go build +GO_CLEAN=go clean +PROTOC=protoc +SERVER_BINARY_NAME=streamer + +PROTO_GENERATED=storage/storage.pb.go + +.DEFAULT_GOAL := build + +%.pb.go: %.proto + $(PROTOC) -I=storage/ --go_opt=paths=source_relative --go_out=storage/ $< + +build: $(PROTO_GENERATED) + $(GO_BUILD) -o /bin/$(SERVER_BINARY_NAME) -v . +.PHONY: build + +clean: + $(GO_CLEAN) + rm -f $(PROTO_GENERATED) +.PHONY: clean + +all: build +.PHONY: all diff --git a/server/db/streams.db b/server/db/streams.db deleted file mode 100644 index 2da3426..0000000 Binary files a/server/db/streams.db and /dev/null differ diff --git a/server/facebookHelp.go b/server/facebookHelp.go deleted file mode 100644 index 06b9417..0000000 --- a/server/facebookHelp.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import ( - "fmt" - "github.com/ystv/streamer/server/templates" - "net/http" - "time" -) - -// facebookHelp is the handler for the Facebook help page -func (web *Web) facebookHelp(w http.ResponseWriter, _ *http.Request) { - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication+"facebookhelp", http.StatusTemporaryRedirect) - return - }*/ - - if verbose { - fmt.Println("Facebook called") - } - - web.t = templates.NewFacebookHelp() - - params := templates.PageParams{ - Base: templates.BaseParams{ - SystemTime: time.Now(), - }, - } - - err := web.t.Page(w, params) - if err != nil { - err = fmt.Errorf("failed to render dashboard: %w", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} diff --git a/server/go.mod b/server/go.mod index 7a7f1b2..e455da9 100644 --- a/server/go.mod +++ b/server/go.mod @@ -3,11 +3,24 @@ module github.com/ystv/streamer/server go 1.21 require ( - github.com/gorilla/mux v1.8.0 + github.com/gorilla/websocket v1.5.1 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 - github.com/mattn/go-sqlite3 v1.14.17 - golang.org/x/crypto v0.13.0 + github.com/labstack/echo/v4 v4.11.3 + github.com/labstack/gommon v0.4.1 + github.com/patrickmn/go-cache v2.1.0+incompatible + golang.org/x/crypto v0.16.0 + google.golang.org/protobuf v1.31.0 ) -require golang.org/x/sys v0.12.0 // indirect +require ( + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect +) diff --git a/server/go.sum b/server/go.sum index 1b1852f..782093e 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1,31 +1,53 @@ -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +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/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220314234633-e4b3678e5f38 h1:3LgXxiGr0Ce5Am+c0s6XfoPPgQnhrX5rjt/Am50smBk= -golang.org/x/crypto v0.0.0-20220314234633-e4b3678e5f38/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= +github.com/labstack/echo/v4 v4.11.3 h1:Upyu3olaqSHkCjs1EJJwQ3WId8b8b1hxbogyommKktM= +github.com/labstack/echo/v4 v4.11.3/go.mod h1:UcGuQ8V6ZNRmSweBIJkPvGfwCMIlFmiqrPqiEBfPYws= +github.com/labstack/gommon v0.4.1 h1:gqEff0p/hTENGMABzezPoPSRtIh1Cvw0ueMOe0/dfOk= +github.com/labstack/gommon v0.4.1/go.mod h1:TyTrpPqxR5KMk8LKVtLmfMjeQ5FEkBYdxLYPw/WfrOM= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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/helper/getBody.go b/server/helper/getBody.go new file mode 100644 index 0000000..b416260 --- /dev/null +++ b/server/helper/getBody.go @@ -0,0 +1,30 @@ +package helper + +import ( + "io" + "net/http" + "strings" +) + +func GetBody(url string) (body string, err error) { + response, err := http.Get(url) + if err != nil { + return + } + defer func(Body io.ReadCloser) { + err = Body.Close() + if err != nil { + return + } + }(response.Body) + + buf := new(strings.Builder) + _, err = io.Copy(buf, response.Body) + if err != nil { + return + } + + body = buf.String() + + return +} diff --git a/server/ssh_remote.go b/server/helper/sshRemote.go similarity index 77% rename from server/ssh_remote.go rename to server/helper/sshRemote.go index bdef1f9..13bd778 100644 --- a/server/ssh_remote.go +++ b/server/helper/sshRemote.go @@ -1,4 +1,4 @@ -package main +package helper import ( "fmt" @@ -6,8 +6,8 @@ import ( "golang.org/x/crypto/ssh" ) -// connectToHostPassword is a general function to ssh to a remote server, any code execution is handled outside this function -func connectToHostPassword(host, username, password string) (*ssh.Client, *ssh.Session, error) { +// ConnectToHostPassword is a general function to ssh to a remote server, any code execution is handled outside this function +func ConnectToHostPassword(host, username, password string, verbose bool) (*ssh.Client, *ssh.Session, error) { if verbose { fmt.Println("Connect To Host Password called") } @@ -34,14 +34,14 @@ func connectToHostPassword(host, username, password string) (*ssh.Client, *ssh.S return client, session, nil } -func RunCommandOnHost(host, username, password, command string) (string, error) { +func RunCommandOnHost(host, username, password, command string, verbose bool) (string, error) { var client *ssh.Client var session *ssh.Session var err error //if forwarderAuth == "PEM" { // client, session, err = connectToHostPEM(forwarder, forwarderUsername, forwarderPrivateKey, forwarderPassphrase) //} else if forwarderAuth == "PASS" { - client, session, err = connectToHostPassword(host, username, password) + client, session, err = ConnectToHostPassword(host, username, password, verbose) //} if err != nil { return "", fmt.Errorf("error connecting to host: %w", err) diff --git a/server/helper/tx/tx.go b/server/helper/tx/tx.go new file mode 100644 index 0000000..df5b6fc --- /dev/null +++ b/server/helper/tx/tx.go @@ -0,0 +1,13 @@ +package tx + +type FunctionTX string + +var ( + TransmissionOn FunctionTX = "transmission_on" + AllOff FunctionTX = "rehearsal_transmission_off" + RehearsalOn FunctionTX = "rehearsal_on" +) + +func (f FunctionTX) String() string { + return string(f) +} diff --git a/server/helpers/existingStreamCheck.go b/server/helpers/existingStreamCheck.go deleted file mode 100644 index a401d22..0000000 --- a/server/helpers/existingStreamCheck.go +++ /dev/null @@ -1,59 +0,0 @@ -package helpers - -import ( - "database/sql" - "fmt" -) - -// ExistingStreamCheck checks if there are any existing streams still registered in the database -func ExistingStreamCheck(verbose bool) bool { - if verbose { - fmt.Println("Existing Stream Check called") - } - db, err := sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - } else { - rows, err := db.Query("SELECT stream FROM (SELECT stream FROM streams UNION ALL SELECT stream FROM stored)") - if err != nil { - fmt.Println(err) - } - - err = db.Close() - if err != nil { - fmt.Println(err) - } - - var stream string - - for rows.Next() { - err = rows.Scan(&stream) - if err != nil { - fmt.Println(err) - } - err = rows.Close() - if err != nil { - fmt.Println(err) - } - err = db.Close() - if err != nil { - fmt.Println(err) - } - return true - } - err = rows.Close() - if err != nil { - fmt.Println(err) - } - err = db.Close() - if err != nil { - fmt.Println(err) - } - return false - } - err = db.Close() - if err != nil { - fmt.Println(err) - } - return false -} diff --git a/server/home.go b/server/home.go deleted file mode 100644 index 41f1ac7..0000000 --- a/server/home.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "fmt" - "github.com/ystv/streamer/server/templates" - "net/http" - "time" -) - -// home is the basic html writer that provides the main page for Streamer -func (web *Web) home(w http.ResponseWriter, r *http.Request) { - _ = r - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication+"authenticate1", http.StatusTemporaryRedirect) - return - }*/ - if verbose { - fmt.Println("Home called") - } - /*tmpl := template.Must(template.ParseFiles("html/main.html")) - err := tmpl.Execute(w, nil) - if err != nil { - fmt.Println(err) - }*/ - web.t = templates.NewMain() - - params := templates.PageParams{ - Base: templates.BaseParams{ - SystemTime: time.Now(), - }, - } - - err := web.t.Page(w, params) - if err != nil { - err = fmt.Errorf("failed to render dashboard: %w", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} diff --git a/server/list.go b/server/list.go deleted file mode 100644 index b82941c..0000000 --- a/server/list.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "github.com/ystv/streamer/server/templates" - "net/http" - "strings" - "time" -) - -// list lists all current streams that are registered in the database -func (web *Web) list(w http.ResponseWriter, r *http.Request) { - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication+"list", http.StatusTemporaryRedirect) - return - }*/ - if r.Method == "GET" { - if verbose { - fmt.Println("Stop GET called") - } - web.t = templates.NewList() - - params := templates.PageParams{ - Base: templates.BaseParams{ - SystemTime: time.Now(), - }, - } - - err := web.t.Page(w, params) - if err != nil { - err = fmt.Errorf("failed to render dashboard: %w", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else if r.Method == "POST" { - if verbose { - fmt.Println("Stop POST called") - } - db, err := sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - } else { - - } - - rows, err := db.Query("SELECT stream FROM streams") - if err != nil { - fmt.Println(err) - } - var stream string - - var streams []string - - data := false - - for rows.Next() { - err = rows.Scan(&stream) - if err != nil { - fmt.Println(err) - } - data = true - streams = append(streams, stream) - } - - err = rows.Close() - if err != nil { - fmt.Println(err) - } - - err = db.Close() - if err != nil { - fmt.Println(err) - } - - if !data { - _, err = w.Write([]byte("No current streams")) - if err != nil { - fmt.Println(err) - } - } else { - stringByte := strings.Join(streams, "\x20") - _, err = w.Write([]byte(stringByte)) - if err != nil { - fmt.Println(err) - } - } - } -} diff --git a/server/main.go b/server/main.go index c514264..e0822b4 100644 --- a/server/main.go +++ b/server/main.go @@ -1,95 +1,36 @@ package main import ( - //"encoding/pem" - "encoding/xml" - //"errors" + "embed" + "encoding/json" "fmt" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/ystv/streamer/server/store" + "github.com/ystv/streamer/server/views" "log" - "math/rand" - "net" "net/http" "os" - "strconv" "strings" - "time" - "github.com/ystv/streamer/server/templates" - - "github.com/gorilla/mux" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" - _ "github.com/mattn/go-sqlite3" ) type ( - Web struct { - mux *mux.Router - t *templates.Templater - cfg Config - } - - Config struct { - Forwarder string `envconfig:"FORWARDER"` - Recorder string `envconfig:"RECORDER"` - ForwarderUsername string `envconfig:"FORWARDER_USERNAME"` - RecorderUsername string `envconfig:"RECORDER_USERNAME"` - ForwarderPassword string `envconfig:"FORWARDER_PASSWORD"` - RecorderPassword string `envconfig:"RECORDER_PASSWORD"` - StreamChecker string `envconfig:"STREAM_CHECKER"` - TransmissionLight string `envconfig:"TRANSMISSION_LIGHT"` - KeyChecker string `envconfig:"KEY_CHECKER"` - ServerPort int `envconfig:"SERVER_PORT"` - } - - RTMP struct { - XMLName xml.Name `xml:"rtmp"` - Server Server `xml:"server"` - } - - Server struct { - XMLName xml.Name `xml:"server"` - Applications []Application `xml:"application"` - } - - Application struct { - XMLName xml.Name `xml:"application"` - Name string `xml:"name"` - Live Live `xml:"live"` - } - - Live struct { - XMLName xml.Name `xml:"live"` - Streams []Stream `xml:"stream"` - } - - Stream struct { - XMLName xml.Name `xml:"stream"` - Name string `xml:"name"` + Router struct { + config views.Config + router *echo.Echo + views *views.Views } - - /*Claims struct { - Id int `json:"id"` - Perms []Permission `json:"perms"` - Exp int64 `json:"exp"` - jwt.StandardClaims - }*/ - - /*Permission struct { - Permission string `json:"perms"` - jwt.StandardClaims - }*/ - - /*Views struct { - cookie *sessions.CookieStore - }*/ ) -const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -var verbose bool +var ( + verbose bool +) -var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) +//go:embed public/* +var embeddedFiles embed.FS // main function is the start and the root for the website func main() { @@ -120,298 +61,92 @@ func main() { log.Printf("error loading .env file: %s", err) } - var cfg Config - err = envconfig.Process("", &cfg) + var config views.Config + err = envconfig.Process("", &config) if err != nil { log.Fatalf("failed to process env vars: %s", err) } - web := Web{ - mux: mux.NewRouter(), - cfg: cfg, - } - web.mux.HandleFunc("/", web.home) - //web.mux.HandleFunc("/authenticate1", web.authenticate1) - web.mux.HandleFunc("/endpoints", web.endpoints) - web.mux.HandleFunc("/streams", web.streams) - web.mux.HandleFunc("/start", web.start) - web.mux.HandleFunc("/resume", web.resume) - web.mux.HandleFunc("/status", web.status) - web.mux.HandleFunc("/stop", web.stop) - web.mux.HandleFunc("/list", web.list) - web.mux.HandleFunc("/save", web.save) - web.mux.HandleFunc("/recall", web.recall) - web.mux.HandleFunc("/youtubehelp", web.youtubeHelp) - web.mux.HandleFunc("/facebookhelp", web.facebookHelp) - web.mux.HandleFunc("/public/{id:[a-zA-Z0-9_.-]+}", web.public) // This handles all the public pages that the webpage can request, e.g. css, images and jquery - - fmt.Println("Server listening on port", web.cfg.ServerPort, "...") + root := false - err = http.ListenAndServe(net.JoinHostPort("", strconv.Itoa(web.cfg.ServerPort)), web.mux) - if err != nil { - log.Fatal(err) + _, err = os.ReadFile("/store/backend.go") + if err == nil { + root = true } -} -/*func authenticate(w http.ResponseWriter, r *http.Request) bool { - _ = w - response, err := http.Get("https://auth.dev.ystv.co.uk/api/set_token") + newStore, err := store.NewStore(root) if err != nil { - fmt.Println(err) - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - fmt.Println(err) - } - }(response.Body) - buf := new(strings.Builder) - _, err = io.Copy(buf, response.Body) - if err != nil { - fmt.Println(err) - } - fmt.Println(buf.String()) - reqToken := r.Header.Get("Authorization") - //splitToken := strings.Split(reqToken, "Bearer ") - //reqToken = splitToken[1] - fmt.Println("Token - ", reqToken) - err = godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) + log.Fatal("Failed to create store: ", err) } - sess := session.Get(r) - if sess == nil { - fmt.Println("None") - } else { - fmt.Println(sess) + r := &Router{ + config: config, + router: echo.New(), + views: views.New(config, newStore), } + r.router.HideBanner = true - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") + r.router.Debug = verbose - http.Redirect(w, r, jwtAuthentication+"authenticate1", http.StatusTemporaryRedirect) - return false -} + r.middleware() -// -func (web *Web) authenticate1(w http.ResponseWriter, r *http.Request) { - _ = w - reqToken := r.Header.Get("Authorization") - //splitToken := strings.Split(reqToken, "Bearer ") - //reqToken = splitToken[1] - fmt.Println("Token - ", reqToken) - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - jwtKey := os.Getenv("JWT_KEY") - - //fmt.Println(r.Cookies()) - - fmt.Println(r) - - view := Views{} - - view.cookie = sessions.NewCookieStore( - []byte("444bd23239f14b804af0ae40375c8feec80b699684f4d1a6d86f59658edb3706caaa306fd3361e6353bf54c0df66adb7c1e395cac79a72ee0339dc1892fd478e"), - []byte("444bd23239f14b804af0ae40375c8feec80b699684f4d1a6d86f59658edb3706"), - ) - - sess, err := view.cookie.Get(r, "session") - - fmt.Println(sess) - - _ = sess - - //store := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY"))) - - //session, err := store.Get(r, "session-name") - - //fmt.Println(err) - - //fmt.Println(session.ID) - - //fmt.Println(session) + r.loadRoutes() - //fmt.Println(session.Values["token"]) - - response, err := http.Get("https://auth.dev.ystv.co.uk/api/set_token") - if err != nil { - fmt.Println(err) - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - fmt.Println(err) - } - }(response.Body) - - buf := new(strings.Builder) - _, err = io.Copy(buf, response.Body) - if err != nil { - fmt.Println(err) - } - - tokenPage := buf.String() - - _ = tokenPage - - //fmt.Println(tokenPage) - - fmt.Println(r.Cookie("session")) - - c, err := r.Cookie("token") - if err != nil { - if err == http.ErrNoCookie { - fmt.Println(err) - - } - fmt.Println(err) - return - } - - claims := &Claims{} - - // Parse the JWT string and store the result in `claims`. - // Note that we are passing the key in this method as well. This method will return an error - // if the token is invalid (if it has expired according to the expiry time we set on sign in), - // or if the signature does not match - tkn, err := jwt.ParseWithClaims(c.Value, claims, func(token *jwt.Token) (interface{}, error) { - return jwtKey, nil - }) - fmt.Println(tkn) - if err != nil { - if err == jwt.ErrSignatureInvalid { - fmt.Println("Unauthorised") - fmt.Println(err) - return - } - fmt.Println(err) - return - } - if !tkn.Valid { - fmt.Println("Unauthorised") - return - } - if time.Now().Unix() > claims.Exp { - fmt.Println("Expired") - return - } - for _, perm := range claims.Perms { - if perm.Permission == "Streamer" { - fmt.Println("~~~Success~~~") - return - } - } - fmt.Println("Unauthorised") - return + r.router.Logger.Error(r.router.Start(r.config.ServerAddress)) + log.Fatalf("failed to start router on address %s", r.config.ServerAddress) } - - - - - - - - -func connectToHostPEM(host, username, privateKeyPath, privateKeyPassword string) (*ssh.Client, *ssh.Session, error) { - pemBytes, err := ioutil.ReadFile(privateKeyPath) - signer, err := signerFromPem(pemBytes, []byte(privateKeyPassword)) - if err != nil { - return nil, nil, err - } - sshConfig := &ssh.ClientConfig{ - User: username, - Auth: []ssh.AuthMethod{ - ssh.PublicKeys(signer), - }, - } - - sshConfig.HostKeyCallback = ssh.InsecureIgnoreHostKey() - - client, err := ssh.Dial("tcp", host, sshConfig) - if err != nil { - return nil, nil, err - } - - session, err := client.NewSession() - if err != nil { - err := client.Close() - if err != nil { - return nil, nil, err - } - return nil, nil, err - } - - return client, session, nil +func (r *Router) middleware() { + r.router.Pre(middleware.RemoveTrailingSlash()) + r.router.Use(middleware.Recover()) + r.router.Use(middleware.BodyLimit("15M")) + r.router.Use(middleware.GzipWithConfig(middleware.GzipConfig{ + Level: 5, + })) } -func signerFromPem(pemBytes []byte, password []byte) (ssh.Signer, error) { - - // read pem block - err := errors.New("Pem decode failed, no key found") - pemBlock, _ := pem.Decode(pemBytes) - if pemBlock == nil { - return nil, err - } - - // handle encrypted key - if x509.IsEncryptedPEMBlock(pemBlock) { - // decrypt PEM - pemBlock.Bytes, err = x509.DecryptPEMBlock(pemBlock, password) - if err != nil { - return nil, fmt.Errorf("Decrypting PEM block failed %v", err) - } - - // get RSA, EC or DSA key - key, err := parsePemBlock(pemBlock) - if err != nil { - return nil, err - } - - // generate signer instance from key - signer, err := ssh.NewSignerFromKey(key) +func (r *Router) loadRoutes() { + r.router.RouteNotFound("/*", r.views.Error404) + + r.router.HTTPErrorHandler = r.views.CustomHTTPErrorHandler + + assetHandler := http.FileServer(http.FS(echo.MustSubFS(embeddedFiles, "public/"))) + + r.router.GET("/public/*", echo.WrapHandler(http.StripPrefix("/public/", assetHandler))) + + validMethods := []string{http.MethodGet, http.MethodPost} + r.router.Match(validMethods, "/", r.views.HomeFunc) + r.router.Match(validMethods, "/endpoints", r.views.EndpointsFunc) + r.router.Match(validMethods, "/streams", r.views.StreamsFunc) // Call made by home to view all active streams for the endpoints + r.router.Match(validMethods, "/start", r.views.StartFunc) // Call made by home to start forwarding + r.router.Match(validMethods, "/resume", r.views.ResumeFunc) // To return to the page that controls a stream + r.router.Match(validMethods, "/status", r.views.StatusFunc) // Call made by home to view status + r.router.Match(validMethods, "/stop", r.views.StopFunc) // Call made by home to stop forwarding + r.router.Match(validMethods, "/list", r.views.ListFunc) // List view of current forwards + r.router.Match(validMethods, "/save", r.views.SaveFunc) // Where you can save a stream for later + r.router.Match(validMethods, "/recall", r.views.RecallFunc) // Where you can recall a saved stream to modify it if needed and start it + r.router.Match(validMethods, "/delete", r.views.DeleteFunc) // Deletes the saved stream if it is no longer needed + r.router.Match(validMethods, "/startUnique", r.views.StartUniqueFunc) // Call made by home to start forwarding from a recalled stream + r.router.Match(validMethods, "/youtubehelp", r.views.YoutubeHelpFunc) // YouTube help page + r.router.Match(validMethods, "/facebookhelp", r.views.FacebookHelpFunc) // Facebook help page + r.router.Match(validMethods, "/"+r.config.StreamerWebsocketPath, r.views.Websocket) // Websocket for the recorder and forwarder to communicate on + r.router.Match(validMethods, "/activeStreams", r.views.ActiveStreamsFunc) + r.router.GET("/api/health", func(c echo.Context) error { + marshal, err := json.Marshal(struct { + Status int `json:"status"` + }{ + Status: http.StatusOK, + }) if err != nil { - return nil, fmt.Errorf("Creating signer from encrypted key failed %v", err) - } - - return signer, nil - } else { - // generate signer instance from plain key - signer, err := ssh.ParsePrivateKey(pemBytes) - if err != nil { - return nil, fmt.Errorf("Parsing plain private key failed %v", err) + fmt.Println(err) + return &echo.HTTPError{ + Code: http.StatusBadRequest, + Message: err.Error(), + Internal: err, + } } - return signer, nil - } + c.Response().Header().Set("Content-Type", "application/json") + return c.JSON(http.StatusOK, marshal) + }) } - -func parsePemBlock(block *pem.Block) (interface{}, error) { - switch block.Type { - case "RSA PRIVATE KEY": - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("Parsing PKCS private key failed %v", err) - } else { - return key, nil - } - case "EC PRIVATE KEY": - key, err := x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("Parsing EC private key failed %v", err) - } else { - return key, nil - } - case "DSA PRIVATE KEY": - key, err := ssh.ParseDSAPrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("Parsing DSA private key failed %v", err) - } else { - return key, nil - } - default: - return nil, fmt.Errorf("Parsing private key failed, unsupported key type %q", block.Type) - } -}*/ diff --git a/server/public.go b/server/public.go deleted file mode 100644 index a00d6de..0000000 --- a/server/public.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "fmt" - "github.com/gorilla/mux" - "net/http" -) - -// public is the handler for any public documents, for example, the style sheet or images -func (web *Web) public(w http.ResponseWriter, r *http.Request) { - if verbose { - fmt.Println("Public called") - } - vars := mux.Vars(r) - http.ServeFile(w, r, "public/"+vars["id"]) -} diff --git a/server/public/stylesheet.css b/server/public/stylesheet.css index 98e3079..6bf0281 100644 --- a/server/public/stylesheet.css +++ b/server/public/stylesheet.css @@ -15,7 +15,7 @@ [data-theme="dark"] { --background-colour: #3B3B3B; --title-colour: #C9C9C9; - --text-colour: #B5B5B5; + --text-colour: #DDDDDD; --border-colour: #C2C2C2; --card-shaddow: 0 .5em 1em -.125em rgba(245, 245, 245, .1), 0 0 0 1px rgba(245, 245, 245, .02); --hyperlink-colour: #3273DC; @@ -50,10 +50,7 @@ img { height: 2.5em; justify-content: flex-start; line-height: 1.5; - padding-bottom: calc(.5em - 1px); - padding-left: calc(.75em - 1px); - padding-right: calc(.75em - 1px); - padding-top: calc(.5em - 1px); + padding: calc(.5em - 1px) calc(.75em - 1px); position: relative; vertical-align: top; } @@ -73,10 +70,7 @@ img { color: var(--text-colour); cursor: pointer; justify-content: center; - padding-bottom: calc(.5em - 1px); - padding-left: 1em; - padding-right: 1em; - padding-top: calc(.5em - 1px); + padding: calc(.5em - 1px) 1em; text-align: center; white-space: nowrap } @@ -307,3 +301,171 @@ footer { .select:not(.is-multiple) { height: 2.5em } + +.tabs { + -webkit-overflow-scrolling: touch; + align-items: stretch; + display: flex; + font-size: 1rem; + justify-content: space-between; + overflow: hidden; + overflow-x: auto; + white-space:nowrap +} + +.tabs a { + align-items: center; + border-bottom-color: #dbdbdb; + border-bottom-style: solid; + border-bottom-width: 1px; + /*color: #4a4a4a;*/ + color: var(--text-colour); + display: flex; + justify-content: center; + margin-bottom: -1px; + padding: .5em 1em; + vertical-align:top +} + +.tabs a:hover { + border-bottom-color: #363636; + color: #363636; + /*color: var(--background-colour);*/ +} + +.tabs li { + display:block +} + +.tabs li.is-active a { + border-bottom-color: #485fc7; + color:#485fc7 +} + +.tabs ul { + align-items: center; + border-bottom-color: #dbdbdb; + border-bottom-style: solid; + border-bottom-width: 1px; + display: flex; + flex-grow: 1; + flex-shrink: 0; + justify-content:flex-start +} + +.tabs ul.is-left { + padding-right:.75em +} + +.tabs ul.is-center { + flex: none; + justify-content: center; + padding-left: .75em; + padding-right:.75em +} + +.tabs ul.is-right { + justify-content: flex-end; + padding-left:.75em +} + +.tabs .icon:first-child { + margin-right:.5em +} + +.tabs .icon:last-child { + margin-left:.5em +} + +.tabs.is-centered ul { + justify-content:center +} + +.tabs.is-right ul { + justify-content:flex-end +} + +.tabs.is-boxed a { + border: 1px solid transparent; + border-radius:4px 4px 0 0 +} + +.tabs.is-boxed a:hover { + background-color: #f5f5f5; + border-bottom-color:#dbdbdb +} + +.tabs.is-boxed li.is-active a { + background-color: #fff; + border-color: #dbdbdb; + border-bottom-color:transparent !important +} + +.tabs.is-fullwidth li { + flex-grow: 1; + flex-shrink:0 +} + +.tabs.is-toggle a { + border-color: #dbdbdb; + border-style: solid; + border-width: 1px; + margin-bottom: 0; + position:relative +} + +.tabs.is-toggle a:hover { + background-color: #f5f5f5; + border-color: #b5b5b5; + z-index:2 +} + +.tabs.is-toggle li + li { + margin-left:-1px +} + +.tabs.is-toggle li:first-child a { + border-top-left-radius: 4px; + border-bottom-left-radius:4px +} + +.tabs.is-toggle li:last-child a { + border-top-right-radius: 4px; + border-bottom-right-radius:4px +} + +.tabs.is-toggle li.is-active a { + background-color: #485fc7; + border-color: #485fc7; + color: #FFF; + z-index:1 +} + +.tabs.is-toggle ul { + border-bottom:none +} + +.tabs.is-toggle.is-toggle-rounded li:first-child a { + border-bottom-left-radius: 9999px; + border-top-left-radius: 9999px; + padding-left:1.25em +} + +.tabs.is-toggle.is-toggle-rounded li:last-child a { + border-bottom-right-radius: 9999px; + border-top-right-radius: 9999px; + padding-right:1.25em +} + +.tabs.is-small { + font-size:.75rem +} + +.tabs.is-medium { + font-size:1.25rem +} + +.tabs.is-large { + font-size:1.5rem +} + diff --git a/server/recall.go b/server/recall.go deleted file mode 100644 index f6b7c14..0000000 --- a/server/recall.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "fmt" - "github.com/ystv/streamer/server/templates" - "net/http" - "time" -) - -// recall can pull back up stream details from the save function and allows you to start a stored stream -func (web *Web) recall(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - if verbose { - fmt.Println("Recall GET called") - } - web.t = templates.NewRecall() - - params := templates.PageParams{ - Base: templates.BaseParams{ - SystemTime: time.Now(), - }, - } - - err := web.t.Page(w, params) - if err != nil { - err = fmt.Errorf("failed to render dashboard: %w", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else if r.Method == "POST" { - if verbose { - fmt.Println("Recall POST called") - } - } -} diff --git a/server/resume.go b/server/resume.go deleted file mode 100644 index 8073cf6..0000000 --- a/server/resume.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "github.com/ystv/streamer/server/templates" - "net/http" - "strconv" - "time" -) - -// resume is used if the user decides to return at a later date then they can, by inputting the unique code that they were given then they can go to the resume page and enter the code -func (web *Web) resume(w http.ResponseWriter, r *http.Request) { - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication, http.StatusTemporaryRedirect) - return - }*/ - if r.Method == "GET" { - if verbose { - fmt.Println("Resume GET called") - } - web.t = templates.NewResume() - - params := templates.PageParams{ - Base: templates.BaseParams{ - SystemTime: time.Now(), - }, - } - - err := web.t.Page(w, params) - if err != nil { - err = fmt.Errorf("failed to render dashboard: %w", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else if r.Method == "POST" { - if verbose { - fmt.Println("Resume POST called") - } - db, err := sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - } else { - - } - - rows, err := db.Query("SELECT * FROM streams WHERE stream = ?", r.FormValue("unique")) - if err != nil { - fmt.Println(err) - } - var stream string - var recording, website bool - var streams int - - data := false - - accepted := false - - for rows.Next() { - err = rows.Scan(&stream, &recording, &website, &streams) - if err != nil { - fmt.Println(err) - } - data = true - if stream == r.FormValue("unique") { - accepted = true - } - } - - if !data { - fmt.Println("No data") - } - - err = rows.Close() - if err != nil { - fmt.Println(err) - } - - err = db.Close() - if err != nil { - fmt.Println(err) - } - - if accepted { - fmt.Println("ACCEPTED!") - _, err := w.Write([]byte("ACCEPTED!~" + strconv.FormatBool(recording) + "~" + strconv.FormatBool(website) + "~" + strconv.Itoa(streams))) - if err != nil { - fmt.Println(err) - } - } else { - fmt.Println("REJECTED!") - _, err := w.Write([]byte("REJECTED!")) - if err != nil { - fmt.Println(err) - } - } - } -} diff --git a/server/save.go b/server/save.go deleted file mode 100644 index a90feab..0000000 --- a/server/save.go +++ /dev/null @@ -1,34 +0,0 @@ -package main - -import ( - "fmt" - "github.com/ystv/streamer/server/templates" - "net/http" - "time" -) - -// save allows for the functionality of saving a stream's details for later in order to make things easier for massive operations where you have multiple streams at once -func (web *Web) save(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { - if verbose { - fmt.Println("Save GET called") - } - web.t = templates.NewSave() - - params := templates.PageParams{ - Base: templates.BaseParams{ - SystemTime: time.Now(), - }, - } - - err := web.t.Page(w, params) - if err != nil { - err = fmt.Errorf("failed to render dashboard: %w", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - } - } else if r.Method == "POST" { - if verbose { - fmt.Println("Save POST called") - } - } -} diff --git a/server/start.go b/server/start.go deleted file mode 100644 index 927088c..0000000 --- a/server/start.go +++ /dev/null @@ -1,252 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "log" - "math" - "net/http" - "sort" - "strconv" - "strings" - "sync" -) - -// start is the core of the program, where it takes the values set by the user in the webpage and processes the data and sends it to the recorder and the forwarder -func (web *Web) start(w http.ResponseWriter, r *http.Request) { - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication, http.StatusTemporaryRedirect) - return - }*/ - errors := false - var errorMessage string - if r.Method == "POST" { - if verbose { - fmt.Println("Start POST called") - } - err := r.ParseForm() - if err != nil { - fmt.Println(err) - errorMessage = err.Error() - errors = true - } else { - recording := false - websiteStream := false - streams := 0 - websiteValid := true - var forwarderStart string - if r.FormValue("website_stream") == "on" { - websiteStream = true - if web.websiteCheck(r.FormValue("website_stream_endpoint")) { - websiteValid = true - forwarderStart = "./forwarder_start " + r.FormValue("stream_selector") + " " + r.FormValue("website_stream_endpoint") + " " - } else { - websiteValid = false - errors = true - errorMessage = "Website key check has failed" - } - } else { - forwarderStart = "./forwarder_start \"" + r.FormValue("stream_selector") + "\" no " - } - if websiteValid { - largest := 0 - var numbers []int - for s := range r.PostForm { - if strings.Contains(s, "stream_server_") { - split := strings.Split(s, "_") - conv, _ := strconv.ParseInt(split[2], 10, 64) - largest = int(math.Max(float64(largest), float64(conv))) - numbers = append(numbers, int(conv)) - } - } - sort.Ints(numbers) - - var b []byte - - loop := true - - db, err := sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - for loop { - b = make([]byte, 10) - for i := range b { - b[i] = charset[seededRand.Intn(len(charset))] - } - - rows, err := db.Query("SELECT stream FROM streams") - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - var stream string - data := false - - for rows.Next() { - err = rows.Scan(&stream) - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } else { - data = true - if stream == string(b) { - loop = true - break - } else { - loop = false - } - } - } - - if !data { - loop = false - } - - err = rows.Close() - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - } - - stmt, err := db.Prepare("INSERT INTO streams(stream, recording, website, streams) values(?, false, false, 0)") - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - res, err := stmt.Exec(string(b)) - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - id, err := res.LastInsertId() - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - err = db.Close() - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } else if id != 0 && !errors { - forwarderStart += string(b) + " " - for _, index := range numbers { - server := r.FormValue("stream_server_" + strconv.Itoa(index)) - if server[len(server)-1] != '/' { - server += "/" - } - forwarderStart += "\"" + server + "\" \"" + r.FormValue("stream_key_"+strconv.Itoa(index)) + "\" " - streams++ - } - forwarderStart += "| bash" - - recorderStart := "./recorder_start \"" + r.FormValue("stream_selector") + "\" \"" + r.FormValue("save_path") + "\" " + string(b) + " | bash" - - var wg sync.WaitGroup - if r.FormValue("record") == "on" { - recording = true - wg.Add(1) - go func() { - defer wg.Done() - _, err := RunCommandOnHost(web.cfg.Recorder, web.cfg.RecorderUsername, web.cfg.RecorderPassword, recorderStart) - if err != nil { - log.Printf("Error starting recorder: %v", err) - errors = true - errorMessage += err.Error() - } - }() - } - wg.Add(1) - go func() { - defer wg.Done() - _, err := RunCommandOnHost(web.cfg.Forwarder, web.cfg.ForwarderUsername, web.cfg.ForwarderPassword, forwarderStart) - if err != nil { - log.Printf("Error starting forwarder: %v", err) - errors = true - errorMessage += err.Error() - } - }() - wg.Wait() - - if errors == false { - _, _ = http.Get(web.cfg.TransmissionLight + "transmission_on") - - db, err = sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } else { - stmt, err := db.Prepare("UPDATE streams SET recording = ?, website = ?, streams = ? WHERE stream = ?") - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - res, err := stmt.Exec(recording, websiteStream, streams, string(b)) - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - id, err = res.LastInsertId() - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - - err = db.Close() - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } else { - _, err = w.Write(b) - if err != nil { - fmt.Println(err) - errors = true - errorMessage = err.Error() - } - } - } - } - } - } else { - fmt.Println("Failed to authenticate website stream") - errors = true - errorMessage = "Failed to authenticate website stream" - } - } - } - if errors { - fmt.Println("An error has occurred...\n" + errorMessage) - _, err := w.Write([]byte("An error has occurred...\n" + errorMessage)) - if err != nil { - fmt.Println(err) - } - } -} diff --git a/server/status.go b/server/status.go deleted file mode 100644 index 4a3f31f..0000000 --- a/server/status.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "fmt" - "log" - "net/http" - "strconv" - "strings" - "sync" -) - -// status is used to check the status of the streams and does this by tail command of the output logs -func (web *Web) status(w http.ResponseWriter, r *http.Request) { - if r.Method == "POST" { - if verbose { - fmt.Println("Status POST called") - } - db, err := sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - } else { - rows, err := db.Query("SELECT recording, website, streams FROM streams WHERE stream = ?", r.FormValue("unique")) - if err != nil { - fmt.Println(err) - } - - var recording, website bool - var streams int - - data := false - - for rows.Next() { - err = rows.Scan(&recording, &website, &streams) - if err != nil { - fmt.Println(err) - } - data = true - } - - err = rows.Close() - if err != nil { - fmt.Println(err) - } - - err = db.Close() - if err != nil { - fmt.Println(err) - } - - m := make(map[string]string) - var wg sync.WaitGroup - if data { - if recording { - wg.Add(2) - go func() { - defer wg.Done() - statusCmd := "./recorder_status.sh " + r.FormValue("unique") - dataOut, err := RunCommandOnHost(web.cfg.Recorder, web.cfg.RecorderUsername, web.cfg.RecorderPassword, statusCmd) - if err != nil { - log.Printf("error running recorder status: %s", err) - return - } - - dataOut1 := dataOut[:len(dataOut)-2] - - if len(dataOut1) > 0 { - if strings.Contains(dataOut1, "frame=") { - first := strings.Index(dataOut1, "frame=") - 1 - last := strings.LastIndex(dataOut1, "\r") - dataOut1 = dataOut1[:last] - last = strings.LastIndex(dataOut1, "\r") + 1 - m["recording"] = dataOut1[:first] + "\n" + dataOut1[last:] - } else { - m["recording"] = dataOut1 - } - } - - fmt.Println("Recorder status success") - }() - } else { - wg.Add(1) - } - go func() { - defer wg.Done() - - statusCmd := "./forwarder_status " + strconv.FormatBool(website) + " " + strconv.Itoa(streams) + " " + r.FormValue("unique") - dataOut, err := RunCommandOnHost(web.cfg.Forwarder, web.cfg.ForwarderUsername, web.cfg.ForwarderPassword, statusCmd) - if err != nil { - log.Printf("error running forwarder status: %s", err) - return - } - - dataOut1 := dataOut[4 : len(dataOut)-2] - - dataOut2 := strings.Split(dataOut1, "\u0000") - - for _, dataOut3 := range dataOut2 { - if len(dataOut3) > 0 { - if strings.Contains(dataOut3, "frame=") { - dataOut4 := strings.Split(dataOut3, "~:~") - first := strings.Index(dataOut4[1], "frame=") - 1 - last := strings.LastIndex(dataOut4[1], "\r") - dataOut4[1] = dataOut4[1][:last] - last = strings.LastIndex(dataOut4[1], "\r") + 1 - m[strings.Trim(dataOut4[0], " ")] = dataOut4[1][:first] + "\n" + dataOut4[1][last:] - } else { - dataOut4 := strings.Split(dataOut3, "~:~") - m[strings.Trim(dataOut4[0], " ")] = dataOut4[1] - } - } - } - - fmt.Println("Forwarder status success") - }() - wg.Wait() - jsonStr, err := json.Marshal(m) - output := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(string(jsonStr[1:len(jsonStr)-1]), "\\n", "
"), "\"", ""), " , ", "


"), " ,", "


"), "
,", "

") - _, err = w.Write([]byte(output)) - if err != nil { - fmt.Println(err.Error()) - } - } else { - fmt.Println("ERROR DATA STATUS") - } - } - } -} diff --git a/server/stop.go b/server/stop.go deleted file mode 100644 index d33aa89..0000000 --- a/server/stop.go +++ /dev/null @@ -1,133 +0,0 @@ -package main - -import ( - "database/sql" - "fmt" - "github.com/ystv/streamer/server/helpers" - "log" - "net/http" - "strings" - "sync" -) - -// stop is used when the stream is finished then you can stop the stream by pressing the stop button and that would kill all the ffmpeg commands -func (web *Web) stop(w http.ResponseWriter, r *http.Request) { - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication+"list", http.StatusTemporaryRedirect) - return - }*/ - if r.Method == "POST" { - if verbose { - fmt.Println("Stop POST called") - } - db, err := sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - } else { - - } - - rows, err := db.Query("SELECT recording FROM streams WHERE stream = ?", r.FormValue("unique")) - if err != nil { - fmt.Println(err) - } - var recording bool - - data := false - - for rows.Next() { - err = rows.Scan(&recording) - if err != nil { - fmt.Println(err) - } - data = true - } - err = rows.Close() - if err != nil { - fmt.Println(err) - } - err = db.Close() - if err != nil { - fmt.Println(err) - } - if data { - var wg sync.WaitGroup - if recording { - wg.Add(2) - go func() { - defer wg.Done() - - stopCmd := "./recorder_stop " + r.FormValue("unique") + " | bash" - _, err := RunCommandOnHost(web.cfg.Recorder, web.cfg.RecorderUsername, web.cfg.RecorderPassword, stopCmd) - if err != nil { - log.Printf("error running recorder stop: %s", err) - return - } - }() - } else { - wg.Add(1) - } - go func() { - defer wg.Done() - - stopCmd := "./forwarder_stop " + r.FormValue("unique") + " | bash" - _, err := RunCommandOnHost(web.cfg.Forwarder, web.cfg.ForwarderUsername, web.cfg.ForwarderPassword, stopCmd) - if err != nil { - log.Printf("error running forwarder stop: %s", err) - return - } - - fmt.Println("Forwarder stop success") - }() - wg.Wait() - fmt.Println("STOPPED!") - - db, err = sql.Open("sqlite3", "db/streams.db") - if err != nil { - fmt.Println(err) - } else { - stmt, err := db.Prepare("DELETE FROM streams WHERE stream = ?") - if err != nil { - fmt.Println(err, "DELETE") - } - - res, err := stmt.Exec(r.FormValue("unique")) - if err != nil { - fmt.Println(err) - } - - affect, err := res.RowsAffected() - if err != nil { - fmt.Println(err) - } - - if affect != 0 { - _, err = w.Write([]byte("STOPPED!")) - if err != nil { - fmt.Println(err) - } - } - } - err = db.Close() - if err != nil { - fmt.Println(err.Error()) - } - fmt.Println(helpers.ExistingStreamCheck(verbose)) - if !helpers.ExistingStreamCheck(verbose) { - _, err := http.Get(web.cfg.TransmissionLight + "rehearsal_transmission_off") // Output is ignored as it returns a 204 status and there's a weird bug with no content - if err != nil && !strings.Contains(err.Error(), "unexpected EOF") { - fmt.Println(err.Error()) - } - } - } else { - - } - } -} diff --git a/server/storage/storage.pb.go b/server/storage/storage.pb.go new file mode 100644 index 0000000..15d0756 --- /dev/null +++ b/server/storage/storage.pb.go @@ -0,0 +1,358 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc v3.20.3 +// source: storage.proto + +package storage + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Streamer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stored []*Stored `protobuf:"bytes,1,rep,name=stored,proto3" json:"stored,omitempty"` + Stream []*Stream `protobuf:"bytes,2,rep,name=stream,proto3" json:"stream,omitempty"` +} + +func (x *Streamer) Reset() { + *x = Streamer{} + if protoimpl.UnsafeEnabled { + mi := &file_storage_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Streamer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Streamer) ProtoMessage() {} + +func (x *Streamer) ProtoReflect() protoreflect.Message { + mi := &file_storage_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Streamer.ProtoReflect.Descriptor instead. +func (*Streamer) Descriptor() ([]byte, []int) { + return file_storage_proto_rawDescGZIP(), []int{0} +} + +func (x *Streamer) GetStored() []*Stored { + if x != nil { + return x.Stored + } + return nil +} + +func (x *Streamer) GetStream() []*Stream { + if x != nil { + return x.Stream + } + return nil +} + +type Stored struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stream string `protobuf:"bytes,1,opt,name=stream,proto3" json:"stream,omitempty"` + Input string `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + Recording string `protobuf:"bytes,3,opt,name=recording,proto3" json:"recording,omitempty"` + Website string `protobuf:"bytes,4,opt,name=website,proto3" json:"website,omitempty"` + Streams []string `protobuf:"bytes,5,rep,name=streams,proto3" json:"streams,omitempty"` +} + +func (x *Stored) Reset() { + *x = Stored{} + if protoimpl.UnsafeEnabled { + mi := &file_storage_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Stored) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stored) ProtoMessage() {} + +func (x *Stored) ProtoReflect() protoreflect.Message { + mi := &file_storage_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stored.ProtoReflect.Descriptor instead. +func (*Stored) Descriptor() ([]byte, []int) { + return file_storage_proto_rawDescGZIP(), []int{1} +} + +func (x *Stored) GetStream() string { + if x != nil { + return x.Stream + } + return "" +} + +func (x *Stored) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +func (x *Stored) GetRecording() string { + if x != nil { + return x.Recording + } + return "" +} + +func (x *Stored) GetWebsite() string { + if x != nil { + return x.Website + } + return "" +} + +func (x *Stored) GetStreams() []string { + if x != nil { + return x.Streams + } + return nil +} + +type Stream struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Stream string `protobuf:"bytes,1,opt,name=stream,proto3" json:"stream,omitempty"` + Input string `protobuf:"bytes,2,opt,name=input,proto3" json:"input,omitempty"` + Recording bool `protobuf:"varint,3,opt,name=recording,proto3" json:"recording,omitempty"` + Website bool `protobuf:"varint,4,opt,name=website,proto3" json:"website,omitempty"` + Streams uint64 `protobuf:"varint,5,opt,name=streams,proto3" json:"streams,omitempty"` +} + +func (x *Stream) Reset() { + *x = Stream{} + if protoimpl.UnsafeEnabled { + mi := &file_storage_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Stream) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Stream) ProtoMessage() {} + +func (x *Stream) ProtoReflect() protoreflect.Message { + mi := &file_storage_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Stream.ProtoReflect.Descriptor instead. +func (*Stream) Descriptor() ([]byte, []int) { + return file_storage_proto_rawDescGZIP(), []int{2} +} + +func (x *Stream) GetStream() string { + if x != nil { + return x.Stream + } + return "" +} + +func (x *Stream) GetInput() string { + if x != nil { + return x.Input + } + return "" +} + +func (x *Stream) GetRecording() bool { + if x != nil { + return x.Recording + } + return false +} + +func (x *Stream) GetWebsite() bool { + if x != nil { + return x.Website + } + return false +} + +func (x *Stream) GetStreams() uint64 { + if x != nil { + return x.Streams + } + return 0 +} + +var File_storage_proto protoreflect.FileDescriptor + +var file_storage_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x07, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x22, 0x5c, 0x0a, 0x08, 0x53, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x65, 0x72, 0x12, 0x27, 0x0a, 0x06, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, + 0x74, 0x6f, 0x72, 0x65, 0x64, 0x52, 0x06, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x64, 0x12, 0x27, 0x0a, + 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x06, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x22, 0x88, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x6f, 0x72, 0x65, + 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, + 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, + 0x07, 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x77, 0x65, 0x62, 0x73, 0x69, 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x73, 0x22, 0x88, 0x01, 0x0a, 0x06, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x16, 0x0a, 0x06, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, + 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x72, + 0x65, 0x63, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x77, 0x65, 0x62, 0x73, + 0x69, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x77, 0x65, 0x62, 0x73, 0x69, + 0x74, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x07, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x42, 0x29, 0x5a, 0x27, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x79, 0x73, 0x74, 0x76, 0x2f, + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x65, 0x72, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, + 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_storage_proto_rawDescOnce sync.Once + file_storage_proto_rawDescData = file_storage_proto_rawDesc +) + +func file_storage_proto_rawDescGZIP() []byte { + file_storage_proto_rawDescOnce.Do(func() { + file_storage_proto_rawDescData = protoimpl.X.CompressGZIP(file_storage_proto_rawDescData) + }) + return file_storage_proto_rawDescData +} + +var file_storage_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_storage_proto_goTypes = []interface{}{ + (*Streamer)(nil), // 0: storage.Streamer + (*Stored)(nil), // 1: storage.Stored + (*Stream)(nil), // 2: storage.Stream +} +var file_storage_proto_depIdxs = []int32{ + 1, // 0: storage.Streamer.stored:type_name -> storage.Stored + 2, // 1: storage.Streamer.stream:type_name -> storage.Stream + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_storage_proto_init() } +func file_storage_proto_init() { + if File_storage_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_storage_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Streamer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storage_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Stored); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_storage_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Stream); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_storage_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_storage_proto_goTypes, + DependencyIndexes: file_storage_proto_depIdxs, + MessageInfos: file_storage_proto_msgTypes, + }.Build() + File_storage_proto = out.File + file_storage_proto_rawDesc = nil + file_storage_proto_goTypes = nil + file_storage_proto_depIdxs = nil +} diff --git a/server/storage/storage.proto b/server/storage/storage.proto new file mode 100644 index 0000000..afdaaa9 --- /dev/null +++ b/server/storage/storage.proto @@ -0,0 +1,24 @@ +syntax = "proto3"; +package storage; +option go_package = "github.com/ystv/streamer/server/storage"; + +message Streamer { + repeated Stored stored = 1; + repeated Stream stream = 2; +} + +message Stored { + string stream = 1; + string input = 2; + string recording = 3; + string website = 4; + repeated string streams = 5; +} + +message Stream { + string stream = 1; + string input = 2; + bool recording = 3; + bool website = 4; + uint64 streams = 5; +} diff --git a/server/store/backend.go b/server/store/backend.go new file mode 100644 index 0000000..d5d6b75 --- /dev/null +++ b/server/store/backend.go @@ -0,0 +1,10 @@ +package store + +import ( + "github.com/ystv/streamer/server/storage" +) + +type Backend interface { + Read() (*storage.Streamer, error) + Write(state *storage.Streamer) error +} diff --git a/server/store/file.go b/server/store/file.go new file mode 100644 index 0000000..e8107ac --- /dev/null +++ b/server/store/file.go @@ -0,0 +1,107 @@ +package store + +import ( + "fmt" + "github.com/ystv/streamer/server/storage" + "google.golang.org/protobuf/proto" + "log" + "os" + "sync" + "time" +) + +// FileBackend Applications: apps, Prefix: prefix +type FileBackend struct { + path string + cache *storage.Streamer + mutex sync.RWMutex +} + +func NewFileBackend(root bool) (Backend, error) { + var fb *FileBackend + + if root { + fb = &FileBackend{path: "/db/store.db"} + } else { + fb = &FileBackend{path: "./db/store.db"} + } + + state, err := fb.read(root) + if err != nil { + return nil, err + } + // persist state + err = fb.save(state) + if err != nil { + return nil, err + } + fb.cache = state + return fb, nil +} + +// Read parses the store state from a file +func (fb *FileBackend) read(root bool) (*storage.Streamer, error) { + var streamer storage.Streamer + + if root { + _, err := os.Stat("/db") + if err != nil { + err = os.Mkdir("/db", 0777) + if err != nil { + return nil, fmt.Errorf("failed to make folder /db: %w", err) + } + } + } else { + _, err := os.Stat("./db") + if err != nil { + err = os.Mkdir("./db", 0777) + if err != nil { + return nil, fmt.Errorf("failed to make folder ./db: %w", err) + } + } + } + + data, err := os.ReadFile(fb.path) + // Non-existing streamer is ok + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("no previous file read: %w", err) + } + if err == nil { + if err := proto.Unmarshal(data, &streamer); err != nil { + return nil, fmt.Errorf("failed to parse stream streamer: %w", err) + } + } + + log.Printf("db file from: %s", fb.path) + return &streamer, nil +} + +// Save stores the store state in a file +func (fb *FileBackend) save(streamer *storage.Streamer) error { + out, err := proto.Marshal(streamer) + if err != nil { + return fmt.Errorf("failed to encode streamer: %w", err) + } + tmp := fmt.Sprintf(fb.path+".%v", time.Now().Format("2006-01-02T15-04-05")) + if err := os.WriteFile(tmp, out, 0600); err != nil { + return fmt.Errorf("failed to write streamer: %w", err) + } + err = os.Rename(tmp, fb.path) + if err != nil { + return fmt.Errorf("failed to move streamer: %w", err) + } + return nil +} + +func (fb *FileBackend) Read() (*storage.Streamer, error) { + fb.mutex.RLock() + defer fb.mutex.RUnlock() + return fb.cache, nil +} + +func (fb *FileBackend) Write(state *storage.Streamer) error { + fb.mutex.Lock() + defer fb.mutex.Unlock() + fb.cache = state + return fb.save(state) +} diff --git a/server/store/store.go b/server/store/store.go new file mode 100644 index 0000000..67e5981 --- /dev/null +++ b/server/store/store.go @@ -0,0 +1,139 @@ +package store + +import ( + "fmt" + _ "fmt" + "github.com/ystv/streamer/server/storage" +) + +type Store struct { + backend Backend +} + +func NewStore(root bool) (*Store, error) { + backend, err := NewFileBackend(root) + if err != nil { + return nil, err + } + return &Store{backend: backend}, nil +} + +func (store *Store) GetStreams() ([]*storage.Stream, error) { + streamer, err := store.Get() + if err != nil { + return nil, err + } + return streamer.Stream, err +} + +func (store *Store) FindStream(unique string) (*storage.Stream, error) { + streamer, err := store.Get() + if err != nil { + return nil, err + } + for _, c1 := range streamer.Stream { + if c1.Stream == unique { + return c1, nil + } + } + return nil, fmt.Errorf("unable to find stream for FindStream: %s", unique) +} + +func (store *Store) AddStream(stream *storage.Stream) (*storage.Stream, error) { + streamer, err := store.Get() + if err != nil { + return nil, err + } + + for _, c := range streamer.Stream { + if c.Stream == stream.Stream { + return nil, fmt.Errorf("unable to add stream duplicate id for AddStream") + } + } + + streamer.Stream = append(streamer.Stream, stream) + + if err = store.backend.Write(streamer); err != nil { + return nil, err + } + + return stream, nil +} + +func (store *Store) DeleteStream(unique string) error { + streamer, err := store.backend.Read() + if err != nil { + return err + } + + s := streamer.Stream + found := false + var index int + var v *storage.Stream + for index, v = range s { + if v.Stream == unique { + found = true + break + } + } + + if found { + copy(s[index:], s[index+1:]) // Shift a[i+1:] left one index + s[len(s)-1] = nil // Erase last element (write zero value) + streamer.Stream = s[:len(s)-1] // Truncate slice + } else { + return fmt.Errorf("stream not found for DeleteStream") + } + + if err = store.backend.Write(streamer); err != nil { + return err + } + + return nil +} + +func (store *Store) GetStored() ([]*storage.Stored, error) { + streamer, err := store.Get() + if err != nil { + return nil, err + } + return streamer.Stored, err +} + +func (store *Store) FindStored(unique string) (*storage.Stored, error) { + streamer, err := store.Get() + if err != nil { + return nil, err + } + for _, c1 := range streamer.Stored { + if c1.Stream == unique { + return c1, nil + } + } + return nil, fmt.Errorf("unable to find stored for FindStored: %s", unique) +} + +func (store *Store) AddStored(stored *storage.Stored) (*storage.Stored, error) { + streamer, err := store.Get() + if err != nil { + return nil, err + } + + for _, c := range streamer.Stored { + if c.Stream == stored.Stream { + return nil, fmt.Errorf("unable to add stored duplicate id for AddStored") + } + } + + streamer.Stored = append(streamer.Stored, stored) + + if err = store.backend.Write(streamer); err != nil { + return nil, err + } + + return stored, nil +} + +func (store *Store) Get() (*storage.Streamer, error) { + return store.backend.Read() +} diff --git a/server/streams.go b/server/streams.go deleted file mode 100644 index fb4095d..0000000 --- a/server/streams.go +++ /dev/null @@ -1,91 +0,0 @@ -package main - -import ( - "encoding/xml" - "fmt" - "github.com/joho/godotenv" - "io" - "net/http" - "strings" -) - -// streams collects the data from the rtmp stat page of nginx and produces a list of active streaming endpoints from given endpoints -func (web *Web) streams(w http.ResponseWriter, r *http.Request) { - /*if !authenticate(w, r) { - err := godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - jwtAuthentication := os.Getenv("JWT_AUTHENTICATION") - - http.Redirect(w, r, jwtAuthentication, http.StatusTemporaryRedirect) - return - }*/ - if r.Method == "POST" { - if verbose { - fmt.Println("Streams POST called") - } - err := r.ParseForm() - if err != nil { - fmt.Println(err) - } - - err = godotenv.Load() - if err != nil { - fmt.Printf("error loading .env file: %s", err) - } - - response, err := http.Get(web.cfg.StreamChecker) - if err != nil { - fmt.Println(err) - } - defer func(Body io.ReadCloser) { - err := Body.Close() - if err != nil { - fmt.Println(err) - } - }(response.Body) - - buf := new(strings.Builder) - _, err = io.Copy(buf, response.Body) - if err != nil { - fmt.Println(err) - } - - streamPageContent := buf.String() - - var rtmp RTMP - - err = xml.Unmarshal([]byte(streamPageContent), &rtmp) - if err != nil { - fmt.Println(err) - } - - var endpoints []string - - for key := range r.Form { - endpoint := strings.Split(key, "~") - for i := 0; i < len(rtmp.Server.Applications); i++ { - if rtmp.Server.Applications[i].Name == endpoint[1] { - for j := 0; j < len(rtmp.Server.Applications[i].Live.Streams); j++ { - endpoints = append(endpoints, endpoint[1]+"/"+rtmp.Server.Applications[i].Live.Streams[j].Name) - } - } - } - } - - if len(endpoints) != 0 { - stringByte := strings.Join(endpoints, "\x20") - _, err := w.Write([]byte(stringByte)) - if err != nil { - fmt.Println(err) - } - } else { - _, err := w.Write([]byte("No active streams with the current selection")) - if err != nil { - fmt.Println(err) - } - } - } -} diff --git a/server/templates/404NotFound.tmpl b/server/templates/404NotFound.tmpl new file mode 100644 index 0000000..059e5ae --- /dev/null +++ b/server/templates/404NotFound.tmpl @@ -0,0 +1,14 @@ +{{define "title"}}404 - Not found{{end}} +{{define "content"}} +
+
+
+
+
+

404 - Not found

+
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/server/templates/base.tmpl b/server/templates/_base.tmpl similarity index 92% rename from server/templates/base.tmpl rename to server/templates/_base.tmpl index 13119ce..48c18f8 100644 --- a/server/templates/base.tmpl +++ b/server/templates/_base.tmpl @@ -22,8 +22,11 @@

Dark Mode

+

{{block "content" .}}{{end}} - +

+ {{ $year := thisYear }} + +
+
+

Listing all streams, active and saved

+

If the stream is prefixed by "Active - " then go to https://streamer.dev.ystv.co.uk/resume In order to stop them or view the current status
+ If the stream is prefixed by "Saved - " then go to https://streamer.dev.ystv.co.uk/recall In order to start them +


+

+


+ +
+
+ {{end}} \ No newline at end of file diff --git a/server/templates/main.tmpl b/server/templates/main.tmpl index 36f5057..18ab3a5 100644 --- a/server/templates/main.tmpl +++ b/server/templates/main.tmpl @@ -1,196 +1,244 @@ {{define "content"}} -
-
-
-

This tool allows you to select the endpoint that your streaming software is streaming to and then forward - it - onto - your platforms of choice.
- Streamer is designed to be simple to use, you select the endpoint that you are streaming to by selecting - the - possible endpoints using the checkboxes below and then pressing "Refresh streams".


-

Stream endpoint selector

-

Endpoints

-


-

Selection

-
-
+ +
+
+
+

This tool allows you to select the endpoint that your streaming software is streaming to and then + forward + it + onto + your platforms of choice.
+ Streamer is designed to be simple to use, you select the endpoint that you are streaming to by + selecting + the + possible endpoints using the checkboxes below and then pressing "Refresh streams".


+

{{.Error}}


+

Stream endpoint selector

+

Endpoints

+


+

Selection

+ +
+
+ +
- - -
+
-
-
-
-
+
+
+ -

After refreshing what streams are active, you can select the endpoint with the dropdown box below

-
-

Select stream

-
- - -
-

- - - +

Select stream

+
+ + +
+

+ + + +
-
-