1/*
2 * Copyright © 2018-2021 A Bunch Tell LLC.
3 *
4 * This file is part of WriteFreely.
5 *
6 * WriteFreely is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU Affero General Public License, included
8 * in the LICENSE file in this source code package.
9 */
10
11// Package config holds and assists in the configuration of a writefreely instance.
12package config
13
14import (
15	"net/url"
16	"strings"
17
18	"github.com/writeas/web-core/log"
19	"golang.org/x/net/idna"
20	"gopkg.in/ini.v1"
21)
22
23const (
24	// FileName is the default configuration file name
25	FileName = "config.ini"
26
27	UserNormal UserType = "user"
28	UserAdmin           = "admin"
29)
30
31type (
32	UserType string
33
34	// ServerCfg holds values that affect how the HTTP server runs
35	ServerCfg struct {
36		HiddenHost string `ini:"hidden_host"`
37		Port       int    `ini:"port"`
38		Bind       string `ini:"bind"`
39
40		TLSCertPath string `ini:"tls_cert_path"`
41		TLSKeyPath  string `ini:"tls_key_path"`
42		Autocert    bool   `ini:"autocert"`
43
44		TemplatesParentDir string `ini:"templates_parent_dir"`
45		StaticParentDir    string `ini:"static_parent_dir"`
46		PagesParentDir     string `ini:"pages_parent_dir"`
47		KeysParentDir      string `ini:"keys_parent_dir"`
48
49		HashSeed string `ini:"hash_seed"`
50
51		GopherPort int `ini:"gopher_port"`
52
53		Dev bool `ini:"-"`
54	}
55
56	// DatabaseCfg holds values that determine how the application connects to a datastore
57	DatabaseCfg struct {
58		Type     string `ini:"type"`
59		FileName string `ini:"filename"`
60		User     string `ini:"username"`
61		Password string `ini:"password"`
62		Database string `ini:"database"`
63		Host     string `ini:"host"`
64		Port     int    `ini:"port"`
65		TLS      bool   `ini:"tls"`
66	}
67
68	WriteAsOauthCfg struct {
69		ClientID         string `ini:"client_id"`
70		ClientSecret     string `ini:"client_secret"`
71		AuthLocation     string `ini:"auth_location"`
72		TokenLocation    string `ini:"token_location"`
73		InspectLocation  string `ini:"inspect_location"`
74		CallbackProxy    string `ini:"callback_proxy"`
75		CallbackProxyAPI string `ini:"callback_proxy_api"`
76	}
77
78	GitlabOauthCfg struct {
79		ClientID         string `ini:"client_id"`
80		ClientSecret     string `ini:"client_secret"`
81		Host             string `ini:"host"`
82		DisplayName      string `ini:"display_name"`
83		CallbackProxy    string `ini:"callback_proxy"`
84		CallbackProxyAPI string `ini:"callback_proxy_api"`
85	}
86
87	GiteaOauthCfg struct {
88		ClientID         string `ini:"client_id"`
89		ClientSecret     string `ini:"client_secret"`
90		Host             string `ini:"host"`
91		DisplayName      string `ini:"display_name"`
92		CallbackProxy    string `ini:"callback_proxy"`
93		CallbackProxyAPI string `ini:"callback_proxy_api"`
94	}
95
96	SlackOauthCfg struct {
97		ClientID         string `ini:"client_id"`
98		ClientSecret     string `ini:"client_secret"`
99		TeamID           string `ini:"team_id"`
100		CallbackProxy    string `ini:"callback_proxy"`
101		CallbackProxyAPI string `ini:"callback_proxy_api"`
102	}
103
104	GenericOauthCfg struct {
105		ClientID         string `ini:"client_id"`
106		ClientSecret     string `ini:"client_secret"`
107		Host             string `ini:"host"`
108		DisplayName      string `ini:"display_name"`
109		CallbackProxy    string `ini:"callback_proxy"`
110		CallbackProxyAPI string `ini:"callback_proxy_api"`
111		TokenEndpoint    string `ini:"token_endpoint"`
112		InspectEndpoint  string `ini:"inspect_endpoint"`
113		AuthEndpoint     string `ini:"auth_endpoint"`
114		Scope            string `ini:"scope"`
115		AllowDisconnect  bool   `ini:"allow_disconnect"`
116		MapUserID        string `ini:"map_user_id"`
117		MapUsername      string `ini:"map_username"`
118		MapDisplayName   string `ini:"map_display_name"`
119		MapEmail         string `ini:"map_email"`
120	}
121
122	// AppCfg holds values that affect how the application functions
123	AppCfg struct {
124		SiteName string `ini:"site_name"`
125		SiteDesc string `ini:"site_description"`
126		Host     string `ini:"host"`
127
128		// Site appearance
129		Theme      string `ini:"theme"`
130		Editor     string `ini:"editor"`
131		JSDisabled bool   `ini:"disable_js"`
132		WebFonts   bool   `ini:"webfonts"`
133		Landing    string `ini:"landing"`
134		SimpleNav  bool   `ini:"simple_nav"`
135		WFModesty  bool   `ini:"wf_modesty"`
136
137		// Site functionality
138		Chorus        bool `ini:"chorus"`
139		Forest        bool `ini:"forest"` // The admin cares about the forest, not the trees. Hide unnecessary technical info.
140		DisableDrafts bool `ini:"disable_drafts"`
141
142		// Users
143		SingleUser       bool `ini:"single_user"`
144		OpenRegistration bool `ini:"open_registration"`
145		OpenDeletion     bool `ini:"open_deletion"`
146		MinUsernameLen   int  `ini:"min_username_len"`
147		MaxBlogs         int  `ini:"max_blogs"`
148
149		// Options for public instances
150		// Federation
151		Federation   bool `ini:"federation"`
152		PublicStats  bool `ini:"public_stats"`
153		Monetization bool `ini:"monetization"`
154		NotesOnly    bool `ini:"notes_only"`
155
156		// Access
157		Private bool `ini:"private"`
158
159		// Additional functions
160		LocalTimeline bool   `ini:"local_timeline"`
161		UserInvites   string `ini:"user_invites"`
162
163		// Defaults
164		DefaultVisibility string `ini:"default_visibility"`
165
166		// Check for Updates
167		UpdateChecks bool `ini:"update_checks"`
168
169		// Disable password authentication if use only Oauth
170		DisablePasswordAuth bool `ini:"disable_password_auth"`
171	}
172
173	// Config holds the complete configuration for running a writefreely instance
174	Config struct {
175		Server       ServerCfg       `ini:"server"`
176		Database     DatabaseCfg     `ini:"database"`
177		App          AppCfg          `ini:"app"`
178		SlackOauth   SlackOauthCfg   `ini:"oauth.slack"`
179		WriteAsOauth WriteAsOauthCfg `ini:"oauth.writeas"`
180		GitlabOauth  GitlabOauthCfg  `ini:"oauth.gitlab"`
181		GiteaOauth   GiteaOauthCfg   `ini:"oauth.gitea"`
182		GenericOauth GenericOauthCfg `ini:"oauth.generic"`
183	}
184)
185
186// New creates a new Config with sane defaults
187func New() *Config {
188	c := &Config{
189		Server: ServerCfg{
190			Port: 8080,
191			Bind: "localhost", /* IPV6 support when not using localhost? */
192		},
193		App: AppCfg{
194			Host:           "http://localhost:8080",
195			Theme:          "write",
196			WebFonts:       true,
197			SingleUser:     true,
198			MinUsernameLen: 3,
199			MaxBlogs:       1,
200			Federation:     true,
201			PublicStats:    true,
202		},
203	}
204	c.UseMySQL(true)
205	return c
206}
207
208// UseMySQL resets the Config's Database to use default values for a MySQL setup.
209func (cfg *Config) UseMySQL(fresh bool) {
210	cfg.Database.Type = "mysql"
211	if fresh {
212		cfg.Database.Host = "localhost"
213		cfg.Database.Port = 3306
214	}
215}
216
217// UseSQLite resets the Config's Database to use default values for a SQLite setup.
218func (cfg *Config) UseSQLite(fresh bool) {
219	cfg.Database.Type = "sqlite3"
220	if fresh {
221		cfg.Database.FileName = "writefreely.db"
222	}
223}
224
225// IsSecureStandalone returns whether or not the application is running as a
226// standalone server with TLS enabled.
227func (cfg *Config) IsSecureStandalone() bool {
228	return cfg.Server.Port == 443 && cfg.Server.TLSCertPath != "" && cfg.Server.TLSKeyPath != ""
229}
230
231func (ac *AppCfg) LandingPath() string {
232	if !strings.HasPrefix(ac.Landing, "/") {
233		return "/" + ac.Landing
234	}
235	return ac.Landing
236}
237
238func (ac AppCfg) SignupPath() string {
239	if !ac.OpenRegistration {
240		return ""
241	}
242	if ac.Chorus || ac.Private || (ac.Landing != "" && ac.Landing != "/") {
243		return "/signup"
244	}
245	return "/"
246}
247
248// Load reads the given configuration file, then parses and returns it as a Config.
249func Load(fname string) (*Config, error) {
250	if fname == "" {
251		fname = FileName
252	}
253	cfg, err := ini.Load(fname)
254	if err != nil {
255		return nil, err
256	}
257
258	// Parse INI file
259	uc := &Config{}
260	err = cfg.MapTo(uc)
261	if err != nil {
262		return nil, err
263	}
264
265	// Do any transformations
266	u, err := url.Parse(uc.App.Host)
267	if err != nil {
268		return nil, err
269	}
270	d, err := idna.ToASCII(u.Hostname())
271	if err != nil {
272		log.Error("idna.ToASCII for %s: %s", u.Hostname(), err)
273		return nil, err
274	}
275	uc.App.Host = u.Scheme + "://" + d
276	if u.Port() != "" {
277		uc.App.Host += ":" + u.Port()
278	}
279
280	return uc, nil
281}
282
283// Save writes the given Config to the given file.
284func Save(uc *Config, fname string) error {
285	cfg := ini.Empty()
286	err := ini.ReflectFrom(cfg, uc)
287	if err != nil {
288		return err
289	}
290
291	if fname == "" {
292		fname = FileName
293	}
294	return cfg.SaveTo(fname)
295}
296