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