diff --git a/cmd/create.go b/cmd/create.go index 731d0310..532b8f8e 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -72,7 +72,7 @@ func runCreate(cmd *cobra.Command, _ []string) { var file *os.File if writePath != "" { - file, err = os.OpenFile(writePath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666) + file, err = os.OpenFile(filepath.Clean(writePath), os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0600) if err != nil { panic(err) diff --git a/cmd/seed.go b/cmd/seed.go index a5637fa9..53138902 100644 --- a/cmd/seed.go +++ b/cmd/seed.go @@ -87,7 +87,7 @@ func initSeed() { rootCmd.AddCommand(seedCmd) seedCmd.Flags().StringP("layer", "l", "", "The ID of the layer to seed") - seedCmd.MarkFlagRequired("layer") //nolint:errcheck + err := seedCmd.MarkFlagRequired("layer") seedCmd.Flags().BoolP("verbose", "v", false, "Output verbose information including every tile being requested and success or error status") seedCmd.Flags().UintSliceP("zoom", "z", []uint{0, 1, 2, 3, 4, 5}, "The zoom level(s) to seed") seedCmd.Flags().Float32P("min-latitude", "s", -90, "The minimum latitude to seed. The south side of the bounding box") @@ -97,4 +97,8 @@ func initSeed() { seedCmd.Flags().Bool("force", false, "Perform the seeding even if it'll produce an excessive number of tiles. Without this flag seeds over 10k tiles will error out. \nWarning: Overriding this protection absolutely can cause an Out-of-Memory error") seedCmd.Flags().Uint16P("threads", "t", 1, "How many concurrent requests to use to perform seeding. Be mindful of spamming upstream providers") // TODO: support some way to support writing just to a specific cache when Multi cache is being used + + if err != nil { + panic(err) + } } diff --git a/cmd/test.go b/cmd/test.go index 3892d5a4..b07741b9 100644 --- a/cmd/test.go +++ b/cmd/test.go @@ -39,9 +39,9 @@ Example: func runTest(cmd *cobra.Command, _ []string) { layerNames, err1 := cmd.Flags().GetStringSlice("layer") - z, err2 := cmd.Flags().GetUint("z-coordinate") - x, err3 := cmd.Flags().GetUint("y-coordinate") - y, err4 := cmd.Flags().GetUint("x-coordinate") + z, err2 := cmd.Flags().GetInt("z-coordinate") + x, err3 := cmd.Flags().GetInt("y-coordinate") + y, err4 := cmd.Flags().GetInt("x-coordinate") noCache, err5 := cmd.Flags().GetBool("no-cache") numThread, err6 := cmd.Flags().GetUint16("threads") out := rootCmd.OutOrStdout() @@ -59,7 +59,7 @@ func runTest(cmd *cobra.Command, _ []string) { return } - errCount, err := tg.Test(cfg, tg.TestOptions{LayerNames: layerNames, Z: int(z), X: int(x), Y: int(y), NumThread: numThread, NoCache: noCache}, out) + errCount, err := tg.Test(cfg, tg.TestOptions{LayerNames: layerNames, Z: z, X: x, Y: y, NumThread: numThread, NoCache: noCache}, out) if err != nil { fmt.Fprintf(out, "Error: %v", err) @@ -87,9 +87,9 @@ func initTest() { rootCmd.AddCommand(testCmd) testCmd.Flags().StringSliceP("layer", "l", []string{}, "The ID(s) of the layer to test. Tests all layers by default") - testCmd.Flags().UintP("z-coordinate", "z", 10, "The z coordinate to use to test") //nolint:mnd - testCmd.Flags().UintP("x-coordinate", "x", 123, "The x coordinate to use to test") //nolint:mnd - testCmd.Flags().UintP("y-coordinate", "y", 534, "The y coordinate to use to test") //nolint:mnd + testCmd.Flags().IntP("z-coordinate", "z", 10, "The z coordinate to use to test") //nolint:mnd + testCmd.Flags().IntP("x-coordinate", "x", 123, "The x coordinate to use to test") //nolint:mnd + testCmd.Flags().IntP("y-coordinate", "y", 534, "The y coordinate to use to test") //nolint:mnd testCmd.Flags().Bool("no-cache", false, "Don't write to the cache. The Cache configuration must still be syntactically valid") testCmd.Flags().Uint16P("threads", "t", 1, "How many layers to test at once. Be mindful of spamming upstream providers") //TODO: output in custom format or write to file diff --git a/internal/caches/disk.go b/internal/caches/disk.go index 0c25a85b..05d7c9a6 100644 --- a/internal/caches/disk.go +++ b/internal/caches/disk.go @@ -76,7 +76,7 @@ func (s DiskRegistration) Initialize(configAny any, errorMessages config.ErrorMe func (c Disk) Lookup(_ context.Context, t pkg.TileRequest) (*pkg.Image, error) { filename := requestToFilename(t) - b, err := os.ReadFile(filepath.Join(c.Path, filename)) + b, err := os.ReadFile(filepath.Clean(filepath.Join(c.Path, filename))) if errors.Is(err, os.ErrNotExist) { return nil, nil diff --git a/internal/caches/memcache.go b/internal/caches/memcache.go index 0fa4b583..eb0a4ef2 100644 --- a/internal/caches/memcache.go +++ b/internal/caches/memcache.go @@ -35,7 +35,7 @@ type MemcacheConfig struct { HostAndPort `mapstructure:",squash"` Servers []HostAndPort // The list of servers to use. KeyPrefix string // Prefix to keynames stored in cache - TTL uint32 // Cache expiration in seconds. Max of 30 days. Default to 1 day + TTL uint // Cache expiration in seconds. Max of 30 days. Default to 1 day } type Memcache struct { @@ -107,5 +107,5 @@ func (c Memcache) Save(_ context.Context, t pkg.TileRequest, img *pkg.Image) err return err } - return c.client.Set(&memcache.Item{Key: c.KeyPrefix + t.String(), Value: val, Expiration: int32(c.TTL)}) + return c.client.Set(&memcache.Item{Key: c.KeyPrefix + t.String(), Value: val, Expiration: int32(c.TTL)}) // #nosec G115 -- max value applied in Initialize } diff --git a/internal/checks/cache.go b/internal/checks/cache.go index 834f4d52..0f72b59b 100644 --- a/internal/checks/cache.go +++ b/internal/checks/cache.go @@ -76,7 +76,7 @@ const hexBase = 16 const maxColorInt = 0xFFFFFF func makeImage() (pkg.Image, error) { - col := strconv.FormatUint(rand.Uint64N(maxColorInt), hexBase) + col := strconv.FormatUint(rand.Uint64N(maxColorInt), hexBase) // #nosec G404 if len(col) < numColorDigits { col = strings.Repeat("0", numColorDigits-len(col)) + col } diff --git a/internal/images/images.go b/internal/images/images.go index a761c217..3fe9c57d 100644 --- a/internal/images/images.go +++ b/internal/images/images.go @@ -26,6 +26,7 @@ import ( "image/png" "math/rand" "os" + "path/filepath" "strings" ) @@ -117,6 +118,7 @@ func GetStaticImage(path string) (*[]byte, error) { } if failedImages[path] != nil { + //#nosec G404 if rand.Float32()*100 > 1 { return nil, failedImages[path] } @@ -126,7 +128,7 @@ func GetStaticImage(path string) (*[]byte, error) { return getColorImage(path) } - img, err := os.ReadFile(path) + img, err := os.ReadFile(filepath.Clean(path)) if img != nil { dynamicImages[path] = &img @@ -160,7 +162,12 @@ func getColorImage(path string) (*[]byte, error) { return nil, err } - writer.Flush() + err = writer.Flush() + + if err != nil { + return nil, err + } + output := buf.Bytes() dynamicImages[path] = &output diff --git a/internal/providers/blend.go b/internal/providers/blend.go index 9b11ca6e..53510b4a 100644 --- a/internal/providers/blend.go +++ b/internal/providers/blend.go @@ -261,10 +261,20 @@ func (t Blend) GenerateTile(ctx context.Context, providerContext layer.ProviderC writer := bufio.NewWriter(&buf) err := png.Encode(writer, combinedImg) - writer.Flush() + + if err != nil { + return nil, err + } + + err = writer.Flush() + + if err != nil { + return nil, err + } + output := buf.Bytes() - return &pkg.Image{Content: output, ContentType: mimePng, ForceSkipCache: skipWrite.Load()}, err + return &pkg.Image{Content: output, ContentType: mimePng, ForceSkipCache: skipWrite.Load()}, nil } func (t Blend) blendImage(ctx context.Context, img image.Image, size image.Point, combinedImg image.Image) image.Image { diff --git a/internal/providers/effect.go b/internal/providers/effect.go index 4067c25a..98e131dc 100644 --- a/internal/providers/effect.go +++ b/internal/providers/effect.go @@ -142,8 +142,18 @@ func (t Effect) GenerateTile(ctx context.Context, providerContext layer.Provider writer := bufio.NewWriter(&buf) err = png.Encode(writer, realImage) - writer.Flush() + + if err != nil { + return nil, err + } + + err = writer.Flush() + + if err != nil { + return nil, err + } + output := buf.Bytes() - return &pkg.Image{Content: output, ContentType: mimePng, ForceSkipCache: img.ForceSkipCache}, err + return &pkg.Image{Content: output, ContentType: mimePng, ForceSkipCache: img.ForceSkipCache}, nil } diff --git a/internal/providers/postgis_mvt.go b/internal/providers/postgis_mvt.go index af593cd4..95f98202 100644 --- a/internal/providers/postgis_mvt.go +++ b/internal/providers/postgis_mvt.go @@ -18,6 +18,7 @@ import ( "context" "fmt" "log/slog" + "math" "regexp" "slices" "strconv" @@ -37,14 +38,14 @@ var columnRegex = regexp.MustCompile("^[a-zA-Z0-9_]+$") type PostgisMvtConfig struct { Datastore string Table string - Extent uint + Extent uint16 Buffer float64 GID string Geometry string Attributes []string Filter string SourceSRID uint - Limit uint + Limit uint16 } type PostgisMvt struct { @@ -133,7 +134,11 @@ func (t PostgisMvt) GenerateTile(ctx context.Context, _ layer.ProviderContext, r rawEnv := bounds.ToEWKT() bufEnv := bounds.BufferRelative(t.Buffer).ToEWKT() - params := []any{int(t.SourceSRID), rawEnv, t.Extent, int(t.Buffer * float64(t.Extent)), bufEnv, req.LayerName, t.GID} + if t.SourceSRID > math.MaxInt { + return nil, pkg.InvalidSridError{} + } + + params := []any{int(t.SourceSRID), rawEnv, t.Extent, int(t.Buffer * float64(t.Extent)), bufEnv, req.LayerName, t.GID} // #nosec G115 query := `WITH mvtgeom AS(SELECT ST_AsMVTGeom(ST_Transform(ST_SetSRID("` + t.Geometry + `", $1::integer), 3857), $2::geometry, extent => $3, buffer => $4) AS "geom"` diff --git a/internal/providers/transform.go b/internal/providers/transform.go index 09c90e61..959e2c75 100644 --- a/internal/providers/transform.go +++ b/internal/providers/transform.go @@ -114,6 +114,7 @@ func (t Transform) PreAuth(ctx context.Context, providerContext layer.ProviderCo return t.provider.PreAuth(ctx, providerContext) } +// #nosec G115 func (t Transform) transform(ctx context.Context, col color.Color) color.Color { r1, g1, b1, a1 := col.RGBA() r1b := uint8(r1) @@ -201,8 +202,17 @@ func (t Transform) GenerateTile(ctx context.Context, providerContext layer.Provi writer := bufio.NewWriter(&buf) err = png.Encode(writer, resultImage) - writer.Flush() + + if err != nil { + return nil, err + } + + err = writer.Flush() + + if err != nil { + return nil, err + } output := buf.Bytes() - return &pkg.Image{Content: output, ContentType: mimePng, ForceSkipCache: img.ForceSkipCache}, err + return &pkg.Image{Content: output, ContentType: mimePng, ForceSkipCache: img.ForceSkipCache}, nil } diff --git a/internal/providers/url_template.go b/internal/providers/url_template.go index 357852c0..38ac8830 100644 --- a/internal/providers/url_template.go +++ b/internal/providers/url_template.go @@ -96,7 +96,7 @@ func (t URLTemplate) GenerateTile(ctx context.Context, _ layer.ProviderContext, url = strings.ReplaceAll(url, "$zoom", strconv.Itoa(tileRequest.Z)) url = strings.ReplaceAll(url, "$width", strconv.Itoa(int(t.Width))) url = strings.ReplaceAll(url, "$height", strconv.Itoa(int(t.Height))) - url = strings.ReplaceAll(url, "$srs", strconv.Itoa(int(t.Srid))) + url = strings.ReplaceAll(url, "$srs", strconv.FormatUint(uint64(t.Srid), 10)) return getTile(ctx, t.clientConfig, url, make(map[string]string)) } diff --git a/internal/providers/utility.go b/internal/providers/utility.go index c9f7872b..954ea80a 100644 --- a/internal/providers/utility.go +++ b/internal/providers/utility.go @@ -163,6 +163,10 @@ func getTile(ctx context.Context, clientConfig config.ClientConfig, url string, req.Header.Set(h, v) } + if clientConfig.Timeout > math.MaxInt64 { + clientConfig.Timeout = math.MaxInt64 + } + transport := otelhttp.NewTransport(http.DefaultTransport, otelhttp.WithMessageEvents(otelhttp.ReadEvents)) client := http.Client{Transport: transport, Timeout: time.Duration(clientConfig.Timeout) * time.Second} diff --git a/internal/server/health.go b/internal/server/health.go index 89a9891f..7d4b0df5 100644 --- a/internal/server/health.go +++ b/internal/server/health.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" "log/slog" + "math" "net" "net/http" "reflect" @@ -186,9 +187,10 @@ func setupHealthEndpoints(ctx context.Context, h config.HealthConfig, checks []h r.Handle("/health", healthHandler{checks, checkResultCache}) srv := &http.Server{ - Addr: httpHostPort, - BaseContext: func(_ net.Listener) context.Context { return ctx }, - Handler: &r, + Addr: httpHostPort, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + Handler: &r, + ReadHeaderTimeout: time.Second, } go func() { srvErr <- srv.ListenAndServe() }() @@ -219,7 +221,12 @@ func setupCheckRoutines(ctx context.Context, h config.HealthConfig, layerGroup * } for i, check := range checks { - ttl := time.Second * time.Duration(check.GetDelay()) + delay := check.GetDelay() + + if delay > math.MaxInt64 { + delay = math.MaxInt64 + } + ttl := time.Second * time.Duration(delay) // #nosec G115 ticker := time.NewTicker(ttl) done := make(chan bool) diff --git a/internal/server/log.go b/internal/server/log.go index b0354f43..8a1be5f5 100644 --- a/internal/server/log.go +++ b/internal/server/log.go @@ -21,6 +21,7 @@ import ( "log/slog" "net/http" "os" + "path/filepath" "slices" "strings" @@ -44,7 +45,7 @@ func (h slogContextHandler) Handle(ctx context.Context, r slog.Record) error { } func makeLogFileWriter(path string, alsoStdOut bool) (io.Writer, error) { - logFile, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666) + logFile, err := os.OpenFile(filepath.Clean(path), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0600) if err != nil { return nil, err diff --git a/internal/server/server.go b/internal/server/server.go index a7700f7b..0670db2f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log/slog" + "math" "net" "net/http" "os" @@ -98,8 +99,12 @@ func setupHandlers(config *config.Config, layerGroup *layer.LayerGroup, auth aut rootHandler = handlers.CompressHandler(rootHandler) } + if config.Server.Timeout > math.MaxInt64 { + config.Server.Timeout = math.MaxInt64 + } + rootHandler = httpContextHandler{rootHandler, config.Error} - rootHandler = http.TimeoutHandler(rootHandler, time.Duration(config.Server.Timeout)*time.Second, config.Error.Messages.Timeout) + rootHandler = http.TimeoutHandler(rootHandler, time.Duration(config.Server.Timeout)*time.Second, config.Error.Messages.Timeout) // #nosec G115 rootHandler, err = configureAccessLogging(config.Logging.Access, config.Error.Messages, rootHandler) if err != nil { @@ -115,8 +120,14 @@ func listenAndServeTLS(config *config.Config, srvErr chan error, srv *http.Serve if config.Server.Encrypt.Certificate != "" && config.Server.Encrypt.KeyFile != "" { if httpPort != 0 { + srv := &http.Server{ + Addr: httpHostPort, + Handler: httpRedirectHandler{protoAndHost: "https://" + config.Server.Encrypt.Domain}, + ReadHeaderTimeout: time.Second, + } + go func() { - srvErr <- http.ListenAndServe(httpHostPort, httpRedirectHandler{protoAndHost: "https://" + config.Server.Encrypt.Domain}) + srvErr <- srv.ListenAndServe() }() } @@ -136,7 +147,13 @@ func listenAndServeTLS(config *config.Config, srvErr chan error, srv *http.Serve } if httpPort != 0 { - go func() { srvErr <- http.ListenAndServe(httpHostPort, certManager.HTTPHandler(nil)) }() + srv := &http.Server{ + Addr: httpHostPort, + Handler: certManager.HTTPHandler(nil), + ReadHeaderTimeout: time.Second, + } + + go func() { srvErr <- srv.ListenAndServe() }() } srv.TLSConfig = certManager.TLSConfig() @@ -186,9 +203,10 @@ func ListenAndServe(config *config.Config, layerGroup *layer.LayerGroup, auth au } srv := &http.Server{ - Addr: config.Server.BindHost + ":" + strconv.Itoa(config.Server.Port), - BaseContext: func(_ net.Listener) context.Context { return ctx }, - Handler: rootHandler, + Addr: config.Server.BindHost + ":" + strconv.Itoa(config.Server.Port), + BaseContext: func(_ net.Listener) context.Context { return ctx }, + Handler: rootHandler, + ReadHeaderTimeout: time.Second, } srvErr := make(chan error, 1) diff --git a/pkg/entry/seed.go b/pkg/entry/seed.go index 62e1ed1b..d6bc3cdb 100644 --- a/pkg/entry/seed.go +++ b/pkg/entry/seed.go @@ -74,6 +74,10 @@ func Seed(cfg *config.Config, opts SeedOptions, out io.Writer) error { numReq := len(tileRequests) + if numReq > math.MaxUint16 { + return fmt.Errorf("more than %v tiles requested", math.MaxUint16) + } + if opts.NumThread > uint16(numReq) { fmt.Fprintln(os.Stderr, "Warning: more threads requested than tiles") opts.NumThread = uint16(numReq) @@ -116,13 +120,13 @@ func createTileRequests(z uint, curCount int, opts SeedOptions) (*[]pkg.TileRequ tileRequests, err := opts.Bounds.FindTiles(opts.LayerName, z, opts.Force) if err != nil || (curCount > maxCount && !opts.Force) { - count := curCount + count := uint64(curCount) // #nosec G115 if err != nil { var tilesError pkg.TooManyTilesError if errors.As(err, &tilesError) { - count = int(tilesError.NumTiles) + count = tilesError.NumTiles } else { return nil, err } diff --git a/pkg/entry/test.go b/pkg/entry/test.go index 5cd5021d..a0d44283 100644 --- a/pkg/entry/test.go +++ b/pkg/entry/test.go @@ -75,6 +75,10 @@ func Test(cfg *config.Config, opts TestOptions, out io.Writer) (uint32, error) { numReq := len(tileRequests) + if numReq > math.MaxUint16 { + return 0, fmt.Errorf("more than %v tiles requested", math.MaxUint16) + } + if opts.NumThread > uint16(numReq) { fmt.Fprintln(os.Stderr, "Warning: more threads requested than tiles") opts.NumThread = uint16(numReq) @@ -110,8 +114,9 @@ func Test(cfg *config.Config, opts TestOptions, out io.Writer) (uint32, error) { wg.Wait() - writer.Flush() - return errCount, nil + err = writer.Flush() + + return errCount, err } func testTileRequests(layerObjects *layer.LayerGroup, opts TestOptions, errCount *uint32, writer *tabwriter.Writer, wg *sync.WaitGroup, t int, myReqs []pkg.TileRequest) { diff --git a/pkg/tile_request.go b/pkg/tile_request.go index 5ce5a685..52c4851a 100644 --- a/pkg/tile_request.go +++ b/pkg/tile_request.go @@ -143,6 +143,10 @@ func NewBoundsFromGeohash(hashStr string) (Bounds, error) { // Turns a bounding box into a list of the tiles contained in the bounds for an arbitrary zoom level. Limited to 10k tiles unless force is true, then it's limited to 2^32 tiles. func (b Bounds) FindTiles(layerName string, zoom uint, force bool) (*[]TileRequest, error) { + if zoom > MaxZoom { + return nil, RangeError{"z", 0, MaxZoom} + } + z := float64(zoom) lonMin := b.West @@ -181,7 +185,7 @@ func (b Bounds) FindTiles(layerName string, zoom uint, force bool) (*[]TileReque yMax = yMin + 1 } - numTiles := uint64(xMax-xMin) * uint64(yMax-yMin) + numTiles := uint64(xMax-xMin) * uint64(yMax-yMin) // #nosec G115 -- int->uint64 can't overflow until 128 bit processors come out if numTiles > 10000 && !force { return nil, TooManyTilesError{NumTiles: numTiles} diff --git a/pkg/utility.go b/pkg/utility.go index 2f18111a..1697a98a 100644 --- a/pkg/utility.go +++ b/pkg/utility.go @@ -215,8 +215,8 @@ func RandomString() string { if err != nil { // Fallback on v2 rand since better that than a potentially unrecoverable error - i = rand.Uint64() - i2 = rand.Uint64() + i = rand.Uint64() // #nosec G404 + i2 = rand.Uint64() // #nosec G404 } else { i = binary.BigEndian.Uint64(b[0:(length / 2)]) i2 = binary.BigEndian.Uint64(b[(length / 2):length])