mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-10-16 09:13:17 +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"),
|
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
|
// IsPublicIP reports whether addr is reachable on the public internet. It
|
||||||
// returns false for loopback, private (RFC1918), link-local, unspecified,
|
// returns false for loopback, private (RFC1918), link-local, unspecified,
|
||||||
// multicast, and unique-local addresses. IPv4-mapped IPv6 addresses are
|
// 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,
|
// (6to4, Teredo, NAT64) are rejected wholesale rather than recursed into,
|
||||||
// because a host that routes them implicitly trusts the IPv4 mapping and
|
// because a host that routes them implicitly trusts the IPv4 mapping and
|
||||||
// the prefixes themselves are deprecated or translation-only. See
|
// 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 {
|
func IsPublicIP(addr netip.Addr) bool {
|
||||||
if !addr.IsValid() {
|
if !addr.IsValid() {
|
||||||
return false
|
return false
|
||||||
@@ -105,6 +118,13 @@ func IsPublicIP(addr netip.Addr) bool {
|
|||||||
addr.IsInterfaceLocalMulticast():
|
addr.IsInterfaceLocalMulticast():
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if addr.Is4() {
|
||||||
|
for _, p := range nonPublicIPv4Prefixes {
|
||||||
|
if p.Contains(addr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if addr.Is6() {
|
if addr.Is6() {
|
||||||
for _, p := range nonPublicIPv6Prefixes {
|
for _, p := range nonPublicIPv6Prefixes {
|
||||||
if p.Contains(addr) {
|
if p.Contains(addr) {
|
||||||
|
|||||||
@@ -40,6 +40,24 @@ func TestIsPublicIP(t *testing.T) {
|
|||||||
|
|
||||||
// Link-local.
|
// Link-local.
|
||||||
{"169.254.169.254", false},
|
{"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},
|
{"fe80::1", false},
|
||||||
|
|
||||||
// Unique-local.
|
// Unique-local.
|
||||||
|
|||||||
@@ -120,6 +120,30 @@ func (engine *QPdf) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue
|
|||||||
return append(attrs, extra...)
|
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.
|
// Split splits a given PDF file.
|
||||||
func (engine *QPdf) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
|
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",
|
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())
|
span.SetStatus(codes.Error, err.Error())
|
||||||
return nil, err
|
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, inputPath)
|
||||||
args = append(args, engine.globalArgs...)
|
args = append(args, engine.globalArgs...)
|
||||||
args = append(args, "--pages", ".", mode.Span)
|
args = append(args, "--pages", ".", mode.Span)
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package qpdf
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStripQpdfStringPrefix(t *testing.T) {
|
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