detached a909bde

cef: remove /api/transcode scheme route

commit
a909bde088706fd7c654a90f1f9219ecd8eea45c
author
brice · · 2026-04-28 03:44 UTC
parent
b15abc4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +0 -110
M internal/infrastructure/cef/scheme_handler.go +0 -91
@@ -10,7 +10,6 @@ import (
1010 "html"
1111 "io"
1212 "io/fs"
13- "net"
1413 "net/http"
1514 "net/url"
1615 "os"
@@ -18,7 +17,6 @@ import (
1817 "strconv"
1918 "strings"
2019 "sync"
21- "time"
2220 "unsafe"
2321
2422 "github.com/andybalholm/brotli"
@@ -26,7 +24,6 @@ import (
2624
2725 "github.com/bnema/dumber/internal/application/port"
2826 domainurl "github.com/bnema/dumber/internal/domain/url"
29- "github.com/bnema/dumber/internal/infrastructure/netguard"
3027 "github.com/bnema/dumber/internal/infrastructure/webutil"
3128 "github.com/bnema/dumber/internal/logging"
3229 "github.com/rs/zerolog"
@@ -227,8 +224,6 @@ func (h *dumbSchemeHandler) handleAPIGet(requestPath string, request purecef.Req
227224 return denied, true
228225 }
229226 return h.handleConfigAPI(h.defaultConfigPayload), true
230- case "/api/transcode":
231- return h.handleTranscodeAPI(request), true
232227 case "/api/favicon":
233228 return h.handleFaviconAPI(request), true
234229 default:
@@ -370,92 +365,6 @@ func isTrustedSystemviewURL(raw string) bool {
370365 return isInternalPageHost(host)
371366 }
372367
373-func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef.ResourceHandler {
374- if h.transcoder == nil {
375- return h.newAPIJSONResourceHandler(http.StatusServiceUnavailable, map[string]string{"error": "transcoder unavailable"})
376- }
377-
378- reqURL := request.GetURL()
379- parsed, err := url.Parse(reqURL)
380- if err != nil {
381- return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid request URL"})
382- }
383-
384- sourceURL := strings.TrimSpace(parsed.Query().Get("src"))
385- if sourceURL == "" {
386- return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "missing src"})
387- }
388-
389- sourceParsed, err := url.Parse(sourceURL)
390- if err != nil || sourceParsed.Host == "" || (sourceParsed.Scheme != "http" && sourceParsed.Scheme != actualInternalScheme) {
391- return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid src"})
392- }
393- headers := make(map[string]string)
394- if userAgent := request.GetHeaderByName("User-Agent"); userAgent != "" {
395- headers["User-Agent"] = userAgent
396- }
397- if referer := strings.TrimSpace(parsed.Query().Get("referer")); referer != "" {
398- headers["Referer"] = referer
399- } else if referer := request.GetReferrerURL(); referer != "" {
400- headers["Referer"] = referer
401- }
402- if origin := strings.TrimSpace(parsed.Query().Get("origin")); origin != "" {
403- headers["Origin"] = origin
404- }
405-
406- ctx, cancel := context.WithTimeout(h.ctx, 5*time.Minute)
407- h.logger.Info().
408- Str("source_url", logging.TruncateURL(sourceURL, maxSchemeTruncatedURLLength)).
409- Int("forwarded_header_count", len(headers)).
410- Msg("scheme handler returning transcoding stream")
411-
412- return cefNewResourceHandler(&transcodingResourceHandler{
413- transcoder: h.transcoder,
414- sourceURL: sourceURL,
415- headers: headers,
416- ctx: ctx,
417- cancel: cancel,
418- validateSource: func(ctx context.Context) error {
419- return validateTranscodeSourceURL(ctx, sourceParsed)
420- },
421- logf: func() zerolog.Logger {
422- return h.logger.With().Str("component", "scheme-transcoding").Logger()
423- },
424- })
425-}
426-
427-func validateTranscodeSourceURL(ctx context.Context, source *url.URL) error {
428- if source == nil || source.Host == "" {
429- return fmt.Errorf("invalid src")
430- }
431- host := source.Hostname()
432- if host == "" {
433- return fmt.Errorf("invalid src host")
434- }
435- if ip := net.ParseIP(host); ip != nil {
436- if isBlockedTranscodeIP(ip) {
437- return fmt.Errorf("private src host not allowed")
438- }
439- return nil
440- }
441-
442- lookupCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
443- defer cancel()
444- addrs, err := net.DefaultResolver.LookupIPAddr(lookupCtx, host)
445- if err != nil || len(addrs) == 0 {
446- return fmt.Errorf("src host could not be resolved")
447- }
448- for _, addr := range addrs {
449- if isBlockedTranscodeIP(addr.IP) {
450- return fmt.Errorf("private src host not allowed")
451- }
452- }
453- return nil
454-}
455-
456-func isBlockedTranscodeIP(ip net.IP) bool {
457- return netguard.IsBlockedTranscodeIP(ip)
458-}
459368
460369 func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) {
461370 if build == nil {
@@ -10,7 +10,6 @@ import (
10 "html"10 "html"
11 "io"11 "io"
12 "io/fs"12 "io/fs"
13- "net"
14 "net/http"13 "net/http"
15 "net/url"14 "net/url"
16 "os"15 "os"
@@ -18,7 +17,6 @@ import (
18 "strconv"17 "strconv"
19 "strings"18 "strings"
20 "sync"19 "sync"
21- "time"
22 "unsafe"20 "unsafe"
23 21
24 "github.com/andybalholm/brotli"22 "github.com/andybalholm/brotli"
@@ -26,7 +24,6 @@ import (
26 24
27 "github.com/bnema/dumber/internal/application/port"25 "github.com/bnema/dumber/internal/application/port"
28 domainurl "github.com/bnema/dumber/internal/domain/url"26 domainurl "github.com/bnema/dumber/internal/domain/url"
29- "github.com/bnema/dumber/internal/infrastructure/netguard"
30 "github.com/bnema/dumber/internal/infrastructure/webutil"27 "github.com/bnema/dumber/internal/infrastructure/webutil"
31 "github.com/bnema/dumber/internal/logging"28 "github.com/bnema/dumber/internal/logging"
32 "github.com/rs/zerolog"29 "github.com/rs/zerolog"
@@ -227,8 +224,6 @@ func (h *dumbSchemeHandler) handleAPIGet(requestPath string, request purecef.Req
227 return denied, true224 return denied, true
228 }225 }
229 return h.handleConfigAPI(h.defaultConfigPayload), true226 return h.handleConfigAPI(h.defaultConfigPayload), true
230- case "/api/transcode":
231- return h.handleTranscodeAPI(request), true
232 case "/api/favicon":227 case "/api/favicon":
233 return h.handleFaviconAPI(request), true228 return h.handleFaviconAPI(request), true
234 default:229 default:
@@ -370,92 +365,6 @@ func isTrustedSystemviewURL(raw string) bool {
370 return isInternalPageHost(host)365 return isInternalPageHost(host)
371 }366 }
372 367
373-func (h *dumbSchemeHandler) handleTranscodeAPI(request purecef.Request) purecef.ResourceHandler {
374- if h.transcoder == nil {
375- return h.newAPIJSONResourceHandler(http.StatusServiceUnavailable, map[string]string{"error": "transcoder unavailable"})
376- }
377-
378- reqURL := request.GetURL()
379- parsed, err := url.Parse(reqURL)
380- if err != nil {
381- return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid request URL"})
382- }
383-
384- sourceURL := strings.TrimSpace(parsed.Query().Get("src"))
385- if sourceURL == "" {
386- return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "missing src"})
387- }
388-
389- sourceParsed, err := url.Parse(sourceURL)
390- if err != nil || sourceParsed.Host == "" || (sourceParsed.Scheme != "http" && sourceParsed.Scheme != actualInternalScheme) {
391- return h.newAPIJSONResourceHandler(http.StatusBadRequest, map[string]string{"error": "invalid src"})
392- }
393- headers := make(map[string]string)
394- if userAgent := request.GetHeaderByName("User-Agent"); userAgent != "" {
395- headers["User-Agent"] = userAgent
396- }
397- if referer := strings.TrimSpace(parsed.Query().Get("referer")); referer != "" {
398- headers["Referer"] = referer
399- } else if referer := request.GetReferrerURL(); referer != "" {
400- headers["Referer"] = referer
401- }
402- if origin := strings.TrimSpace(parsed.Query().Get("origin")); origin != "" {
403- headers["Origin"] = origin
404- }
405-
406- ctx, cancel := context.WithTimeout(h.ctx, 5*time.Minute)
407- h.logger.Info().
408- Str("source_url", logging.TruncateURL(sourceURL, maxSchemeTruncatedURLLength)).
409- Int("forwarded_header_count", len(headers)).
410- Msg("scheme handler returning transcoding stream")
411-
412- return cefNewResourceHandler(&transcodingResourceHandler{
413- transcoder: h.transcoder,
414- sourceURL: sourceURL,
415- headers: headers,
416- ctx: ctx,
417- cancel: cancel,
418- validateSource: func(ctx context.Context) error {
419- return validateTranscodeSourceURL(ctx, sourceParsed)
420- },
421- logf: func() zerolog.Logger {
422- return h.logger.With().Str("component", "scheme-transcoding").Logger()
423- },
424- })
425-}
426-
427-func validateTranscodeSourceURL(ctx context.Context, source *url.URL) error {
428- if source == nil || source.Host == "" {
429- return fmt.Errorf("invalid src")
430- }
431- host := source.Hostname()
432- if host == "" {
433- return fmt.Errorf("invalid src host")
434- }
435- if ip := net.ParseIP(host); ip != nil {
436- if isBlockedTranscodeIP(ip) {
437- return fmt.Errorf("private src host not allowed")
438- }
439- return nil
440- }
441-
442- lookupCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
443- defer cancel()
444- addrs, err := net.DefaultResolver.LookupIPAddr(lookupCtx, host)
445- if err != nil || len(addrs) == 0 {
446- return fmt.Errorf("src host could not be resolved")
447- }
448- for _, addr := range addrs {
449- if isBlockedTranscodeIP(addr.IP) {
450- return fmt.Errorf("private src host not allowed")
451- }
452- }
453- return nil
454-}
455-
456-func isBlockedTranscodeIP(ip net.IP) bool {
457- return netguard.IsBlockedTranscodeIP(ip)
458-}
459 368
460 func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) {369 func resolveConfigPayload(build func() ([]byte, error)) ([]byte, error) {
461 if build == nil {370 if build == nil {
M internal/infrastructure/cef/scheme_handler_test.go +0 -19
@@ -4,9 +4,7 @@ import (
44 "context"
55 "encoding/base64"
66 "errors"
7- "net"
87 "net/http"
9- "net/url"
108 "testing"
119
1210 purecef "github.com/bnema/purego-cef/cef"
@@ -315,23 +313,6 @@ func TestHandleFaviconAPIDefersFaviconDiskChecksUntilResourceOpen(t *testing.T)
315313 require.NotNil(t, h.handleFaviconAPI(request))
316314 }
317315
318-func TestValidateTranscodeSourceURL_RejectsPrivateHosts(t *testing.T) {
319- privateURL, err := url.Parse("http://127.0.0.1/video.mp4")
320- require.NoError(t, err)
321-
322- err = validateTranscodeSourceURL(context.Background(), privateURL)
323- require.Error(t, err)
324- require.Contains(t, err.Error(), "private src host")
325-}
326-
327-func TestIsBlockedTranscodeIPRejectsReservedInternalRanges(t *testing.T) {
328- for _, rawIP := range []string{"100.64.0.1", "198.18.0.1", "192.0.2.1", "64:ff9b::a00:1", "2001::1", "2001:db8::1", "2002:0a00:0001::1"} {
329- t.Run(rawIP, func(t *testing.T) {
330- require.True(t, isBlockedTranscodeIP(net.ParseIP(rawIP)))
331- })
332- }
333-}
334-
335316 func TestReadBodyFromHeader_DecodesBase64Payload(t *testing.T) {
336317 request := cefmocks.NewMockRequest(t)
337318 encoded := base64.StdEncoding.EncodeToString([]byte(`{"text":"copied from js"}`))
@@ -4,9 +4,7 @@ import (
4 "context"4 "context"
5 "encoding/base64"5 "encoding/base64"
6 "errors"6 "errors"
7- "net"
8 "net/http"7 "net/http"
9- "net/url"
10 "testing"8 "testing"
11 9
12 purecef "github.com/bnema/purego-cef/cef"10 purecef "github.com/bnema/purego-cef/cef"
@@ -315,23 +313,6 @@ func TestHandleFaviconAPIDefersFaviconDiskChecksUntilResourceOpen(t *testing.T)
315 require.NotNil(t, h.handleFaviconAPI(request))313 require.NotNil(t, h.handleFaviconAPI(request))
316 }314 }
317 315
318-func TestValidateTranscodeSourceURL_RejectsPrivateHosts(t *testing.T) {
319- privateURL, err := url.Parse("http://127.0.0.1/video.mp4")
320- require.NoError(t, err)
321-
322- err = validateTranscodeSourceURL(context.Background(), privateURL)
323- require.Error(t, err)
324- require.Contains(t, err.Error(), "private src host")
325-}
326-
327-func TestIsBlockedTranscodeIPRejectsReservedInternalRanges(t *testing.T) {
328- for _, rawIP := range []string{"100.64.0.1", "198.18.0.1", "192.0.2.1", "64:ff9b::a00:1", "2001::1", "2001:db8::1", "2002:0a00:0001::1"} {
329- t.Run(rawIP, func(t *testing.T) {
330- require.True(t, isBlockedTranscodeIP(net.ParseIP(rawIP)))
331- })
332- }
333-}
334-
335 func TestReadBodyFromHeader_DecodesBase64Payload(t *testing.T) {316 func TestReadBodyFromHeader_DecodesBase64Payload(t *testing.T) {
336 request := cefmocks.NewMockRequest(t)317 request := cefmocks.NewMockRequest(t)
337 encoded := base64.StdEncoding.EncodeToString([]byte(`{"text":"copied from js"}`))318 encoded := base64.StdEncoding.EncodeToString([]byte(`{"text":"copied from js"}`))

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close