parent
11094a2b18
commit
3d30033794
5 changed files with 197 additions and 204 deletions
|
@ -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 }}
|
||||
|
|
|
@ -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
|
||||
|
|
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
|
||||
|
|
171
httppaths.go
171
httppaths.go
|
@ -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
142
main.go
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue