diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 8cda90f..c408c44 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -1,40 +1,76 @@ -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 AMD64 + 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 image + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/amd64 + # labels: ${{ steps.meta.outputs.labels }} + push: true + tags: ${{ steps.meta.outputs.tags }} + + - name: Docker meta ARM64 + id: meta-arm64 + uses: docker/metadata-action@v5 + with: + images: git.nadeko.net/fijxu/http3-ytproxy + flavor: | + suffix=-arm64 + tags: | + type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'main') }} + + - name: Build and push Docker ARM64 + uses: docker/build-push-action@v5 + with: + context: . + platforms: linux/arm64/v8 + # labels: ${{ steps.meta-arm64.outputs.labels }} + push: true + tags: ${{ steps.meta-arm64.outputs.tags }} + + # - name: Build images + # uses: https://code.forgejo.org/docker/build-push-action@v5 + # with: + # context: . + # tags: git.nadeko.net/fijxu/http3-ytproxy:latest + # platforms: linux/amd64,linux/arm64/v8 + # push: true diff --git a/Dockerfile b/Dockerfile index 3a8d608..5021b4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md index 830f43c..d5403ee 100644 --- a/README.md +++ b/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 diff --git a/httppaths.go b/httppaths.go index 49728f3..6d6e155 100644 --- a/httppaths.go +++ b/httppaths.go @@ -7,39 +7,30 @@ import ( "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 +39,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 +52,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 +74,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 +119,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 +133,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//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 +153,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) @@ -267,10 +194,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) } diff --git a/main.go b/main.go index b611a23..8d8b5a3 100644 --- a/main.go +++ b/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 {