package srnd import ( "database/sql" "fmt" "github.com/gorilla/mux" _ "github.com/lib/pq" "github.com/majestrate/configparser" "golang.org/x/text/language" "gopkg.in/tylerb/graceful.v1" "log" "net" "net/http" "net/url" "os" "path/filepath" "time" ) type handlePost func(*dialogNode, url.Values, *configparser.Configuration) (*dialogNode, error) type templateModel map[string]interface{} type prepareModel func(*dialogNode, error, *configparser.Configuration) templateModel type dialogNode struct { parent *dialogNode children map[string]*dialogNode post handlePost model prepareModel templateName string } type Installer struct { root *dialogNode currentNode *dialogNode currentErr error result chan *configparser.Configuration config *configparser.Configuration srv *graceful.Server hasTranslations bool } func handleDBTypePost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { db := form.Get("db") log.Println("DB chosen: ", db) if db == "postgres" { return self.children["postgres"], nil } return self, nil } func prepareDefaultModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) param["dialog"] = &BaseDialogModel{ErrorModel{err}, StepModel{self}} return param } func preparePostgresDBModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) sect, _ := conf.Section("database") host := sect.ValueOf("host") port := sect.ValueOf("port") user := sect.ValueOf("user") param["dialog"] = &DBModel{ErrorModel{err}, StepModel{self}, user, host, port} return param } func handlePostgresDBPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("database") host := form.Get("host") port := form.Get("port") passwd := form.Get("password") user := form.Get("user") err := checkPostgresConnection(host, port, user, passwd) if err != nil { return self, err } sect.Add("type", "postgres") sect.Add("schema", "srnd") sect.Add("host", host) sect.Add("port", port) sect.Add("password", passwd) sect.Add("user", user) return self.children["next"], nil } func prepareNNTPModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) sect, _ := conf.Section("nntp") name := sect.ValueOf("instance_name") param["dialog"] = &NameModel{ErrorModel{err}, StepModel{self}, name} return param } func handleNNTPPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("nntp") name := form.Get("nntp_name") allow_attachments := form.Get("allow_attachments") if allow_attachments != "1" { allow_attachments = "0" } allow_anon := form.Get("allow_anon") if allow_anon != "1" { allow_anon = "0" } allow_anon_attachments := form.Get("allow_anon_attachments") if allow_anon_attachments != "1" { allow_anon_attachments = "0" } require_tls := form.Get("require_tls") if require_tls != "1" { require_tls = "0" } sect.Add("instance_name", name) sect.Add("allow_attachments", allow_attachments) sect.Add("allow_anon", allow_anon) sect.Add("require_tls", require_tls) return self.children["next"], nil } func handleCryptoPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("crypto") host := form.Get("host") key := form.Get("key") err := checkHost(host) if err != nil { return self, err } sect.Add("tls-hostname", host) sect.Add("tls-keyname", key) return self.children["next"], nil } func prepareCryptoModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) sect, _ := conf.Section("crypto") host := sect.ValueOf("tls-hostname") key := sect.ValueOf("tls-keyname") param["dialog"] = &CryptoModel{ErrorModel{err}, StepModel{self}, host, key} return param } func prepareBinModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) sect, _ := conf.Section("articles") convert := sect.ValueOf("convert_bin") ffmpeg := sect.ValueOf("ffmpegthumbnailer_bin") sox := sect.ValueOf("sox_bin") param["dialog"] = &BinaryModel{ErrorModel{err}, StepModel{self}, convert, ffmpeg, sox} return param } func handleBinPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("articles") convert := form.Get("convert") ffmpeg := form.Get("ffmpeg") sox := form.Get("sox") err := checkFile(convert) if err == nil { err = checkFile(ffmpeg) if err == nil { err = checkFile(sox) } } sect.Add("convert_bin", convert) sect.Add("ffmpegthumbnailer_bin", ffmpeg) sect.Add("sox_bin", sox) if err != nil { return self, err } return self.children["next"], nil } func handleCacheTypePost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("cache") cache := form.Get("cache") log.Println("Cache chosen: ", cache) sect.Add("type", cache) if cache == "file" || cache == "null" || cache == "varnish" { return self.children["next"], nil } return self, nil } func prepareFrontendModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) sect, _ := conf.Section("frontend") name := sect.ValueOf("name") locale := sect.ValueOf("locale") param["dialog"] = &FrontendModel{ErrorModel{err}, StepModel{self}, name, locale} return param } func handleFrontendPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } var next *dialogNode sect, _ := conf.Section("frontend") name := form.Get("name") locale := form.Get("locale") allow_files := form.Get("allow_files") if allow_files != "1" { allow_files = "0" } json_api := form.Get("json") if json_api != "1" { json_api = "0" next = self.children["next"] } else { next = self.children["json"] } sect.Add("name", name) sect.Add("locale", locale) sect.Add("allow_files", allow_files) sect.Add("json-api", json_api) err := checkLocale(locale) if err != nil { return self, err } return next, nil } func handleAPIPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("frontend") user := form.Get("user") pass := form.Get("pass") secret := form.Get("secret") sect.Add("json-api-username", user) sect.Add("json-api-password", pass) sect.Add("api-secret", secret) return self.children["next"], nil } func prepareAPIModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) sect, _ := conf.Section("frontend") user := sect.ValueOf("json-api-username") secret := sect.ValueOf("api-secret") param["dialog"] = &APIModel{ErrorModel{err}, StepModel{self}, user, secret} return param } func handleKeyPost(self *dialogNode, form url.Values, conf *configparser.Configuration) (*dialogNode, error) { if form.Get("back") == "true" { return self.parent, nil } sect, _ := conf.Section("frontend") public := form.Get("public") sect.Add("admin_key", public) return self.children["next"], nil } func prepareKeyModel(self *dialogNode, err error, conf *configparser.Configuration) templateModel { param := make(map[string]interface{}) public, secret := newSignKeypair() param["dialog"] = &KeyModel{ErrorModel{err}, StepModel{self}, public, secret} return param } func (self *Installer) HandleInstallerGet(wr http.ResponseWriter, r *http.Request) { if !self.hasTranslations { t, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language")) locale := "" if len(t) > 0 { locale = t[0].String() } InitI18n(locale, filepath.Join("contrib", "translations")) self.hasTranslations = true } if self.currentNode == nil { wr.WriteHeader(404) } else { m := self.currentNode.model(self.currentNode, self.currentErr, self.config) template.writeTemplate(self.currentNode.templateName, m, wr) } } func (self *Installer) HandleInstallerPost(wr http.ResponseWriter, r *http.Request) { err := r.ParseForm() if err == nil { next, newErr := self.currentNode.post(self.currentNode, r.PostForm, self.config) if next == nil { self.result <- self.config //defer self.srv.Stop(10 * time.Second) } self.currentNode = next self.currentErr = newErr http.Redirect(wr, r, r.URL.String(), http.StatusSeeOther) //redirect to the same url, but with a GET return } http.Error(wr, "Bad Request", http.StatusBadRequest) } func NewInstaller(result chan *configparser.Configuration) *Installer { inst := new(Installer) inst.root = initInstallerTree() inst.currentNode = inst.root inst.result = result inst.config = GenSRNdConfig() inst.hasTranslations = false m := mux.NewRouter() m.Path("/").HandlerFunc(inst.HandleInstallerGet).Methods("GET") m.Path("/").HandlerFunc(inst.HandleInstallerPost).Methods("POST") inst.srv = &graceful.Server{ Timeout: 10 * time.Second, NoSignalHandling: true, Server: &http.Server{ Addr: ":18000", Handler: m, }, } return inst } func initInstallerTree() *dialogNode { root := &dialogNode{ parent: nil, children: make(map[string]*dialogNode), post: handleDBTypePost, model: prepareDefaultModel, templateName: "inst_db.mustache", } postgresDB := &dialogNode{ parent: root, children: make(map[string]*dialogNode), post: handlePostgresDBPost, model: preparePostgresDBModel, templateName: "inst_postgres_db.mustache", } root.children["postgres"] = postgresDB nntp := &dialogNode{ parent: root, children: make(map[string]*dialogNode), post: handleNNTPPost, model: prepareNNTPModel, templateName: "inst_nntp.mustache", } postgresDB.children["next"] = nntp crypto := &dialogNode{ parent: nntp, children: make(map[string]*dialogNode), post: handleCryptoPost, model: prepareCryptoModel, templateName: "inst_crypto.mustache", } nntp.children["next"] = crypto bins := &dialogNode{ parent: crypto, children: make(map[string]*dialogNode), post: handleBinPost, model: prepareBinModel, templateName: "inst_bins.mustache", } crypto.children["next"] = bins cache := &dialogNode{ parent: bins, children: make(map[string]*dialogNode), post: handleCacheTypePost, model: prepareDefaultModel, templateName: "inst_cache.mustache", } bins.children["next"] = cache frontend := &dialogNode{ parent: cache, children: make(map[string]*dialogNode), post: handleFrontendPost, model: prepareFrontendModel, templateName: "inst_frontend.mustache", } cache.children["next"] = frontend api := &dialogNode{ parent: frontend, children: make(map[string]*dialogNode), post: handleAPIPost, model: prepareAPIModel, templateName: "inst_api.mustache", } frontend.children["json"] = api key := &dialogNode{ parent: frontend, children: make(map[string]*dialogNode), post: handleKeyPost, model: prepareKeyModel, templateName: "inst_key.mustache", } frontend.children["next"] = key api.children["next"] = key return root } func checkPostgresConnection(host, port, user, password string) error { var db_str string if len(user) > 0 { if len(password) > 0 { db_str = fmt.Sprintf("user=%s password=%s host=%s port=%s client_encoding='UTF8' connect_timeout=3", user, password, host, port) } else { db_str = fmt.Sprintf("user=%s host=%s port=%s client_encoding='UTF8' connect_timeout=3", user, host, port) } } else { if len(port) > 0 { db_str = fmt.Sprintf("host=%s port=%s client_encoding='UTF8' connect_timeout=3", host, port) } else { db_str = fmt.Sprintf("host=%s client_encoding='UTF8' connect_timeout=3", host) } } conn, err := sql.Open("postgres", db_str) defer conn.Close() if err == nil { _, err = conn.Exec("SELECT datname FROM pg_database") } return err } func checkLocale(locale string) error { _, err := language.Parse(locale) return err } func checkFile(path string) error { _, err := os.Stat(path) return err } func checkHost(host string) error { _, err := net.LookupHost(host) return err } func (self *Installer) Start() { log.Println("starting installer on", self.srv.Server.Addr) log.Println("open up http://127.0.0.1:18000 to do initial configuration") self.srv.ListenAndServe() } func (self *Installer) Stop() { self.srv.Stop(1 * time.Second) } func InstallerEnabled() bool { return os.Getenv("SRND_NO_INSTALLER") != "1" }