Skip to content

Commit

Permalink
feat: validate icon images
Browse files Browse the repository at this point in the history
- check that icon is square and meets minimal size
- check icon file size is not excessive
- check that png icon is not animated
- add libraries to handle webp and apng
  • Loading branch information
teleclimber committed Jul 13, 2023
1 parent 0ffed35 commit 8daf0f8
Show file tree
Hide file tree
Showing 7 changed files with 170 additions and 92 deletions.
74 changes: 4 additions & 70 deletions cmd/ds-host/appops/appgetter.go
Expand Up @@ -6,12 +6,9 @@ import (
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path"
"path/filepath"
"sort"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -649,39 +646,14 @@ func (g *AppGetter) validateAppIcon(keyData appGetData, meta *domain.AppGetMeta)
if meta.VersionManifest.Icon == "" {
return nil
}
// steps to validate:
// - Stat the file
// - if no exist or is dir, warn/err
// - get mime type, look for image
// - verify filename suffix is legit .png, jpb, jpeg, ....
// - check filesize? Warn if bigger than...?

icon = filepath.Join(g.AppLocation2Path.Files(keyData.locationKey), icon)
mimeType, err := getFileMimeType(icon)
if os.IsNotExist(err) {
meta.Warnings["icon"] = "App icon not found at package path " + meta.VersionManifest.Icon
return nil
}
if err != nil {
meta.Warnings["icon"] = "Error processing app icon: " + err.Error()
return nil
}
mimeTypes := []string{"image/jpeg", "image/png", "image/svg+xml", "image/webp"}
typeOk := false
for _, t := range mimeTypes {
if t == mimeType {
typeOk = true
if validateIcon(meta, icon) {
err = g.AppFilesModel.WriteFileLink(keyData.locationKey, "app-icon", meta.VersionManifest.Icon)
if err != nil {
return err
}
}
if !typeOk {
meta.Warnings["icon"] = "App icon type not supported: " + mimeType + " Jpeg, png, svg and webp are supported."
return nil
}

err = g.AppFilesModel.WriteFileLink(keyData.locationKey, "app-icon", meta.VersionManifest.Icon)
if err != nil {
return err
}

return nil
}
Expand Down Expand Up @@ -910,41 +882,3 @@ func randomKey() domain.AppGetKey {
}
return domain.AppGetKey(string(b))
}

func getFileMimeType(p string) (string, error) {
f, err := os.Open(p)
if err != nil {
return "", err
}
defer f.Close()
fInfo, err := f.Stat()
if err != nil {
return "", err
}
if fInfo.IsDir() {
return "", errors.New("path is a directory")
}
byteSlice := make([]byte, 512)
_, err = f.Read(byteSlice)
if err != nil {
return "", fmt.Errorf("error reading bytes from file: %w", err)
}
contentType := http.DetectContentType(byteSlice)

return contentType, nil
}

func validatePackagePath(p string) (string, bool) {
if p == "" {
return "", true
}
p = path.Clean(p)
if p == "" || p == "." || p == "/" || strings.Contains(p, "..") || strings.Contains(p, "\\") {
return "", false
}
if path.IsAbs(p) {
// accept abolute but turn it to relative
p = p[1:]
}
return p, true
}
139 changes: 139 additions & 0 deletions cmd/ds-host/appops/appvalidations.go
Expand Up @@ -2,8 +2,21 @@ package appops

import (
"fmt"
"image"
"net/http"
"os"
"path"
"strings"

_ "image/gif"
_ "image/jpeg"
_ "image/png"

"golang.org/x/image/webp"

"github.com/blang/semver/v4"
"github.com/inhies/go-bytesize"
"github.com/kettek/apng"
"github.com/mazznoer/csscolorparser"
"github.com/rivo/uniseg"
"github.com/teleclimber/DropServer/cmd/ds-host/domain"
Expand Down Expand Up @@ -94,3 +107,129 @@ func validateSoftData(meta *domain.AppGetMeta) {
}
}
}

func validateIcon(meta *domain.AppGetMeta, iconPath string) bool {
f, err := os.Open(iconPath)
if os.IsNotExist(err) {
meta.Warnings["icon"] = "App icon not found at package path " + meta.VersionManifest.Icon
return false
}
if err != nil {
meta.Warnings["icon"] = "Error processing app icon: " + err.Error()
return false
}
defer f.Close()

fInfo, err := f.Stat()
if err != nil {
meta.Warnings["icon"] = "Error getting icon file info: " + err.Error()
return false
}
if fInfo.IsDir() {
meta.Warnings["icon"] = "Error: icon path is a directory"
return false
}

mimeType, err := getFileMimeType(iconPath)
if err != nil {
meta.Warnings["icon"] = "Error getting app icon mime type: " + err.Error()
return false
}

mimeTypes := []string{"image/jpeg", "image/png", "image/svg+xml", "image/webp"}
typeOk := false
for _, t := range mimeTypes {
if t == mimeType {
typeOk = true
}
}
if !typeOk {
meta.Warnings["icon"] = "App icon type not supported: " + mimeType + " Jpeg, png, svg and webp are supported."
return false
}

// get w and h and check: is square and then size.
var config image.Config
if mimeType == "image/jpeg" || mimeType == "image/png" {
config, _, err = image.DecodeConfig(f)
if err != nil {
meta.Warnings["icon"] = "Error reading app icon file. " + err.Error()
return false
}
} else if mimeType == "image/webp" {
config, err = webp.DecodeConfig(f)
if err != nil {
meta.Warnings["icon"] = "Error reading app icon file. " + err.Error()
return false
}
}
if mimeType == "image/jpeg" || mimeType == "image/png" || mimeType == "image/webp" {
if config.Height != config.Width {
meta.Warnings["icon"] = fmt.Sprintf("App icon is not square: %v x %v.", config.Width, config.Height)
} else if config.Height < domain.AppIconMinPixelSize {
meta.Warnings["icon"] = fmt.Sprintf("App icon should be at least %v pixels. It is %v pixels.", domain.AppIconMinPixelSize, config.Width)
}
}

if fInfo.Size() > domain.AppIconMaxFileSize {
appendWarning(meta, "icon", fmt.Sprintf("App icon file is large: %s (under %s is recommended).",
bytesize.New(float64(fInfo.Size())), bytesize.New(float64(domain.AppIconMaxFileSize))))
}

if mimeType == "image/png" {
// need to open again so the decoder can work from the beginning
fPng, err := os.Open(iconPath)
if err != nil {
meta.Warnings["icon"] = "Error opening PNG app icon: " + err.Error()
return false
}
defer fPng.Close()
a, err := apng.DecodeAll(fPng)
if err != nil {
meta.Warnings["icon"] = "Error opening decoding PNG app icon: " + err.Error()
}
if len(a.Frames) > 1 {
appendWarning(meta, "icon", "App icon appears to be animated. Non-animated icons are preferred.")
}
}

return true
}

func getFileMimeType(p string) (string, error) {
f, err := os.Open(p)
if err != nil {
return "", err
}
byteSlice := make([]byte, 512)
_, err = f.Read(byteSlice)
if err != nil {
return "", fmt.Errorf("error reading bytes from file: %w", err)
}
contentType := http.DetectContentType(byteSlice)

return contentType, nil
}

func validatePackagePath(p string) (string, bool) {
if p == "" {
return "", true
}
p = path.Clean(p)
if p == "" || p == "." || p == "/" || strings.Contains(p, "..") || strings.Contains(p, "\\") {
return "", false
}
if path.IsAbs(p) {
// accept abolute but turn it to relative
p = p[1:]
}
return p, true
}

func appendWarning(meta *domain.AppGetMeta, key string, warning string) {
w := meta.Warnings[key]
if w != "" {
w = w + " "
}
meta.Warnings[key] = w + warning
}
3 changes: 3 additions & 0 deletions cmd/ds-host/domain/constants.go
Expand Up @@ -9,6 +9,9 @@ const AppManifestMaxFileSize = int64(1 << 10 * 10) // 10kb
const AppNameMaxLength = 30
const AppShortDescriptionMaxLength = 60

const AppIconMinPixelSize = 160
const AppIconMaxFileSize = int64(1 << 10 * 50) // 50kb

// ZipBackupExtractedPackageMaxSize is the max size that a backup file is allowed to inflate to.
// 1Gb for now.
const ZipBackupExtractedPackageMaxSize = int64(1 << 30)
31 changes: 11 additions & 20 deletions cmd/ds-host/domain/domain.go
Expand Up @@ -428,45 +428,36 @@ type AppVersionManifest struct {
Schema int `json:"schema"`
// Migrations is list of migrations provided by this app version
Migrations []MigrationStep `json:"migrations"`
// LibVersion is the version of the lib support.
// Determined automatically at packaging time (?)
// Required.
LibVersion string `json:"lib-version"`

// CodeState tells the system of processing needed before running the app
// Like remote fetch of modules and compile TS.
// Note that this can be determined entirely from the installer instance, so provided here for uer information purposes only?
CodeState string `json:"code-state"` // Unclear on actual need for this. I think it is needed, just not clrear how/where. really an enum: "remote", "ts", ""

// Icon is a package-relative path to an icon file to display within the installer instance UI.
// Optional.
Icon string `json:"icon"`
//AccentColor is a CSS color used to highlight the app in the UI
//AccentColor is a CSS color used to differentiate the app in the Dropserver UI
AccentColor string `json:"accent-color"`

Description string `json:"description"` // link to markdown file? I18N??
ReleaseNotes string `json:"release-notes"` // link to release notes markdown?
// Both of these are not currently handled.
// Description string `json:"description"` // link to markdown file? I18N??
// ReleaseNotes string `json:"release-notes"` // link to release notes markdown?

// Authors
Authors []ManifestAuthor `json:"authors"`

// Code, typically URL of git repo
Code string `json:"code"` // code repo
// Code is the URL of the code repository
Code string `json:"code"`
// Website for the app
Website string `json:"website"`
// Funding website or site where funding situation is explained
Funding string `json:"funding"` // should maybe not be a string only...
// License in SPDX form
// License in SPDX string form
License string `json:"license"`
// LicenseFile is a package-relative path to a txt file containing the license text.
LicenseFile string `json:"license-file"` // Rel path to license file within package.

//ReleaseDate YYYY-MM-DD of software release date. Should be set automatically by packaging code.
ReleaseDate string `json:"release-date"` // date of packaging.

// Signature of the package. Should be omitted if metadata is inside the package.
Signature string `json:"signature,omitempty"` // is string enough by itself?

// Size of the installed package in bytes (except that additional space will be taken up when fetching remote modules if applicable)
// Although maybe the actual installed size can be measured by the packaging system?
Size int `json:"size"`
// Size int `json:"size"`
}

type AppGetKey string
Expand Down
3 changes: 2 additions & 1 deletion frontend-ds-dev/src/components/AppPanel.vue
Expand Up @@ -80,8 +80,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5 inline">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg>
{{ p_event.warnings.icon }}
</span>
<img v-if="appData.manifest?.icon && !p_event.warnings.icon" :src="app_icon" class="border border-gray-300 h-20 w-20"/>
<img v-if="appData.manifest?.icon" :src="app_icon" class="border border-gray-300 h-20 w-20"/>
</p>
<p class="flex items-center">
<span class="mr-1">Accent Color:</span>
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Expand Up @@ -18,7 +18,9 @@ require (
github.com/google/go-cmp v0.5.9
github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.5.0 // indirect
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
github.com/jmoiron/sqlx v1.3.5
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/mattn/go-sqlite3 v1.14.16
Expand All @@ -38,6 +40,7 @@ require (
go.uber.org/multierr v1.9.0 // indirect
go.uber.org/zap v1.24.0 // indirect
golang.org/x/crypto v0.7.0
golang.org/x/image v0.9.0
golang.org/x/sys v0.6.0
gopkg.in/validator.v2 v2.0.1
)
Expand Down
9 changes: 8 additions & 1 deletion go.sum
Expand Up @@ -178,6 +178,8 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
Expand All @@ -189,6 +191,8 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 h1:8tP9cdXzcGX2AvweVVG/lxbI7BSjWbNNUustwJ9dQVA=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
Expand Down Expand Up @@ -346,6 +350,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.9.0 h1:QrzfX26snvCM20hIhBwuHI/ThTg18b/+kcKdXHvnR+g=
golang.org/x/image v0.9.0/go.mod h1:jtrku+n79PfroUbvDdeUWMAI+heR786BofxrbiSF+J0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down Expand Up @@ -505,8 +511,9 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
Expand Down

0 comments on commit 8daf0f8

Please sign in to comment.