parent
e434e8d201
commit
11094a2b18
8 changed files with 412 additions and 333 deletions
|
@ -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
|
||||
|
||||
|
|
38
.github/workflows/docker-build.yml
vendored
38
.github/workflows/docker-build.yml
vendored
|
@ -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
|
23
go.mod
23
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
|
||||
)
|
||||
|
|
28
go.sum
28
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=
|
||||
|
|
|
@ -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
|
276
httppaths.go
Normal file
276
httppaths.go
Normal file
|
@ -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)
|
||||
}
|
283
main.go
283
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
|
||||
type statusJson struct {
|
||||
RequestCount int64 `json:"requestCount"`
|
||||
RequestsForbidden int64 `json:"requestsForbidden"`
|
||||
}
|
||||
|
||||
q := req.URL.Query()
|
||||
host := q.Get("host")
|
||||
q.Del("host")
|
||||
|
||||
if len(host) <= 0 {
|
||||
host = q.Get("hls_chunk_host")
|
||||
func root(w http.ResponseWriter, req *http.Request) {
|
||||
io.WriteString(w, "HTTP youtube proxy for https://inv.nadeko.net\n")
|
||||
}
|
||||
|
||||
if len(host) <= 0 {
|
||||
host = getHost(req.URL.EscapedPath())
|
||||
func status(w http.ResponseWriter, req *http.Request) {
|
||||
response := statusJson{
|
||||
RequestCount: reqs,
|
||||
RequestsForbidden: reqs_Forbidden,
|
||||
}
|
||||
|
||||
// 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
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 getHost(path string) (host string) {
|
||||
|
||||
host = ""
|
||||
|
||||
if strings.HasPrefix(path, "/vi/") || strings.HasPrefix(path, "/vi_webp/") || strings.HasPrefix(path, "/sb/") {
|
||||
host = "i.ytimg.com"
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/ggpht/") {
|
||||
host = "yt3.ggpht.com"
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
57
utils.go
Normal file
57
utils.go
Normal file
|
@ -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()
|
||||
}
|
Loading…
Reference in a new issue