Compare commits
30 commits
Author | SHA1 | Date | |
---|---|---|---|
a6a33526a4 | |||
dcb87b225a | |||
a9c845651d | |||
71aa6edd67 | |||
356e523bc8 | |||
e13b2e02a7 | |||
cb370138b8 | |||
235a2c48a6 | |||
6a8c4ea562 | |||
76e9807a34 | |||
40faa994da | |||
6e970fcb43 | |||
6bd0f28d77 | |||
81aa259a31 | |||
83fff3d861 | |||
77c8730391 | |||
d268f1a2c2 | |||
3563b4e819 | |||
f7e75ce5e7 | |||
1045548164 | |||
c50e482085 | |||
94edee02d1 | |||
f9b7cf20ed | |||
6f4567df0c | |||
f66aa3efa6 | |||
340ee021bb | |||
8821540bd9 | |||
cc4671c677 | |||
319991c7b8 | |||
197a807b90 |
21 changed files with 1024 additions and 1112 deletions
|
@ -2,9 +2,8 @@ name: "CI"
|
|||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
push:
|
||||
branches: ["*"]
|
||||
branches: ["main", "master"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
@ -14,21 +13,6 @@ jobs:
|
|||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# - 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:
|
||||
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
|
||||
|
||||
|
@ -48,11 +32,12 @@ jobs:
|
|||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Build and push Docker AMD64/ARM64
|
||||
- name: Build and push Docker
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
# platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
FROM golang:alpine3.21 AS build
|
||||
|
||||
WORKDIR /app/
|
||||
RUN go env -w GOMODCACHE=/root/.cache/go-build
|
||||
|
||||
RUN apk add --no-cache build-base libwebp-dev git
|
||||
|
||||
|
@ -8,7 +9,7 @@ COPY .git .git
|
|||
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)'"
|
||||
go build -ldflags "-s -w -X 'main.version=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)'" ./cmd/http3-ytproxy
|
||||
|
||||
FROM alpine:3.21
|
||||
|
||||
|
|
226
cmd/http3-ytproxy/main.go
Normal file
226
cmd/http3-ytproxy/main.go
Normal file
|
@ -0,0 +1,226 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/config"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/paths"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
|
||||
"github.com/prometheus/procfs"
|
||||
)
|
||||
|
||||
type ConnectionWatcher struct {
|
||||
totalEstablished int64
|
||||
established int64
|
||||
active int64
|
||||
idle int64
|
||||
}
|
||||
|
||||
var version string
|
||||
var cw ConnectionWatcher
|
||||
var tx uint64
|
||||
|
||||
// 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:
|
||||
metrics.Metrics.EstablishedConnections.Inc()
|
||||
metrics.Metrics.TotalConnEstablished.Inc()
|
||||
case http.StateClosed, http.StateHijacked:
|
||||
metrics.Metrics.EstablishedConnections.Dec()
|
||||
}
|
||||
}
|
||||
|
||||
func blockCheckerCalc(p *procfs.Proc) {
|
||||
var last uint64
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
// p.NetDev should never fail.
|
||||
stat, _ := p.NetDev()
|
||||
current := stat.Total().TxBytes
|
||||
tx = current - last
|
||||
last = current
|
||||
}
|
||||
}
|
||||
|
||||
// Detects if a backend has been blocked based on the amount of bandwidth
|
||||
// reported by procfs.
|
||||
// This may be the best way to detect if the IP has been blocked from googlevideo
|
||||
// servers. I would like to detect blockages using the status code that googlevideo
|
||||
// returns, which most of the time is 403 (Forbidden). But this error code is not
|
||||
// exclusive to IP blocks, it's also returned for other reasons like a wrong
|
||||
// query parameter like `pot` (po_token) or anything like that.
|
||||
func blockChecker(gh string, cooldown int) {
|
||||
log.Println("[INFO] Starting blockchecker")
|
||||
// Sleep for 60 seconds before commencing the loop
|
||||
time.Sleep(60 * time.Second)
|
||||
url := "http://" + gh + "/v1/openvpn/status"
|
||||
|
||||
p, err := procfs.Self()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [procfs]: Could not get process: %s\n", err)
|
||||
log.Println("[INFO] Blockchecker will not run, so if the VPN IP used on gluetun gets blocked, it will not be rotated!")
|
||||
return
|
||||
}
|
||||
go blockCheckerCalc(&p)
|
||||
|
||||
for {
|
||||
time.Sleep(time.Duration(cooldown) * time.Second)
|
||||
if float64(tx)*0.000008 < 2.0 {
|
||||
body := "{\"status\":\"stopped\"}\""
|
||||
// This should never fail too
|
||||
request, _ := http.NewRequest("PUT", url, strings.NewReader(body))
|
||||
_, err = httpc.Client.Do(request)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to send request to gluetun.")
|
||||
} else {
|
||||
log.Printf("[INFO] Request to change IP sent to gluetun successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func beforeMisc(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
defer utils.PanicHandler(w)
|
||||
next(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func beforeProxy(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, req *http.Request) {
|
||||
defer utils.PanicHandler(w)
|
||||
|
||||
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 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
|
||||
}
|
||||
|
||||
metrics.Metrics.RequestCount.Inc()
|
||||
next(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
config.LoadConfig()
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.BoolVar(&config.Cfg.Enable_http, "http", config.Cfg.Enable_http, "Enable HTTP Server")
|
||||
flag.BoolVar(&config.Cfg.Uds, "uds", config.Cfg.Uds, "Enable UDS (Unix socket domain)")
|
||||
flag.IntVar(&config.Cfg.Http_client_ver, "http-client-ver", config.Cfg.Http_client_ver, "Specify the HTTP Version that is going to be used on the client, accepted values are '1', '2 'and '3'")
|
||||
flag.BoolVar(&config.Cfg.Ipv6_only, "ipv6-only", config.Cfg.Ipv6_only, "Only use ipv6 for requests")
|
||||
flag.StringVar(&config.Cfg.Uds_path, "s", config.Cfg.Uds_path, "Specify the UDS (Unix socket domain) path\nExample: /run/http3-ytproxy.sock")
|
||||
flag.StringVar(&config.Cfg.Proxy, "pr", config.Cfg.Proxy, "Specify the proxy that is going to be used for requests\nExample: http://127.0.0.1:8090")
|
||||
flag.StringVar(&config.Cfg.Port, "p", config.Cfg.Port, "Specify a port number")
|
||||
flag.StringVar(&config.Cfg.Host, "l", config.Cfg.Host, "Specify a listen address")
|
||||
flag.Parse()
|
||||
|
||||
log.Printf("[INFO] Current config values: %+v\n", config.Cfg)
|
||||
|
||||
switch config.Cfg.Http_client_ver {
|
||||
case 1:
|
||||
log.Println("[INFO] Using HTTP/1.1 Client")
|
||||
httpc.Client = httpc.H1_1client
|
||||
case 2:
|
||||
log.Println("[INFO] Using HTTP/2 Client")
|
||||
httpc.Client = httpc.H2client
|
||||
case 3:
|
||||
log.Println("[INFO] Using HTTP/3 Client")
|
||||
httpc.Client = httpc.H3client
|
||||
default:
|
||||
log.Println("[INFO] Using HTTP/1.1 Client")
|
||||
httpc.Client = httpc.H1_1client
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// MISC ROUTES
|
||||
mux.HandleFunc("/", beforeMisc(paths.Root))
|
||||
mux.HandleFunc("/health", beforeMisc(paths.Health))
|
||||
|
||||
metrics.Register()
|
||||
|
||||
mux.Handle("/metrics", paths.MetricsHandler())
|
||||
|
||||
// PROXY ROUTES
|
||||
mux.HandleFunc("/videoplayback", beforeProxy(paths.Videoplayback))
|
||||
mux.HandleFunc("/vi/", beforeProxy(paths.Vi))
|
||||
mux.HandleFunc("/vi_webp/", beforeProxy(paths.Vi))
|
||||
mux.HandleFunc("/sb/", beforeProxy(paths.Vi))
|
||||
mux.HandleFunc("/ggpht/", beforeProxy(paths.Ggpht))
|
||||
mux.HandleFunc("/a/", beforeProxy(paths.Ggpht))
|
||||
mux.HandleFunc("/ytc/", beforeProxy(paths.Ggpht))
|
||||
|
||||
if config.Cfg.Gluetun.Block_checker {
|
||||
go blockChecker(config.Cfg.Gluetun.Gluetun_api, config.Cfg.Gluetun.Block_checker_cooldown)
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: mux,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 1 * time.Hour,
|
||||
ConnState: cw.OnStateChange,
|
||||
Addr: config.Cfg.Host + ":" + config.Cfg.Port,
|
||||
}
|
||||
|
||||
if config.Cfg.Uds {
|
||||
syscall.Unlink(config.Cfg.Uds_path)
|
||||
socket_listener, err := net.Listen("unix", config.Cfg.Uds_path)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Failed to bind to UDS, please check the socket path", err.Error())
|
||||
}
|
||||
defer socket_listener.Close()
|
||||
err = os.Chmod(config.Cfg.Uds_path, 0777)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Failed to set socket permissions to 777:", err.Error())
|
||||
return
|
||||
} else {
|
||||
log.Println("[INFO] Setting socket permissions to 777")
|
||||
}
|
||||
|
||||
go func() {
|
||||
err := srv.Serve(socket_listener)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Failed to listen serve UDS:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// To allow everyone to access the socket
|
||||
log.Println("[INFO] Unix socket listening at:", config.Cfg.Uds_path)
|
||||
}
|
||||
|
||||
if config.Cfg.Enable_http {
|
||||
log.Println("[INFO] Serving HTTP server at port", config.Cfg.Port)
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatalf("[FATAL] Failed to listen on '%s:%s': %s\n", config.Cfg.Host, config.Cfg.Port, err)
|
||||
}
|
||||
}
|
||||
}
|
16
go.mod
16
go.mod
|
@ -1,24 +1,23 @@
|
|||
module git.nadeko.net/Fijxu/http3-ytproxy/v3
|
||||
module git.nadeko.net/Fijxu/http3-ytproxy
|
||||
|
||||
go 1.22.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.23.0
|
||||
|
||||
require github.com/quic-go/quic-go v0.48.1
|
||||
require (
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/prometheus/procfs v0.15.1
|
||||
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
|
||||
|
@ -28,7 +27,6 @@ require (
|
|||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
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
|
||||
)
|
||||
|
|
42
go.sum
42
go.sum
|
@ -2,32 +2,20 @@ 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=
|
||||
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/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
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=
|
||||
|
@ -46,57 +34,31 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg
|
|||
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=
|
||||
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=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
369
httppaths.go
369
httppaths.go
|
@ -1,369 +0,0 @@
|
|||
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()
|
||||
|
||||
// log.Println(req.URL.RawQuery)
|
||||
|
||||
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 {
|
||||
// w.WriteHeader(400)
|
||||
// io.WriteString(w, "'mvi' query string undefined")
|
||||
// return
|
||||
// }
|
||||
|
||||
// if len(mn) <= 0 {
|
||||
// w.WriteHeader(400)
|
||||
// io.WriteString(w, "'mn' query string undefined")
|
||||
// return
|
||||
// }
|
||||
|
||||
// host = "rr" + mvi + "---" + mn[0] + ".googlevideo.com"
|
||||
// }
|
||||
|
||||
parts := strings.Split(strings.ToLower(host), ".")
|
||||
if len(parts) < 2 {
|
||||
w.WriteHeader(400)
|
||||
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 {
|
||||
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()
|
||||
// fmt.Println("esc path:", path)
|
||||
|
||||
proxyURL, err := url.Parse("https://" + host + path)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
// fmt.Println("query params:", q)
|
||||
proxyURL.RawQuery = q.Encode()
|
||||
// fmt.Println("esc path:", proxyURL.RawQuery)
|
||||
|
||||
// https://github.com/FreeTubeApp/FreeTube/blob/5a4cd981cdf2c2a20ab68b001746658fd0c6484e/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js#L1097
|
||||
body := []byte{0x78, 0} // protobuf body
|
||||
|
||||
postRequest, err := http.NewRequest("POST", proxyURL.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Panic("Failed to create postRequest:", err)
|
||||
}
|
||||
// headRequest, err := http.Head(proxyURL.String())
|
||||
headRequest, err := http.NewRequest("HEAD", proxyURL.String(), nil)
|
||||
if err != nil {
|
||||
log.Panic("Failed to create headRequest:", err)
|
||||
}
|
||||
copyHeaders(req.Header, postRequest.Header, false)
|
||||
copyHeaders(req.Header, headRequest.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 := &http.Response{}
|
||||
|
||||
// doPostRequest := func () {
|
||||
|
||||
// }
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
// log.Println("headRequest URL:", headRequest.URL)
|
||||
resp, err = client.Do(headRequest)
|
||||
if err != nil {
|
||||
log.Panic("Failed to do HEAD request:", err)
|
||||
}
|
||||
// log.Println("HEAD request made to:", headRequest.URL, "returned", strconv.Itoa(resp.StatusCode))
|
||||
// log.Println("HEAD Location header:", resp.Header.Get("Location"))
|
||||
if resp.Header.Get("Location") != "" {
|
||||
log.Println("")
|
||||
location := resp.Header.Get("Location")
|
||||
// log.Println("location:", location)
|
||||
new_url, _ := url.Parse(location)
|
||||
new_host := new_url.Host
|
||||
log.Println("new_host:", new_host)
|
||||
// log.Println("new_url:", new_url.Host)
|
||||
// log.Println("old_request:", proxyURL.String())
|
||||
|
||||
postRequest.URL = new_url
|
||||
headRequest.URL = new_url
|
||||
// headRequest.Header.Set("Host", new_url.Host)
|
||||
// postRequest.Header.Set("Host", new_url.Host)
|
||||
postRequest.Host = new_url.Host
|
||||
headRequest.Host = new_url.Host
|
||||
log.Println("postRequest.Host after location:", postRequest.Host)
|
||||
log.Println("headRequest.Host after location:", headRequest.Host)
|
||||
// log.Println("postrequest new_url:", postRequest.URL)
|
||||
// resp, err = client.Do(postRequest)
|
||||
// if err != nil {
|
||||
// log.Panic("Failed to do POST request")
|
||||
// }
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = client.Do(postRequest)
|
||||
if err != nil {
|
||||
log.Panic("Failed to do POST request:", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
io.Copy(w, resp.Body)
|
||||
|
||||
// resp, err = client.Do(postRequest)
|
||||
// if err != nil {
|
||||
// log.Panic("Failed to do POST request:", err)
|
||||
// }
|
||||
|
||||
// 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)
|
||||
// metrics.RequestForbidden.Videoplayback.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)
|
||||
|
||||
// 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) {
|
||||
const host string = "i.ytimg.com"
|
||||
q := req.URL.Query()
|
||||
|
||||
path := req.URL.EscapedPath()
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/*
|
||||
Required for /sb/ endpoints
|
||||
You can't access https://i.ytimg.com/sb/<VIDEOID>/storyboard3_L2/M3.jpg
|
||||
without it's parameters `sqp` and `sigh`
|
||||
*/
|
||||
proxyURL.RawQuery = q.Encode()
|
||||
|
||||
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
|
||||
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)
|
||||
}
|
||||
|
||||
if err := forbiddenChecker(resp, w); err != nil {
|
||||
atomic.AddInt64(&stats_.RequestsForbidden.Vi, 1)
|
||||
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)
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
|
||||
func ggpht(w http.ResponseWriter, req *http.Request) {
|
||||
const host string = "yt3.ggpht.com"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
|
||||
copyHeaders(req.Header, request.Header, false)
|
||||
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)
|
||||
}
|
||||
|
||||
if err := forbiddenChecker(resp, w); err != nil {
|
||||
atomic.AddInt64(&stats_.RequestsForbidden.Ggpht, 1)
|
||||
metrics.RequestForbidden.Ggpht.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)
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
102
internal/config/config.go
Normal file
102
internal/config/config.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var Cfg *config
|
||||
|
||||
type config struct {
|
||||
Enable_http bool
|
||||
Uds bool
|
||||
Uds_path string
|
||||
Host string
|
||||
Port string
|
||||
Proxy string
|
||||
Http_client_ver int
|
||||
Ipv6_only bool
|
||||
Gluetun struct {
|
||||
Gluetun_api string
|
||||
Block_checker bool
|
||||
Block_checker_cooldown int
|
||||
}
|
||||
Companion struct {
|
||||
// Used for videoplayback query encryption
|
||||
Secret_key string
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key string) string {
|
||||
// `YTPROXY_` as a prefix
|
||||
v, _ := syscall.Getenv("YTPROXY_" + key)
|
||||
return v
|
||||
}
|
||||
|
||||
func getEnvBool(key string, def bool) bool {
|
||||
v := strings.ToLower(getenv(key))
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
return v == "true"
|
||||
}
|
||||
|
||||
func getEnvString(key string, def string, tolower bool) string {
|
||||
var v string
|
||||
if tolower {
|
||||
v = strings.ToLower(getenv(key))
|
||||
}
|
||||
v = getenv(key)
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func getEnvInt(key string, def int) int {
|
||||
v := strings.ToLower(getenv(key))
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
i, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
log.Panicf("[FATAL] Failed to convert env variable '%s' to int", v)
|
||||
}
|
||||
return int(i)
|
||||
}
|
||||
|
||||
func LoadConfig() {
|
||||
Cfg = &config{
|
||||
Enable_http: getEnvBool("ENABLE_HTTP", true),
|
||||
Uds: getEnvBool("ENABLE_UDS", true),
|
||||
// I would use `/run/http3-proxy` here, but `/run` is not user writable
|
||||
// which is kinda anoying when developing.
|
||||
Uds_path: getEnvString("UDS_PATH", "/tmp/http-ytproxy.sock", true),
|
||||
Host: getEnvString("HOST", "0.0.0.0", true),
|
||||
Port: getEnvString("PORT", "8080", true),
|
||||
Proxy: getEnvString("PROXY", "", true),
|
||||
Http_client_ver: getEnvInt("HTTP_CLIENT_VER", 1),
|
||||
Ipv6_only: getEnvBool("IPV6_ONLY", false),
|
||||
Gluetun: struct {
|
||||
Gluetun_api string
|
||||
Block_checker bool
|
||||
Block_checker_cooldown int
|
||||
}{
|
||||
Gluetun_api: getEnvString("GLUETUN_API", "127.0.0.1:8000", true),
|
||||
Block_checker: getEnvBool("BLOCK_CHECKER", true),
|
||||
Block_checker_cooldown: getEnvInt("BLOCK_CHECKER_COOLDOWN", 60),
|
||||
},
|
||||
Companion: struct{ Secret_key string }{
|
||||
Secret_key: getEnvString("SECRET_KEY", "", false),
|
||||
},
|
||||
}
|
||||
checkConfig()
|
||||
}
|
||||
|
||||
func checkConfig() {
|
||||
if len(Cfg.Companion.Secret_key) > 16 {
|
||||
log.Fatalln("The value of 'companion.secret_key' (YTPROXY_SECRET_KEY) needs to be a size of 16 characters.")
|
||||
}
|
||||
}
|
93
internal/httpc/client.go
Normal file
93
internal/httpc/client.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
package httpc
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/config"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
var Client *http.Client
|
||||
|
||||
var dialer = &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
// http/1.1 client
|
||||
var H1_1client = &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
var net string
|
||||
if config.Cfg.Ipv6_only {
|
||||
net = "tcp6"
|
||||
} else {
|
||||
net = "tcp4"
|
||||
}
|
||||
return dialer.Dial(net, addr)
|
||||
},
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 20 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ReadBufferSize: 16 * 1024,
|
||||
MaxConnsPerHost: 0,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxIdleConns: 0,
|
||||
Proxy: func(r *http.Request) (*url.URL, error) {
|
||||
if config.Cfg.Proxy != "" {
|
||||
return url.Parse(config.Cfg.Proxy)
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
// Prevent switching to HTTP/2
|
||||
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
|
||||
},
|
||||
}
|
||||
|
||||
// http/2 client
|
||||
var H2client = &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
Dial: func(network, addr string) (net.Conn, error) {
|
||||
var net string
|
||||
if config.Cfg.Ipv6_only {
|
||||
net = "tcp6"
|
||||
} else {
|
||||
net = "tcp4"
|
||||
}
|
||||
return dialer.Dial(net, addr)
|
||||
},
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 20 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
ReadBufferSize: 16 * 1024,
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxConnsPerHost: 0,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxIdleConns: 0,
|
||||
Proxy: func(r *http.Request) (*url.URL, error) {
|
||||
if config.Cfg.Proxy != "" {
|
||||
return url.Parse(config.Cfg.Proxy)
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
1
internal/httpc/server.go
Normal file
1
internal/httpc/server.go
Normal file
|
@ -0,0 +1 @@
|
|||
package httpc
|
76
internal/metrics/metrics.go
Normal file
76
internal/metrics/metrics.go
Normal file
|
@ -0,0 +1,76 @@
|
|||
package metrics
|
||||
|
||||
import "github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
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 Register() {
|
||||
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)
|
||||
}
|
33
internal/paths/consts.go
Normal file
33
internal/paths/consts.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
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"
|
||||
ggpht_host = "yt3.ggpht.com"
|
||||
)
|
||||
|
||||
var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`)
|
||||
|
||||
var allowed_hosts = []string{
|
||||
"youtube.com",
|
||||
"googlevideo.com",
|
||||
"ytimg.com",
|
||||
"ggpht.com",
|
||||
"googleusercontent.com",
|
||||
}
|
||||
|
||||
var videoplayback_headers = &http.Header{
|
||||
"Accept": {"*/*"},
|
||||
"Accept-Encoding": {"gzip, deflate, br, zstd"},
|
||||
"Accept-Language": {"en-us,en;q=0.5"},
|
||||
"Origin": {"https://www.youtube.com"},
|
||||
"Referer": {"https://www.youtube.com/"},
|
||||
"User-Agent": {default_ua},
|
||||
}
|
||||
|
||||
// https://github.com/FreeTubeApp/FreeTube/blob/5a4cd981cdf2c2a20ab68b001746658fd0c6484e/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js#L1097
|
||||
var protobuf_body = []byte{0x78, 0} // protobuf body
|
49
internal/paths/ggpht.go
Normal file
49
internal/paths/ggpht.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
|
||||
)
|
||||
|
||||
func Ggpht(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.EscapedPath()
|
||||
path = strings.Replace(path, "/ggpht", "", 1)
|
||||
path = strings.Replace(path, "/i/", "/", 1)
|
||||
|
||||
proxyURL, err := url.Parse("https://" + ggpht_host + path)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
|
||||
utils.CopyHeaders(req.Header, request.Header, false)
|
||||
request.Header.Set("User-Agent", default_ua)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
resp, err := httpc.Client.Do(request)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if err := forbiddenChecker(resp, w); err != nil {
|
||||
metrics.Metrics.RequestForbidden.Ggpht.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
|
||||
utils.CopyHeaders(resp.Header, w.Header(), NoRewrite)
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
11
internal/paths/health.go
Normal file
11
internal/paths/health.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Health(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
io.WriteString(w, "OK")
|
||||
}
|
27
internal/paths/metrics.go
Normal file
27
internal/paths/metrics.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var programInit = time.Now()
|
||||
|
||||
// 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.Metrics.Uptime.Set(float64(time.Duration(time.Since(programInit).Seconds())))
|
||||
promhttp.Handler().ServeHTTP(w, req)
|
||||
})
|
||||
}
|
17
internal/paths/root.go
Normal file
17
internal/paths/root.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
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:
|
||||
/stats
|
||||
/health`
|
||||
io.WriteString(w, msg)
|
||||
}
|
59
internal/paths/vi.go
Normal file
59
internal/paths/vi.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
|
||||
)
|
||||
|
||||
func Vi(w http.ResponseWriter, req *http.Request) {
|
||||
const host string = "i.ytimg.com"
|
||||
q := req.URL.Query()
|
||||
|
||||
path := req.URL.EscapedPath()
|
||||
|
||||
proxyURL, err := url.Parse("https://" + host + path)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if strings.HasSuffix(proxyURL.EscapedPath(), "maxres.jpg") {
|
||||
proxyURL.Path = utils.GetBestThumbnail(proxyURL.EscapedPath())
|
||||
}
|
||||
|
||||
/*
|
||||
Required for /sb/ endpoints
|
||||
You can't access https://i.ytimg.com/sb/<VIDEOID>/storyboard3_L2/M3.jpg
|
||||
without it's parameters `sqp` and `sigh`
|
||||
*/
|
||||
proxyURL.RawQuery = q.Encode()
|
||||
|
||||
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", default_ua)
|
||||
|
||||
resp, err := httpc.Client.Do(request)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if err := forbiddenChecker(resp, w); err != nil {
|
||||
metrics.Metrics.RequestForbidden.Vi.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
244
internal/paths/videoplayback.go
Normal file
244
internal/paths/videoplayback.go
Normal file
|
@ -0,0 +1,244 @@
|
|||
package paths
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/config"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
|
||||
)
|
||||
|
||||
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 checkRequest(w http.ResponseWriter, req *http.Request, params url.Values) bool {
|
||||
host := params.Get("host")
|
||||
|
||||
parts := strings.Split(strings.ToLower(host), ".")
|
||||
if len(parts) < 2 {
|
||||
w.WriteHeader(400)
|
||||
io.WriteString(w, "Invalid hostname.")
|
||||
return true
|
||||
}
|
||||
|
||||
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
||||
disallowed := true
|
||||
for _, value := range allowed_hosts {
|
||||
if domain == value {
|
||||
disallowed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if disallowed {
|
||||
w.WriteHeader(401)
|
||||
io.WriteString(w, "Non YouTube domains are not supported.")
|
||||
return true
|
||||
}
|
||||
|
||||
expire, err := strconv.ParseInt(params.Get("expire"), 10, 64)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, "Expire query string undefined")
|
||||
return true
|
||||
}
|
||||
|
||||
// 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 true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func Videoplayback(w http.ResponseWriter, req *http.Request) {
|
||||
q := req.URL.Query()
|
||||
|
||||
if q.Get("enc") == "true" {
|
||||
decryptedQueryParams, err := utils.DecryptQueryParams(req.URL.Query().Get("data"), config.Cfg.Companion.Secret_key)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error:\nFailed to decrypt query parameters", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var structuredDecryptedQueryParams [][]string
|
||||
|
||||
err = json.Unmarshal([]byte(decryptedQueryParams), &structuredDecryptedQueryParams)
|
||||
if err != nil {
|
||||
http.Error(w, "Internal Server Error:\nFailed to parse query parameters from the decrypted query parameters", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
pot := structuredDecryptedQueryParams[1][1]
|
||||
ip := structuredDecryptedQueryParams[0][1]
|
||||
q.Del("enc")
|
||||
q.Del("data")
|
||||
q.Set("pot", pot)
|
||||
q.Set("ip", ip)
|
||||
}
|
||||
|
||||
if checkRequest(w, req, q) {
|
||||
return
|
||||
}
|
||||
|
||||
host := q.Get("host")
|
||||
title := q.Get("title")
|
||||
q.Del("host")
|
||||
q.Del("title")
|
||||
|
||||
rangeHeader := req.Header.Get("range")
|
||||
var requestBytes string
|
||||
if rangeHeader != "" {
|
||||
requestBytes = strings.Split(rangeHeader, "=")[1]
|
||||
} else {
|
||||
requestBytes = ""
|
||||
}
|
||||
if requestBytes != "" {
|
||||
q.Set("range", requestBytes)
|
||||
}
|
||||
|
||||
path := req.URL.EscapedPath()
|
||||
|
||||
proxyURL, err := url.Parse("https://" + host + path)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
proxyURL.RawQuery = q.Encode()
|
||||
|
||||
postRequest, err := http.NewRequest("POST", proxyURL.String(), bytes.NewReader(protobuf_body))
|
||||
if err != nil {
|
||||
log.Panic("Failed to create postRequest:", err)
|
||||
}
|
||||
headRequest, err := http.NewRequest("HEAD", proxyURL.String(), nil)
|
||||
if err != nil {
|
||||
log.Panic("Failed to create headRequest:", err)
|
||||
}
|
||||
|
||||
postRequest.Header = *videoplayback_headers
|
||||
headRequest.Header = *videoplayback_headers
|
||||
|
||||
resp := &http.Response{}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
resp, err = httpc.Client.Do(headRequest)
|
||||
if err != nil {
|
||||
log.Panic("Failed to do HEAD request:", err)
|
||||
}
|
||||
if resp.Header.Get("Location") != "" {
|
||||
new_url, _ := url.Parse(resp.Header.Get("Location"))
|
||||
postRequest.URL = new_url
|
||||
headRequest.URL = new_url
|
||||
postRequest.Host = new_url.Host
|
||||
headRequest.Host = new_url.Host
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resp, err = httpc.Client.Do(postRequest)
|
||||
if err != nil {
|
||||
log.Panic("Failed to do POST request:", err)
|
||||
}
|
||||
|
||||
if err := forbiddenChecker(resp, w); err != nil {
|
||||
metrics.Metrics.RequestForbidden.Videoplayback.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
utils.CopyHeadersNew(resp.Header, w.Header())
|
||||
|
||||
if title != "" {
|
||||
content := "attachment; filename=\"" + url.PathEscape(title) + "\"; filename*=UTF-8''" + url.PathEscape(title)
|
||||
w.Header().Set("content-disposition", content)
|
||||
}
|
||||
|
||||
if requestBytes != "" && resp.StatusCode == http.StatusOK {
|
||||
// check for range headers in the forms:
|
||||
// "bytes=0-" get full length from start
|
||||
// "bytes=500-" get full length from 500 bytes in
|
||||
// "bytes=500-1000" get 500 bytes starting from 500
|
||||
byteParts := strings.Split(requestBytes, "-")
|
||||
firstByte, lastByte := byteParts[0], byteParts[1]
|
||||
if lastByte != "" {
|
||||
clen := q.Get("clen")
|
||||
|
||||
if clen == "" {
|
||||
w.Header().Add("content-range", "bytes "+requestBytes+"/*")
|
||||
} else {
|
||||
w.Header().Add("content-range", "bytes "+requestBytes+"/"+clen)
|
||||
}
|
||||
|
||||
w.WriteHeader(206)
|
||||
} else {
|
||||
// i.e. "bytes=0-", "bytes=600-"
|
||||
// full size of content is able to be calculated, so a full Content-Range header can be constructed
|
||||
bytesReceived := resp.Header.Get("content-length")
|
||||
firstByteInt, _ := strconv.Atoi(firstByte)
|
||||
bytesReceivedInt, _ := strconv.Atoi(bytesReceived)
|
||||
// last byte should always be one less than the length
|
||||
totalContentLength := firstByteInt + bytesReceivedInt
|
||||
lastByte := totalContentLength - 1
|
||||
lastByteString := strconv.Itoa(lastByte)
|
||||
totalContentLengthString := strconv.Itoa(totalContentLength)
|
||||
w.Header().Add("content-range", "bytes "+firstByte+"-"+lastByteString+"/"+totalContentLengthString)
|
||||
if firstByte != "0" {
|
||||
// only part of the total content returned, 206
|
||||
w.WriteHeader(206)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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] = utils.RelativeUrl(line)
|
||||
}
|
||||
|
||||
if manifest_re.MatchString(line) {
|
||||
url := manifest_re.FindStringSubmatch(line)[1]
|
||||
lines[i] = strings.Replace(line, url, utils.RelativeUrl(url), 1)
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, strings.Join(lines, "\n"))
|
||||
} else {
|
||||
io.Copy(w, resp.Body)
|
||||
}
|
||||
}
|
28
internal/utils/consts.go
Normal file
28
internal/utils/consts.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package utils
|
||||
|
||||
const (
|
||||
path_prefix = ""
|
||||
)
|
||||
|
||||
var strip_headers = []string{
|
||||
"Accept-Encoding",
|
||||
"Authorization",
|
||||
"Origin",
|
||||
"Referer",
|
||||
"Cookie",
|
||||
"Set-Cookie",
|
||||
"Etag",
|
||||
"Alt-Svc",
|
||||
"Server",
|
||||
"Cache-Control",
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
|
||||
"report-to",
|
||||
}
|
||||
|
||||
var headers_for_response = []string{
|
||||
"Content-Length",
|
||||
"Accept-Ranges",
|
||||
"Content-Type",
|
||||
"Expires",
|
||||
"Last-Modified",
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
package main
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
|
||||
)
|
||||
|
||||
func copyHeaders(from http.Header, to http.Header, length bool) {
|
||||
func CopyHeaders(from http.Header, to http.Header, length bool) {
|
||||
// Loop over header names
|
||||
outer:
|
||||
for name, values := range from {
|
||||
|
@ -28,14 +32,24 @@ outer:
|
|||
}
|
||||
}
|
||||
|
||||
func getBestThumbnail(path string) (newpath string) {
|
||||
func CopyHeadersNew(from http.Header, to http.Header) {
|
||||
for from_header, value := range from {
|
||||
for _, header := range headers_for_response {
|
||||
if from_header == header {
|
||||
to.Add(header, value[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
resp, _ := httpc.Client.Head(url)
|
||||
if resp.StatusCode == 200 {
|
||||
return newpath
|
||||
}
|
||||
|
@ -56,9 +70,33 @@ func RelativeUrl(in string) (newurl string) {
|
|||
return segment_url.RequestURI()
|
||||
}
|
||||
|
||||
func panicHandler(w http.ResponseWriter) {
|
||||
func PanicHandler(w http.ResponseWriter) {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic: %v", r)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/41652605
|
||||
func DecryptQueryParams(encryptedQuery string, key string) (string, error) {
|
||||
se, err := base64.StdEncoding.DecodeString(encryptedQuery)
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Error when decoding base64 string:", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
cipher, err := aes.NewCipher([]byte(key)[0:16])
|
||||
if err != nil {
|
||||
log.Println("[ERROR] Error initializating cipher.Block:", err)
|
||||
return "", err
|
||||
}
|
||||
decrypted := make([]byte, len(se))
|
||||
size := 16
|
||||
|
||||
for bs, be := 0, size; bs < len(se); bs, be = bs+size, be+size {
|
||||
cipher.Decrypt(decrypted[bs:be], se[bs:be])
|
||||
}
|
||||
|
||||
paddingSize := int(decrypted[len(decrypted)-1])
|
||||
return string(decrypted[0 : len(decrypted)-paddingSize]), nil
|
||||
}
|
650
main.go
650
main.go
|
@ -1,650 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/conduitio/bwlimit"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/prometheus/procfs"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
var (
|
||||
wl = flag.Int("w", 8000, "Write limit in Kbps")
|
||||
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{
|
||||
EnableDatagrams: false,
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
var dialer = &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
var proxy string
|
||||
|
||||
// var proxyUrl, _ = url.Parse("http://127.0.0.1:8080")
|
||||
var proxyUrl, _ = url.Parse("socks5://127.0.0.1:1080")
|
||||
|
||||
var protocols *http.Protocols
|
||||
|
||||
// http/2 client
|
||||
var h2client = &http.Client{
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
TLSNextProto: make(map[string]func(string, *tls.Conn) http.RoundTripper),
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true, VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { return nil }},
|
||||
// Proxy: http.ProxyURL(proxyUrl),
|
||||
},
|
||||
}
|
||||
|
||||
// var h2client_test = &http.Client{
|
||||
// // Yeah I know I can just use http.Transport and it will use HTTP/2 automatically
|
||||
// // I prefer to set it up explicitly
|
||||
// Transport: &http.Transport{},
|
||||
// }
|
||||
|
||||
var client *http.Client
|
||||
|
||||
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",
|
||||
"googlevideo.com",
|
||||
"ytimg.com",
|
||||
"ggpht.com",
|
||||
"googleusercontent.com",
|
||||
}
|
||||
|
||||
var strip_headers = []string{
|
||||
"Accept-Encoding",
|
||||
"Authorization",
|
||||
"Origin",
|
||||
"Referer",
|
||||
"Cookie",
|
||||
"Set-Cookie",
|
||||
"Etag",
|
||||
"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 = ""
|
||||
|
||||
var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`)
|
||||
|
||||
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"`
|
||||
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"`
|
||||
RequestsForbiddenPerSec struct {
|
||||
Videoplayback int64 `json:"videoplayback"`
|
||||
}
|
||||
RequestsForbidden struct {
|
||||
Videoplayback int64 `json:"videoplayback"`
|
||||
Vi int64 `json:"vi"`
|
||||
Ggpht int64 `json:"ggpht"`
|
||||
} `json:"requestsForbidden"`
|
||||
}
|
||||
|
||||
var stats_ = statusJson{
|
||||
Version: version + "-" + runtime.GOARCH,
|
||||
Uptime: 0,
|
||||
RequestCount: 0,
|
||||
RequestPerSecond: 0,
|
||||
RequestPerMinute: 0,
|
||||
TotalConnEstablished: 0,
|
||||
EstablishedConnections: 0,
|
||||
ActiveConnections: 0,
|
||||
IdleConnections: 0,
|
||||
RequestsForbiddenPerSec: struct {
|
||||
Videoplayback int64 `json:"videoplayback"`
|
||||
}{
|
||||
Videoplayback: 0,
|
||||
},
|
||||
RequestsForbidden: struct {
|
||||
Videoplayback int64 `json:"videoplayback"`
|
||||
Vi int64 `json:"vi"`
|
||||
Ggpht int64 `json:"ggpht"`
|
||||
}{
|
||||
Videoplayback: 0,
|
||||
Vi: 0,
|
||||
Ggpht: 0,
|
||||
},
|
||||
}
|
||||
|
||||
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:
|
||||
/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)
|
||||
}
|
||||
}
|
||||
|
||||
func health(w http.ResponseWriter, req *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
io.WriteString(w, "OK")
|
||||
}
|
||||
|
||||
func requestPerSecond() {
|
||||
var last int64
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
current := stats_.RequestCount
|
||||
stats_.RequestPerSecond = current - last
|
||||
metrics.RequestPerSecond.Set(float64(stats_.RequestPerSecond))
|
||||
last = current
|
||||
}
|
||||
}
|
||||
|
||||
func requestPerMinute() {
|
||||
var last int64
|
||||
for {
|
||||
time.Sleep(60 * time.Second)
|
||||
current := stats_.RequestCount
|
||||
stats_.RequestPerMinute = current - last
|
||||
metrics.RequestPerMinute.Set(float64(stats_.RequestPerMinute))
|
||||
last = current
|
||||
}
|
||||
}
|
||||
|
||||
var tx uint64
|
||||
|
||||
func blockCheckerCalc(p *procfs.Proc) {
|
||||
var last uint64
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
// p.NetDev should never fail.
|
||||
stat, _ := p.NetDev()
|
||||
current := stat.Total().TxBytes
|
||||
tx = current - last
|
||||
last = current
|
||||
}
|
||||
}
|
||||
|
||||
// Detects if a backend has been blocked based on the amount of bandwidth
|
||||
// reported by procfs.
|
||||
// This may be the best way to detect if the IP has been blocked from googlevideo
|
||||
// servers. I would like to detect blockages using the status code that googlevideo
|
||||
// returns, which most of the time is 403 (Forbidden). But this error code is not
|
||||
// exclusive to IP blocks, it's also returned for other reasons like a wrong
|
||||
// query parameter like `pot` (po_token) or anything like that.
|
||||
func blockChecker(gh string, cooldown int) {
|
||||
log.Println("[INFO] Starting blockchecker")
|
||||
// Sleep for 60 seconds before commencing the loop
|
||||
time.Sleep(60 * time.Second)
|
||||
url := "http://" + gh + "/v1/openvpn/status"
|
||||
|
||||
p, err := procfs.Self()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] [procfs]: Could not get process: %s\n", err)
|
||||
log.Println("[INFO] Blockchecker will not run, so if the VPN IP used on gluetun gets blocked, it will not be rotated!")
|
||||
return
|
||||
}
|
||||
go blockCheckerCalc(&p)
|
||||
|
||||
for {
|
||||
time.Sleep(time.Duration(cooldown) * time.Second)
|
||||
if float64(tx)*0.000008 < 2.0 {
|
||||
body := "{\"status\":\"stopped\"}\""
|
||||
// This should never fail too
|
||||
request, _ := http.NewRequest("PUT", url, strings.NewReader(body))
|
||||
_, err = client.Do(request)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to send request to gluetun.")
|
||||
} else {
|
||||
log.Printf("[INFO] Request to change IP sent to gluetun successfully")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func beforeMisc(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
|
||||
}
|
||||
|
||||
next(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
protocols = &http.Protocols{}
|
||||
protocols.SetHTTP1(true)
|
||||
protocols.SetHTTP2(false)
|
||||
protocols.SetUnencryptedHTTP2(false)
|
||||
|
||||
defaultHost := "0.0.0.0"
|
||||
defaultPort := "8080"
|
||||
defaultSock := "/tmp/http-ytproxy.sock"
|
||||
defaultTLSCert := "/data/cert.pem"
|
||||
defaultTLSKey := "/data/key.key"
|
||||
|
||||
var https bool = false
|
||||
var h3c bool = false
|
||||
var ipv6 bool = false
|
||||
var bc bool = true
|
||||
|
||||
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("BLOCK_CHECKER")) == "false" {
|
||||
bc = false
|
||||
}
|
||||
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
|
||||
}
|
||||
// gh is where the gluetun api is located
|
||||
gh := os.Getenv("GLUETUN_HOSTNAME")
|
||||
if gh == "" {
|
||||
gh = "127.0.0.1:8000"
|
||||
}
|
||||
bc_cooldown := os.Getenv("BLOCK_CHECKER_COOLDOWN")
|
||||
if bc_cooldown == "" {
|
||||
bc_cooldown = "60"
|
||||
}
|
||||
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 h3c {
|
||||
log.Println("[INFO] Using HTTP/3 client")
|
||||
client = h3client
|
||||
} else {
|
||||
log.Println("[INFO] Using HTTP/2 client")
|
||||
client = h2client
|
||||
}
|
||||
|
||||
if https {
|
||||
if len(tls_cert) <= 0 {
|
||||
log.Fatal("tls-cert argument is missing, you need a TLS certificate for HTTPS")
|
||||
}
|
||||
|
||||
if len(tls_key) <= 0 {
|
||||
log.Fatal("tls-key argument is missing, you need a TLS key for HTTPS")
|
||||
}
|
||||
}
|
||||
|
||||
ipv6_only = ipv6
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// MISC ROUTES
|
||||
mux.HandleFunc("/", beforeMisc(root))
|
||||
mux.HandleFunc("/health", beforeMisc(health))
|
||||
mux.HandleFunc("/stats", beforeMisc(stats))
|
||||
|
||||
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()
|
||||
if bc {
|
||||
num, err := strconv.Atoi(bc_cooldown)
|
||||
if err != nil {
|
||||
log.Fatalf("[FATAL] Error while setting BLOCK_CHECKER_COOLDOWN: %s", err)
|
||||
}
|
||||
go blockChecker(gh, num)
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", host+":"+port)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen: %v", err)
|
||||
}
|
||||
|
||||
// 1Kbit = 125Bytes
|
||||
var (
|
||||
writeLimit = bwlimit.Byte(*wl) * bwlimit.Byte(125)
|
||||
readLimit = bwlimit.Byte(*rl) * bwlimit.Byte(125)
|
||||
)
|
||||
|
||||
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,
|
||||
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 {
|
||||
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 {
|
||||
log.Println("Failed to set socket permissions to 777:", err.Error())
|
||||
return
|
||||
} else {
|
||||
log.Println("Setting socket permissions to 777")
|
||||
}
|
||||
|
||||
go srv.Serve(socket_listener)
|
||||
log.Println("Unix socket listening at:", string(sock))
|
||||
|
||||
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 {
|
||||
log.Println("Serving HTTP at port", string(port))
|
||||
if err := srv.Serve(ln); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
19
nginx.conf
19
nginx.conf
|
@ -1,19 +0,0 @@
|
|||
user www-data;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
server {
|
||||
listen 3000;
|
||||
listen [::]:3000;
|
||||
access_log off;
|
||||
location / {
|
||||
resolver 127.0.0.11;
|
||||
set $backend "http3-proxy";
|
||||
proxy_pass http://$backend:8080;
|
||||
proxy_http_version 1.1; # to keep alive
|
||||
proxy_set_header Connection ""; # to keep alive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue