style: refactor project to use the standard go project layout

This commit is contained in:
Fijxu 2025-02-20 01:13:40 -03:00
parent cc4671c677
commit 8821540bd9
Signed by: Fijxu
GPG key ID: 32C1DDF333EDA6A4
16 changed files with 506 additions and 416 deletions

View file

@ -2,16 +2,13 @@ package main
import ( import (
"crypto/tls" "crypto/tls"
"encoding/json"
"errors" "errors"
"flag" "flag"
"io" "io"
"log" "log"
"net" "net"
"net/http" "net/http"
"net/url"
"os" "os"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -19,9 +16,11 @@ import (
"syscall" "syscall"
"time" "time"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/paths"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
"github.com/conduitio/bwlimit" "github.com/conduitio/bwlimit"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/prometheus/procfs" "github.com/prometheus/procfs"
"github.com/quic-go/quic-go" "github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3" "github.com/quic-go/quic-go/http3"
@ -32,94 +31,12 @@ var (
rl = flag.Int("r", 8000, "Read limit in Kbps") rl = flag.Int("r", 8000, "Read limit in Kbps")
) )
// QUIC doesn't seem to support HTTP nor SOCKS5 proxies due to how it's made.
// (Since it's UDP)
var h3client = &http.Client{
Transport: &http3.Transport{},
Timeout: 10 * time.Second,
}
var dialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
var proxy string
// http/2 client
var h2client = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
var net string
if ipv6_only {
net = "tcp6"
} else {
net = "tcp4"
}
return dialer.Dial(net, addr)
},
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 20 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 30 * time.Second,
ReadBufferSize: 16 * 1024,
ForceAttemptHTTP2: true,
MaxConnsPerHost: 0,
MaxIdleConnsPerHost: 10,
MaxIdleConns: 0,
Proxy: func(r *http.Request) (*url.URL, error) {
if proxy != "" {
return url.Parse(proxy)
}
return nil, nil
},
},
}
var client *http.Client
var default_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
var allowed_hosts = []string{
"youtube.com",
"googlevideo.com",
"ytimg.com",
"ggpht.com",
"googleusercontent.com",
}
var strip_headers = []string{
"Accept-Encoding",
"Authorization",
"Origin",
"Referer",
"Cookie",
"Set-Cookie",
"Etag",
"Alt-Svc",
"Server",
"Cache-Control",
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
"report-to",
}
var path_prefix = ""
var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`)
var ipv6_only = false
var version string var version string
var h3s bool var h3s bool
var domain_only_access bool = false var domain_only_access bool = false
var programInit = time.Now()
type ConnectionWatcher struct { type ConnectionWatcher struct {
totalEstablished int64 totalEstablished int64
established int64 established int64
@ -136,14 +53,14 @@ func (cw *ConnectionWatcher) OnStateChange(conn net.Conn, state http.ConnState)
switch state { switch state {
case http.StateNew: case http.StateNew:
atomic.AddInt64(&stats_.EstablishedConnections, 1) atomic.AddInt64(&stats_.EstablishedConnections, 1)
metrics.EstablishedConnections.Inc() metrics.Metrics.EstablishedConnections.Inc()
atomic.AddInt64(&stats_.TotalConnEstablished, 1) atomic.AddInt64(&stats_.TotalConnEstablished, 1)
metrics.TotalConnEstablished.Inc() metrics.Metrics.TotalConnEstablished.Inc()
// case http.StateActive: // case http.StateActive:
// atomic.AddInt64(&cw.active, 1) // atomic.AddInt64(&cw.active, 1)
case http.StateClosed, http.StateHijacked: case http.StateClosed, http.StateHijacked:
atomic.AddInt64(&stats_.EstablishedConnections, -1) atomic.AddInt64(&stats_.EstablishedConnections, -1)
metrics.EstablishedConnections.Dec() metrics.Metrics.EstablishedConnections.Dec()
} }
} }
@ -160,161 +77,13 @@ func (cw *ConnectionWatcher) OnStateChange(conn net.Conn, state http.ConnState)
var cw ConnectionWatcher var cw ConnectionWatcher
type statusJson struct {
Version string `json:"version"`
Uptime time.Duration `json:"uptime"`
RequestCount int64 `json:"requestCount"`
RequestPerSecond int64 `json:"requestPerSecond"`
RequestPerMinute int64 `json:"requestPerMinute"`
TotalConnEstablished int64 `json:"totalEstablished"`
EstablishedConnections int64 `json:"establishedConnections"`
ActiveConnections int64 `json:"activeConnections"`
IdleConnections int64 `json:"idleConnections"`
RequestsForbiddenPerSec struct {
Videoplayback int64 `json:"videoplayback"`
}
RequestsForbidden struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
} `json:"requestsForbidden"`
}
var stats_ = statusJson{
Version: version + "-" + runtime.GOARCH,
Uptime: 0,
RequestCount: 0,
RequestPerSecond: 0,
RequestPerMinute: 0,
TotalConnEstablished: 0,
EstablishedConnections: 0,
ActiveConnections: 0,
IdleConnections: 0,
RequestsForbiddenPerSec: struct {
Videoplayback int64 `json:"videoplayback"`
}{
Videoplayback: 0,
},
RequestsForbidden: struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
}{
Videoplayback: 0,
Vi: 0,
Ggpht: 0,
},
}
type Metrics struct {
Uptime prometheus.Gauge
RequestCount prometheus.Counter
RequestPerSecond prometheus.Gauge
RequestPerMinute prometheus.Gauge
TotalConnEstablished prometheus.Counter
EstablishedConnections prometheus.Gauge
ActiveConnections prometheus.Gauge
IdleConnections prometheus.Gauge
RequestForbidden struct {
Videoplayback prometheus.Counter
Vi prometheus.Counter
Ggpht prometheus.Counter
}
}
var metrics = Metrics{
Uptime: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_uptime",
}),
RequestCount: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_count",
}),
RequestPerSecond: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_per_second",
}),
RequestPerMinute: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_per_minute",
}),
TotalConnEstablished: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_total_conn_established",
}),
EstablishedConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_established_conns",
}),
ActiveConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_active_conns",
}),
IdleConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_idle_conns",
}),
RequestForbidden: struct {
Videoplayback prometheus.Counter
Vi prometheus.Counter
Ggpht prometheus.Counter
}{
Videoplayback: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_videoplayback",
}),
Vi: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_vi",
}),
Ggpht: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_ggpht",
}),
},
}
func root(w http.ResponseWriter, req *http.Request) {
const msg = `
HTTP youtube proxy for https://inv.nadeko.net
https://git.nadeko.net/Fijxu/http3-ytproxy
Routes:
/stats
/health`
io.WriteString(w, msg)
}
// CustomHandler wraps the default promhttp.Handler with custom logic
func metricsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// To prevent accessing from the bare IP address
if req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil {
w.WriteHeader(444)
return
}
metrics.Uptime.Set(float64(time.Duration(time.Since(programInit).Seconds())))
promhttp.Handler().ServeHTTP(w, req)
})
}
func stats(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
stats_.Uptime = time.Duration(time.Since(programInit).Seconds())
// stats_.TotalEstablished = int64(cw.totalEstablished)
// stats_.EstablishedConnections = int64(cw.established)
// stats_.ActiveConnections = int64(cw.active)
// stats_.IdleConnections = int64(cw.idle)
if err := json.NewEncoder(w).Encode(stats_); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func health(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
io.WriteString(w, "OK")
}
func requestPerSecond() { func requestPerSecond() {
var last int64 var last int64
for { for {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
current := stats_.RequestCount current := stats_.RequestCount
stats_.RequestPerSecond = current - last stats_.RequestPerSecond = current - last
metrics.RequestPerSecond.Set(float64(stats_.RequestPerSecond)) metrics.Metrics.RequestPerSecond.Set(float64(stats_.RequestPerSecond))
last = current last = current
} }
} }
@ -325,7 +94,7 @@ func requestPerMinute() {
time.Sleep(60 * time.Second) time.Sleep(60 * time.Second)
current := stats_.RequestCount current := stats_.RequestCount
stats_.RequestPerMinute = current - last stats_.RequestPerMinute = current - last
metrics.RequestPerMinute.Set(float64(stats_.RequestPerMinute)) metrics.Metrics.RequestPerMinute.Set(float64(stats_.RequestPerMinute))
last = current last = current
} }
} }
@ -371,7 +140,7 @@ func blockChecker(gh string, cooldown int) {
body := "{\"status\":\"stopped\"}\"" body := "{\"status\":\"stopped\"}\""
// This should never fail too // This should never fail too
request, _ := http.NewRequest("PUT", url, strings.NewReader(body)) request, _ := http.NewRequest("PUT", url, strings.NewReader(body))
_, err = client.Do(request) _, err = httpc.Client.Do(request)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed to send request to gluetun.") log.Printf("[ERROR] Failed to send request to gluetun.")
} else { } else {
@ -383,7 +152,7 @@ func blockChecker(gh string, cooldown int) {
func beforeMisc(next http.HandlerFunc) http.HandlerFunc { func beforeMisc(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
defer panicHandler(w) defer utils.PanicHandler(w)
// To prevent accessing from the bare IP address // To prevent accessing from the bare IP address
if domain_only_access && (req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil) { if domain_only_access && (req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil) {
@ -397,7 +166,7 @@ func beforeMisc(next http.HandlerFunc) http.HandlerFunc {
func beforeProxy(next http.HandlerFunc) http.HandlerFunc { func beforeProxy(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) {
defer panicHandler(w) defer utils.PanicHandler(w)
// To prevent accessing from the bare IP address // To prevent accessing from the bare IP address
if domain_only_access && (req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil) { if domain_only_access && (req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil) {
@ -428,7 +197,7 @@ func beforeProxy(next http.HandlerFunc) http.HandlerFunc {
} }
atomic.AddInt64(&stats_.RequestCount, 1) atomic.AddInt64(&stats_.RequestCount, 1)
metrics.RequestCount.Inc() metrics.Metrics.RequestCount.Inc()
next(w, req) next(w, req)
} }
} }
@ -493,23 +262,24 @@ func main() {
if bc_cooldown == "" { if bc_cooldown == "" {
bc_cooldown = "60" bc_cooldown = "60"
} }
proxy = os.Getenv("PROXY") httpc.Proxy = os.Getenv("PROXY")
flag.BoolVar(&https, "https", https, "Use built-in https server (recommended)") 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(&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(&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.BoolVar(&httpc.Ipv6_only, "ipv6_only", httpc.Ipv6_only, "Only use ipv6 for requests")
flag.StringVar(&tls_cert, "tls-cert", tls_cert, "TLS Certificate path") 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(&tls_key, "tls-key", tls_key, "TLS Certificate Key path")
flag.StringVar(&sock, "s", sock, "Specify a socket name") flag.StringVar(&sock, "s", sock, "Specify a socket name")
flag.StringVar(&port, "p", port, "Specify a port number") flag.StringVar(&port, "p", port, "Specify a port number")
flag.StringVar(&host, "l", host, "Specify a listen address") flag.StringVar(&host, "l", host, "Specify a listen address")
flag.Parse() flag.Parse()
httpc.Ipv6_only = ipv6
if h3c { if h3c {
client = h3client httpc.Client = httpc.H3client
} else { } else {
client = h2client httpc.Client = httpc.H2client
} }
if https { if https {
@ -522,37 +292,25 @@ func main() {
} }
} }
ipv6_only = ipv6
mux := http.NewServeMux() mux := http.NewServeMux()
// MISC ROUTES // MISC ROUTES
mux.HandleFunc("/", beforeMisc(root)) mux.HandleFunc("/", beforeMisc(paths.Root))
mux.HandleFunc("/health", beforeMisc(health)) mux.HandleFunc("/health", beforeMisc(paths.Health))
mux.HandleFunc("/stats", beforeMisc(stats)) mux.HandleFunc("/stats", beforeMisc(paths.Stats))
prometheus.MustRegister(metrics.Uptime) metrics.Register()
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()) mux.Handle("/metrics", paths.MetricsHandler())
// PROXY ROUTES // PROXY ROUTES
mux.HandleFunc("/videoplayback", beforeProxy(videoplayback)) mux.HandleFunc("/videoplayback", beforeProxy(paths.Videoplayback))
mux.HandleFunc("/vi/", beforeProxy(vi)) mux.HandleFunc("/vi/", beforeProxy(paths.Vi))
mux.HandleFunc("/vi_webp/", beforeProxy(vi)) mux.HandleFunc("/vi_webp/", beforeProxy(paths.Vi))
mux.HandleFunc("/sb/", beforeProxy(vi)) mux.HandleFunc("/sb/", beforeProxy(paths.Vi))
mux.HandleFunc("/ggpht/", beforeProxy(ggpht)) mux.HandleFunc("/ggpht/", beforeProxy(paths.Ggpht))
mux.HandleFunc("/a/", beforeProxy(ggpht)) mux.HandleFunc("/a/", beforeProxy(paths.Ggpht))
mux.HandleFunc("/ytc/", beforeProxy(ggpht)) mux.HandleFunc("/ytc/", beforeProxy(paths.Ggpht))
go requestPerSecond() go requestPerSecond()
go requestPerMinute() go requestPerMinute()

2
go.mod
View file

@ -1,4 +1,4 @@
module git.nadeko.net/Fijxu/http3-ytproxy/v3 module git.nadeko.net/Fijxu/http3-ytproxy
go 1.22.0 go 1.22.0

59
internal/httpc/client.go Normal file
View file

@ -0,0 +1,59 @@
package httpc
import (
"net"
"net/http"
"net/url"
"time"
"github.com/quic-go/quic-go/http3"
)
var Ipv6_only = false
var Proxy string
var Client *http.Client
var dialer = &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}
// QUIC doesn't seem to support HTTP nor SOCKS5 proxies due to how it's made.
// (Since it's UDP)
var H3client = &http.Client{
Transport: &http3.Transport{},
Timeout: 10 * time.Second,
}
// http/2 client
var H2client = &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Transport: &http.Transport{
Dial: func(network, addr string) (net.Conn, error) {
var net string
if Ipv6_only {
net = "tcp6"
} else {
net = "tcp4"
}
return dialer.Dial(net, addr)
},
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 20 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
IdleConnTimeout: 30 * time.Second,
ReadBufferSize: 16 * 1024,
ForceAttemptHTTP2: true,
MaxConnsPerHost: 0,
MaxIdleConnsPerHost: 10,
MaxIdleConns: 0,
Proxy: func(r *http.Request) (*url.URL, error) {
if Proxy != "" {
return url.Parse(Proxy)
}
return nil, nil
},
},
}

1
internal/httpc/server.go Normal file
View file

@ -0,0 +1 @@
package httpc

View file

@ -0,0 +1,76 @@
package metrics
import "github.com/prometheus/client_golang/prometheus"
type metrics struct {
Uptime prometheus.Gauge
RequestCount prometheus.Counter
RequestPerSecond prometheus.Gauge
RequestPerMinute prometheus.Gauge
TotalConnEstablished prometheus.Counter
EstablishedConnections prometheus.Gauge
ActiveConnections prometheus.Gauge
IdleConnections prometheus.Gauge
RequestForbidden struct {
Videoplayback prometheus.Counter
Vi prometheus.Counter
Ggpht prometheus.Counter
}
}
var Metrics = metrics{
Uptime: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_uptime",
}),
RequestCount: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_count",
}),
RequestPerSecond: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_per_second",
}),
RequestPerMinute: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_request_per_minute",
}),
TotalConnEstablished: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_total_conn_established",
}),
EstablishedConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_established_conns",
}),
ActiveConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_active_conns",
}),
IdleConnections: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "http3_ytproxy_idle_conns",
}),
RequestForbidden: struct {
Videoplayback prometheus.Counter
Vi prometheus.Counter
Ggpht prometheus.Counter
}{
Videoplayback: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_videoplayback",
}),
Vi: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_vi",
}),
Ggpht: prometheus.NewCounter(prometheus.CounterOpts{
Name: "http3_ytproxy_request_forbidden_ggpht",
}),
},
}
func Register() {
prometheus.MustRegister(Metrics.Uptime)
prometheus.MustRegister(Metrics.ActiveConnections)
prometheus.MustRegister(Metrics.IdleConnections)
prometheus.MustRegister(Metrics.EstablishedConnections)
prometheus.MustRegister(Metrics.TotalConnEstablished)
prometheus.MustRegister(Metrics.RequestCount)
prometheus.MustRegister(Metrics.RequestPerSecond)
prometheus.MustRegister(Metrics.RequestPerMinute)
prometheus.MustRegister(Metrics.RequestForbidden.Videoplayback)
prometheus.MustRegister(Metrics.RequestForbidden.Vi)
prometheus.MustRegister(Metrics.RequestForbidden.Ggpht)
}

52
internal/metrics/stats.go Normal file
View file

@ -0,0 +1,52 @@
package metrics
import (
"runtime"
"time"
)
type statusJson struct {
Version string `json:"version"`
Uptime time.Duration `json:"uptime"`
RequestCount int64 `json:"requestCount"`
RequestPerSecond int64 `json:"requestPerSecond"`
RequestPerMinute int64 `json:"requestPerMinute"`
TotalConnEstablished int64 `json:"totalEstablished"`
EstablishedConnections int64 `json:"establishedConnections"`
ActiveConnections int64 `json:"activeConnections"`
IdleConnections int64 `json:"idleConnections"`
RequestsForbiddenPerSec struct {
Videoplayback int64 `json:"videoplayback"`
}
RequestsForbidden struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
} `json:"requestsForbidden"`
}
var stats_ = statusJson{
Version: version + "-" + runtime.GOARCH,
Uptime: 0,
RequestCount: 0,
RequestPerSecond: 0,
RequestPerMinute: 0,
TotalConnEstablished: 0,
EstablishedConnections: 0,
ActiveConnections: 0,
IdleConnections: 0,
RequestsForbiddenPerSec: struct {
Videoplayback int64 `json:"videoplayback"`
}{
Videoplayback: 0,
},
RequestsForbidden: struct {
Videoplayback int64 `json:"videoplayback"`
Vi int64 `json:"vi"`
Ggpht int64 `json:"ggpht"`
}{
Videoplayback: 0,
Vi: 0,
Ggpht: 0,
},
}

33
internal/paths/consts.go Normal file
View file

@ -0,0 +1,33 @@
package paths
import "regexp"
const (
default_ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
ggpht_host = "yt3.ggpht.com"
)
var manifest_re = regexp.MustCompile(`(?m)URI="([^"]+)"`)
var allowed_hosts = []string{
"youtube.com",
"googlevideo.com",
"ytimg.com",
"ggpht.com",
"googleusercontent.com",
}
var strip_headers = []string{
"Accept-Encoding",
"Authorization",
"Origin",
"Referer",
"Cookie",
"Set-Cookie",
"Etag",
"Alt-Svc",
"Server",
"Cache-Control",
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
"report-to",
}

50
internal/paths/ggpht.go Normal file
View file

@ -0,0 +1,50 @@
package paths
import (
"io"
"log"
"net/http"
"net/url"
"strings"
"sync/atomic"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
)
func Ggpht(w http.ResponseWriter, req *http.Request) {
path := req.URL.EscapedPath()
path = strings.Replace(path, "/ggpht", "", 1)
path = strings.Replace(path, "/i/", "/", 1)
proxyURL, err := url.Parse("https://" + ggpht_host + path)
if err != nil {
log.Panic(err)
}
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
utils.CopyHeaders(req.Header, request.Header, false)
request.Header.Set("User-Agent", default_ua)
if err != nil {
log.Panic(err)
}
resp, err := httpc.Client.Do(request)
if err != nil {
log.Panic(err)
}
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Ggpht, 1)
metrics.RequestForbidden.Ggpht.Inc()
return
}
defer resp.Body.Close()
NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
utils.CopyHeaders(resp.Header, w.Header(), NoRewrite)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}

11
internal/paths/health.go Normal file
View file

@ -0,0 +1,11 @@
package paths
import (
"io"
"net/http"
)
func Health(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(200)
io.WriteString(w, "OK")
}

27
internal/paths/metrics.go Normal file
View file

@ -0,0 +1,27 @@
package paths
import (
"net"
"net/http"
"strings"
"time"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/metrics"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var programInit = time.Now()
// CustomHandler wraps the default promhttp.Handler with custom logic
func MetricsHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// To prevent accessing from the bare IP address
if req.Host == "" || net.ParseIP(strings.Split(req.Host, ":")[0]) != nil {
w.WriteHeader(444)
return
}
metrics.Metrics.Uptime.Set(float64(time.Duration(time.Since(programInit).Seconds())))
promhttp.Handler().ServeHTTP(w, req)
})
}

17
internal/paths/root.go Normal file
View file

@ -0,0 +1,17 @@
package paths
import (
"io"
"net/http"
)
func Root(w http.ResponseWriter, req *http.Request) {
const msg = `
HTTP youtube proxy for https://inv.nadeko.net
https://git.nadeko.net/Fijxu/http3-ytproxy
Routes:
/stats
/health`
io.WriteString(w, msg)
}

20
internal/paths/stats.go Normal file
View file

@ -0,0 +1,20 @@
package paths
import (
"encoding/json"
"net/http"
"time"
)
func Stats(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "application/json")
stats_.Uptime = time.Duration(time.Since(programInit).Seconds())
// stats_.TotalEstablished = int64(cw.totalEstablished)
// stats_.EstablishedConnections = int64(cw.established)
// stats_.ActiveConnections = int64(cw.active)
// stats_.IdleConnections = int64(cw.idle)
if err := json.NewEncoder(w).Encode(stats_); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

62
internal/paths/vi.go Normal file
View file

@ -0,0 +1,62 @@
package paths
import (
"io"
"log"
"net/http"
"net/url"
"strings"
"sync/atomic"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
)
func Vi(w http.ResponseWriter, req *http.Request) {
const host string = "i.ytimg.com"
q := req.URL.Query()
path := req.URL.EscapedPath()
proxyURL, err := url.Parse("https://" + host + path)
if err != nil {
log.Panic(err)
}
if strings.HasSuffix(proxyURL.EscapedPath(), "maxres.jpg") {
proxyURL.Path = utils.GetBestThumbnail(proxyURL.EscapedPath())
}
/*
Required for /sb/ endpoints
You can't access https://i.ytimg.com/sb/<VIDEOID>/storyboard3_L2/M3.jpg
without it's parameters `sqp` and `sigh`
*/
proxyURL.RawQuery = q.Encode()
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
if err != nil {
log.Panic(err)
}
request.Header.Set("User-Agent", default_ua)
resp, err := httpc.Client.Do(request)
if err != nil {
log.Panic(err)
}
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Vi, 1)
metrics.RequestForbidden.Vi.Inc()
return
}
defer resp.Body.Close()
// NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
// copyHeaders(resp.Header, w.Header(), NoRewrite)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}

View file

@ -1,4 +1,4 @@
package main package paths
import ( import (
"bytes" "bytes"
@ -11,6 +11,9 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"time" "time"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/utils"
) )
func forbiddenChecker(resp *http.Response, w http.ResponseWriter) error { func forbiddenChecker(resp *http.Response, w http.ResponseWriter) error {
@ -23,9 +26,38 @@ func forbiddenChecker(resp *http.Response, w http.ResponseWriter) error {
return nil return nil
} }
func videoplayback(w http.ResponseWriter, req *http.Request) { func checkRequest(w http.ResponseWriter, req *http.Request, params url.Values) {
host := params.Get("host")
parts := strings.Split(strings.ToLower(host), ".")
if len(parts) < 2 {
w.WriteHeader(400)
io.WriteString(w, "Invalid hostname.")
return
}
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
disallowed := true
for _, value := range allowed_hosts {
if domain == value {
disallowed = false
break
}
}
if disallowed {
w.WriteHeader(401)
io.WriteString(w, "Non YouTube domains are not supported.")
return
}
}
func Videoplayback(w http.ResponseWriter, req *http.Request) {
q := req.URL.Query() q := req.URL.Query()
checkRequest(w, req, q)
expire, err := strconv.ParseInt(q.Get("expire"), 10, 64) expire, err := strconv.ParseInt(q.Get("expire"), 10, 64)
if err != nil { if err != nil {
w.WriteHeader(500) w.WriteHeader(500)
@ -71,28 +103,6 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
// host = "rr" + mvi + "---" + mn[0] + ".googlevideo.com" // host = "rr" + mvi + "---" + mn[0] + ".googlevideo.com"
// } // }
parts := strings.Split(strings.ToLower(host), ".")
if len(parts) < 2 {
w.WriteHeader(400)
io.WriteString(w, "Invalid hostname.")
return
}
domain := parts[len(parts)-2] + "." + parts[len(parts)-1]
disallowed := true
for _, value := range allowed_hosts {
if domain == value {
disallowed = false
break
}
}
if disallowed {
w.WriteHeader(401)
io.WriteString(w, "Non YouTube domains are not supported.")
return
}
// if c == "WEB" { // if c == "WEB" {
// q.Set("alr", "yes") // q.Set("alr", "yes")
// } // }
@ -120,8 +130,8 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
if err != nil { if err != nil {
log.Panic("Failed to create headRequest:", err) log.Panic("Failed to create headRequest:", err)
} }
copyHeaders(req.Header, postRequest.Header, false) utils.CopyHeaders(req.Header, postRequest.Header, false)
copyHeaders(req.Header, headRequest.Header, false) utils.CopyHeaders(req.Header, headRequest.Header, false)
switch c { switch c {
case "ANDROID": case "ANDROID":
@ -146,7 +156,7 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
resp := &http.Response{} resp := &http.Response{}
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
resp, err = client.Do(headRequest) resp, err = httpc.Client.Do(headRequest)
if err != nil { if err != nil {
log.Panic("Failed to do HEAD request:", err) log.Panic("Failed to do HEAD request:", err)
} }
@ -162,7 +172,7 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
} }
} }
resp, err = client.Do(postRequest) resp, err = httpc.Client.Do(postRequest)
if err != nil { if err != nil {
log.Panic("Failed to do POST request:", err) log.Panic("Failed to do POST request:", err)
} }
@ -176,7 +186,7 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
defer resp.Body.Close() defer resp.Body.Close()
NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video") NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
copyHeaders(resp.Header, w.Header(), NoRewrite) utils.CopyHeaders(resp.Header, w.Header(), NoRewrite)
w.WriteHeader(resp.StatusCode) w.WriteHeader(resp.StatusCode)
@ -196,12 +206,12 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
line = "https://" + reqUrl.Hostname() + path + line line = "https://" + reqUrl.Hostname() + path + line
} }
if strings.HasPrefix(line, "https://") { if strings.HasPrefix(line, "https://") {
lines[i] = RelativeUrl(line) lines[i] = utils.RelativeUrl(line)
} }
if manifest_re.MatchString(line) { if manifest_re.MatchString(line) {
url := manifest_re.FindStringSubmatch(line)[1] url := manifest_re.FindStringSubmatch(line)[1]
lines[i] = strings.Replace(line, url, RelativeUrl(url), 1) lines[i] = strings.Replace(line, url, utils.RelativeUrl(url), 1)
} }
} }
@ -210,91 +220,3 @@ func videoplayback(w http.ResponseWriter, req *http.Request) {
io.Copy(w, resp.Body) io.Copy(w, resp.Body)
} }
} }
func vi(w http.ResponseWriter, req *http.Request) {
const host string = "i.ytimg.com"
q := req.URL.Query()
path := req.URL.EscapedPath()
proxyURL, err := url.Parse("https://" + host + path)
if err != nil {
log.Panic(err)
}
if strings.HasSuffix(proxyURL.EscapedPath(), "maxres.jpg") {
proxyURL.Path = getBestThumbnail(proxyURL.EscapedPath())
}
/*
Required for /sb/ endpoints
You can't access https://i.ytimg.com/sb/<VIDEOID>/storyboard3_L2/M3.jpg
without it's parameters `sqp` and `sigh`
*/
proxyURL.RawQuery = q.Encode()
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
if err != nil {
log.Panic(err)
}
request.Header.Set("User-Agent", default_ua)
resp, err := client.Do(request)
if err != nil {
log.Panic(err)
}
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Vi, 1)
metrics.RequestForbidden.Vi.Inc()
return
}
defer resp.Body.Close()
// NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
// copyHeaders(resp.Header, w.Header(), NoRewrite)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func ggpht(w http.ResponseWriter, req *http.Request) {
const host string = "yt3.ggpht.com"
path := req.URL.EscapedPath()
path = strings.Replace(path, "/ggpht", "", 1)
path = strings.Replace(path, "/i/", "/", 1)
proxyURL, err := url.Parse("https://" + host + path)
if err != nil {
log.Panic(err)
}
request, err := http.NewRequest(req.Method, proxyURL.String(), nil)
copyHeaders(req.Header, request.Header, false)
request.Header.Set("User-Agent", default_ua)
if err != nil {
log.Panic(err)
}
resp, err := client.Do(request)
if err != nil {
log.Panic(err)
}
if err := forbiddenChecker(resp, w); err != nil {
atomic.AddInt64(&stats_.RequestsForbidden.Ggpht, 1)
metrics.RequestForbidden.Ggpht.Inc()
return
}
defer resp.Body.Close()
NoRewrite := strings.HasPrefix(resp.Header.Get("Content-Type"), "audio") || strings.HasPrefix(resp.Header.Get("Content-Type"), "video")
copyHeaders(resp.Header, w.Header(), NoRewrite)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}

View file

@ -1,13 +1,34 @@
package main package utils
import ( import (
"log" "log"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"git.nadeko.net/Fijxu/http3-ytproxy/internal/httpc"
) )
func copyHeaders(from http.Header, to http.Header, length bool) { const (
path_prefix = ""
)
var strip_headers = []string{
"Accept-Encoding",
"Authorization",
"Origin",
"Referer",
"Cookie",
"Set-Cookie",
"Etag",
"Alt-Svc",
"Server",
"Cache-Control",
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to
"report-to",
}
func CopyHeaders(from http.Header, to http.Header, length bool) {
// Loop over header names // Loop over header names
outer: outer:
for name, values := range from { for name, values := range from {
@ -28,14 +49,14 @@ outer:
} }
} }
func getBestThumbnail(path string) (newpath string) { func GetBestThumbnail(path string) (newpath string) {
formats := [4]string{"maxresdefault.jpg", "sddefault.jpg", "hqdefault.jpg", "mqdefault.jpg"} formats := [4]string{"maxresdefault.jpg", "sddefault.jpg", "hqdefault.jpg", "mqdefault.jpg"}
for _, format := range formats { for _, format := range formats {
newpath = strings.Replace(path, "maxres.jpg", format, 1) newpath = strings.Replace(path, "maxres.jpg", format, 1)
url := "https://i.ytimg.com" + newpath url := "https://i.ytimg.com" + newpath
resp, _ := h2client.Head(url) resp, _ := httpc.Client.Head(url)
if resp.StatusCode == 200 { if resp.StatusCode == 200 {
return newpath return newpath
} }
@ -56,7 +77,7 @@ func RelativeUrl(in string) (newurl string) {
return segment_url.RequestURI() return segment_url.RequestURI()
} }
func panicHandler(w http.ResponseWriter) { func PanicHandler(w http.ResponseWriter) {
if r := recover(); r != nil { if r := recover(); r != nil {
log.Printf("Panic: %v", r) log.Printf("Panic: %v", r)
http.Error(w, "Internal Server Error", http.StatusInternalServerError) http.Error(w, "Internal Server Error", http.StatusInternalServerError)

View file

@ -1,19 +0,0 @@
user www-data;
events {
worker_connections 1024;
}
http {
server {
listen 3000;
listen [::]:3000;
access_log off;
location / {
resolver 127.0.0.11;
set $backend "http3-proxy";
proxy_pass http://$backend:8080;
proxy_http_version 1.1; # to keep alive
proxy_set_header Connection ""; # to keep alive
}
}
}