v2
All checks were successful
CI / build (push) Successful in 5m7s

This commit is contained in:
Fijxu 2024-10-29 15:01:35 -03:00
parent 11094a2b18
commit 3d30033794
Signed by: Fijxu
GPG key ID: 32C1DDF333EDA6A4
5 changed files with 197 additions and 204 deletions

View file

@ -1,40 +1,47 @@
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: Setup Docker BuildX system
uses: docker/setup-buildx-action@v3
- 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: Login to Docker Container Registry (git.nadeko.net)
uses: docker/login-action@v3
with:
registry: git.nadeko.net
username: ${{ secrets.USERNAME }}
password: ${{ secrets.TOKEN }}
- 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: 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@v5
with:
context: .
platforms: linux/amd64,linux/arm64/v8
push: true
tags: ${{ steps.meta.outputs.tags }}

View file

@ -7,7 +7,7 @@ RUN apk add --no-cache build-base libwebp-dev
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -ldflags "-s -w" main.go
go build -ldflags "-s -w"
FROM alpine:edge
@ -15,6 +15,6 @@ 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

View file

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

View file

@ -1,45 +1,35 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"strings"
"sync/atomic"
)
func videoplayback(w http.ResponseWriter, req *http.Request) {
mu.Lock()
reqs++
mu.Unlock()
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Max-Age", "1728000")
if req.Method == "OPTIONS" {
w.WriteHeader(200)
return
}
q := req.URL.Query()
host := q.Get("host")
mvi := q.Get("mvi")
mn := strings.Split(q.Get("mn"), ",")
if len(host) <= 0 {
mvi := q.Get("mvi")
mn := strings.Split(q.Get("mn"), ",")
if len(mvi) <= 0 {
io.WriteString(w, "No `mvi` in query parameters")
return
if len(mvi) <= 0 {
io.WriteString(w, "No `mvi` in query parameters")
return
}
if len(mn) <= 0 {
io.WriteString(w, "No `mn` in query parameters")
return
}
host = "rr" + mvi + "---" + mn[0] + ".googlevideo.com"
}
if len(mn) <= 0 {
io.WriteString(w, "No `mn` in query parameters")
return
}
host := "rr" + mvi + "---" + mn[0] + ".googlevideo.com"
parts := strings.Split(strings.ToLower(host), ".")
if len(parts) < 2 {
@ -48,9 +38,7 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
}
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
disallowed := true
for _, value := range allowed_hosts {
if domain == value {
disallowed = false
@ -63,11 +51,6 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
return
}
if req.Method != "GET" && req.Method != "HEAD" {
io.WriteString(w, "Only GET and HEAD requests are allowed.")
return
}
path := req.URL.EscapedPath()
proxyURL, err := url.Parse("https://" + host + path)
@ -90,13 +73,19 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
log.Panic(err)
}
w.WriteHeader(resp.StatusCode)
if resp.StatusCode == 403 {
atomic.AddInt64(&stats_.RequestsForbidden.Videoplayback, 1)
io.WriteString(w, "Forbidden 403\n")
io.WriteString(w, "Maybe Youtube blocked the IP of this proxy?\n")
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 {
@ -129,49 +118,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,7 +132,15 @@ 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 {
@ -194,55 +152,23 @@ func vi(w http.ResponseWriter, req *http.Request) {
log.Panic(err)
}
w.WriteHeader(resp.StatusCode)
if resp.StatusCode == 403 {
atomic.AddInt64(&stats_.RequestsForbidden.Vi, 1)
io.WriteString(w, "Forbidden 403")
return
}
defer resp.Body.Close()
copyHeaders(resp.Header, w.Header(), false)
w.WriteHeader(resp.StatusCode)
NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
copyHeaders(resp.Header, w.Header(), NoRewrite)
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,8 +179,6 @@ 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)
@ -267,10 +191,17 @@ func ggpht(w http.ResponseWriter, req *http.Request) {
log.Panic(err)
}
w.WriteHeader(resp.StatusCode)
if resp.StatusCode == 403 {
atomic.AddInt64(&stats_.RequestsForbidden.Ggpht, 1)
io.WriteString(w, "Forbidden 403")
return
}
defer resp.Body.Close()
copyHeaders(resp.Header, w.Header(), false)
w.WriteHeader(resp.StatusCode)
NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
copyHeaders(resp.Header, w.Header(), NoRewrite)
io.Copy(w, resp.Body)
}

142
main.go
View file

@ -10,16 +10,15 @@ import (
"net/http"
"os"
"regexp"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/quic-go/quic-go/http3"
)
// http/3 client
var h3client = &http.Client{
Transport: &http3.RoundTripper{},
Transport: &http3.Transport{},
Timeout: 10 * time.Second,
}
@ -85,66 +84,147 @@ var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`)
var ipv6_only = false
var reqs int64
var reqs_Forbidden int64
var mu sync.Mutex
type statusJson struct {
RequestCount int64 `json:"requestCount"`
RequestsForbidden int64 `json:"requestsForbidden"`
RequestPerSecond int64 `json:"requestPerSecond"`
RequestPerMinute int64 `json:"requestPerMinute"`
RequestsForbidden struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
} `json:"requestsForbidden"`
}
var stats_ = statusJson{
RequestCount: 0,
RequestPerSecond: 0,
RequestPerMinute: 0,
RequestsForbidden: struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
}{
Videoplayback: 0,
Vi: 0,
Ggpht: 0,
},
}
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,
}
func stats(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
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
last = current
}
}
func requestPerMinute() {
var last int64
for {
time.Sleep(60 * time.Second)
current := stats_.RequestCount
stats_.RequestPerSecond = current - last
last = current
}
}
func beforeAll(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
if req.Method != "GET" && req.Method != "HEAD" {
io.WriteString(w, "Only GET and HEAD requests are allowed.")
return
}
atomic.AddInt64(&stats_.RequestCount, 1)
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Max-Age", "1728000")
if req.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next(w, req)
}
}
func main() {
var sock string
var host string
var port string
var cert string
var key string
var tls_cert string
var tls_key string
path_prefix = os.Getenv("PREFIX_PATH")
ipv6_only = os.Getenv("IPV6_ONLY") == "1"
// disable_webp = os.Getenv("DISABLE_WEBP") == "1"
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(&tls_cert, "tls-cert", "", "TLS Certificate path")
flag.StringVar(&tls_key, "tls-key", "", "TLS Certificate Key path")
flag.StringVar(&sock, "s", "/tmp/http-ytproxy.sock", "Specify a socket name")
flag.StringVar(&port, "p", "8080", "Specify a port number")
flag.StringVar(&host, "l", "0.0.0.0", "Specify a listen address")
flag.Parse()
if *https {
if len(tls_cert) <= 0 {
fmt.Println("tls-cert argument is missing")
fmt.Println("You need a TLS certificate for HTTPS")
}
if len(tls_key) <= 0 {
fmt.Println("tls-key argument is missing")
fmt.Println("You need a TLS key for HTTPS")
}
}
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)
mux.HandleFunc("/health", health)
mux.HandleFunc("/stats", stats)
mux.HandleFunc("/videoplayback", beforeAll(videoplayback))
mux.HandleFunc("/vi/", beforeAll(vi))
mux.HandleFunc("/vi_webp/", beforeAll(vi))
mux.HandleFunc("/sb/", beforeAll(vi))
mux.HandleFunc("/ggpht/", beforeAll(ggpht))
mux.HandleFunc("/a/", beforeAll(ggpht))
mux.HandleFunc("/ytc/", beforeAll(ggpht))
go requestPerSecond()
go requestPerMinute()
srv := &http.Server{
ReadTimeout: 5 * time.Second,
@ -179,7 +259,7 @@ func main() {
go srv.Serve(listener)
if *https {
fmt.Println("Serving HTTPS at port", string(port))
if err := srv.ListenAndServeTLS(cert, key); err != nil {
if err := srv.ListenAndServeTLS(tls_cert, tls_key); err != nil {
log.Fatal(err)
}
} else {