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