diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index c6320d0..8cda90f 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - uses: https://code.forgejo.org/actions/checkout@v2 - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v3 with: platforms: all diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml deleted file mode 100644 index 344ab88..0000000 --- a/.github/workflows/docker-build.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Docker Multi-Architecture Build - -on: - push: - paths-ignore: - - "**.md" - branches: - - main - -jobs: - build-docker: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: all - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - with: - version: latest - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - name: Build and push - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 - push: true - tags: 1337kavin/ytproxy:latest - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/go.mod b/go.mod index 81a0915..da9d1dc 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,23 @@ -module github.com/FireMasterK/http3-ytproxy/v2 +module git.nadeko.net/Fijxu/http3-ytproxy/v3 go 1.22.0 toolchain go1.23.0 -require ( - github.com/kolesa-team/go-webp v1.0.4 - github.com/quic-go/quic-go v0.47.0 -) +require github.com/quic-go/quic-go v0.48.1 require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect + github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e // indirect github.com/onsi/ginkgo/v2 v2.20.2 // indirect github.com/quic-go/qpack v0.5.1 // indirect - go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.27.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect golang.org/x/mod v0.21.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/text v0.18.0 // indirect - golang.org/x/tools v0.25.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/tools v0.26.0 // indirect ) diff --git a/go.sum b/go.sum index 016c135..37f8a01 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,25 @@ +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= +github.com/conduitio/bwlimit v0.1.0 h1:x3ijON0TSghQob4tFKaEvKixFmYKfVJQeSpXluC2JvE= +github.com/conduitio/bwlimit v0.1.0/go.mod h1:E+ASZ1/5L33MTb8hJTERs5Xnmh6Ulq3jbRh7LrdbXWU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 h1:c5FlPPgxOn7kJz3VoPLkQYQXGBS3EklQ4Zfi57uOuqQ= github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e h1:v7R0PZoC2p1KWQmv1+GqCXQe59Ab1TkDF8Y9Lg2W6m4= +github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE= +github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= github.com/kolesa-team/go-webp v1.0.4 h1:wQvU4PLG/X7RS0vAeyhiivhLRoxfLVRlDq4I3frdxIQ= github.com/kolesa-team/go-webp v1.0.4/go.mod h1:oMvdivD6K+Q5qIIkVC2w4k2ZUnI1H+MyP7inwgWq9aA= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= @@ -21,33 +32,50 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= +github.com/quic-go/quic-go v0.48.1 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA= +github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= +golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/http3-ytproxy@.service b/http3-ytproxy@.service deleted file mode 100644 index ecc5c66..0000000 --- a/http3-ytproxy@.service +++ /dev/null @@ -1,38 +0,0 @@ -[Unit] -Description=Http3 YTProxy for Invidious -After=network-online.target -Wants=network-online.target - -[Service] -Type=simple -User=http -Group=http -Environment="DISABLE_WEBP=1" -WorkingDirectory=/opt/http3-ytproxy -ExecStart=/opt/http3-ytproxy/http3-ytproxy -s http-proxy-%i.sock -Restart=on-failure -RestartSec=2s - -ReadWritePaths=/opt/http3-ytproxy/socket -NoNewPrivileges=yes -MemoryDenyWriteExecute=true -PrivateDevices=yes -PrivateTmp=yes -ProtectHome=yes -ProtectSystem=strict -ProtectControlGroups=true -RestrictSUIDSGID=true -RestrictRealtime=true -LockPersonality=true -ProtectKernelLogs=true -ProtectKernelTunables=true -ProtectHostname=true -ProtectKernelModules=true -PrivateUsers=true -ProtectClock=true -SystemCallArchitectures=native -SystemCallErrorNumber=EPERM -SystemCallFilter=@system-service - -[Install] -WantedBy=multi-user.target diff --git a/httppaths.go b/httppaths.go new file mode 100644 index 0000000..49728f3 --- /dev/null +++ b/httppaths.go @@ -0,0 +1,276 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" +) + +func videoplayback(w http.ResponseWriter, req *http.Request) { + mu.Lock() + reqs++ + mu.Unlock() + + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Max-Age", "1728000") + + if req.Method == "OPTIONS" { + w.WriteHeader(200) + return + } + + q := req.URL.Query() + + mvi := q.Get("mvi") + mn := strings.Split(q.Get("mn"), ",") + + if len(mvi) <= 0 { + io.WriteString(w, "No `mvi` in query parameters") + return + } + + if len(mn) <= 0 { + io.WriteString(w, "No `mn` in query parameters") + return + } + + host := "rr" + mvi + "---" + mn[0] + ".googlevideo.com" + + parts := strings.Split(strings.ToLower(host), ".") + + if len(parts) < 2 { + io.WriteString(w, "Invalid hostname.") + return + } + + domain := parts[len(parts)-2] + "." + parts[len(parts)-1] + + disallowed := true + + for _, value := range allowed_hosts { + if domain == value { + disallowed = false + break + } + } + + if disallowed { + io.WriteString(w, "Non YouTube domains are not supported.") + return + } + + if req.Method != "GET" && req.Method != "HEAD" { + io.WriteString(w, "Only GET and HEAD requests are allowed.") + return + } + + path := req.URL.EscapedPath() + + proxyURL, err := url.Parse("https://" + host + path) + if err != nil { + log.Panic(err) + } + + proxyURL.RawQuery = q.Encode() + + request, err := http.NewRequest(req.Method, proxyURL.String(), nil) + + copyHeaders(req.Header, request.Header, false) + request.Header.Set("User-Agent", ua) + if err != nil { + log.Panic(err) + } + + resp, err := client.Do(request) + if err != nil { + log.Panic(err) + } + + defer resp.Body.Close() + + NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video") + copyHeaders(resp.Header, w.Header(), NoRewrite) + + w.WriteHeader(resp.StatusCode) + + if req.Method == "GET" && (resp.Header.Get("Content-Type") == "application/x-mpegurl" || resp.Header.Get("Content-Type") == "application/vnd.apple.mpegurl") { + bytes, err := io.ReadAll(resp.Body) + if err != nil { + log.Panic(err) + } + + lines := strings.Split(string(bytes), "\n") + reqUrl := resp.Request.URL + for i := 0; i < len(lines); i++ { + line := lines[i] + if !strings.HasPrefix(line, "https://") && (strings.HasSuffix(line, ".m3u8") || strings.HasSuffix(line, ".ts")) { + path := reqUrl.EscapedPath() + path = path[0 : strings.LastIndex(path, "/")+1] + line = "https://" + reqUrl.Hostname() + path + line + } + if strings.HasPrefix(line, "https://") { + lines[i] = RelativeUrl(line) + } + + if manifest_re.MatchString(line) { + url := manifest_re.FindStringSubmatch(line)[1] + lines[i] = strings.Replace(line, url, RelativeUrl(url), 1) + } + } + + io.WriteString(w, strings.Join(lines, "\n")) + } else { + io.Copy(w, resp.Body) + } +} + +func vi(w http.ResponseWriter, req *http.Request) { + mu.Lock() + reqs++ + mu.Unlock() + + const host string = "i.ytimg.com" + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Max-Age", "1728000") + + if req.Method == "OPTIONS" { + w.WriteHeader(204) + return + } + + parts := strings.Split(strings.ToLower(host), ".") + if len(parts) < 2 { + io.WriteString(w, "Invalid hostname.") + return + } + + domain := parts[len(parts)-2] + "." + parts[len(parts)-1] + + disallowed := true + + for _, value := range allowed_hosts { + if domain == value { + disallowed = false + break + } + } + + if disallowed { + io.WriteString(w, "Non YouTube domains are not supported.") + return + } + + if req.Method != "GET" && req.Method != "HEAD" { + io.WriteString(w, "Only GET and HEAD requests are allowed.") + return + } + + path := req.URL.EscapedPath() + fmt.Println(path) + + proxyURL, err := url.Parse("https://" + host + path) + if err != nil { + log.Panic(err) + } + + if strings.HasSuffix(proxyURL.EscapedPath(), "maxres.jpg") { + proxyURL.Path = getBestThumbnail(proxyURL.EscapedPath()) + } + + request, err := http.NewRequest(req.Method, proxyURL.String(), nil) + copyHeaders(req.Header, request.Header, false) + request.Header.Set("User-Agent", ua) + if err != nil { + log.Panic(err) + } + + resp, err := client.Do(request) + if err != nil { + log.Panic(err) + } + + defer resp.Body.Close() + + copyHeaders(resp.Header, w.Header(), false) + w.WriteHeader(resp.StatusCode) + + io.Copy(w, resp.Body) +} + +func ggpht(w http.ResponseWriter, req *http.Request) { + mu.Lock() + reqs++ + mu.Unlock() + + const host string = "yt3.ggpht.com" + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "*") + w.Header().Set("Access-Control-Max-Age", "1728000") + + if req.Method == "OPTIONS" { + w.WriteHeader(204) + return + } + + parts := strings.Split(strings.ToLower(host), ".") + if len(parts) < 2 { + io.WriteString(w, "Invalid hostname.") + return + } + + domain := parts[len(parts)-2] + "." + parts[len(parts)-1] + + disallowed := true + + for _, value := range allowed_hosts { + if domain == value { + disallowed = false + break + } + } + + if disallowed { + io.WriteString(w, "Non YouTube domains are not supported.") + return + } + + if req.Method != "GET" && req.Method != "HEAD" { + io.WriteString(w, "Only GET and HEAD requests are allowed.") + return + } + + path := req.URL.EscapedPath() + path = strings.Replace(path, "/ggpht", "", 1) + path = strings.Replace(path, "/i/", "/", 1) + + proxyURL, err := url.Parse("https://" + host + path) + if err != nil { + log.Panic(err) + } + + fmt.Println(proxyURL) + + request, err := http.NewRequest(req.Method, proxyURL.String(), nil) + copyHeaders(req.Header, request.Header, false) + request.Header.Set("User-Agent", ua) + if err != nil { + log.Panic(err) + } + + resp, err := client.Do(request) + if err != nil { + log.Panic(err) + } + + defer resp.Body.Close() + + copyHeaders(resp.Header, w.Header(), false) + w.WriteHeader(resp.StatusCode) + + io.Copy(w, resp.Body) +} diff --git a/main.go b/main.go index 656a90d..b611a23 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,19 @@ package main import ( + "encoding/json" "flag" "fmt" - "image/jpeg" "io" "log" "net" "net/http" - "net/url" "os" "regexp" - "strings" + "sync" "syscall" "time" - "github.com/kolesa-team/go-webp/encoder" - "github.com/kolesa-team/go-webp/webp" "github.com/quic-go/quic-go/http3" ) @@ -55,8 +52,11 @@ var h2client = &http.Client{ }, } -// user agent to use -var ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" +// https://github.com/lucas-clemente/quic-go/issues/2836 +var client = h2client + +// Same user agent as Invidious +var ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36" var allowed_hosts = []string{ "youtube.com", @@ -64,8 +64,6 @@ var allowed_hosts = []string{ "ytimg.com", "ggpht.com", "googleusercontent.com", - "lbryplayer.xyz", - "odycdn.com", } var strip_headers = []string{ @@ -86,236 +84,31 @@ var path_prefix = "" var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`) var ipv6_only = false -var disable_webp = false -type requesthandler struct{} +var reqs int64 +var reqs_Forbidden int64 +var mu sync.Mutex -func (*requesthandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Headers", "*") - w.Header().Set("Access-Control-Max-Age", "1728000") - - if req.Method == "OPTIONS" { - w.WriteHeader(200) - return - } - - q := req.URL.Query() - host := q.Get("host") - q.Del("host") - - if len(host) <= 0 { - host = q.Get("hls_chunk_host") - } - - if len(host) <= 0 { - host = getHost(req.URL.EscapedPath()) - } - - // https://rr(mvi)---(mn).googlevideo.com - if len(host) <= 0 { - mvi := q.Get("mvi") - mn := strings.Split(q.Get("mn"), ",") - if len(mvi) <= 0 { - io.WriteString(w, "No `mvi` in query parameters") - return - } - - if len(mn) <= 0 { - io.WriteString(w, "No `mn` in query parameters") - return - } - host = "rr" + mvi + "---" + mn[0] + ".googlevideo.com" - } - - parts := strings.Split(strings.ToLower(host), ".") - - if len(parts) < 2 { - io.WriteString(w, "Invalid hostname.") - return - } - - domain := parts[len(parts)-2] + "." + parts[len(parts)-1] - - disallowed := true - - for _, value := range allowed_hosts { - if domain == value { - disallowed = false - break - } - } - - if disallowed { - io.WriteString(w, "Non YouTube domains are not supported.") - return - } - - if req.Method != "GET" && req.Method != "HEAD" { - io.WriteString(w, "Only GET and HEAD requests are allowed.") - return - } - - path := req.URL.EscapedPath() - - path = strings.Replace(path, "/ggpht", "", 1) - path = strings.Replace(path, "/i/", "/", 1) - - proxyURL, err := url.Parse("https://" + host + path) - - if err != nil { - log.Panic(err) - } - - proxyURL.RawQuery = q.Encode() - - if strings.HasSuffix(proxyURL.EscapedPath(), "maxres.jpg") { - proxyURL.Path = getBestThumbnail(proxyURL.EscapedPath()) - } - - request, err := http.NewRequest(req.Method, proxyURL.String(), nil) - - copyHeaders(req.Header, request.Header, false) - request.Header.Set("User-Agent", ua) - - if err != nil { - log.Panic(err) - } - - var client *http.Client - - // https://github.com/lucas-clemente/quic-go/issues/2836 - client = h2client - - resp, err := client.Do(request) - - if err != nil { - log.Panic(err) - } - - defer resp.Body.Close() - - NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video") || strings.HasPrefix(resp.Header.Get("Content-Type"), "webp") - copyHeaders(resp.Header, w.Header(), NoRewrite) - - w.WriteHeader(resp.StatusCode) - - if req.Method == "GET" && (resp.Header.Get("Content-Type") == "application/x-mpegurl" || resp.Header.Get("Content-Type") == "application/vnd.apple.mpegurl") { - bytes, err := io.ReadAll(resp.Body) - - if err != nil { - log.Panic(err) - } - - lines := strings.Split(string(bytes), "\n") - reqUrl := resp.Request.URL - for i := 0; i < len(lines); i++ { - line := lines[i] - if !strings.HasPrefix(line, "https://") && (strings.HasSuffix(line, ".m3u8") || strings.HasSuffix(line, ".ts")) { - path := reqUrl.EscapedPath() - path = path[0 : strings.LastIndex(path, "/")+1] - line = "https://" + reqUrl.Hostname() + path + line - } - if strings.HasPrefix(line, "https://") { - lines[i] = RelativeUrl(line) - } - - if manifest_re.MatchString(line) { - url := manifest_re.FindStringSubmatch(line)[1] - lines[i] = strings.Replace(line, url, RelativeUrl(url), 1) - } - } - - io.WriteString(w, strings.Join(lines, "\n")) - } else if !disable_webp && resp.Header.Get("Content-Type") == "image/jpeg" { - img, err := jpeg.Decode(resp.Body) - - if err != nil { - log.Panic(err) - } - - options, _ := encoder.NewLossyEncoderOptions(encoder.PresetDefault, 85) - - w.Header().Set("Content-Type", "image/webp") - - webp.Encode(w, img, options) - } else { - io.Copy(w, resp.Body) - } +type statusJson struct { + RequestCount int64 `json:"requestCount"` + RequestsForbidden int64 `json:"requestsForbidden"` } -func copyHeaders(from http.Header, to http.Header, length bool) { - // Loop over header names -outer: - for name, values := range from { - for _, header := range strip_headers { - if name == header { - continue outer - } - } - if (name != "Content-Length" || length) && !strings.HasPrefix(name, "Access-Control") { - // Loop over all values for the name. - for _, value := range values { - if strings.Contains(value, "jpeg") { - continue - } - to.Set(name, value) - } - } - } +func root(w http.ResponseWriter, req *http.Request) { + io.WriteString(w, "HTTP youtube proxy for https://inv.nadeko.net\n") } -func getHost(path string) (host string) { - - host = "" - - if strings.HasPrefix(path, "/vi/") || strings.HasPrefix(path, "/vi_webp/") || strings.HasPrefix(path, "/sb/") { - host = "i.ytimg.com" +func status(w http.ResponseWriter, req *http.Request) { + response := statusJson{ + RequestCount: reqs, + RequestsForbidden: reqs_Forbidden, } - if strings.HasPrefix(path, "/ggpht/") { - host = "yt3.ggpht.com" + w.Header().Set("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) } - - if strings.HasPrefix(path, "/a/") || strings.HasPrefix(path, "/ytc/") { - host = "yt3.ggpht.com" - } - - if strings.Contains(path, "/host/") { - path = path[(strings.Index(path, "/host/") + 6):] - host = path[0:strings.Index(path, "/")] - } - - return host -} - -func getBestThumbnail(path string) (newpath string) { - - formats := [4]string{"maxresdefault.jpg", "sddefault.jpg", "hqdefault.jpg", "mqdefault.jpg"} - - for _, format := range formats { - newpath = strings.Replace(path, "maxres.jpg", format, 1) - url := "https://i.ytimg.com" + newpath - resp, _ := h2client.Head(url) - if resp.StatusCode == 200 { - return newpath - } - } - - return strings.Replace(path, "maxres.jpg", "mqdefault.jpg", 1) -} - -func RelativeUrl(in string) (newurl string) { - segment_url, err := url.Parse(in) - if err != nil { - log.Panic(err) - } - segment_query := segment_url.Query() - segment_query.Set("host", segment_url.Hostname()) - segment_url.RawQuery = segment_query.Encode() - segment_url.Path = path_prefix + segment_url.Path - return segment_url.RequestURI() } func main() { @@ -328,16 +121,7 @@ func main() { path_prefix = os.Getenv("PREFIX_PATH") ipv6_only = os.Getenv("IPV6_ONLY") == "1" - disable_webp = os.Getenv("DISABLE_WEBP") == "1" - - // if _, err := os.Stat("socket"); os.IsNotExist(err) { - // fmt.Println("socket folder doesn't exists, creating one now.") - // err = os.Mkdir("socket", 0777) - // if err != nil { - // fmt.Println("Failed to create folder, error: ") - // log.Fatal(err) - // } - // } + // disable_webp = os.Getenv("DISABLE_WEBP") == "1" flag.StringVar(&cert, "tls-cert", "", "TLS Certificate path") flag.StringVar(&key, "tls-key", "", "TLS Certificate Key path") @@ -350,16 +134,29 @@ func main() { ipv6_only = *ipv6 + mux := http.NewServeMux() + + mux.HandleFunc("/", root) + mux.HandleFunc("/status", status) + mux.HandleFunc("/videoplayback", videoplayback) + mux.HandleFunc("/vi/", vi) + mux.HandleFunc("/vi_webp/", vi) + mux.HandleFunc("/sb/", vi) + mux.HandleFunc("/ggpht/", ggpht) + mux.HandleFunc("/a/", ggpht) + mux.HandleFunc("/ytc/", ggpht) + srv := &http.Server{ ReadTimeout: 5 * time.Second, WriteTimeout: 1 * time.Hour, Addr: string(host) + ":" + string(port), - Handler: &requesthandler{}, + Handler: mux, } socket := string(sock) syscall.Unlink(socket) listener, err := net.Listen("unix", socket) + fmt.Println("Unix socket listening at:", string(sock)) if err != nil { fmt.Println("Failed to bind to UDS, please check the socket name, falling back to TCP/IP") @@ -381,12 +178,12 @@ func main() { } go srv.Serve(listener) if *https { + fmt.Println("Serving HTTPS at port", string(port)) if err := srv.ListenAndServeTLS(cert, key); err != nil { log.Fatal(err) } - fmt.Println("Serving HTTPS") } else { - fmt.Println("Serving HTTP") + fmt.Println("Serving HTTP at port", string(port)) srv.ListenAndServe() } } diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..781453a --- /dev/null +++ b/utils.go @@ -0,0 +1,57 @@ +package main + +import ( + "log" + "net/http" + "net/url" + "strings" +) + +func copyHeaders(from http.Header, to http.Header, length bool) { + // Loop over header names +outer: + for name, values := range from { + for _, header := range strip_headers { + if name == header { + continue outer + } + } + if (name != "Content-Length" || length) && !strings.HasPrefix(name, "Access-Control") { + // Loop over all values for the name. + for _, value := range values { + if strings.Contains(value, "jpeg") { + continue + } + to.Set(name, value) + } + } + } +} + +func getBestThumbnail(path string) (newpath string) { + + formats := [4]string{"maxresdefault.jpg", "sddefault.jpg", "hqdefault.jpg", "mqdefault.jpg"} + + for _, format := range formats { + newpath = strings.Replace(path, "maxres.jpg", format, 1) + url := "https://i.ytimg.com" + newpath + resp, _ := h2client.Head(url) + if resp.StatusCode == 200 { + return newpath + } + } + + return strings.Replace(path, "maxres.jpg", "mqdefault.jpg", 1) +} + +func RelativeUrl(in string) (newurl string) { + segment_url, err := url.Parse(in) + if err != nil { + log.Panic(err) + } + segment_query := segment_url.Query() + segment_query.Set("host", segment_url.Hostname()) + segment_url.RawQuery = segment_query.Encode() + segment_url.Path = path_prefix + segment_url.Path + return segment_url.RequestURI() +}