Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Build and Test

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Run tests
run: go test -v ./...

- name: Build
run: go build -v ./...

- name: Run security scan (gosec)
run: |
go install github.com/securego/gosec/v2/cmd/gosec@latest
gosec ./...

build-all:
name: Build All Platforms
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Needed for git describe

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Build all platforms
run: bash go-build-all.sh

- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: binaries
path: build/
retention-days: 7
37 changes: 37 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
release:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Build all platforms
run: bash go-build-all.sh

- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: build/*
draft: true
prerelease: false
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
59 changes: 59 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: Security Scan

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
schedule:
# Run security scan daily at 2 AM UTC
- cron: '0 2 * * *'

permissions:
contents: read
security-events: write

jobs:
gosec:
name: Gosec Security Scanner
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'

- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: '-fmt sarif -out gosec-results.sarif ./...'

- name: Upload Gosec results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: gosec-results.sarif

codeql:
name: CodeQL Analysis
runs-on: ubuntu-latest
permissions:
security-events: write
actions: read
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: go

- name: Autobuild
uses: github/codeql-action/autobuild@v3

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*.so
*.dylib
*.zip
proxy4plex

# Test binary, built with `go test -c`
*.test
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM golang:1.17-alpine
FROM golang:1.24-alpine

LABEL maintainer="Kadrim <kadrim@users.noreply.github.com>"

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Beware: Sideloading (i.e. installing the app on the TV) does not work this way,

## Compiling

At the time of writing, this package needs at least [golang](https://golang.org/) v1.17
At the time of writing, this package needs at least [golang](https://golang.org/) v1.24

To compile a binary for your currently running system, simply run this command:

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/kadrim/proxy4plex

go 1.17
go 1.24
103 changes: 90 additions & 13 deletions proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,43 @@ package main

import (
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strconv"
"strings"
"time"
)

type transport struct {
http.RoundTripper
}

func isLocalOrPrivate(hostname string) bool {
host, _, err := net.SplitHostPort(hostname)
if err != nil {
host = hostname
}

if host == "localhost" || host == "127.0.0.1" || host == "::1" {
return true
}

ips, err := net.LookupIP(host)
if err != nil {
return true
}

for _, ip := range ips {
if ip.IsLoopback() || ip.IsPrivate() {
return true
}
}

return false
}

func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
resp, err = t.RoundTripper.RoundTrip(req)
if err != nil {
Expand All @@ -24,8 +50,8 @@ func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error
case 301:
fallthrough
case 302:
redirectURL, _ := url.Parse(resp.Header.Get("Location"))
if redirectURL.Scheme == "https" {
redirectURL, err := url.Parse(resp.Header.Get("Location"))
if err == nil && redirectURL.Scheme == "https" {
resp.Header.Set("Location", "http://"+req.Header.Get("X-Forwarded-Host")+"?url="+resp.Header.Get("Location"))
}
}
Expand All @@ -35,18 +61,47 @@ func (t *transport) RoundTrip(req *http.Request) (resp *http.Response, err error
// handle a request send it to the server
func handleRequest(res http.ResponseWriter, req *http.Request) {
// get the request URI
server, _ := url.Parse("https://" + host)
reqURL, _ := url.Parse(req.RequestURI)
server, err := url.Parse("https://" + host)
if err != nil {
http.Error(res, "Invalid server URL", http.StatusInternalServerError)
return
}

reqURL, err := url.Parse(req.RequestURI)
if err != nil {
http.Error(res, "Invalid request URI", http.StatusBadRequest)
return
}

if reqURL.Query().Get("url") != "" { // special proxy handling
// extract the GET-Param url
server, _ = url.Parse(reqURL.Query().Get("url"))
targetURL := reqURL.Query().Get("url")
server, err = url.Parse(targetURL)
if err != nil {
http.Error(res, "Invalid target URL", http.StatusBadRequest)
return
}

if server.Scheme != "https" && server.Scheme != "http" {
http.Error(res, "Invalid URL scheme", http.StatusBadRequest)
return
}

if isLocalOrPrivate(server.Host) {
http.Error(res, "Access to private addresses is not allowed", http.StatusForbidden)
return
}

// replace request
req.URL = server
req.RequestURI = ""

// mux host
server, _ = url.Parse(server.Scheme + "://" + server.Host)
server, err = url.Parse(server.Scheme + "://" + server.Host)
if err != nil {
http.Error(res, "Invalid server URL", http.StatusInternalServerError)
return
}
}

// prepare reverse proxy
Expand All @@ -72,15 +127,17 @@ func handleRequest(res http.ResponseWriter, req *http.Request) {
func runProxy(disableSideloading bool) {
// handle simple information path
http.HandleFunc("/info", func(res http.ResponseWriter, req *http.Request) {
res.Write([]byte("The Plex proxy service is running on " + req.Host))
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = res.Write([]byte("The Plex proxy service is running on " + req.Host))
})

//handle widgetlist for sideloading
http.HandleFunc("/widgetlist.xml", func(res http.ResponseWriter, req *http.Request) {
buf, err := retreiveZipFile()
if err != nil {
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(err.Error()))
_, _ = res.Write([]byte(err.Error()))
log.Println(err)
return
}
Expand All @@ -96,34 +153,54 @@ func runProxy(disableSideloading bool) {
</list>
</rsp>`

res.Write([]byte(xml))
res.Header().Set("Content-Type", "application/xml; charset=utf-8")
_, _ = res.Write([]byte(xml))
})

// handle app-deployment
http.HandleFunc("/"+modifiedAppFile, func(res http.ResponseWriter, req *http.Request) {
buf, err := retreiveZipFile()
if err != nil {
res.Header().Set("Content-Type", "text/plain; charset=utf-8")
res.WriteHeader(http.StatusInternalServerError)
res.Write([]byte(err.Error()))
_, _ = res.Write([]byte(err.Error()))
log.Println(err)
return
}
// write the http-response
res.Write(buf)
res.Header().Set("Content-Type", "application/zip")
_, _ = res.Write(buf)
})

// start real proxy
http.HandleFunc("/", handleRequest)

serverMain := &http.Server{
Addr: ":" + port,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
}

// try to handle everything on port 80 aswell for serving the app
// Note: this will not work on non-rooted android because only high-ports can be used
go func() {
if !disableSideloading {
log.Println("Trying to start app-deployer on port 80 ...")
http.ListenAndServe(":80", nil)
server80 := &http.Server{
Addr: ":80",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
ReadHeaderTimeout: 5 * time.Second,
}
if err := server80.ListenAndServe(); err != nil {
log.Printf("Port 80 server error (this is expected on non-rooted systems): %v\n", err)
}
}
}()

log.Println("Server starting on Port " + port + " ...")
log.Fatal(http.ListenAndServe(":"+port, nil))
log.Fatal(serverMain.ListenAndServe())
}
2 changes: 1 addition & 1 deletion zip.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func retreiveZipFile() ([]byte, error) {

if download {
// write zipData to local file for caching
err := os.WriteFile(officialAppFile, zipData, 0664)
err := os.WriteFile(officialAppFile, zipData, 0600)
if err != nil {
log.Println("could not save downloaded file, going on anyway")
}
Expand Down