Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
dfd65d6046 | |||
a03d265259 | |||
248d076f47 | |||
b9f02ddbea | |||
273067973f | |||
60c481c9a7 | |||
70232f56bf | |||
538aa2e7e8 | |||
2f88b8487b |
21 changed files with 1112 additions and 1024 deletions
|
@ -2,8 +2,9 @@ name: "CI"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs: {}
|
||||||
push:
|
push:
|
||||||
branches: ["main", "master"]
|
branches: ["*"]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -13,6 +14,21 @@ jobs:
|
||||||
- name: Checkout repo
|
- name: Checkout repo
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Setup Docker BuildX system
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
@ -32,12 +48,11 @@ jobs:
|
||||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
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') }}
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||||
|
|
||||||
- name: Build and push Docker
|
- name: Build and push Docker AMD64/ARM64
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
# platforms: linux/amd64,linux/arm64/v8
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
FROM golang:alpine3.21 AS build
|
FROM golang:alpine3.21 AS build
|
||||||
|
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
RUN go env -w GOMODCACHE=/root/.cache/go-build
|
|
||||||
|
|
||||||
RUN apk add --no-cache build-base libwebp-dev git
|
RUN apk add --no-cache build-base libwebp-dev git
|
||||||
|
|
||||||
|
@ -9,7 +8,7 @@ COPY .git .git
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
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)'" ./cmd/http3-ytproxy
|
go build -ldflags "-s -w -X 'main.version=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)'"
|
||||||
|
|
||||||
FROM alpine:3.21
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
|
|
@ -1,226 +0,0 @@
|
||||||
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,23 +1,24 @@
|
||||||
module git.nadeko.net/Fijxu/http3-ytproxy
|
module git.nadeko.net/Fijxu/http3-ytproxy/v3
|
||||||
|
|
||||||
go 1.24
|
go 1.22.0
|
||||||
|
|
||||||
require (
|
toolchain go1.23.0
|
||||||
github.com/prometheus/client_golang v1.20.5
|
|
||||||
github.com/prometheus/procfs v0.15.1
|
require github.com/quic-go/quic-go v0.48.1
|
||||||
github.com/quic-go/quic-go v0.48.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // 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/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e // indirect
|
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e // indirect
|
||||||
github.com/klauspost/compress v1.17.9 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/onsi/ginkgo/v2 v2.20.2 // 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/client_model v0.6.1 // indirect
|
||||||
github.com/prometheus/common v0.55.0 // 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
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
go.uber.org/mock v0.5.0 // indirect
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/crypto v0.28.0 // indirect
|
||||||
|
@ -27,6 +28,7 @@ require (
|
||||||
golang.org/x/sync v0.8.0 // indirect
|
golang.org/x/sync v0.8.0 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.26.0 // indirect
|
||||||
golang.org/x/text v0.19.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
|
golang.org/x/tools v0.26.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
)
|
)
|
||||||
|
|
42
go.sum
42
go.sum
|
@ -2,20 +2,32 @@ 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/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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
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 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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 h1:v7R0PZoC2p1KWQmv1+GqCXQe59Ab1TkDF8Y9Lg2W6m4=
|
||||||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
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 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
github.com/kolesa-team/go-webp v1.0.4 h1:wQvU4PLG/X7RS0vAeyhiivhLRoxfLVRlDq4I3frdxIQ=
|
||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
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 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
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 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||||
|
@ -34,31 +46,57 @@ 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/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 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
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 h1:y/8xmfWI9qmGTc+lBr4jKRUWLGSlSigv847ULJ4hYXA=
|
||||||
github.com/quic-go/quic-go v0.48.1/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
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 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
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 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
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 h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
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 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
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 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
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 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
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 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
||||||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
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 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
369
httppaths.go
Normal file
369
httppaths.go
Normal file
|
@ -0,0 +1,369 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,102 +0,0 @@
|
||||||
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.")
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
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 +0,0 @@
|
||||||
package httpc
|
|
|
@ -1,76 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
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
|
|
|
@ -1,49 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
package paths
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Health(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
io.WriteString(w, "OK")
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,17 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,244 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,28 +0,0 @@
|
||||||
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",
|
|
||||||
}
|
|
650
main.go
Normal file
650
main.go
Normal file
|
@ -0,0 +1,650 @@
|
||||||
|
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
Normal file
19
nginx.conf
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
package utils
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/aes"
|
|
||||||
"encoding/base64"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"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
|
// Loop over header names
|
||||||
outer:
|
outer:
|
||||||
for name, values := range from {
|
for name, values := range from {
|
||||||
|
@ -32,24 +28,14 @@ outer:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyHeadersNew(from http.Header, to http.Header) {
|
func getBestThumbnail(path string) (newpath string) {
|
||||||
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"}
|
formats := [4]string{"maxresdefault.jpg", "sddefault.jpg", "hqdefault.jpg", "mqdefault.jpg"}
|
||||||
|
|
||||||
for _, format := range formats {
|
for _, format := range formats {
|
||||||
newpath = strings.Replace(path, "maxres.jpg", format, 1)
|
newpath = strings.Replace(path, "maxres.jpg", format, 1)
|
||||||
url := "https://i.ytimg.com" + newpath
|
url := "https://i.ytimg.com" + newpath
|
||||||
resp, _ := httpc.Client.Head(url)
|
resp, _ := h2client.Head(url)
|
||||||
if resp.StatusCode == 200 {
|
if resp.StatusCode == 200 {
|
||||||
return newpath
|
return newpath
|
||||||
}
|
}
|
||||||
|
@ -70,33 +56,9 @@ func RelativeUrl(in string) (newurl string) {
|
||||||
return segment_url.RequestURI()
|
return segment_url.RequestURI()
|
||||||
}
|
}
|
||||||
|
|
||||||
func PanicHandler(w http.ResponseWriter) {
|
func panicHandler(w http.ResponseWriter) {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
log.Printf("Panic: %v", r)
|
log.Printf("Panic: %v", r)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
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
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue