github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/site.go (about) 1 package common 2 3 import ( 4 "encoding/json" 5 "io/ioutil" 6 "log" 7 "net/url" 8 "strconv" 9 "strings" 10 11 "github.com/pkg/errors" 12 ) 13 14 // Site holds the basic settings which should be tweaked when setting up a site, we might move them to the settings table at some point 15 var Site = &site{Name: "Magical Fairy Land", Language: "english"} 16 17 // DbConfig holds the database configuration 18 var DbConfig = &dbConfig{Host: "localhost"} 19 20 // Config holds the more technical settings 21 var Config = new(config) 22 23 // Dev holds build flags and other things which should only be modified during developers or to gather additional test data 24 var Dev = new(devConfig) 25 26 var PluginConfig = map[string]string{} 27 28 type site struct { 29 ShortName string 30 Name string 31 Email string 32 URL string 33 Host string 34 LocalHost bool // Used internally, do not modify as it will be overwritten 35 Port string 36 PortInt int // Alias for efficiency, do not modify, will be overwritten 37 EnableSsl bool 38 EnableEmails bool 39 HasProxy bool 40 Language string 41 42 MaxRequestSize int // Alias, do not modify, will be overwritten 43 } 44 45 type dbConfig struct { 46 // Production database 47 Adapter string 48 Host string 49 Username string 50 Password string 51 Dbname string 52 Port string 53 54 // Test database. Split this into a separate variable? 55 TestAdapter string 56 TestHost string 57 TestUsername string 58 TestPassword string 59 TestDbname string 60 TestPort string 61 } 62 63 type config struct { 64 SslPrivkey string 65 SslFullchain string 66 HashAlgo string // Defaults to bcrypt, and in the future, possibly something stronger 67 ConvoKey string 68 69 MaxRequestSizeStr string 70 MaxRequestSize int 71 UserCache string 72 UserCacheCapacity int 73 TopicCache string 74 TopicCacheCapacity int 75 ReplyCache string 76 ReplyCacheCapacity int 77 78 SMTPServer string 79 SMTPUsername string 80 SMTPPassword string 81 SMTPPort string 82 SMTPEnableTLS bool 83 84 Search string 85 86 DefaultPath string 87 DefaultGroup int // Should be a setting in the database 88 ActivationGroup int // Should be a setting in the database 89 StaffCSS string // ? - Move this into the settings table? Might be better to implement this as Group CSS 90 DefaultForum int // The forum posts go in by default, this used to be covered by the Uncategorised Forum, but we want to replace it with a more robust solution. Make this a setting? 91 MinifyTemplates bool 92 BuildSlugs bool // TODO: Make this a setting? 93 94 PrimaryServer bool 95 ServerCount int 96 LastIPCutoff int // Currently just -1, non--1, but will accept the number of months a user's last IP should be retained for in the future before being purged. Please note that the other two cutoffs below operate off the numbers of days instead. 97 PostIPCutoff int 98 PollIPCutoff int 99 LogPruneCutoff int 100 //SelfDeleteTruncCutoff int // Personal data is stripped from the mod action rows only leaving the TID and the action for later investigation. 101 102 DisableIP bool 103 DisableLastIP bool 104 DisablePostIP bool 105 DisablePollIP bool 106 DisableRegLog bool 107 DisableLoginLog bool 108 //DisableSelfDeleteLog bool 109 110 DisableLiveTopicList bool 111 DisableJSAntispam bool 112 //LooseCSP bool 113 LooseHost bool 114 LoosePort bool 115 SslSchema bool // Pretend we're using SSL, might be useful if a reverse-proxy terminates SSL in-front of Gosora 116 DisableServerPush bool 117 EnableCDNPush bool 118 DisableNoavatarRange bool 119 DisableDefaultNoavatar bool 120 DisableAnalytics bool 121 122 RefNoTrack bool 123 RefNoRef bool 124 NoEmbed bool 125 126 ExtraCSPOrigins string 127 StaticResBase string // /s/ 128 //DynStaticResBase string 129 AvatarResBase string // /uploads/ 130 131 Noavatar string // ? - Move this into the settings table? 132 ItemsPerPage int // ? - Move this into the settings table? 133 MaxTopicTitleLength int 134 MaxUsernameLength int 135 136 ReadTimeout int 137 WriteTimeout int 138 IdleTimeout int 139 140 LogDir string 141 DisableSuspLog bool 142 DisableBadRouteLog bool 143 DisableStdout bool 144 DisableStderr bool 145 } 146 147 type devConfig struct { 148 DebugMode bool 149 SuperDebug bool 150 TemplateDebug bool 151 Profiling bool 152 TestDB bool 153 154 NoFsnotify bool // Super Experimental! 155 FullReqLog bool 156 ExtraTmpls string // Experimental flag for adding compiled templates, we'll likely replace this with a better mechanism 157 158 //QuicPort int // Experimental! 159 160 //ExpFix1 bool // unlisted setting, experimental fix for http/1.1 conn hangs 161 LogLongTick bool // unlisted setting 162 LogNewLongRoute bool // unlisted setting 163 Log4thLongRoute bool // unlisted setting 164 165 HourDBTimeout bool // unlisted setting 166 } 167 168 // configHolder is purely for having a big struct to unmarshal data into 169 type configHolder struct { 170 Site *site 171 Config *config 172 Database *dbConfig 173 Dev *devConfig 174 Plugin map[string]string 175 } 176 177 func LoadConfig() error { 178 data, err := ioutil.ReadFile("./config/config.json") 179 if err != nil { 180 return err 181 } 182 183 var config configHolder 184 err = json.Unmarshal(data, &config) 185 if err != nil { 186 return err 187 } 188 189 Site = config.Site 190 Config = config.Config 191 DbConfig = config.Database 192 Dev = config.Dev 193 PluginConfig = config.Plugin 194 195 return nil 196 } 197 198 var noavatarCache200 []string 199 var noavatarCache48 []string 200 201 /*var noavatarCache200Jpg []string 202 var noavatarCache48Jpg []string 203 var noavatarCache200Avif []string 204 var noavatarCache48Avif []string*/ 205 206 func ProcessConfig() (err error) { 207 // Strip these unnecessary bits, if we find them. 208 Site.URL = strings.TrimPrefix(Site.URL, "http://") 209 Site.URL = strings.TrimPrefix(Site.URL, "https://") 210 Site.Host = Site.URL 211 Site.LocalHost = Site.Host == "localhost" || Site.Host == "127.0.0.1" || Site.Host == "::1" 212 Site.PortInt, err = strconv.Atoi(Site.Port) 213 if err != nil { 214 return errors.New("The port must be a valid integer") 215 } 216 if Site.PortInt != 80 && Site.PortInt != 443 { 217 Site.URL = strings.TrimSuffix(Site.URL, "/") 218 Site.URL = strings.TrimSuffix(Site.URL, "\\") 219 Site.URL = strings.TrimSuffix(Site.URL, ":") 220 Site.URL = Site.URL + ":" + Site.Port 221 } 222 uurl, err := url.Parse(Site.URL) 223 if err != nil { 224 return errors.Wrap(err, "failed to parse Site.URL: ") 225 } 226 if Site.EnableSsl { 227 Config.SslSchema = Site.EnableSsl 228 } 229 if Config.DefaultPath == "" { 230 Config.DefaultPath = "/topics/" 231 } 232 233 // TODO: Bump the size of max request size up, if it's too low 234 Config.MaxRequestSize, err = strconv.Atoi(Config.MaxRequestSizeStr) 235 if err != nil { 236 reqSizeStr := Config.MaxRequestSizeStr 237 if len(reqSizeStr) < 3 { 238 return errors.New("Invalid unit for MaxRequestSizeStr") 239 } 240 241 quantity, err := strconv.Atoi(reqSizeStr[:len(reqSizeStr)-2]) 242 if err != nil { 243 return errors.New("Unable to convert quantity to integer in MaxRequestSizeStr, found " + reqSizeStr[:len(reqSizeStr)-2]) 244 } 245 unit := reqSizeStr[len(reqSizeStr)-2:] 246 247 // TODO: Make it a named error just in case new errors are added in here in the future 248 Config.MaxRequestSize, err = FriendlyUnitToBytes(quantity, unit) 249 if err != nil { 250 return errors.New("Unable to recognise unit for MaxRequestSizeStr, found " + unit) 251 } 252 } 253 if Dev.DebugMode { 254 log.Print("Set MaxRequestSize to ", Config.MaxRequestSize) 255 } 256 if Config.MaxRequestSize <= 0 { 257 log.Fatal("MaxRequestSize should not be zero or below") 258 } 259 Site.MaxRequestSize = Config.MaxRequestSize 260 261 local := func(h string) bool { 262 return h == "localhost" || h == "127.0.0.1" || h == "::1" || h == Site.URL 263 } 264 uurl, err = url.Parse(Config.StaticResBase) 265 if err != nil { 266 return errors.Wrap(err, "failed to parse Config.StaticResBase: ") 267 } 268 host := uurl.Hostname() 269 if !local(host) { 270 Config.ExtraCSPOrigins += " " + host 271 Config.RefNoRef = true // Avoid leaking origin data to the CDN 272 } 273 if Config.StaticResBase != "" { 274 StaticFiles.Prefix = Config.StaticResBase 275 } 276 277 uurl, err = url.Parse(Config.AvatarResBase) 278 if err != nil { 279 return errors.Wrap(err, "failed to parse Config.AvatarResBase: ") 280 } 281 host2 := uurl.Hostname() 282 if host != host2 && !local(host) { 283 Config.ExtraCSPOrigins += " " + host 284 Config.RefNoRef = true // Avoid leaking origin data to the CDN 285 } 286 if Config.AvatarResBase == "" { 287 Config.AvatarResBase = "/uploads/" 288 } 289 290 if !Config.DisableDefaultNoavatar { 291 cap := 11 292 noavatarCache200 = make([]string, cap) 293 noavatarCache48 = make([]string, cap) 294 /*noavatarCache200Jpg = make([]string, cap) 295 noavatarCache48Jpg = make([]string, cap) 296 noavatarCache200Avif = make([]string, cap) 297 noavatarCache48Avif = make([]string, cap)*/ 298 for i := 0; i < cap; i++ { 299 noavatarCache200[i] = StaticFiles.Prefix + "n" + strconv.Itoa(i) + "-" + strconv.Itoa(200) + ".png?i=0" 300 noavatarCache48[i] = StaticFiles.Prefix + "n" + strconv.Itoa(i) + "-" + strconv.Itoa(48) + ".png?i=0" 301 302 /*noavatarCache200Jpg[i] = StaticFiles.Prefix + "n" + strconv.Itoa(i) + "-" + strconv.Itoa(200) + ".jpg?i=0" 303 noavatarCache48Jpg[i] = StaticFiles.Prefix + "n" + strconv.Itoa(i) + "-" + strconv.Itoa(48) + ".jpg?i=0" 304 305 noavatarCache200Avif[i] = StaticFiles.Prefix + "n" + strconv.Itoa(i) + "-" + strconv.Itoa(200) + ".avif?i=0" 306 noavatarCache48Avif[i] = StaticFiles.Prefix + "n" + strconv.Itoa(i) + "-" + strconv.Itoa(48) + ".avif?i=0"*/ 307 } 308 } 309 Config.Noavatar = strings.Replace(Config.Noavatar, "{site_url}", Site.URL, -1) 310 guestAvatar = GuestAvatar{buildNoavatar(0, 200), buildNoavatar(0, 48)} 311 312 if Config.DisableIP { 313 Config.DisableLastIP = true 314 Config.DisablePostIP = true 315 Config.DisablePollIP = true 316 } 317 318 if Config.PostIPCutoff == 0 { 319 Config.PostIPCutoff = 90 // Default cutoff 320 } 321 if Config.LogPruneCutoff == 0 { 322 Config.LogPruneCutoff = 180 // Default cutoff 323 } 324 if Config.LastIPCutoff == 0 { 325 Config.LastIPCutoff = 3 // Default cutoff 326 } 327 if Config.LastIPCutoff > 12 { 328 Config.LastIPCutoff = 12 329 } 330 if Config.PollIPCutoff == 0 { 331 Config.PollIPCutoff = 90 // Default cutoff 332 } 333 if Config.NoEmbed { 334 DefaultParseSettings.NoEmbed = true 335 } 336 337 // ? Find a way of making these unlimited if zero? It might rule out some optimisations, waste memory, and break layouts 338 if Config.MaxTopicTitleLength == 0 { 339 Config.MaxTopicTitleLength = 100 340 } 341 if Config.MaxUsernameLength == 0 { 342 Config.MaxUsernameLength = 100 343 } 344 GuestUser.Avatar, GuestUser.MicroAvatar = BuildAvatar(0, "") 345 346 if Config.HashAlgo != "" { 347 // TODO: Set the alternate hash algo, e.g. argon2 348 } 349 350 if Config.LogDir == "" { 351 Config.LogDir = "./logs/" 352 } 353 354 // We need this in here rather than verifyConfig as switchToTestDB() currently overwrites the values it verifies 355 if DbConfig.TestDbname == DbConfig.Dbname { 356 return errors.New("Your test database can't have the same name as your production database") 357 } 358 if Dev.TestDB { 359 SwitchToTestDB() 360 } 361 return nil 362 } 363 364 func VerifyConfig() (err error) { 365 switch { 366 case !Forums.Exists(Config.DefaultForum): 367 err = errors.New("Invalid default forum") 368 case Config.ServerCount < 1: 369 err = errors.New("You can't have less than one server") 370 case Config.MaxTopicTitleLength > 100: 371 err = errors.New("The max topic title length cannot be over 100 as that's unable to fit in the database row") 372 case Config.MaxUsernameLength > 100: 373 err = errors.New("The max username length cannot be over 100 as that's unable to fit in the database row") 374 } 375 return err 376 } 377 378 func SwitchToTestDB() { 379 DbConfig.Host = DbConfig.TestHost 380 DbConfig.Username = DbConfig.TestUsername 381 DbConfig.Password = DbConfig.TestPassword 382 DbConfig.Dbname = DbConfig.TestDbname 383 DbConfig.Port = DbConfig.TestPort 384 }