1package util 2 3import ( 4 "encoding/base64" 5 "encoding/json" 6 "flag" 7 "fmt" 8 "os" 9 "path" 10 11 "net/url" 12 13 "io" 14 "strings" 15 16 "github.com/gorilla/securecookie" 17 "golang.org/x/crypto/bcrypt" 18) 19 20// Cookie is a runtime generated secure cookie used for authentication 21var Cookie *securecookie.SecureCookie 22 23// Migration indicates that the user wishes to run database migrations, deprecated 24var Migration bool 25 26// InteractiveSetup indicates that the cli should perform interactive setup mode 27var InteractiveSetup bool 28 29// Upgrade indicates that we should perform an upgrade action 30var Upgrade bool 31 32// WebHostURL is the public route to the semaphore server 33var WebHostURL *url.URL 34// FreeBSD's Semaphore Version Information (patched with REINPLACE_CMD call at the do-build stage) 35var Version = "2.6.8" 36 37const ( 38 longPos = "yes" 39 shortPos = "y" 40) 41 42type mySQLConfig struct { 43 Hostname string `json:"host"` 44 Username string `json:"user"` 45 Password string `json:"pass"` 46 DbName string `json:"name"` 47} 48 49type ldapMappings struct { 50 DN string `json:"dn"` 51 Mail string `json:"mail"` 52 UID string `json:"uid"` 53 CN string `json:"cn"` 54} 55 56//ConfigType mapping between Config and the json file that sets it 57type ConfigType struct { 58 MySQL mySQLConfig `json:"mysql"` 59 // Format `:port_num` eg, :3000 60 // if : is missing it will be corrected 61 Port string `json:"port"` 62 63 // Interface ip, put in front of the port. 64 // defaults to empty 65 Interface string `json:"interface"` 66 67 // semaphore stores ephemeral projects here 68 TmpPath string `json:"tmp_path"` 69 70 // cookie hashing & encryption 71 CookieHash string `json:"cookie_hash"` 72 CookieEncryption string `json:"cookie_encryption"` 73 74 // email alerting 75 EmailSender string `json:"email_sender"` 76 EmailHost string `json:"email_host"` 77 EmailPort string `json:"email_port"` 78 79 // web host 80 WebHost string `json:"web_host"` 81 82 // ldap settings 83 LdapBindDN string `json:"ldap_binddn"` 84 LdapBindPassword string `json:"ldap_bindpassword"` 85 LdapServer string `json:"ldap_server"` 86 LdapSearchDN string `json:"ldap_searchdn"` 87 LdapSearchFilter string `json:"ldap_searchfilter"` 88 LdapMappings ldapMappings `json:"ldap_mappings"` 89 90 // telegram alerting 91 TelegramChat string `json:"telegram_chat"` 92 TelegramToken string `json:"telegram_token"` 93 94 // task concurrency 95 ConcurrencyMode string `json:"concurrency_mode"` 96 MaxParallelTasks int `json:"max_parallel_tasks"` 97 98 // configType field ordering with bools at end reduces struct size 99 // (maligned check) 100 101 // feature switches 102 EmailAlert bool `json:"email_alert"` 103 TelegramAlert bool `json:"telegram_alert"` 104 LdapEnable bool `json:"ldap_enable"` 105 LdapNeedTLS bool `json:"ldap_needtls"` 106 107 OldFrontend bool `json:"old_frontend"` 108} 109 110//Config exposes the application configuration storage for use in the application 111var Config *ConfigType 112 113var confPath *string 114 115// NewConfig returns a reference to a new blank configType 116// nolint: golint 117func NewConfig() *ConfigType { 118 return &ConfigType{} 119} 120 121// ConfigInit reads in cli flags, and switches actions appropriately on them 122func ConfigInit() { 123 flag.BoolVar(&InteractiveSetup, "setup", false, "perform interactive setup") 124 flag.BoolVar(&Migration, "migrate", false, "execute migrations") 125 flag.BoolVar(&Upgrade, "upgrade", false, "upgrade semaphore") 126 confPath = flag.String("config", "", "config path") 127 128 var unhashedPwd string 129 flag.StringVar(&unhashedPwd, "hash", "", "generate hash of given password") 130 131 var printConfig bool 132 flag.BoolVar(&printConfig, "printConfig", false, "print example configuration") 133 134 var printVersion bool 135 flag.BoolVar(&printVersion, "version", false, "print the semaphore version") 136 137 flag.Parse() 138 139 if InteractiveSetup { 140 return 141 } 142 143 if printVersion { 144 fmt.Println(Version) 145 os.Exit(0) 146 } 147 148 if printConfig { 149 cfg := &ConfigType{ 150 MySQL: mySQLConfig{ 151 Hostname: "127.0.0.1:3306", 152 Username: "root", 153 DbName: "semaphore", 154 }, 155 Port: ":3000", 156 TmpPath: "/tmp/semaphore", 157 } 158 cfg.GenerateCookieSecrets() 159 160 b, _ := json.MarshalIndent(cfg, "", "\t") 161 fmt.Println(string(b)) 162 163 os.Exit(0) 164 } 165 166 if len(unhashedPwd) > 0 { 167 password, _ := bcrypt.GenerateFromPassword([]byte(unhashedPwd), 11) 168 fmt.Println("Generated password: ", string(password)) 169 170 os.Exit(0) 171 } 172 173 loadConfig() 174 validateConfig() 175 176 var encryption []byte 177 178 hash, _ := base64.StdEncoding.DecodeString(Config.CookieHash) 179 if len(Config.CookieEncryption) > 0 { 180 encryption, _ = base64.StdEncoding.DecodeString(Config.CookieEncryption) 181 } 182 183 Cookie = securecookie.New(hash, encryption) 184 WebHostURL, _ = url.Parse(Config.WebHost) 185 if len(WebHostURL.String()) == 0 { 186 WebHostURL = nil 187 } 188} 189 190func loadConfig() { 191 192 //If the confPath option has been set try to load and decode it 193 if confPath != nil && len(*confPath) > 0 { 194 file, err := os.Open(*confPath) 195 exitOnConfigError(err) 196 decodeConfig(file) 197 } else { 198 // if no confPath look in the cwd 199 cwd, err := os.Getwd() 200 exitOnConfigError(err) 201 cwd = cwd + "/config.json" 202 confPath = &cwd 203 file, err := os.Open(*confPath) 204 exitOnConfigError(err) 205 decodeConfig(file) 206 } 207 fmt.Println("Using config file: " + *confPath) 208} 209 210func validateConfig() { 211 212 validatePort() 213 214 if len(Config.TmpPath) == 0 { 215 Config.TmpPath = "/tmp/semaphore" 216 } 217 218 if Config.MaxParallelTasks < 1 { 219 Config.MaxParallelTasks = 10 220 } 221} 222 223func validatePort() { 224 225 //TODO - why do we do this only with this variable? 226 if len(os.Getenv("PORT")) > 0 { 227 Config.Port = ":" + os.Getenv("PORT") 228 } 229 if len(Config.Port) == 0 { 230 Config.Port = ":3000" 231 } 232 if !strings.HasPrefix(Config.Port, ":") { 233 Config.Port = ":" + Config.Port 234 } 235} 236 237func exitOnConfigError(err error) { 238 if err != nil { 239 fmt.Println("Cannot Find configuration! Use -c parameter to point to a JSON file generated by -setup.\n\n Hint: have you run `-setup` ?") 240 os.Exit(1) 241 } 242} 243 244func decodeConfig(file io.Reader) { 245 if err := json.NewDecoder(file).Decode(&Config); err != nil { 246 fmt.Println("Could not decode configuration!") 247 panic(err) 248 } 249} 250 251//GenerateCookieSecrets generates cookie secret during setup 252func (conf *ConfigType) GenerateCookieSecrets() { 253 hash := securecookie.GenerateRandomKey(32) 254 encryption := securecookie.GenerateRandomKey(32) 255 256 conf.CookieHash = base64.StdEncoding.EncodeToString(hash) 257 conf.CookieEncryption = base64.StdEncoding.EncodeToString(encryption) 258} 259 260//nolint: gocyclo 261func (conf *ConfigType) Scan() { 262 fmt.Print(" > DB Hostname (default 127.0.0.1:3306): ") 263 ScanErrorChecker(fmt.Scanln(&conf.MySQL.Hostname)) 264 if len(conf.MySQL.Hostname) == 0 { 265 conf.MySQL.Hostname = "127.0.0.1:3306" 266 } 267 268 fmt.Print(" > DB User (default root): ") 269 ScanErrorChecker(fmt.Scanln(&conf.MySQL.Username)) 270 if len(conf.MySQL.Username) == 0 { 271 conf.MySQL.Username = "root" 272 } 273 274 fmt.Print(" > DB Password: ") 275 ScanErrorChecker(fmt.Scanln(&conf.MySQL.Password)) 276 277 fmt.Print(" > DB Name (default semaphore): ") 278 ScanErrorChecker(fmt.Scanln(&conf.MySQL.DbName)) 279 if len(conf.MySQL.DbName) == 0 { 280 conf.MySQL.DbName = "semaphore" 281 } 282 283 fmt.Print(" > Playbook path (default /tmp/semaphore): ") 284 ScanErrorChecker(fmt.Scanln(&conf.TmpPath)) 285 286 if len(conf.TmpPath) == 0 { 287 conf.TmpPath = "/tmp/semaphore" 288 } 289 conf.TmpPath = path.Clean(conf.TmpPath) 290 291 fmt.Print(" > Web root URL (optional, example http://localhost:8010/): ") 292 ScanErrorChecker(fmt.Scanln(&conf.WebHost)) 293 294 var EmailAlertAnswer string 295 fmt.Print(" > Enable email alerts (y/n, default n): ") 296 ScanErrorChecker(fmt.Scanln(&EmailAlertAnswer)) 297 if EmailAlertAnswer == longPos || EmailAlertAnswer == shortPos { 298 299 conf.EmailAlert = true 300 301 fmt.Print(" > Mail server host (default localhost): ") 302 ScanErrorChecker(fmt.Scanln(&conf.EmailHost)) 303 304 if len(conf.EmailHost) == 0 { 305 conf.EmailHost = "localhost" 306 } 307 308 fmt.Print(" > Mail server port (default 25): ") 309 ScanErrorChecker(fmt.Scanln(&conf.EmailPort)) 310 311 if len(conf.EmailPort) == 0 { 312 conf.EmailPort = "25" 313 } 314 315 fmt.Print(" > Mail sender address (default semaphore@localhost): ") 316 ScanErrorChecker(fmt.Scanln(&conf.EmailSender)) 317 318 if len(conf.EmailSender) == 0 { 319 conf.EmailSender = "semaphore@localhost" 320 } 321 322 } else { 323 conf.EmailAlert = false 324 } 325 326 var TelegramAlertAnswer string 327 fmt.Print(" > Enable telegram alerts (y/n, default n): ") 328 ScanErrorChecker(fmt.Scanln(&TelegramAlertAnswer)) 329 if TelegramAlertAnswer == longPos || TelegramAlertAnswer == shortPos { 330 331 conf.TelegramAlert = true 332 333 fmt.Print(" > Telegram bot token (you can get it from @BotFather) (default ''): ") 334 ScanErrorChecker(fmt.Scanln(&conf.TelegramToken)) 335 336 if len(conf.TelegramToken) == 0 { 337 conf.TelegramToken = "" 338 } 339 340 fmt.Print(" > Telegram chat ID (default ''): ") 341 ScanErrorChecker(fmt.Scanln(&conf.TelegramChat)) 342 343 if len(conf.TelegramChat) == 0 { 344 conf.TelegramChat = "" 345 } 346 347 } else { 348 conf.TelegramAlert = false 349 } 350 351 var LdapAnswer string 352 fmt.Print(" > Enable LDAP authentication (y/n, default n): ") 353 ScanErrorChecker(fmt.Scanln(&LdapAnswer)) 354 if LdapAnswer == longPos || LdapAnswer == shortPos { 355 356 conf.LdapEnable = true 357 358 fmt.Print(" > LDAP server host (default localhost:389): ") 359 ScanErrorChecker(fmt.Scanln(&conf.LdapServer)) 360 361 if len(conf.LdapServer) == 0 { 362 conf.LdapServer = "localhost:389" 363 } 364 365 var LdapTLSAnswer string 366 fmt.Print(" > Enable LDAP TLS connection (y/n, default n): ") 367 ScanErrorChecker(fmt.Scanln(&LdapTLSAnswer)) 368 if LdapTLSAnswer == longPos || LdapTLSAnswer == shortPos { 369 conf.LdapNeedTLS = true 370 } else { 371 conf.LdapNeedTLS = false 372 } 373 374 fmt.Print(" > LDAP DN for bind (default cn=user,ou=users,dc=example): ") 375 ScanErrorChecker(fmt.Scanln(&conf.LdapBindDN)) 376 377 if len(conf.LdapBindDN) == 0 { 378 conf.LdapBindDN = "cn=user,ou=users,dc=example" 379 } 380 381 fmt.Print(" > Password for LDAP bind user (default pa55w0rd): ") 382 ScanErrorChecker(fmt.Scanln(&conf.LdapBindPassword)) 383 384 if len(conf.LdapBindPassword) == 0 { 385 conf.LdapBindPassword = "pa55w0rd" 386 } 387 388 fmt.Print(" > LDAP DN for user search (default ou=users,dc=example): ") 389 ScanErrorChecker(fmt.Scanln(&conf.LdapSearchDN)) 390 391 if len(conf.LdapSearchDN) == 0 { 392 conf.LdapSearchDN = "ou=users,dc=example" 393 } 394 395 fmt.Print(" > LDAP search filter (default (uid=" + "%" + "s)): ") 396 ScanErrorChecker(fmt.Scanln(&conf.LdapSearchFilter)) 397 398 if len(conf.LdapSearchFilter) == 0 { 399 conf.LdapSearchFilter = "(uid=%s)" 400 } 401 402 fmt.Print(" > LDAP mapping for DN field (default dn): ") 403 ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.DN)) 404 405 if len(conf.LdapMappings.DN) == 0 { 406 conf.LdapMappings.DN = "dn" 407 } 408 409 fmt.Print(" > LDAP mapping for username field (default uid): ") 410 ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.UID)) 411 412 if len(conf.LdapMappings.UID) == 0 { 413 conf.LdapMappings.UID = "uid" 414 } 415 416 fmt.Print(" > LDAP mapping for full name field (default cn): ") 417 ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.CN)) 418 419 if len(conf.LdapMappings.CN) == 0 { 420 conf.LdapMappings.CN = "cn" 421 } 422 423 fmt.Print(" > LDAP mapping for email field (default mail): ") 424 ScanErrorChecker(fmt.Scanln(&conf.LdapMappings.Mail)) 425 426 if len(conf.LdapMappings.Mail) == 0 { 427 conf.LdapMappings.Mail = "mail" 428 } 429 } else { 430 conf.LdapEnable = false 431 } 432} 433