From 67ee4a8559b8a00d6de83a13935bfafd74825fb3 Mon Sep 17 00:00:00 2001 From: Martin Geno Date: Sun, 27 Nov 2016 10:46:50 +0100 Subject: [PATCH] add warehost-host and working on warehost-ftp --- .../config.go | 3 +- .../config.yml.example | 0 .../main.go | 25 +- cmd/warehost-ftp/auth.go | 35 ++ cmd/warehost-ftp/config.go | 31 ++ cmd/warehost-ftp/config.yml.example | 9 + cmd/warehost-ftp/driver.go | 333 ++++++++++++++++++ cmd/warehost-ftp/main.go | 62 ++++ cmd/warehost-host/caddy | 43 +++ cmd/warehost-host/config.go | 38 ++ cmd/warehost-host/config.yml.example | 14 + cmd/warehost-host/main.go | 153 ++++++++ 12 files changed, 727 insertions(+), 19 deletions(-) rename cmd/{warehost-auth-dovecot => warehost-auth}/config.go (94%) rename cmd/{warehost-auth-dovecot => warehost-auth}/config.yml.example (100%) rename cmd/{warehost-auth-dovecot => warehost-auth}/main.go (62%) create mode 100644 cmd/warehost-ftp/auth.go create mode 100644 cmd/warehost-ftp/config.go create mode 100644 cmd/warehost-ftp/config.yml.example create mode 100644 cmd/warehost-ftp/driver.go create mode 100644 cmd/warehost-ftp/main.go create mode 100644 cmd/warehost-host/caddy create mode 100644 cmd/warehost-host/config.go create mode 100644 cmd/warehost-host/config.yml.example create mode 100644 cmd/warehost-host/main.go diff --git a/cmd/warehost-auth-dovecot/config.go b/cmd/warehost-auth/config.go similarity index 94% rename from cmd/warehost-auth-dovecot/config.go rename to cmd/warehost-auth/config.go index b86aa53..28c8c83 100644 --- a/cmd/warehost-auth-dovecot/config.go +++ b/cmd/warehost-auth/config.go @@ -2,7 +2,6 @@ package main import ( "io/ioutil" - "log" "gopkg.in/yaml.v2" ) @@ -18,7 +17,7 @@ func ReadConfigFile(path string) *Config { file, _ := ioutil.ReadFile(path) err := yaml.Unmarshal(file, &config) if err != nil { - log.Fatal(err) + panic(err) } return config } diff --git a/cmd/warehost-auth-dovecot/config.yml.example b/cmd/warehost-auth/config.yml.example similarity index 100% rename from cmd/warehost-auth-dovecot/config.yml.example rename to cmd/warehost-auth/config.yml.example diff --git a/cmd/warehost-auth-dovecot/main.go b/cmd/warehost-auth/main.go similarity index 62% rename from cmd/warehost-auth-dovecot/main.go rename to cmd/warehost-auth/main.go index 3e74995..f072622 100644 --- a/cmd/warehost-auth-dovecot/main.go +++ b/cmd/warehost-auth/main.go @@ -1,9 +1,8 @@ package main import ( - "bufio" + "flag" "os" - "os/exec" "database/sql" _ "github.com/lib/pq" @@ -15,41 +14,33 @@ var ( configFile string username string password string - execCmd string config *Config db *sql.DB err error ) func main() { - configFile = os.Args[1] - execCmd = os.Args[2] - - pipe := os.NewFile(uintptr(3), "/dev/fd/3") - defer pipe.Close() - in := bufio.NewReader(pipe) - data, _ := in.ReadBytes(0) - username = string(data[:len(data)-1]) - data, _ = in.ReadBytes(0) - password = string(data[:len(data)-1]) - + flag.StringVar(&configFile, "c", "config.yml", "path of configuration file") + flag.StringVar(&username, "u", "none", "username") + flag.StringVar(&password, "p", "none", "password") + flag.Parse() config = ReadConfigFile(configFile) db, err = sql.Open("postgres", config.Database) if err != nil { - os.Exit(1) + os.Exit(111) } defer db.Close() var realPassword string err = db.QueryRow("select password from login where mail = $1", username).Scan(&realPassword) if err != nil { - os.Exit(1) + os.Exit(3) } output, _ := libpassword.Validate(realPassword, password) if output { - exec.Command("bash", "-c", execCmd).Run() + os.Exit(0) } else { os.Exit(1) } diff --git a/cmd/warehost-ftp/auth.go b/cmd/warehost-ftp/auth.go new file mode 100644 index 0000000..9236953 --- /dev/null +++ b/cmd/warehost-ftp/auth.go @@ -0,0 +1,35 @@ +package main + +import ( + "github.com/jinzhu/gorm" + + liblog "dev.sum7.eu/sum7/warehost/lib/log" + libpassword "dev.sum7.eu/sum7/warehost/lib/password" + system "dev.sum7.eu/sum7/warehost/system" +) + +type WarehostAuth struct { + db *gorm.DB +} + +func (this WarehostAuth) CheckPasswd(user, pass string) (returndata bool, err error) { + returndata = false + var login = system.Login{Username: user} + this.db.Where("mail = ?", user).First(&login) + if login.ID <= 0 { + liblog.Log.Warn("user not found") + return + } + if login.Active { + output, _ := libpassword.Validate(login.Password, pass) + if output { + returndata = true + liblog.Log.Info("done") + } else { + liblog.Log.Warn("wrong password") + } + } else { + liblog.Log.Warn("not active") + } + return +} diff --git a/cmd/warehost-ftp/config.go b/cmd/warehost-ftp/config.go new file mode 100644 index 0000000..8fca2e3 --- /dev/null +++ b/cmd/warehost-ftp/config.go @@ -0,0 +1,31 @@ +package main + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// Config of warehost webserver +type Config struct { + Database string `yaml:"database"` + Log struct { + Path string `yaml:"path"` + } `yaml:"log"` + DatabaseDebug bool `yaml:"databasedebug"` + FTPPath string `yaml:"data"` + Host string `yaml:"host"` + Web string `yaml:"web"` + Port int `yaml:"port"` +} + +// ReadConfigFile reads a config models by path to a yml file +func ReadConfigFile(path string) *Config { + config := &Config{} + file, _ := ioutil.ReadFile(path) + err := yaml.Unmarshal(file, &config) + if err != nil { + panic(err) + } + return config +} diff --git a/cmd/warehost-ftp/config.yml.example b/cmd/warehost-ftp/config.yml.example new file mode 100644 index 0000000..88c10d5 --- /dev/null +++ b/cmd/warehost-ftp/config.yml.example @@ -0,0 +1,9 @@ +--- +database: "host=localhost user=warehost dbname=warehost password=hallo sslmode=disable" +log: + path: test.log +databasedebug: false +port: 2222 +data: /tmp/ftp/%d/ +host: /tmp/ftp-domain/%s/ +web: /tmp/ftp-web/%d/ diff --git a/cmd/warehost-ftp/driver.go b/cmd/warehost-ftp/driver.go new file mode 100644 index 0000000..cdaa53e --- /dev/null +++ b/cmd/warehost-ftp/driver.go @@ -0,0 +1,333 @@ +package main + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "time" + + ftpd "github.com/goftp/server" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/postgres" + + system "dev.sum7.eu/sum7/warehost/system" +) + +type FileDriver struct { + RootPath string + db *gorm.DB + Perm ftpd.Perm + login system.Login + conn *ftpd.Conn +} + +type FackFileInfo struct { + os.FileInfo + + name string +} + +func (f *FackFileInfo) Mode() os.FileMode { + return os.ModeDir +} +func (f *FackFileInfo) IsDir() bool { + return true +} + +func (f *FackFileInfo) Name() string { + return f.name +} +func (f *FackFileInfo) Owner() string { + return "warehost" +} +func (f *FackFileInfo) Size() int64 { + return 0 +} +func (f *FackFileInfo) ModTime() time.Time { + return time.Now() +} + +func (f *FackFileInfo) Group() string { + return "http" +} + +type FileInfo struct { + os.FileInfo + + mode os.FileMode + owner string + group string +} + +func (f *FileInfo) Mode() os.FileMode { + return f.mode +} + +func (f *FileInfo) Owner() string { + return f.owner +} + +func (f *FileInfo) Group() string { + return f.group +} + +func (driver *FileDriver) chechLogin() { + if driver.login.ID <= 0 && driver.conn.IsLogin() { + driver.db.Where("mail = ?", driver.conn.LoginUser()).First(&driver.login) + fmt.Printf("Connection:%s:%d", driver.conn.LoginUser(), driver.login.ID) + } +} +func (driver *FileDriver) realPath(path string) (string, bool) { + driver.chechLogin() + paths := strings.Split(path, "/") + root := "/dev/null" + real := false + if len(paths) > 1 && driver.login.ID > 0 { + switch paths[1] { + case "data": + root = fmt.Sprintf(driver.RootPath, driver.login.ID) + paths = append([]string{paths[0]}, paths[2:]...) + real = true + } + } + return filepath.Join(append([]string{root}, paths...)...), real +} + +func (driver *FileDriver) Init(conn *ftpd.Conn) { + driver.conn = conn +} + +func (driver *FileDriver) ChangeDir(path string) error { + rPath, real := driver.realPath(path) + if !real { + return nil + } + f, err := os.Lstat(rPath) + if err != nil { + return err + } + if f.IsDir() { + return nil + } + return errors.New("Not a directory") +} + +func (driver *FileDriver) Stat(path string) (ftpd.FileInfo, error) { + basepath, real := driver.realPath(path) + var f os.FileInfo + if real { + + rPath, err := filepath.Abs(basepath) + if err != nil { + return nil, err + } + f, err = os.Lstat(rPath) + if err != nil { + return nil, err + } + } else { + f = &FackFileInfo{name: path} + } + mode, err := driver.Perm.GetMode(path) + if err != nil { + return nil, err + } + if f.IsDir() { + mode |= os.ModeDir + } + owner, err := driver.Perm.GetOwner(path) + if err != nil { + return nil, err + } + group, err := driver.Perm.GetGroup(path) + if err != nil { + return nil, err + } + return &FileInfo{f, mode, owner, group}, nil +} + +func (driver *FileDriver) ListDir(path string, callback func(ftpd.FileInfo) error) error { + basepath, real := driver.realPath(path) + if real { + filepath.Walk(basepath, func(f string, info os.FileInfo, err error) error { + rPath, _ := filepath.Rel(basepath, f) + if rPath == info.Name() { + mode, err := driver.Perm.GetMode(rPath) + if err != nil { + return err + } + if info.IsDir() { + mode |= os.ModeDir + } + owner, err := driver.Perm.GetOwner(rPath) + if err != nil { + return err + } + group, err := driver.Perm.GetGroup(rPath) + if err != nil { + return err + } + err = callback(&FileInfo{info, mode, owner, group}) + if err != nil { + return err + } + } + return nil + }) + return nil + } else { + if path == "/" { + for _, i := range []string{"data", "web", "host"} { + err := callback(&FackFileInfo{name: i}) + if err != nil { + return err + } + } + return nil + } + return errors.New("No path") + } +} + +func (driver *FileDriver) DeleteDir(path string) error { + rPath, real := driver.realPath(path) + if !real { + return errors.New("Warehost folders are not deletable") + } + f, err := os.Lstat(rPath) + if err != nil { + return err + } + if f.IsDir() { + return os.Remove(rPath) + } + return errors.New("Not a directory") +} + +func (driver *FileDriver) DeleteFile(path string) error { + rPath, real := driver.realPath(path) + if !real { + return errors.New("Warehost files are not deletable") + } + f, err := os.Lstat(rPath) + if err != nil { + return err + } + if !f.IsDir() { + return os.Remove(rPath) + } + return errors.New("Not a file") +} + +func (driver *FileDriver) Rename(fromPath string, toPath string) error { + oldPath, realOld := driver.realPath(fromPath) + newPath, realNew := driver.realPath(toPath) + if !realOld || !realNew { + return errors.New("Warehost files/folders are not moveable") + } + return os.Rename(oldPath, newPath) +} + +func (driver *FileDriver) MakeDir(path string) error { + rPath, real := driver.realPath(path) + if !real { + return errors.New("Warehost folders are not createable") + } + return os.Mkdir(rPath, os.ModePerm) + +} + +func (driver *FileDriver) GetFile(path string, offset int64) (int64, io.ReadCloser, error) { + rPath, real := driver.realPath(path) + if !real { + return 0, nil, errors.New("Warehost files are not downloadable") + } + f, err := os.Open(rPath) + if err != nil { + return 0, nil, err + } + + info, err := f.Stat() + if err != nil { + return 0, nil, err + } + + f.Seek(offset, os.SEEK_SET) + + return info.Size(), f, nil +} + +func (driver *FileDriver) PutFile(destPath string, data io.Reader, appendData bool) (int64, error) { + rPath, real := driver.realPath(destPath) + if !real { + return 0, errors.New("Warehost files are not replaceable") + } + var isExist bool + f, err := os.Lstat(rPath) + if err == nil { + isExist = true + if f.IsDir() { + return 0, errors.New("A dir has the same name") + } + } else { + if os.IsNotExist(err) { + isExist = false + } else { + return 0, errors.New(fmt.Sprintln("Put File error:", err)) + } + } + + if appendData && !isExist { + appendData = false + } + + if !appendData { + if isExist { + err = os.Remove(rPath) + if err != nil { + return 0, err + } + } + f, err := os.Create(rPath) + if err != nil { + return 0, err + } + defer f.Close() + bytes, err := io.Copy(f, data) + if err != nil { + return 0, err + } + return bytes, nil + } + + of, err := os.OpenFile(rPath, os.O_APPEND|os.O_RDWR, 0660) + if err != nil { + return 0, err + } + defer of.Close() + + _, err = of.Seek(0, os.SEEK_END) + if err != nil { + return 0, err + } + + bytes, err := io.Copy(of, data) + if err != nil { + return 0, err + } + + return bytes, nil +} + +type FileDriverFactory struct { + RootPath string + db *gorm.DB + Perm ftpd.Perm +} + +func (factory *FileDriverFactory) NewDriver() (ftpd.Driver, error) { + return &FileDriver{RootPath: factory.RootPath, db: factory.db, Perm: factory.Perm}, nil +} diff --git a/cmd/warehost-ftp/main.go b/cmd/warehost-ftp/main.go new file mode 100644 index 0000000..63d9f48 --- /dev/null +++ b/cmd/warehost-ftp/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "database/sql" + "flag" + + ftpd "github.com/goftp/server" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/postgres" + + liblog "dev.sum7.eu/sum7/warehost/lib/log" + host "dev.sum7.eu/sum7/warehost/modul/host" + web "dev.sum7.eu/sum7/warehost/modul/web" + system "dev.sum7.eu/sum7/warehost/system" +) + +var ( + configFile string + dbconnection *gorm.DB + dbDB *sql.DB + config *Config +) + +func main() { + var err error + flag.StringVar(&configFile, "c", "config.yml", "path of configuration file") + flag.Parse() + config = ReadConfigFile(configFile) + liblog.NewLogger(config.Log.Path) + + // Main Databaseconnection + dbconnection, err = gorm.Open("postgres", config.Database) + if err != nil { + liblog.Log.Fatal("database connection: ", err) + } + defer dbconnection.Close() + dbconnection.Callback().Create().Remove("gorm:update_time_stamp") + dbconnection.Callback().Update().Remove("gorm:update_time_stamp") + dbconnection.SingularTable(true) + dbconnection.LogMode(config.DatabaseDebug) + + //load system Models to database + system.SyncModels(dbconnection) + host.SyncModels(dbconnection) + web.SyncModels(dbconnection) + + opt := &ftpd.ServerOpts{ + Name: "", + Factory: &FileDriverFactory{RootPath: config.FTPPath, db: dbconnection, Perm: ftpd.NewSimplePerm("warehost", "http")}, + Port: config.Port, + Auth: WarehostAuth{db: dbconnection}, + } + + // start ftp server + ftpServer := ftpd.NewServer(opt) + liblog.Log.Info("warehost-ftp") + err = ftpServer.ListenAndServe() + if err != nil { + liblog.Log.Fatal("Error starting server:", err) + } + +} diff --git a/cmd/warehost-host/caddy b/cmd/warehost-host/caddy new file mode 100644 index 0000000..7864f5f --- /dev/null +++ b/cmd/warehost-host/caddy @@ -0,0 +1,43 @@ +# ID: {{.ID}} - Domain: {{.Domain.ID}} - Profil: {{.Domain.Profil.ID}} +# Login: {{.Domain.Profil.Login.ID}} -> {{.Domain.Profil.Login.Username}} +# {{.Subdomain}} - {{.Domain.FQDN}} + +{{ define "domain" }}{{if .Subdomain}}{{.Subdomain}}.{{end}}{{.Domain.FQDN}}{{ end }} +{{ define "content"}} + {{if not .Redirect}} + gzip + {{if .Proxy}} + proxy / {{.Proxy}} + {{else}} + root /srv/http/domain/{{template "domain" .}} + {{if .PHP}} + # php not supported + {{end}} + {{end}} + {{else}} + redir https://{{.Redirect}}{uri} + {{end}} +{{end}} + +{{if .SSL}} +{{if .SSLRedirect}} +http://{{template "domain".}} { + {{if not .Redirect}} + redir https://{{template "domain".}}{uri} + {{else}} + {{template "content" .}} + {{end}} +} +https://{{template "domain".}} { + {{template "content" .}} +} +{{else}} +http://{{template "domain" .}},https://{{template "domain".}} { + {{template "content" .}} +} +{{end}} +{{else}} +http://{{template "domain".}} { + {{template "content" .}} +} +{{end}} diff --git a/cmd/warehost-host/config.go b/cmd/warehost-host/config.go new file mode 100644 index 0000000..5c7f3bc --- /dev/null +++ b/cmd/warehost-host/config.go @@ -0,0 +1,38 @@ +package main + +import ( + "io/ioutil" + + "gopkg.in/yaml.v2" +) + +// Config of warehost webserver +type Config struct { + WarehostDatabase string `yaml:"warehostdatabase"` + Log struct { + Path string `yaml:"path"` + } `yaml:"log"` + DatabaseDebug bool `yaml:"databasedebug"` + Web struct { + Enable bool `yaml:"enable"` + ConfigurationTemplate string `yaml:"template"` + ConfigurationFile string `yaml:"config"` + } `yaml:"web"` + Database struct { + Enable bool `yaml:"enable"` + Type string `yaml:"type"` + Connection string `yaml:"connection"` + Prefix string `yaml:"prefix"` + } `yaml:"database"` +} + +// ReadConfigFile reads a config models by path to a yml file +func ReadConfigFile(path string) *Config { + config := &Config{} + file, _ := ioutil.ReadFile(path) + err := yaml.Unmarshal(file, &config) + if err != nil { + panic(err) + } + return config +} diff --git a/cmd/warehost-host/config.yml.example b/cmd/warehost-host/config.yml.example new file mode 100644 index 0000000..46a63db --- /dev/null +++ b/cmd/warehost-host/config.yml.example @@ -0,0 +1,14 @@ +--- +warehostdatabase: "host=localhost user=warehost dbname=warehost password=hallo sslmode=disable" +log: + path: test.log +databasedebug: false +web: + enable: true + template: caddy + config: /tmp/a +database: + enable: true + type: mysql + connection: "root:hallo@/mysql?interpolateParams=true" + prefix: "warehost_db" diff --git a/cmd/warehost-host/main.go b/cmd/warehost-host/main.go new file mode 100644 index 0000000..16c7330 --- /dev/null +++ b/cmd/warehost-host/main.go @@ -0,0 +1,153 @@ +package main + +import ( + "database/sql" + "flag" + "fmt" + "os" + "strconv" + "strings" + "text/template" + + _ "github.com/go-sql-driver/mysql" + "github.com/jinzhu/gorm" + _ "github.com/jinzhu/gorm/dialects/postgres" + + liblog "dev.sum7.eu/sum7/warehost/lib/log" + host "dev.sum7.eu/sum7/warehost/modul/host" + system "dev.sum7.eu/sum7/warehost/system" +) + +var ( + configFile string + dbconnection *gorm.DB + dbDB *sql.DB + config *Config +) + +func main() { + var err error + flag.StringVar(&configFile, "c", "config.yml", "path of configuration file") + flag.Parse() + config = ReadConfigFile(configFile) + liblog.NewLogger(config.Log.Path) + + // Main Databaseconnection + dbconnection, err = gorm.Open("postgres", config.WarehostDatabase) + if err != nil { + liblog.Log.Fatal("database connection: ", err) + } + defer dbconnection.Close() + dbconnection.Callback().Create().Remove("gorm:update_time_stamp") + dbconnection.Callback().Update().Remove("gorm:update_time_stamp") + dbconnection.SingularTable(true) + dbconnection.LogMode(config.DatabaseDebug) + + //load system Models to database + system.SyncModels(dbconnection) + host.SyncModels(dbconnection) + + // Configurate Webserver + if config.Web.Enable { + tmpl, err := template.ParseFiles(config.Web.ConfigurationTemplate) + if err != nil { + liblog.Log.Warning("load template: ", err) + } + file, err := os.OpenFile(config.Web.ConfigurationFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0660) + defer file.Close() + if err != nil { + liblog.Log.Warning("open configfile: ", err) + } + + var web []*host.Web + dbconnection.Preload("Domain.Profil.Login").Order("length(subdomain) asc").Find(&web) + for _, item := range web { + if item.Domain.Active && item.Domain.Web && item.Domain.Profil.Login.Active { + err = tmpl.Execute(file, item) + if err != nil { + liblog.Log.Warning("write config: ", err) + } + } + } + } + + // Configurate Database + if config.Database.Enable { + dbDB, err = sql.Open(config.Database.Type, config.Database.Connection) + if err != nil { + liblog.Log.Fatal("connection to mgmt db: ", err) + } + q, err := dbDB.Query(fmt.Sprintf("show databases LIKE \"%s%%\";", config.Database.Prefix)) + if err != nil { + liblog.Log.Fatal("list current dbs in mgmt db: ", err) + } + defer q.Close() + + var dbName string + currentDBS := map[int64]struct{}{} + for q.Next() { + q.Scan(&dbName) + dbName = strings.TrimPrefix(dbName, config.Database.Prefix) + i, err := strconv.ParseInt(dbName, 10, 64) + if err != nil { + liblog.Log.Warning("listed current dbs to id in mgmt db: ", err) + } else { + currentDBS[i] = struct{}{} + } + } + var dbs []*host.Database + if dbconnection.Find(&dbs).Error != nil { + liblog.Log.Warning("list in mgmt db: ", err) + } else { + liblog.Log.Info("count of mgmt db: ", len(dbs)) + } + shouldDB := map[int64]*host.Database{} + for _, i := range dbs { + name := fmt.Sprintf("%s%d", config.Database.Prefix, i.ID) + shouldDB[i.ID] = i + + _, err = dbDB.Exec("CREATE USER IF NOT EXISTS ?@'localhost' IDENTIFIED BY ?;", name, i.Password) + if err != nil { + liblog.Log.Warning("configurate user of mgmt db: ", err) + continue + } + _, err = dbDB.Exec(fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", name)) + if err != nil { + liblog.Log.Warning("configurate db of mgmt db: ", err) + continue + } + _, err = dbDB.Exec(fmt.Sprintf("GRANT ALL ON %s.* TO ?@'localhost' WITH GRANT OPTION;", name), name) + if err != nil { + liblog.Log.Warning("configurate permission db of mgmt db: ", err) + continue + } + + if len(i.Password) > 1 { + _, err = dbDB.Exec("SET PASSWORD FOR ?@'localhost'=PASSWORD(?);", name, i.Password) + if err != nil { + liblog.Log.Info("configurate new password of mgmt db: ", name) + continue + } else { + i.Password = "" + dbconnection.Save(i) + } + } + liblog.Log.Info("configurate of mgmt db: ", name) + + } + + for id := range currentDBS { + if ok := shouldDB[id]; ok == nil { + name := fmt.Sprintf("%s%d", config.Database.Prefix, id) + _, err = dbDB.Exec(fmt.Sprintf("DROP USER IF EXISTS %s@'localhost';", name)) + if err != nil { + liblog.Log.Info("drop of mgmt db: ", name, err) + } else { + liblog.Log.Warning("count of mgmt db: ", name) + dbDB.Query(fmt.Sprintf("DROP DATABASE IF EXISTS %s;", name)) + } + } + } + + } +}