From 1a4fde0c93c705188d51c3967f598dad6179d120 Mon Sep 17 00:00:00 2001 From: Kleissner Date: Sat, 26 Mar 2022 00:21:25 +0100 Subject: [PATCH] New system submodule to support package files. --- Config Default.yaml | 1 + Config.go | 1 + go.mod | 16 +++-- go.sum | 39 ++++++------ system/Execute.go | 91 +++++++++++++++++++++++++++ system/Read Package.go | 137 +++++++++++++++++++++++++++++++++++++++++ system/readme.md | 52 ++++++++++++++++ 7 files changed, 313 insertions(+), 24 deletions(-) create mode 100644 system/Execute.go create mode 100644 system/Read Package.go create mode 100644 system/readme.md diff --git a/Config Default.yaml b/Config Default.yaml index 655d680..d76f920 100644 --- a/Config Default.yaml +++ b/Config Default.yaml @@ -5,6 +5,7 @@ BlockchainGlobal: "data/blockchain global/" # Blockchain global caches block WarehouseMain: "data/warehouse main/" # Warehouse main stores the actual data of files shared by the end-user. SearchIndex: "data/search index/" # Local search index of blockchain records. Empty to disable. GeoIPDatabase: "data/GeoLite2-City.mmdb" # GeoLite2 City database to provide GeoIP information. +DataFolder: "data/" # Data folder. # Listen defines all IP:Port combinations to listen on. If empty, it will listen on all IPs automatically on available ports. # IPv6 must be in the form "[IPv6]:Port". This setting is only recommended to be set on servers. diff --git a/Config.go b/Config.go index ef79431..8791cc9 100644 --- a/Config.go +++ b/Config.go @@ -29,6 +29,7 @@ type Config struct { WarehouseMain string `yaml:"WarehouseMain"` // Warehouse main stores the actual data of files shared by the end-user. SearchIndex string `yaml:"SearchIndex"` // Local search index of blockchain records. Empty to disable. GeoIPDatabase string `yaml:"GeoIPDatabase"` // GeoLite2 City database to provide GeoIP information. + DataFolder string `yaml:"DataFolder"` // Data folder. // Target for the log messages: 0 = Log file, 1 = Stdout, 2 = Log file + Stdout, 3 = None LogTarget int `yaml:"LogTarget"` diff --git a/go.mod b/go.mod index ce3db04..b3880af 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/PeernetOfficial/core -go 1.16 +go 1.18 require ( github.com/IncSW/geoip2 v0.1.2 @@ -8,10 +8,16 @@ require ( github.com/enfipy/locker v1.1.0 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 - github.com/gorilla/websocket v1.4.2 - golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 - golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd - golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a + github.com/gorilla/websocket v1.5.0 + golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 + golang.org/x/net v0.0.0-20220325170049-de3da57026de + golang.org/x/sys v0.0.0-20220325203850-36772127a21f + gopkg.in/ini.v1 v1.66.4 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b lukechampine.com/blake3 v1.1.7 ) + +require ( + github.com/klauspost/cpuid/v2 v2.0.12 // indirect + github.com/stretchr/testify v1.7.1 // indirect +) diff --git a/go.sum b/go.sum index fb53e45..c21b13f 100644 --- a/go.sum +++ b/go.sum @@ -2,34 +2,35 @@ github.com/IncSW/geoip2 v0.1.2 h1:v7iAyDiNZjHES45P1JPM3SMvkw0VNeJtz0XSVxkRwOY= github.com/IncSW/geoip2 v0.1.2/go.mod h1:adcasR40vXiUBjtzdaTTKL/6wSf+fgO4M8Gve/XzPUk= github.com/akrylysov/pogreb v0.10.1 h1:FqlR8VR7uCbJdfUob916tPM+idpKgeESDXOA1K0DK4w= github.com/akrylysov/pogreb v0.10.1/go.mod h1:pNs6QmpQ1UlTJKDezuRWmaqkgUE2TuU0YTWyqJZ7+lI= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/enfipy/locker v1.1.0 h1:2zVJ0ky7cS1Vjs0x6OQWFiT2dSEiHrI5/O2KCz1fgGc= github.com/enfipy/locker v1.1.0/go.mod h1:uuj+dvWHECshK8rkHcw+ZOb9SLo16yc0Em/JGUqRqko= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a h1:ppl5mZgokTT8uPkmYOyEUmPTr3ypaKkg5eFOGrAmxxE= -golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +github.com/klauspost/cpuid/v2 v2.0.12 h1:p9dKCg8i4gmOxtv35DvrYoWqYzQrvEVdjQ762Y0OqZE= +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20220325170049-de3da57026de h1:pZB1TWnKi+o4bENlbzAgLrEbY4RMYmUIRobMcSmfeYc= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/sys v0.0.0-20220325203850-36772127a21f h1:TrmogKRsSOxRMJbLYGrB4SBbW+LJcEllYBLME5Zk5pU= +golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= +gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= diff --git a/system/Execute.go b/system/Execute.go new file mode 100644 index 0000000..272af34 --- /dev/null +++ b/system/Execute.go @@ -0,0 +1,91 @@ +/* +File Name: Execute.go +Copyright: 2021 Peernet s.r.o. +Author: Peter Kleissner +*/ + +package system + +import ( + "archive/zip" + "errors" + "io" + "os" + "path/filepath" + "strings" +) + +// Execute the actions described in the info header +func (update *UpdatePackage) Execute(DataFolder, PluginFolder string) (err error) { + for _, action := range update.Header.Actions { + switch action.Action { + case "extract": + destination := resolveFolders(action.Target, DataFolder, PluginFolder) + + for _, f := range update.Reader.File { + // filter the filename based on source + if !strings.HasPrefix(f.Name, action.Source) { + continue + } + + err := unzipFile(f, destination) + if err != nil { + return err + } + } + } + } + + return nil +} + +// Deletes the update package file +func (update *UpdatePackage) Delete() (err error) { + return os.Remove(update.Filename) +} + +func unzipFile(f *zip.File, destination string) error { + // 4. Check if file paths are not vulnerable to Zip Slip + filePath := filepath.Join(destination, f.Name) + if !strings.HasPrefix(filePath, filepath.Clean(destination)+string(os.PathSeparator)) { + return errors.New("invalid file path: " + filePath) + } + + // 5. Create directory tree + if f.FileInfo().IsDir() { + if err := os.MkdirAll(filePath, os.ModePerm); err != nil { + return err + } + return nil + } + + if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { + return err + } + + // 6. Create a destination file for unzipped content + destinationFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer destinationFile.Close() + + // 7. Unzip the content of a file and copy it to the destination file + zippedFile, err := f.Open() + if err != nil { + return err + } + defer zippedFile.Close() + + if _, err := io.Copy(destinationFile, zippedFile); err != nil { + return err + } + return nil +} + +func resolveFolders(folder, dataFolder, pluginFolder string) (resolved string) { + resolved = strings.Replace(folder, "%plugin%", pluginFolder, 1) + resolved = strings.Replace(resolved, "%data%", dataFolder, 1) + + return resolved +} diff --git a/system/Read Package.go b/system/Read Package.go new file mode 100644 index 0000000..fac5b1c --- /dev/null +++ b/system/Read Package.go @@ -0,0 +1,137 @@ +/* +File Name: Read Package.go +Copyright: 2021 Peernet s.r.o. +Author: Peter Kleissner +*/ + +package system + +import ( + "archive/zip" + "io/ioutil" + "path" + "strings" + + "gopkg.in/ini.v1" +) + +type UpdatePackage struct { + Filename string // Filename of the update package + Err error // Parsing error if any + Header *IniFile // Header info + Reader *zip.ReadCloser // Access to files in the ZIP file +} + +// IniFile contains the parsed data from the info.ini file +type IniFile struct { + Name string // Name of the package + Organization string // Organization that created this package + Architecture string // Target architecture. For example "windows/amd64". + Actions []IniAction // Actions to take +} + +type IniAction struct { + Action string `ini:"action"` // Action: extract, delete + Source string `ini:"source"` // Folder or file in the ZIP file + Target string `ini:"target"` // Target folder or file. Certain virtual folders such as "%plugins%" are supported. +} + +const IniFilename = "info.ini" + +// ParseUpdateFiles returns a list of parsed update packages. +// It will check each file in the directory if a ZIP file containing a valid info.ini file. +// The caller must close all returned readers. +func ParseUpdateFiles(Directory string) (files []UpdatePackage, err error) { + // check all files in the directory + filesDir, err := ioutil.ReadDir(Directory) + if err != nil { + return nil, err + } + + for _, file := range filesDir { + if file.IsDir() { + continue + } + + filename := file.Name() + + // ZIP file? + if strings.ToLower(path.Ext(filename)) == ".zip" { + filenamePath := path.Join(Directory, filename) + + // check if the ZIP archive contains a info.ini file + reader, err := zip.OpenReader(filenamePath) + if err != nil { // invalid ZIP file + continue + } + + // read info.ini file + file, err := reader.Open(IniFilename) + if err != nil { + continue + } + + data, err := ioutil.ReadAll(file) + if err != nil { + continue + } + + header, err := ParseIniFile(data) + + files = append(files, UpdatePackage{Filename: filenamePath, Err: err, Header: header, Reader: reader}) + } + } + + return files, nil +} + +func ParseIniFile(data []byte) (header *IniFile, err error) { + inidata, err := ini.Load(data) + if err != nil { + return nil, err + } + + // parse the main section first + section, err := inidata.GetSection("main") + if err != nil { + return nil, err + } + + name, err := section.GetKey("name") + if err != nil { + return nil, err + } + + organization, err := section.GetKey("organization") + if err != nil { + return nil, err + } + + architecture, err := section.GetKey("architecture") + if err != nil { + return nil, err + } + + header = &IniFile{ + Name: name.String(), + Organization: organization.String(), + Architecture: architecture.String(), + } + + // parse any other section + for _, section := range inidata.Sections() { + if section.Name() == "main" || section.Name() == "DEFAULT" { + continue + } + + var action IniAction + + if section.MapTo(&action) != nil { + continue + } + + header.Actions = append(header.Actions, action) + } + + return header, nil +} diff --git a/system/readme.md b/system/readme.md new file mode 100644 index 0000000..59bb773 --- /dev/null +++ b/system/readme.md @@ -0,0 +1,52 @@ +# System + +This submodule defines how files and plugins are managed and updated. + +## Package Files + +Package files are ZIP files containing a file `info.ini` that describes which actions to take. They provide updates and installation of individual files, plugins, or entire clients. + +Actions may specify target files or folders. Such paths may use virtual paths that will be automatically resolved. + +``` +%plugin% = Plugin folder +%data% = Data folder +``` + +## Sample Package File + +A sample package file `TextViewer.zip` looks like this: + +``` +│ info.ini +│ +└───TextViewer + Microsoft.Extensions.DependencyInjection.Abstractions.dll + Peernet.Browser.Plugins.TextViewer.deps.json + Peernet.Browser.Plugins.TextViewer.dll + Peernet.SDK.dll +``` + +The `info.ini` file: + +```ini +[main] +name = "Text Viewer Plugin" +organization = "Peernet s.r.o." +architecture = "windows/amd64" + +[action1] +action = extract +source = TextViewer +target = "%plugin%" +``` + +## Security Implications + +The actions currently only allow to extract any arbitrary file to the local disk. Later, the destination directory will be limited to the Peernet installation directory. + +Signatures for packages are not implemented yet. They will be added in a subsequent version. + +## Target OS + +It is up to the client to select and download the appropriate packages.