Big refactor
Some checks failed
CI / build (push) Failing after 1m0s

This commit is contained in:
Fijxu 2024-10-29 01:29:55 -03:00
parent e434e8d201
commit 11094a2b18
Signed by: Fijxu
GPG key ID: 32C1DDF333EDA6A4
8 changed files with 412 additions and 333 deletions

View file

@ -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

View file

@ -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
View file

@ -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
View file

@ -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=

View file

@ -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
View 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
View file

@ -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
View 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()
}