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  }