mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-08 05:23:18 +01:00
fix(outbound): treat CGNAT and benchmarking ranges as non-public
This commit is contained in:
@@ -79,6 +79,18 @@ var nonPublicIPv6Prefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("100::/64"),
|
||||
}
|
||||
|
||||
// nonPublicIPv4Prefixes lists IPv4 ranges that the [netip.Addr] helpers do
|
||||
// not classify but that must not be considered public:
|
||||
//
|
||||
// - 100.64.0.0/10 Carrier-grade NAT (RFC 6598). Routable inside provider
|
||||
// and cluster networks, and Alibaba Cloud serves instance metadata from
|
||||
// 100.100.100.200.
|
||||
// - 198.18.0.0/15 Benchmarking (RFC 2544). Never routed on the internet.
|
||||
var nonPublicIPv4Prefixes = []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.0.0/10"),
|
||||
netip.MustParsePrefix("198.18.0.0/15"),
|
||||
}
|
||||
|
||||
// IsPublicIP reports whether addr is reachable on the public internet. It
|
||||
// returns false for loopback, private (RFC1918), link-local, unspecified,
|
||||
// multicast, and unique-local addresses. IPv4-mapped IPv6 addresses are
|
||||
@@ -89,7 +101,8 @@ var nonPublicIPv6Prefixes = []netip.Prefix{
|
||||
// (6to4, Teredo, NAT64) are rejected wholesale rather than recursed into,
|
||||
// because a host that routes them implicitly trusts the IPv4 mapping and
|
||||
// the prefixes themselves are deprecated or translation-only. See
|
||||
// [nonPublicIPv6Prefixes] for the full list and rationale.
|
||||
// [nonPublicIPv6Prefixes] and [nonPublicIPv4Prefixes] for the full lists
|
||||
// and rationale.
|
||||
func IsPublicIP(addr netip.Addr) bool {
|
||||
if !addr.IsValid() {
|
||||
return false
|
||||
@@ -105,6 +118,13 @@ func IsPublicIP(addr netip.Addr) bool {
|
||||
addr.IsInterfaceLocalMulticast():
|
||||
return false
|
||||
}
|
||||
if addr.Is4() {
|
||||
for _, p := range nonPublicIPv4Prefixes {
|
||||
if p.Contains(addr) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if addr.Is6() {
|
||||
for _, p := range nonPublicIPv6Prefixes {
|
||||
if p.Contains(addr) {
|
||||
|
||||
@@ -40,6 +40,24 @@ func TestIsPublicIP(t *testing.T) {
|
||||
|
||||
// Link-local.
|
||||
{"169.254.169.254", false},
|
||||
{"169.254.170.2", false},
|
||||
|
||||
// Carrier-grade NAT (RFC 6598). Alibaba Cloud serves instance
|
||||
// metadata from 100.100.100.200.
|
||||
{"100.64.0.0", false},
|
||||
{"100.100.100.200", false},
|
||||
{"100.127.255.255", false},
|
||||
{"::ffff:100.100.100.200", false},
|
||||
|
||||
// Benchmarking (RFC 2544).
|
||||
{"198.18.0.1", false},
|
||||
{"198.19.255.255", false},
|
||||
|
||||
// Adjacent to the ranges above, and public.
|
||||
{"100.63.255.255", true},
|
||||
{"100.128.0.0", true},
|
||||
{"198.17.255.255", true},
|
||||
{"198.20.0.0", true},
|
||||
{"fe80::1", false},
|
||||
|
||||
// Unique-local.
|
||||
|
||||
@@ -120,6 +120,30 @@ func (engine *QPdf) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue
|
||||
return append(attrs, extra...)
|
||||
}
|
||||
|
||||
// qpdfPageRange matches the page range syntax qpdf accepts, and nothing else.
|
||||
//
|
||||
// A term is a page number, "z" for the last page, or "rN" counting from the
|
||||
// end, optionally prefixed with "x" to exclude it. Terms combine into ranges
|
||||
// with "-", ranges join with ",", and the whole thing takes an optional ":odd"
|
||||
// or ":even".
|
||||
var qpdfPageRange = regexp.MustCompile(`^x?(?:z|r\d+|\d+)(?:-x?(?:z|r\d+|\d+))?(?:,x?(?:z|r\d+|\d+)(?:-x?(?:z|r\d+|\d+))?)*(?::odd|:even)?$`)
|
||||
|
||||
// validateSplitSpan returns span when it is a qpdf page range.
|
||||
//
|
||||
// qpdf reads the argument after "--pages ." as either a page range or another
|
||||
// source file, so a span carrying a path makes qpdf append that file's pages
|
||||
// to the output. Other engines in the split chain accept spellings qpdf does
|
||||
// not, such as pdfcpu's "2-end", so a span this rejects is not necessarily
|
||||
// invalid. Returning an error lets the chain move on to an engine that
|
||||
// understands it.
|
||||
func validateSplitSpan(span string) error {
|
||||
if qpdfPageRange.MatchString(span) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("split span '%s' is not a QPDF page range: %w", span, gotenberg.ErrPdfSplitModeNotSupported)
|
||||
}
|
||||
|
||||
// Split splits a given PDF file.
|
||||
func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
||||
ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.Split",
|
||||
@@ -139,6 +163,12 @@ func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenbe
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
err := validateSplitSpan(mode.Span)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return nil, err
|
||||
}
|
||||
args = append(args, inputPath)
|
||||
args = append(args, engine.globalArgs...)
|
||||
args = append(args, "--pages", ".", mode.Span)
|
||||
|
||||
@@ -3,9 +3,12 @@ package qpdf
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||
)
|
||||
|
||||
func TestStripQpdfStringPrefix(t *testing.T) {
|
||||
@@ -270,3 +273,54 @@ func TestSetStreamSubtype(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateSplitSpan(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
span string
|
||||
valid bool
|
||||
}{
|
||||
// qpdf page ranges.
|
||||
{"1", true},
|
||||
{"12", true},
|
||||
{"1-5", true},
|
||||
{"2-z", true},
|
||||
{"z", true},
|
||||
{"r1", true},
|
||||
{"r3-r1", true},
|
||||
{"1,3,5-9", true},
|
||||
{"1-5,x3", true},
|
||||
{"1-z:odd", true},
|
||||
{"1-z:even", true},
|
||||
|
||||
// Other engines' spellings. Not valid here, so the chain moves on.
|
||||
{"2-end", false},
|
||||
{"2-", false},
|
||||
{"foo", false},
|
||||
|
||||
// A span qpdf would read as a source file.
|
||||
{"/tmp/secret.pdf", false},
|
||||
{"secret.pdf", false},
|
||||
{"./secret.pdf", false},
|
||||
{"../../etc/hosts", false},
|
||||
{"1,/tmp/secret.pdf", false},
|
||||
{"1 /tmp/secret.pdf", false},
|
||||
{"", false},
|
||||
{"--password=x", false},
|
||||
} {
|
||||
t.Run(tc.span, func(t *testing.T) {
|
||||
err := validateSplitSpan(tc.span)
|
||||
if tc.valid && err != nil {
|
||||
t.Fatalf("validateSplitSpan(%q) = %v, want nil", tc.span, err)
|
||||
}
|
||||
if !tc.valid {
|
||||
if err == nil {
|
||||
t.Fatalf("validateSplitSpan(%q) = nil, want an error", tc.span)
|
||||
}
|
||||
// The chain must be able to try the next engine.
|
||||
if !errors.Is(err, gotenberg.ErrPdfSplitModeNotSupported) {
|
||||
t.Fatalf("error %v does not wrap ErrPdfSplitModeNotSupported", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user