Compare commits
41 commits
Author | SHA1 | Date | |
---|---|---|---|
f66ae3187f | |||
3fc14dd18b | |||
a98a4ba1bf | |||
71abe2ae58 | |||
b156a52420 | |||
e28d86018e | |||
86170fd39e | |||
2a6e80380d | |||
985ef449c5 | |||
279570d47b | |||
571a2351e9 | |||
161c61bcce | |||
200a536207 | |||
0a4dd54393 | |||
03a37009c4 | |||
46d11bfa53 | |||
e698c1df4d | |||
ff9f99c1b6 | |||
802dd65edf | |||
d225323628 | |||
848ad555f7 | |||
939f4da3f7 | |||
89c880bb27 | |||
40436dcf92 | |||
7d40f898a6 | |||
21036c3e30 | |||
0d4bd67afb | |||
b150f128b1 | |||
56345e5bae | |||
3b89ea41e7 | |||
654610ecd3 | |||
bdb1afbf61 | |||
78ae56be37 | |||
fa0c7e9373 | |||
900b6bd3e7 | |||
a37a1a5ff1 | |||
cc63b84a55 | |||
5101648c94 | |||
1549833bfb | |||
6885fcfc28 | |||
3d30033794 |
12 changed files with 736 additions and 264 deletions
|
@ -3,7 +3,6 @@
|
|||
.dockerignore
|
||||
Dockerfile
|
||||
LICENSE
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
cache
|
||||
|
|
4
.env
Normal file
4
.env
Normal file
|
@ -0,0 +1,4 @@
|
|||
VPN_PROVIDER=""
|
||||
WIREGUARD_KEY=""
|
||||
WIREGUARD_ADDRESSES=""
|
||||
SERVER_HOSTNAMES=""
|
|
@ -1,40 +1,58 @@
|
|||
name: 'CI'
|
||||
name: "CI"
|
||||
|
||||
on:
|
||||
# workflow_dispatch:
|
||||
# inputs: {}
|
||||
# schedule:
|
||||
# - cron: '0 7 * * 0'
|
||||
workflow_dispatch:
|
||||
inputs: {}
|
||||
push:
|
||||
branches: ["*"]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: runner
|
||||
|
||||
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v2
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: all
|
||||
# - name: Set up QEMU for ARM64 builds
|
||||
# uses: docker/setup-qemu-action@v3
|
||||
# with:
|
||||
# platforms: arm64
|
||||
|
||||
- uses: https://code.forgejo.org/docker/setup-buildx-action@v3
|
||||
name: Setup Docker BuildX system
|
||||
- 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: Login to Docker Container Registry
|
||||
uses: https://code.forgejo.org/docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: git.nadeko.net
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
- name: Setup Docker BuildX system
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- uses: https://code.forgejo.org/docker/build-push-action@v5
|
||||
name: Build images
|
||||
with:
|
||||
context: .
|
||||
tags: git.nadeko.net/fijxu/http3-ytproxy:latest
|
||||
platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
- name: Login to Docker Container Registry (git.nadeko.net)
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.nadeko.net
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: git.nadeko.net/fijxu/http3-ytproxy
|
||||
tags: |
|
||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
||||
|
||||
- name: Build and push Docker AMD64/ARM64
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64
|
||||
# platforms: linux/amd64,linux/arm64/v8
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -16,3 +16,8 @@
|
|||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Certificates!
|
||||
*.pem
|
||||
*.cer
|
||||
*.key
|
18
Dockerfile
18
Dockerfile
|
@ -1,20 +1,26 @@
|
|||
FROM golang:alpine AS build
|
||||
FROM golang:alpine3.21 AS build
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
RUN apk add --no-cache build-base libwebp-dev
|
||||
RUN apk add --no-cache build-base libwebp-dev git
|
||||
|
||||
COPY .git .git
|
||||
COPY . .
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||
go build -ldflags "-s -w" main.go
|
||||
go build -ldflags "-s -w -X 'main.version=$(date '+%Y-%m-%d')-$(git rev-list --abbrev-commit -1 HEAD)'"
|
||||
|
||||
FROM alpine:edge
|
||||
FROM alpine:3.21
|
||||
|
||||
RUN adduser -u 10001 -S appuser
|
||||
|
||||
RUN apk add --no-cache libwebp
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
COPY --from=build /app/main /app/http3-ytproxy
|
||||
COPY --from=build /app/http3-ytproxy /app/http3-ytproxy
|
||||
|
||||
CMD ./http3-ytproxy -l 0.0.0.0
|
||||
# Switch to non-privileged user
|
||||
USER appuser
|
||||
|
||||
ENTRYPOINT ["/app/http3-ytproxy"]
|
25
README.md
25
README.md
|
@ -1,27 +1,2 @@
|
|||
# http3-ytproxy
|
||||
|
||||
A fork of http3-ytproxy adding support for dynamic socket names and port numbers, just because I'am too lazy to change the code enough to use Go routines. So I prefer to run different threads instead.
|
||||
|
||||
The socket folder will be created automatically.
|
||||
|
||||
## Arguments:
|
||||
|
||||
```
|
||||
-p string
|
||||
Specify a port number (default "8080")
|
||||
-s string
|
||||
Specify a socket name (default "http-proxy.sock")
|
||||
```
|
||||
|
||||
## SystemD service
|
||||
|
||||
Copy the `http3-ytproxy@.service` to `/etc/systemd/system/` and use it like this:
|
||||
|
||||
```
|
||||
# This will create the http-proxy-1.sock file
|
||||
$ sudo systemctl enable --now http3-ytproxy@1.service
|
||||
# And this one will be http-proxy-2.sock
|
||||
$ sudo systemctl enable --now http3-ytproxy@2.service
|
||||
```
|
||||
|
||||
lolxdxdxd fastest invidious instance in the fucking world wtfffffffffffffffffffffff
|
||||
|
|
|
@ -1,22 +1,44 @@
|
|||
# Docker compose file for http3-proxy used in Invidious
|
||||
# Docker compose file for http3-ytproxy used in inv.nadeko.net
|
||||
|
||||
services:
|
||||
|
||||
http3-proxy:
|
||||
image: git.nadeko.net/fijxu/http3-proxy:latest
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: 6
|
||||
environment:
|
||||
DISABLE_WEBP: 1
|
||||
|
||||
http3-proxy-nginx:
|
||||
image: nginx:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- http3-proxy
|
||||
ports:
|
||||
- "127.0.0.1:10012:3000"
|
||||
build: .
|
||||
image: git.nadeko.net/fijxu/http3-ytproxy:latest
|
||||
restart: always
|
||||
# Uncomment this IF YOU ARE using gluetun!
|
||||
# network_mode: "service:gluetun"
|
||||
# Uncomment this IF YOU ARE NOT using gluetun!
|
||||
# ports:
|
||||
# - "0.0.0.0:8443:8443/tcp" # HTTP/2
|
||||
# - "0.0.0.0:8443:8443/udp" # HTTP/3 (QUIC)
|
||||
# Make sure that the key and the certificate files exist!
|
||||
volumes:
|
||||
- ./key.key:/data/key.key:ro
|
||||
- ./fullchain.pem:/data/cert.pem:ro
|
||||
depends_on:
|
||||
gluetun:
|
||||
condition: service_healthy
|
||||
# Needed for HTTP/3, otherwise, quic-go will output this depending of the machine:
|
||||
# "failed to sufficiently increase receive buffer size (was: 208 kiB, wanted: 7168 kiB, got: 416 kiB).
|
||||
# See https://github.com/quic-go/quic-go/wiki/UDP-Buffer-Sizes for details."
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
|
||||
# You can comment this whole service if you are not going to use Gluetun at all
|
||||
gluetun:
|
||||
image: qmcgaw/gluetun:latest
|
||||
restart: always
|
||||
ports:
|
||||
# THIS IS ACTUALLY THE PORT OF HTTP3-PROXY
|
||||
# SINCE THE HTTP3-PTOXY SERVICE IS RUNNING
|
||||
# UNDER GLUETUN NETWORK.
|
||||
- "0.0.0.0:8443:8443/tcp" # HTTP/2
|
||||
- "0.0.0.0:8443:8443/udp" # HTTP/3 (QUIC)
|
||||
env_file:
|
||||
- .env
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun:/dev/net/tun
|
||||
volumes:
|
||||
- ./gluetun:/gluetun
|
||||
|
|
11
go.mod
11
go.mod
|
@ -7,9 +7,18 @@ toolchain go1.23.0
|
|||
require github.com/quic-go/quic-go v0.48.1
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/conduitio/bwlimit v0.1.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 // indirect
|
||||
github.com/prometheus/client_golang v1.20.5 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
golang.org/x/crypto v0.28.0 // indirect
|
||||
|
@ -19,5 +28,7 @@ require (
|
|||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.26.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
)
|
||||
|
|
18
go.sum
18
go.sum
|
@ -1,3 +1,7 @@
|
|||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
|
@ -20,14 +24,26 @@ github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e h1:v7R0PZoC2p1KWQmv1+
|
|||
github.com/google/pprof v0.0.0-20241029010322-833c56d90c8e/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465 h1:KwWnWVWCNtNq/ewIX7HIKnELmEx2nDP42yskD/pi7QE=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20240312041847-bd984b5ce465/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kolesa-team/go-webp v1.0.4 h1:wQvU4PLG/X7RS0vAeyhiivhLRoxfLVRlDq4I3frdxIQ=
|
||||
github.com/kolesa-team/go-webp v1.0.4/go.mod h1:oMvdivD6K+Q5qIIkVC2w4k2ZUnI1H+MyP7inwgWq9aA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4=
|
||||
github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y=
|
||||
|
@ -78,6 +94,8 @@ golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
|
|||
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
|
262
httppaths.go
262
httppaths.go
|
@ -1,56 +1,97 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
func videoplayback(w http.ResponseWriter, req *http.Request) {
|
||||
mu.Lock()
|
||||
reqs++
|
||||
mu.Unlock()
|
||||
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Access-Control-Max-Age", "1728000")
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
w.WriteHeader(200)
|
||||
return
|
||||
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()
|
||||
|
||||
mvi := q.Get("mvi")
|
||||
mn := strings.Split(q.Get("mn"), ",")
|
||||
|
||||
if len(mvi) <= 0 {
|
||||
io.WriteString(w, "No `mvi` in query parameters")
|
||||
expire, err := strconv.ParseInt(q.Get("expire"), 10, 64)
|
||||
if err != nil {
|
||||
w.WriteHeader(500)
|
||||
io.WriteString(w, "Expire query string undefined")
|
||||
return
|
||||
}
|
||||
|
||||
if len(mn) <= 0 {
|
||||
io.WriteString(w, "No `mn` in query parameters")
|
||||
// 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
|
||||
}
|
||||
|
||||
host := "rr" + mvi + "---" + mn[0] + ".googlevideo.com"
|
||||
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
|
||||
|
@ -59,14 +100,17 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
if disallowed {
|
||||
w.WriteHeader(401)
|
||||
io.WriteString(w, "Non YouTube domains are not supported.")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
io.WriteString(w, "Only GET and HEAD requests are allowed.")
|
||||
return
|
||||
}
|
||||
// if c == "WEB" {
|
||||
// q.Set("alr", "yes")
|
||||
// }
|
||||
// if req.Header.Get("Range") != "" {
|
||||
// q.Set("range", req.Header.Get("Range"))
|
||||
// }
|
||||
|
||||
path := req.URL.EscapedPath()
|
||||
|
||||
|
@ -77,19 +121,56 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
|
|||
|
||||
proxyURL.RawQuery = q.Encode()
|
||||
|
||||
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
|
||||
// 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
|
||||
|
||||
copyHeaders(req.Header, request.Header, false)
|
||||
request.Header.Set("User-Agent", ua)
|
||||
request, err := http.NewRequest("POST", proxyURL.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
copyHeaders(req.Header, request.Header, false)
|
||||
|
||||
switch c {
|
||||
case "ANDROID":
|
||||
request.Header.Set("User-Agent", "com.google.android.youtube/1537338816 (Linux; U; Android 13; en_US; ; Build/TQ2A.230505.002; Cronet/113.0.5672.24)")
|
||||
case "IOS":
|
||||
request.Header.Set("User-Agent", "com.google.ios.youtube/19.32.8 (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)")
|
||||
case "WEB":
|
||||
request.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36")
|
||||
default:
|
||||
request.Header.Set("User-Agent", default_ua)
|
||||
}
|
||||
|
||||
request.Header.Add("Origin", "https://www.youtube.com")
|
||||
request.Header.Add("Referer", "https://www.youtube.com/")
|
||||
|
||||
if connectionChecker(req.Context()) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if resp.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")
|
||||
|
@ -129,49 +210,10 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
func vi(w http.ResponseWriter, req *http.Request) {
|
||||
mu.Lock()
|
||||
reqs++
|
||||
mu.Unlock()
|
||||
|
||||
const host string = "i.ytimg.com"
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Access-Control-Max-Age", "1728000")
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.ToLower(host), ".")
|
||||
if len(parts) < 2 {
|
||||
io.WriteString(w, "Invalid hostname.")
|
||||
return
|
||||
}
|
||||
|
||||
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
||||
|
||||
disallowed := true
|
||||
|
||||
for _, value := range allowed_hosts {
|
||||
if domain == value {
|
||||
disallowed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if disallowed {
|
||||
io.WriteString(w, "Non YouTube domains are not supported.")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
io.WriteString(w, "Only GET and HEAD requests are allowed.")
|
||||
return
|
||||
}
|
||||
q := req.URL.Query()
|
||||
|
||||
path := req.URL.EscapedPath()
|
||||
fmt.Println(path)
|
||||
|
||||
proxyURL, err := url.Parse("https://" + host + path)
|
||||
if err != nil {
|
||||
|
@ -182,67 +224,46 @@ func vi(w http.ResponseWriter, req *http.Request) {
|
|||
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)
|
||||
copyHeaders(req.Header, request.Header, false)
|
||||
request.Header.Set("User-Agent", ua)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
request.Header.Set("User-Agent", default_ua)
|
||||
|
||||
if connectionChecker(req.Context()) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if err := forbiddenChecker(resp, w); err != nil {
|
||||
atomic.AddInt64(&stats_.RequestsForbidden.Vi, 1)
|
||||
metrics.RequestForbidden.Vi.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
copyHeaders(resp.Header, w.Header(), false)
|
||||
// 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) {
|
||||
mu.Lock()
|
||||
reqs++
|
||||
mu.Unlock()
|
||||
|
||||
const host string = "yt3.ggpht.com"
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Access-Control-Max-Age", "1728000")
|
||||
|
||||
if req.Method == "OPTIONS" {
|
||||
w.WriteHeader(204)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.ToLower(host), ".")
|
||||
if len(parts) < 2 {
|
||||
io.WriteString(w, "Invalid hostname.")
|
||||
return
|
||||
}
|
||||
|
||||
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
|
||||
|
||||
disallowed := true
|
||||
|
||||
for _, value := range allowed_hosts {
|
||||
if domain == value {
|
||||
disallowed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if disallowed {
|
||||
io.WriteString(w, "Non YouTube domains are not supported.")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Method != "GET" && req.Method != "HEAD" {
|
||||
io.WriteString(w, "Only GET and HEAD requests are allowed.")
|
||||
return
|
||||
}
|
||||
|
||||
path := req.URL.EscapedPath()
|
||||
path = strings.Replace(path, "/ggpht", "", 1)
|
||||
|
@ -253,23 +274,32 @@ func ggpht(w http.ResponseWriter, req *http.Request) {
|
|||
log.Panic(err)
|
||||
}
|
||||
|
||||
fmt.Println(proxyURL)
|
||||
|
||||
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
|
||||
copyHeaders(req.Header, request.Header, false)
|
||||
request.Header.Set("User-Agent", ua)
|
||||
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()
|
||||
|
||||
copyHeaders(resp.Header, w.Header(), false)
|
||||
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)
|
||||
|
|
521
main.go
521
main.go
|
@ -1,25 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"sync"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/conduitio/bwlimit"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/quic-go/quic-go"
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
)
|
||||
|
||||
// http/3 client
|
||||
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.RoundTripper{},
|
||||
Transport: &http3.Transport{},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
|
@ -28,6 +42,8 @@ var dialer = &net.Dialer{
|
|||
KeepAlive: 30 * time.Second,
|
||||
}
|
||||
|
||||
var proxy string
|
||||
|
||||
// http/2 client
|
||||
var h2client = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
|
@ -49,14 +65,18 @@ var h2client = &http.Client{
|
|||
MaxConnsPerHost: 0,
|
||||
MaxIdleConnsPerHost: 10,
|
||||
MaxIdleConns: 0,
|
||||
Proxy: func(r *http.Request) (*url.URL, error) {
|
||||
if proxy != "" {
|
||||
return url.Parse(proxy)
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// https://github.com/lucas-clemente/quic-go/issues/2836
|
||||
var client = h2client
|
||||
var client *http.Client
|
||||
|
||||
// Same user agent as Invidious
|
||||
var ua = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
|
||||
var default_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
||||
|
||||
var allowed_hosts = []string{
|
||||
"youtube.com",
|
||||
|
@ -77,6 +97,8 @@ var strip_headers = []string{
|
|||
"Alt-Svc",
|
||||
"Server",
|
||||
"Cache-Control",
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
|
||||
"report-to",
|
||||
}
|
||||
|
||||
var path_prefix = ""
|
||||
|
@ -85,106 +107,461 @@ var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`)
|
|||
|
||||
var ipv6_only = false
|
||||
|
||||
var reqs int64
|
||||
var reqs_Forbidden int64
|
||||
var mu sync.Mutex
|
||||
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 {
|
||||
RequestCount int64 `json:"requestCount"`
|
||||
RequestsForbidden int64 `json:"requestsForbidden"`
|
||||
Version string `json:"version"`
|
||||
Uptime time.Duration `json:"uptime"`
|
||||
RequestCount int64 `json:"requestCount"`
|
||||
RequestPerSecond int64 `json:"requestPerSecond"`
|
||||
RequestPerMinute int64 `json:"requestPerMinute"`
|
||||
TotalConnEstablished int64 `json:"totalEstablished"`
|
||||
EstablishedConnections int64 `json:"establishedConnections"`
|
||||
ActiveConnections int64 `json:"activeConnections"`
|
||||
IdleConnections int64 `json:"idleConnections"`
|
||||
RequestsForbidden struct {
|
||||
Videoplayback int64 `json:"videoplayback"`
|
||||
Vi int64 `json:"vi"`
|
||||
Ggpht int64 `json:"ggpht"`
|
||||
} `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,
|
||||
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) {
|
||||
io.WriteString(w, "HTTP youtube proxy for https://inv.nadeko.net\n")
|
||||
const msg = `
|
||||
HTTP youtube proxy for https://inv.nadeko.net
|
||||
https://git.nadeko.net/Fijxu/http3-ytproxy
|
||||
|
||||
Routes:
|
||||
/stats
|
||||
/health`
|
||||
io.WriteString(w, msg)
|
||||
}
|
||||
|
||||
func status(w http.ResponseWriter, req *http.Request) {
|
||||
response := statusJson{
|
||||
RequestCount: reqs,
|
||||
RequestsForbidden: reqs_Forbidden,
|
||||
}
|
||||
// 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(response); err != nil {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
var sock string
|
||||
var host string
|
||||
var port string
|
||||
var cert string
|
||||
var key string
|
||||
defaultHost := "0.0.0.0"
|
||||
defaultPort := "8080"
|
||||
defaultSock := "/tmp/http-ytproxy.sock"
|
||||
defaultTLSCert := "/data/cert.pem"
|
||||
defaultTLSKey := "/data/key.key"
|
||||
|
||||
path_prefix = os.Getenv("PREFIX_PATH")
|
||||
var https bool = false
|
||||
var h3c bool = false
|
||||
var ipv6 bool = false
|
||||
|
||||
ipv6_only = os.Getenv("IPV6_ONLY") == "1"
|
||||
// disable_webp = os.Getenv("DISABLE_WEBP") == "1"
|
||||
if strings.ToLower(os.Getenv("HTTPS")) == "true" {
|
||||
https = true
|
||||
}
|
||||
if strings.ToLower(os.Getenv("H3C")) == "true" {
|
||||
h3c = true
|
||||
}
|
||||
if strings.ToLower(os.Getenv("H3S")) == "true" {
|
||||
h3s = true
|
||||
}
|
||||
if strings.ToLower(os.Getenv("IPV6_ONLY")) == "true" {
|
||||
ipv6 = true
|
||||
}
|
||||
if strings.ToLower(os.Getenv("DOMAIN_ONLY_ACCESS")) == "true" {
|
||||
domain_only_access = true
|
||||
}
|
||||
|
||||
flag.StringVar(&cert, "tls-cert", "", "TLS Certificate path")
|
||||
flag.StringVar(&key, "tls-key", "", "TLS Certificate Key path")
|
||||
var https = flag.Bool("https", false, "Use built-in https server")
|
||||
var ipv6 = flag.Bool("ipv6_only", false, "Only use ipv6 for requests")
|
||||
flag.StringVar(&sock, "s", "/tmp/http-ytproxy.sock", "Specify a socket name")
|
||||
flag.StringVar(&port, "p", "8080", "Specify a port number")
|
||||
flag.StringVar(&host, "l", "0.0.0.0", "Specify a listen address")
|
||||
tls_cert := os.Getenv("TLS_CERT")
|
||||
if tls_cert == "" {
|
||||
tls_cert = defaultTLSCert
|
||||
}
|
||||
tls_key := os.Getenv("TLS_KEY")
|
||||
if tls_key == "" {
|
||||
tls_key = defaultTLSKey
|
||||
}
|
||||
sock := os.Getenv("SOCK_PATH")
|
||||
if sock == "" {
|
||||
sock = defaultSock
|
||||
}
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = defaultPort
|
||||
}
|
||||
host := os.Getenv("HOST")
|
||||
if host == "" {
|
||||
host = defaultHost
|
||||
}
|
||||
proxy = os.Getenv("PROXY")
|
||||
|
||||
flag.BoolVar(&https, "https", https, "Use built-in https server (recommended)")
|
||||
flag.BoolVar(&h3c, "h3c", h3c, "Use HTTP/3 for client requests (high CPU usage)")
|
||||
flag.BoolVar(&h3s, "h3s", h3s, "Use HTTP/3 for server requests, (requires HTTPS)")
|
||||
flag.BoolVar(&ipv6_only, "ipv6_only", ipv6_only, "Only use ipv6 for requests")
|
||||
flag.StringVar(&tls_cert, "tls-cert", tls_cert, "TLS Certificate path")
|
||||
flag.StringVar(&tls_key, "tls-key", tls_key, "TLS Certificate Key path")
|
||||
flag.StringVar(&sock, "s", sock, "Specify a socket name")
|
||||
flag.StringVar(&port, "p", port, "Specify a port number")
|
||||
flag.StringVar(&host, "l", host, "Specify a listen address")
|
||||
flag.Parse()
|
||||
|
||||
ipv6_only = *ipv6
|
||||
if h3c {
|
||||
client = h3client
|
||||
} else {
|
||||
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()
|
||||
|
||||
mux.HandleFunc("/", root)
|
||||
mux.HandleFunc("/status", status)
|
||||
mux.HandleFunc("/videoplayback", videoplayback)
|
||||
mux.HandleFunc("/vi/", vi)
|
||||
mux.HandleFunc("/vi_webp/", vi)
|
||||
mux.HandleFunc("/sb/", vi)
|
||||
mux.HandleFunc("/ggpht/", ggpht)
|
||||
mux.HandleFunc("/a/", ggpht)
|
||||
mux.HandleFunc("/ytc/", ggpht)
|
||||
// MISC ROUTES
|
||||
mux.HandleFunc("/", beforeMisc(root))
|
||||
mux.HandleFunc("/health", beforeMisc(health))
|
||||
mux.HandleFunc("/stats", beforeMisc(stats))
|
||||
|
||||
srv := &http.Server{
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 1 * time.Hour,
|
||||
Addr: string(host) + ":" + string(port),
|
||||
Handler: mux,
|
||||
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()
|
||||
|
||||
ln, err := net.Listen("tcp", host+":"+port)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen: %v", err)
|
||||
}
|
||||
|
||||
socket := string(sock)
|
||||
syscall.Unlink(socket)
|
||||
listener, err := net.Listen("unix", socket)
|
||||
fmt.Println("Unix socket listening at:", string(sock))
|
||||
// 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 {
|
||||
fmt.Println("Failed to bind to UDS, please check the socket name, falling back to TCP/IP")
|
||||
fmt.Println(err.Error())
|
||||
err := srv.ListenAndServe()
|
||||
if err != nil {
|
||||
fmt.Println("Cannot bind to port", string(port), "Error:", err)
|
||||
fmt.Println("Please try changing the port number")
|
||||
}
|
||||
log.Println("Failed to bind to UDS, please check the socket name", err.Error())
|
||||
} else {
|
||||
defer listener.Close()
|
||||
defer socket_listener.Close()
|
||||
// To allow everyone to access the socket
|
||||
err = os.Chmod(socket, 0777)
|
||||
err = os.Chmod(sock, 0777)
|
||||
if err != nil {
|
||||
fmt.Println("Error setting permissions:", err)
|
||||
log.Println("Failed to set socket permissions to 777:", err.Error())
|
||||
return
|
||||
} else {
|
||||
fmt.Println("Setting socket permissions to 777")
|
||||
log.Println("Setting socket permissions to 777")
|
||||
}
|
||||
go srv.Serve(listener)
|
||||
if *https {
|
||||
fmt.Println("Serving HTTPS at port", string(port))
|
||||
if err := srv.ListenAndServeTLS(cert, key); err != nil {
|
||||
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
fmt.Println("Serving HTTP at port", string(port))
|
||||
srv.ListenAndServe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
7
utils.go
7
utils.go
|
@ -55,3 +55,10 @@ func RelativeUrl(in string) (newurl string) {
|
|||
segment_url.Path = path_prefix + segment_url.Path
|
||||
return segment_url.RequestURI()
|
||||
}
|
||||
|
||||
func panicHandler(w http.ResponseWriter) {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("Panic: %v", r)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue