Compare commits

...

32 commits
lol ... main

Author SHA1 Message Date
f66ae3187f
feat(http client): Add environment variable to use a proxy (http, socks5, socks5h, etc...)
All checks were successful
CI / build (push) Successful in 1m12s
2025-01-06 22:11:29 -03:00
3fc14dd18b
the 'c' query params is not is not strictly necessary. It can break hls streams
All checks were successful
CI / build (push) Successful in 53s
2025-01-01 01:03:17 -03:00
a98a4ba1bf
check if the connection has been closed before doing the request to google servers
All checks were successful
CI / build (push) Successful in 54s
2024-12-22 01:09:19 -03:00
71abe2ae58
Add option to disable the host restriction
Some checks are pending
CI / build (push) Waiting to run
2024-12-21 14:54:46 -03:00
b156a52420
test
Some checks are pending
CI / build (push) Waiting to run
2024-12-21 14:53:33 -03:00
e28d86018e
and also disable range query becuase I'm not willing to fix it right now
All checks were successful
CI / build (push) Successful in 53s
2024-12-21 01:37:52 -03:00
86170fd39e
disable arm builds for now
All checks were successful
CI / build (push) Successful in 1m4s
2024-12-21 00:59:12 -03:00
2a6e80380d
fix bool environment variables
Some checks failed
CI / build (push) Has been cancelled
2024-12-21 00:58:55 -03:00
985ef449c5
alr query param makes youtube return an URL and not video data
All checks were successful
CI / build (push) Successful in 5m8s
2024-12-21 00:34:04 -03:00
279570d47b
show version of the proxy on x-powered-by headers
All checks were successful
CI / build (push) Successful in 5m2s
2024-12-20 23:52:43 -03:00
571a2351e9
strip report-to header from responses
Some checks failed
CI / build (push) Failing after 15m11s
2024-12-20 17:28:19 -03:00
161c61bcce
use videoplayback conventions from invidious-companion
Some checks failed
CI / build (push) Failing after 25s
2024-12-20 17:13:23 -03:00
200a536207
better env variables handling
Some checks failed
CI / build (push) Has been cancelled
2024-12-20 17:12:59 -03:00
0a4dd54393
update dockerfile and docker compose file
Some checks failed
CI / build (push) Failing after 31s
2024-12-20 17:12:17 -03:00
03a37009c4
handle user-agent header based on  query param
All checks were successful
CI / build (push) Successful in 3m39s
2024-12-16 02:34:09 -03:00
46d11bfa53
Update docker compose file
All checks were successful
CI / build (push) Successful in 4m16s
2024-12-13 17:25:01 -03:00
e698c1df4d
use log. instead of fmt. for logging 2024-12-13 17:24:52 -03:00
ff9f99c1b6
better 403 request handling 2024-12-13 17:24:03 -03:00
802dd65edf
Add HSTS header 2024-12-11 13:53:06 -03:00
d225323628
Revert 848ad555f7 and 939f4da3f7
All checks were successful
CI / build (push) Successful in 4m45s
If the user on Invidious uses HD720, the playback is broken becuase the
"Origin" header is not sent (unknown reason).

This also appears to break third party clients like Clipious.

I'm retarted sorry n.n
2024-11-13 21:55:01 -03:00
848ad555f7
fixup! security: restrict the setting of CORS headers to inv.nadeko.net related domains
All checks were successful
CI / build (push) Successful in 4m31s
2024-11-12 09:58:02 -03:00
939f4da3f7
security: restrict the setting of CORS headers to inv.nadeko.net related domains
All checks were successful
CI / build (push) Successful in 5m12s
security: restrict the setting of CORS headers to inv.nadeko.net related domains
2024-11-12 09:24:29 -03:00
89c880bb27
Fix CORS when OPTIONS method is requested
All checks were successful
CI / build (push) Successful in 5m43s
2024-11-08 13:34:54 -03:00
40436dcf92
Disallow access from IP addresses
All checks were successful
CI / build (push) Successful in 4m16s
2024-11-06 15:54:40 -03:00
7d40f898a6
fixup! Support for prometheus /metrics endpoint
All checks were successful
CI / build (push) Successful in 4m14s
2024-11-06 00:53:01 -03:00
21036c3e30
Support for prometheus /metrics endpoint
All checks were successful
CI / build (push) Successful in 5m5s
2024-11-06 00:36:54 -03:00
0d4bd67afb
Update CI
All checks were successful
CI / build (push) Successful in 4m48s
2024-11-05 17:58:10 -03:00
b150f128b1
Update docker-compose.yml and provide .env 2024-11-05 17:58:10 -03:00
56345e5bae
Prevent processing of already expired videoplayback links
All checks were successful
CI / build (push) Successful in 5m4s
2024-11-05 13:50:53 -03:00
3b89ea41e7
Add uptime to /stats
All checks were successful
CI / build (push) Successful in 4m0s
2024-11-04 12:05:59 -03:00
654610ecd3
Track established connections on /stats
All checks were successful
CI / build (push) Successful in 4m13s
2024-11-04 11:56:46 -03:00
bdb1afbf61
HTTP/3 Server side support
All checks were successful
CI / build (push) Successful in 4m31s
2024-11-04 10:48:00 -03:00
9 changed files with 553 additions and 122 deletions

4
.env Normal file
View file

@ -0,0 +1,4 @@
VPN_PROVIDER=""
WIREGUARD_KEY=""
WIREGUARD_ADDRESSES=""
SERVER_HOSTNAMES=""

View file

@ -14,10 +14,20 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up QEMU for ARM64 builds
uses: docker/setup-qemu-action@v3
# - name: Set up QEMU for ARM64 builds
# uses: docker/setup-qemu-action@v3
# with:
# platforms: arm64
- name: Cache Go packages
uses: actions/cache@v3
with:
platforms: arm64
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golang-
- name: Setup Docker BuildX system
uses: docker/setup-buildx-action@v3
@ -39,9 +49,10 @@ jobs:
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
- name: Build and push Docker AMD64/ARM64
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64/v8
platforms: linux/amd64
# platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}

5
.gitignore vendored
View file

@ -16,3 +16,8 @@
# Dependency directories (remove the comment below to include it)
# vendor/
# Certificates!
*.pem
*.cer
*.key

View file

@ -1,4 +1,4 @@
FROM golang:alpine AS build
FROM golang:alpine3.21 AS build
WORKDIR /app/
@ -10,7 +10,9 @@ COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -ldflags "-s -w -X 'main.version=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)'"
FROM alpine:edge
FROM alpine:3.21
RUN adduser -u 10001 -S appuser
RUN apk add --no-cache libwebp
@ -18,4 +20,7 @@ WORKDIR /app/
COPY --from=build /app/http3-ytproxy /app/http3-ytproxy
CMD ./http3-ytproxy -l 0.0.0.0
# Switch to non-privileged user
USER appuser
ENTRYPOINT ["/app/http3-ytproxy"]

View file

@ -1,22 +1,44 @@
# Docker compose file for http3-proxy used in Invidious
# Docker compose file for http3-ytproxy used in inv.nadeko.net
services:
http3-proxy:
image: git.nadeko.net/fijxu/http3-proxy:latest
restart: unless-stopped
deploy:
replicas: 6
environment:
DISABLE_WEBP: 1
http3-proxy-nginx:
image: nginx:latest
restart: unless-stopped
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- http3-proxy
ports:
- "127.0.0.1:10012:3000"
build: .
image: git.nadeko.net/fijxu/http3-ytproxy:latest
restart: always
# Uncomment this IF YOU ARE using gluetun!
# network_mode: "service:gluetun"
# Uncomment this IF YOU ARE NOT using gluetun!
# ports:
# - "0.0.0.0:8443:8443/tcp" # HTTP/2
# - "0.0.0.0:8443:8443/udp" # HTTP/3 (QUIC)
# Make sure that the key and the certificate files exist!
volumes:
- ./key.key:/data/key.key:ro
- ./fullchain.pem:/data/cert.pem:ro
depends_on:
gluetun:
condition: service_healthy
# Needed for HTTP/3, otherwise, quic-go will output this depending of the machine:
# "failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB).
# See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details."
cap_add:
- NET_ADMIN
# You can comment this whole service if you are not going to use Gluetun at all
gluetun:
image: qmcgaw/gluetun:latest
restart: always
ports:
# THIS IS ACTUALLY THE PORT OF HTTP3-PROXY
# SINCE THE HTTP3-PTOXY SERVICE IS RUNNING
# UNDER GLUETUN NETWORK.
- "0.0.0.0:8443:8443/tcp" # HTTP/2
- "0.0.0.0:8443:8443/udp" # HTTP/3 (QUIC)
env_file:
- .env
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
volumes:
- ./gluetun:/gluetun

9
go.mod
View file

@ -7,10 +7,18 @@ toolchain go1.23.0
require github.com/quic-go/quic-go v0.48.1
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/conduitio/bwlimit v0.1.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
go.uber.org/mock v0.5.0 // indirect
golang.org/x/crypto v0.28.0 // indirect
@ -22,4 +30,5 @@ require (
golang.org/x/text v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.26.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
)

18
go.sum
View file

@ -1,3 +1,7 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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=
@ -20,14 +24,26 @@ github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e h1:v7R0PZoC2p1KWQmv1+
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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
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=
@ -78,6 +94,8 @@ 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=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -2,30 +2,81 @@ package main
import (
"bytes"
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"sync/atomic"
"time"
)
func forbiddenChecker(resp *http.Response, w http.ResponseWriter) error {
if resp.StatusCode == 403 {
w.WriteHeader(403)
io.WriteString(w, "Forbidden 403\n")
io.WriteString(w, "Maybe Youtube blocked the IP of this proxy?\n")
return fmt.Errorf("%s returned %d", resp.Request.Host, resp.StatusCode)
}
return nil
}
func connectionChecker(ctx context.Context) bool {
// To check if the connection has been closed. To prevent
// doing a useless request to google servers
select {
case <-ctx.Done():
return true
default:
return false
}
}
func videoplayback(w http.ResponseWriter, req *http.Request) {
q := req.URL.Query()
expire, err := strconv.ParseInt(q.Get("expire"), 10, 64)
if err != nil {
w.WriteHeader(500)
io.WriteString(w, "Expire query string undefined")
return
}
// Prevent the process of already expired playbacks
// since they will return 403 from googlevideo server
if (expire - time.Now().Unix()) <= 0 {
w.WriteHeader(403)
io.WriteString(w, "Videoplayback URL has expired.")
return
}
c := q.Get("c")
// if c == "" {
// w.WriteHeader(400)
// io.WriteString(w, "'c' query string undefined.")
// return
// }
host := q.Get("host")
q.Del("host")
if len(host) <= 0 {
// Fallback to use mvi and mn to build a host
mvi := q.Get("mvi")
mn := strings.Split(q.Get("mn"), ",")
if len(mvi) <= 0 {
io.WriteString(w, "No `mvi` in query parameters")
w.WriteHeader(400)
io.WriteString(w, "'mvi' query string undefined")
return
}
if len(mn) <= 0 {
io.WriteString(w, "No `mn` in query parameters")
w.WriteHeader(400)
io.WriteString(w, "'mn' query string undefined")
return
}
@ -33,8 +84,8 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
}
parts := strings.Split(strings.ToLower(host), ".")
if len(parts) < 2 {
w.WriteHeader(400)
io.WriteString(w, "Invalid hostname.")
return
}
@ -49,10 +100,18 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
}
if disallowed {
w.WriteHeader(401)
io.WriteString(w, "Non YouTube domains are not supported.")
return
}
// if c == "WEB" {
// q.Set("alr", "yes")
// }
// if req.Header.Get("Range") != "" {
// q.Set("range", req.Header.Get("Range"))
// }
path := req.URL.EscapedPath()
proxyURL, err := url.Parse("https://" + host + path)
@ -66,21 +125,49 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
body := []byte{0x78, 0} // protobuf body
request, err := http.NewRequest("POST", proxyURL.String(), bytes.NewReader(body))
copyHeaders(req.Header, request.Header, false)
request.Header.Set("User-Agent", ua)
if err != nil {
log.Panic(err)
}
copyHeaders(req.Header, request.Header, false)
switch c {
case "ANDROID":
request.Header.Set("User-Agent", "com.google.android.youtube/1537338816 (Linux; U; Android 13; en_US; ; Build/TQ2A.230505.002; Cronet/113.0.5672.24)")
case "IOS":
request.Header.Set("User-Agent", "com.google.ios.youtube/19.32.8 (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)")
case "WEB":
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
default:
request.Header.Set("User-Agent", default_ua)
}
request.Header.Add("Origin", "https://www.youtube.com")
request.Header.Add("Referer", "https://www.youtube.com/")
if connectionChecker(req.Context()) {
return
}
resp, err := client.Do(request)
if err != nil {
log.Panic(err)
}
if resp.StatusCode == 403 {
if resp.Header.Get("location") != "" {
new_url, err := url.Parse(resp.Header.Get("location"))
if err != nil {
log.Panic(err)
}
request.URL = new_url
resp, err = client.Do(request)
if err != nil {
log.Panic(err)
}
}
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Videoplayback, 1)
io.WriteString(w, "Forbidden 403\n")
io.WriteString(w, "Maybe Youtube blocked the IP of this proxy?\n")
metrics.RequestForbidden.Videoplayback.Inc()
return
}
@ -145,29 +232,32 @@ func vi(w http.ResponseWriter, req *http.Request) {
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)
}
request.Header.Set("User-Agent", default_ua)
if connectionChecker(req.Context()) {
return
}
resp, err := client.Do(request)
if err != nil {
log.Panic(err)
}
w.WriteHeader(resp.StatusCode)
if resp.StatusCode == 403 {
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Vi, 1)
io.WriteString(w, "Forbidden 403")
metrics.RequestForbidden.Vi.Inc()
return
}
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)
// 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)
io.Copy(w, resp.Body)
}
@ -186,20 +276,23 @@ func ggpht(w http.ResponseWriter, req *http.Request) {
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
copyHeaders(req.Header, request.Header, false)
request.Header.Set("User-Agent", ua)
request.Header.Set("User-Agent", default_ua)
if err != nil {
log.Panic(err)
}
if connectionChecker(req.Context()) {
return
}
resp, err := client.Do(request)
if err != nil {
log.Panic(err)
}
w.WriteHeader(resp.StatusCode)
if resp.StatusCode == 403 {
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Ggpht, 1)
io.WriteString(w, "Forbidden 403")
metrics.RequestForbidden.Ggpht.Inc()
return
}
@ -207,6 +300,7 @@ func ggpht(w http.ResponseWriter, req *http.Request) {
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)
io.Copy(w, resp.Body)
}

415
main.go
View file

@ -1,21 +1,27 @@
package main
import (
"crypto/tls"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"runtime"
"strings"
"sync/atomic"
"syscall"
"time"
"github.com/conduitio/bwlimit"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
)
@ -24,6 +30,8 @@ var (
rl = flag.Int("r", 8000, "Read limit in Kbps")
)
// QUIC doesn't seem to support HTTP nor SOCKS5 proxies due to how it's made.
// (Since it's UDP)
var h3client = &http.Client{
Transport: &http3.Transport{},
Timeout: 10 * time.Second,
@ -34,6 +42,8 @@ var dialer = &net.Dialer{
KeepAlive: 30 * time.Second,
}
var proxy string
// http/2 client
var h2client = &http.Client{
Transport: &http.Transport{
@ -55,14 +65,18 @@ var h2client = &http.Client{
MaxConnsPerHost: 0,
MaxIdleConnsPerHost: 10,
MaxIdleConns: 0,
Proxy: func(r *http.Request) (*url.URL, error) {
if proxy != "" {
return url.Parse(proxy)
}
return nil, nil
},
},
}
// https://github.com/lucas-clemente/quic-go/issues/2836
var client *http.Client
// 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 default_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
var allowed_hosts = []string{
"youtube.com",
@ -83,6 +97,8 @@ var strip_headers = []string{
"Alt-Svc",
"Server",
"Cache-Control",
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
"report-to",
}
var path_prefix = ""
@ -93,12 +109,63 @@ var ipv6_only = false
var version string
var h3s bool
var domain_only_access bool = false
var programInit = time.Now()
type ConnectionWatcher struct {
totalEstablished int64
established int64
active int64
idle int64
}
// https://stackoverflow.com/questions/51317122/how-to-get-number-of-idle-and-active-connections-in-go
// OnStateChange records open connections in response to connection
// state changes. Set net/http Server.ConnState to this method
// as value.
func (cw *ConnectionWatcher) OnStateChange(conn net.Conn, state http.ConnState) {
switch state {
case http.StateNew:
atomic.AddInt64(&stats_.EstablishedConnections, 1)
metrics.EstablishedConnections.Inc()
atomic.AddInt64(&stats_.TotalConnEstablished, 1)
metrics.TotalConnEstablished.Inc()
// case http.StateActive:
// atomic.AddInt64(&cw.active, 1)
case http.StateClosed, http.StateHijacked:
atomic.AddInt64(&stats_.EstablishedConnections, -1)
metrics.EstablishedConnections.Dec()
}
}
// // Count returns the number of connections at the time
// // the call.
// func (cw *ConnectionWatcher) Count() int {
// return int(atomic.LoadInt64(&cw.n))
// }
// // Add adds c to the number of active connections.
// func (cw *ConnectionWatcher) Add(c int64) {
// atomic.AddInt64(&cw.n, c)
// }
var cw ConnectionWatcher
type statusJson struct {
Version string `json:"version"`
RequestCount int64 `json:"requestCount"`
RequestPerSecond int64 `json:"requestPerSecond"`
RequestPerMinute int64 `json:"requestPerMinute"`
RequestsForbidden struct {
Version string `json:"version"`
Uptime time.Duration `json:"uptime"`
RequestCount int64 `json:"requestCount"`
RequestPerSecond int64 `json:"requestPerSecond"`
RequestPerMinute int64 `json:"requestPerMinute"`
TotalConnEstablished int64 `json:"totalEstablished"`
EstablishedConnections int64 `json:"establishedConnections"`
ActiveConnections int64 `json:"activeConnections"`
IdleConnections int64 `json:"idleConnections"`
RequestsForbidden struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
@ -106,10 +173,15 @@ type statusJson struct {
}
var stats_ = statusJson{
Version: version + "-" + runtime.GOARCH,
RequestCount: 0,
RequestPerSecond: 0,
RequestPerMinute: 0,
Version: version + "-" + runtime.GOARCH,
Uptime: 0,
RequestCount: 0,
RequestPerSecond: 0,
RequestPerMinute: 0,
TotalConnEstablished: 0,
EstablishedConnections: 0,
ActiveConnections: 0,
IdleConnections: 0,
RequestsForbidden: struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
@ -121,19 +193,97 @@ var stats_ = statusJson{
},
}
type Metrics struct {
Uptime prometheus.Gauge
RequestCount prometheus.Counter
RequestPerSecond prometheus.Gauge
RequestPerMinute prometheus.Gauge
TotalConnEstablished prometheus.Counter
EstablishedConnections prometheus.Gauge
ActiveConnections prometheus.Gauge
IdleConnections prometheus.Gauge
RequestForbidden struct {
Videoplayback prometheus.Counter
Vi prometheus.Counter
Ggpht prometheus.Counter
}
}
var metrics = Metrics{
Uptime: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_uptime",
}),
RequestCount: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_count",
}),
RequestPerSecond: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_per_second",
}),
RequestPerMinute: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_per_minute",
}),
TotalConnEstablished: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_total_conn_established",
}),
EstablishedConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_established_conns",
}),
ActiveConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_active_conns",
}),
IdleConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_idle_conns",
}),
RequestForbidden: struct {
Videoplayback prometheus.Counter
Vi prometheus.Counter
Ggpht prometheus.Counter
}{
Videoplayback: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_videoplayback",
}),
Vi: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_vi",
}),
Ggpht: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_ggpht",
}),
},
}
func root(w http.ResponseWriter, req *http.Request) {
const msg = `
HTTP youtube proxy for https://inv.nadeko.net
https://git.nadeko.net/Fijxu/http3-ytproxy
Routes:
Routes:
/stats
/health`
io.WriteString(w, msg)
}
// CustomHandler wraps the default promhttp.Handler with custom logic
func metricsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// To prevent accessing from the bare IP address
if req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil {
w.WriteHeader(444)
return
}
metrics.Uptime.Set(float64(time.Duration(time.Since(programInit).Seconds())))
promhttp.Handler().ServeHTTP(w, req)
})
}
func stats(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
stats_.Uptime = time.Duration(time.Since(programInit).Seconds())
// stats_.TotalEstablished = int64(cw.totalEstablished)
// stats_.EstablishedConnections = int64(cw.established)
// stats_.ActiveConnections = int64(cw.active)
// stats_.IdleConnections = int64(cw.idle)
if err := json.NewEncoder(w).Encode(stats_); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@ -151,6 +301,7 @@ func requestPerSecond() {
time.Sleep(1 * time.Second)
current := stats_.RequestCount
stats_.RequestPerSecond = current - last
metrics.RequestPerSecond.Set(float64(stats_.RequestPerSecond))
last = current
}
}
@ -161,31 +312,18 @@ func requestPerMinute() {
time.Sleep(60 * time.Second)
current := stats_.RequestCount
stats_.RequestPerMinute = current - last
metrics.RequestPerMinute.Set(float64(stats_.RequestPerMinute))
last = current
}
}
func beforeAll(next http.HandlerFunc) http.HandlerFunc {
func beforeMisc(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
defer panicHandler(w)
if req.Method != "GET" && req.Method != "HEAD" {
io.WriteString(w, "Only GET and HEAD requests are allowed.")
return
}
atomic.AddInt64(&stats_.RequestCount, 1)
// To look like more like a browser
req.Header.Add("Origin", "https://www.youtube.com")
req.Header.Add("Referer", "https://www.youtube.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(http.StatusOK)
// To prevent accessing from the bare IP address
if domain_only_access && (req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil) {
w.WriteHeader(444)
return
}
@ -193,59 +331,151 @@ func beforeAll(next http.HandlerFunc) http.HandlerFunc {
}
}
func beforeProxy(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
defer panicHandler(w)
// To prevent accessing from the bare IP address
if domain_only_access && (req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil) {
w.WriteHeader(444)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
w.Header().Set("Access-Control-Max-Age", "1728000")
w.Header().Set("Strict-Transport-Security", "max-age=86400")
w.Header().Set("X-Powered-By", "http3-ytproxy "+version+"-"+runtime.GOARCH)
if h3s {
w.Header().Set("Alt-Svc", "h3=\":8443\"; ma=86400")
}
if req.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
if req.Method != "GET" && req.Method != "HEAD" {
w.WriteHeader(405)
io.WriteString(w, "Only GET and HEAD requests are allowed.")
return
}
atomic.AddInt64(&stats_.RequestCount, 1)
metrics.RequestCount.Inc()
next(w, req)
}
}
func main() {
var sock string
var host string
var port string
var tls_cert string
var tls_key string
defaultHost := "0.0.0.0"
defaultPort := "8080"
defaultSock := "/tmp/http-ytproxy.sock"
defaultTLSCert := "/data/cert.pem"
defaultTLSKey := "/data/key.key"
path_prefix = os.Getenv("PREFIX_PATH")
ipv6_only = os.Getenv("IPV6_ONLY") == "1"
var https bool = false
var h3c bool = false
var ipv6 bool = false
var https = flag.Bool("https", false, "Use built-in https server (recommended)")
var h3 = flag.Bool("h3", false, "Use HTTP/3 for requests (high CPU usage)")
var ipv6 = flag.Bool("ipv6_only", false, "Only use ipv6 for requests")
flag.StringVar(&tls_cert, "tls-cert", "", "TLS Certificate path")
flag.StringVar(&tls_key, "tls-key", "", "TLS Certificate Key path")
flag.StringVar(&sock, "s", "/tmp/http-ytproxy.sock", "Specify a socket name")
flag.StringVar(&port, "p", "8080", "Specify a port number")
flag.StringVar(&host, "l", "0.0.0.0", "Specify a listen address")
if strings.ToLower(os.Getenv("HTTPS")) == "true" {
https = true
}
if strings.ToLower(os.Getenv("H3C")) == "true" {
h3c = true
}
if strings.ToLower(os.Getenv("H3S")) == "true" {
h3s = true
}
if strings.ToLower(os.Getenv("IPV6_ONLY")) == "true" {
ipv6 = true
}
if strings.ToLower(os.Getenv("DOMAIN_ONLY_ACCESS")) == "true" {
domain_only_access = true
}
tls_cert := os.Getenv("TLS_CERT")
if tls_cert == "" {
tls_cert = defaultTLSCert
}
tls_key := os.Getenv("TLS_KEY")
if tls_key == "" {
tls_key = defaultTLSKey
}
sock := os.Getenv("SOCK_PATH")
if sock == "" {
sock = defaultSock
}
port := os.Getenv("PORT")
if port == "" {
port = defaultPort
}
host := os.Getenv("HOST")
if host == "" {
host = defaultHost
}
proxy = os.Getenv("PROXY")
flag.BoolVar(&https, "https", https, "Use built-in https server (recommended)")
flag.BoolVar(&h3c, "h3c", h3c, "Use HTTP/3 for client requests (high CPU usage)")
flag.BoolVar(&h3s, "h3s", h3s, "Use HTTP/3 for server requests, (requires HTTPS)")
flag.BoolVar(&ipv6_only, "ipv6_only", ipv6_only, "Only use ipv6 for requests")
flag.StringVar(&tls_cert, "tls-cert", tls_cert, "TLS Certificate path")
flag.StringVar(&tls_key, "tls-key", tls_key, "TLS Certificate Key path")
flag.StringVar(&sock, "s", sock, "Specify a socket name")
flag.StringVar(&port, "p", port, "Specify a port number")
flag.StringVar(&host, "l", host, "Specify a listen address")
flag.Parse()
if *h3 {
if h3c {
client = h3client
} else {
client = h2client
}
if *https {
if https {
if len(tls_cert) <= 0 {
fmt.Println("tls-cert argument is missing")
fmt.Println("You need a TLS certificate for HTTPS")
log.Fatal("tls-cert argument is missing, you need a TLS certificate for HTTPS")
}
if len(tls_key) <= 0 {
fmt.Println("tls-key argument is missing")
fmt.Println("You need a TLS key for HTTPS")
log.Fatal("tls-key argument is missing, you need a TLS key for HTTPS")
}
}
ipv6_only = *ipv6
ipv6_only = ipv6
mux := http.NewServeMux()
mux.HandleFunc("/", root)
mux.HandleFunc("/health", health)
mux.HandleFunc("/stats", stats)
// MISC ROUTES
mux.HandleFunc("/", beforeMisc(root))
mux.HandleFunc("/health", beforeMisc(health))
mux.HandleFunc("/stats", beforeMisc(stats))
mux.HandleFunc("/videoplayback", beforeAll(videoplayback))
mux.HandleFunc("/vi/", beforeAll(vi))
mux.HandleFunc("/vi_webp/", beforeAll(vi))
mux.HandleFunc("/sb/", beforeAll(vi))
mux.HandleFunc("/ggpht/", beforeAll(ggpht))
mux.HandleFunc("/a/", beforeAll(ggpht))
mux.HandleFunc("/ytc/", beforeAll(ggpht))
prometheus.MustRegister(metrics.Uptime)
prometheus.MustRegister(metrics.ActiveConnections)
prometheus.MustRegister(metrics.IdleConnections)
prometheus.MustRegister(metrics.EstablishedConnections)
prometheus.MustRegister(metrics.TotalConnEstablished)
prometheus.MustRegister(metrics.RequestCount)
prometheus.MustRegister(metrics.RequestPerSecond)
prometheus.MustRegister(metrics.RequestPerMinute)
prometheus.MustRegister(metrics.RequestForbidden.Videoplayback)
prometheus.MustRegister(metrics.RequestForbidden.Vi)
prometheus.MustRegister(metrics.RequestForbidden.Ggpht)
mux.Handle("/metrics", metricsHandler())
// PROXY ROUTES
mux.HandleFunc("/videoplayback", beforeProxy(videoplayback))
mux.HandleFunc("/vi/", beforeProxy(vi))
mux.HandleFunc("/vi_webp/", beforeProxy(vi))
mux.HandleFunc("/sb/", beforeProxy(vi))
mux.HandleFunc("/ggpht/", beforeProxy(ggpht))
mux.HandleFunc("/a/", beforeProxy(ggpht))
mux.HandleFunc("/ytc/", beforeProxy(ggpht))
go requestPerSecond()
go requestPerMinute()
@ -262,40 +492,73 @@ func main() {
)
ln = bwlimit.NewListener(ln, writeLimit, readLimit)
// srvDialer := bwlimit.NewDialer(&net.Dialer{}, writeLimit, readLimit)
srv := &http.Server{
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 1 * time.Hour,
Handler: mux,
ConnState: cw.OnStateChange,
}
srvh3 := &http3.Server{
Handler: mux,
EnableDatagrams: false, // https://quic.video/blog/never-use-datagrams/ (Read it)
IdleTimeout: 120 * time.Second,
TLSConfig: http3.ConfigureTLSConfig(&tls.Config{}),
QUICConfig: &quic.Config{
// KeepAlivePeriod: 10 * time.Second,
MaxIncomingStreams: 256, // I'm not sure if this is correct.
MaxIncomingUniStreams: 256, // Same as above
},
Addr: host + ":" + port,
}
syscall.Unlink(sock)
socket_listener, err := net.Listen("unix", sock)
if err != nil {
fmt.Println("Failed to bind to UDS, please check the socket name")
fmt.Println(err.Error())
log.Println("Failed to bind to UDS, please check the socket name", err.Error())
} else {
defer socket_listener.Close()
// To allow everyone to access the socket
err = os.Chmod(sock, 0777)
if err != nil {
fmt.Println("Error setting permissions:", err)
log.Println("Failed to set socket permissions to 777:", err.Error())
return
} else {
fmt.Println("Setting socket permissions to 777")
log.Println("Setting socket permissions to 777")
}
go srv.Serve(socket_listener)
fmt.Println("Unix socket listening at:", string(sock))
log.Println("Unix socket listening at:", string(sock))
if *https {
fmt.Println("Serving HTTPS at port", string(port))
if err := srv.ServeTLS(ln, tls_cert, tls_key); err != nil {
log.Fatal(err)
if https {
if _, err := os.Open(tls_cert); errors.Is(err, os.ErrNotExist) {
log.Panicf("Certificate file does not exist at path '%s'", tls_cert)
}
if _, err := os.Open(tls_key); errors.Is(err, os.ErrNotExist) {
log.Panicf("Key file does not exist at path '%s'", tls_key)
}
log.Println("Serving HTTPS at port", string(port)+"/tcp")
go func() {
if err := srv.ServeTLS(ln, tls_cert, tls_key); err != nil {
log.Fatal("Failed to server HTTP/2", err.Error())
}
}()
if h3s {
log.Println("Serving HTTP/3 (HTTPS) via QUIC at port", string(port)+"/udp")
go func() {
if err := srvh3.ListenAndServeTLS(tls_cert, tls_key); err != nil {
log.Fatal("Failed to serve HTTP/3:", err.Error())
}
}()
}
select {}
} else {
fmt.Println("Serving HTTP at port", string(port))
log.Println("Serving HTTP at port", string(port))
if err := srv.Serve(ln); err != nil {
log.Fatal(err)
}