2020-04-29 17:44:29 -07:00
package config
2021-03-02 17:42:07 -08:00
import (
"encoding/json"
2022-08-30 11:30:59 -07:00
"flag"
2021-03-02 17:42:07 -08:00
"fmt"
"os"
"path"
"reflect"
"time"
2021-04-21 17:03:00 -07:00
"github.com/gochan-org/gochan/pkg/gcutil"
2021-03-02 17:42:07 -08:00
)
2021-07-16 19:12:25 -07:00
var (
criticalFields = [ ] string {
"ListenIP" , "Port" , "Username" , "UseFastCGI" , "DocumentRoot" , "TemplateDir" , "LogDir" ,
"DBtype" , "DBhost" , "DBname" , "DBusername" , "DBpassword" , "SiteDomain" , "Styles" ,
}
)
2021-03-02 17:42:07 -08:00
// MissingField represents a field missing from the configuration file
type MissingField struct {
Name string
Critical bool
Description string
}
// ErrInvalidValue represents a GochanConfig field with a bad value
type ErrInvalidValue struct {
Field string
Value interface { }
Details string
}
func ( iv * ErrInvalidValue ) Error ( ) string {
str := fmt . Sprintf ( "invalid %s value: %#v" , iv . Field , iv . Value )
if iv . Details != "" {
str += " - " + iv . Details
}
return str
}
2020-04-29 17:44:29 -07:00
2021-07-11 18:12:02 -07:00
func GetDefaultBool ( key string ) bool {
2021-07-16 19:12:25 -07:00
boolInterface := defaults [ key ]
2021-07-11 18:12:02 -07:00
if boolInterface == nil {
return false
}
b , ok := boolInterface . ( bool )
return b && ok
}
func GetDefaultInt ( key string ) int {
2021-07-16 19:12:25 -07:00
intInterface := defaults [ key ]
2021-07-11 18:12:02 -07:00
if intInterface == nil {
return 0
}
i , ok := intInterface . ( int )
if ! ok {
return 0
}
return i
}
func GetDefaultString ( key string ) string {
2021-07-16 19:12:25 -07:00
i := defaults [ key ]
2021-07-11 18:12:02 -07:00
if i == nil {
return ""
}
str , ok := i . ( string )
if ! ok {
return ""
}
return str
}
2021-03-02 17:42:07 -08:00
// ParseJSON loads and parses JSON data, returning a GochanConfig pointer, any critical missing
// fields that don't have defaults, and any error from parsing the file. This doesn't mean that the
// values are valid, just that they exist
func ParseJSON ( ba [ ] byte ) ( * GochanConfig , [ ] MissingField , error ) {
var missing [ ] MissingField
cfg := & GochanConfig { }
err := json . Unmarshal ( ba , cfg )
if err != nil {
// checking for malformed JSON, invalid field types
return cfg , nil , err
}
var checker map [ string ] interface { } // using this for checking for missing fields
json . Unmarshal ( ba , & checker )
cVal := reflect . ValueOf ( cfg ) . Elem ( )
cType := reflect . TypeOf ( * cfg )
numFields := cType . NumField ( )
for f := 0 ; f < numFields ; f ++ {
fType := cType . Field ( f )
fVal := cVal . Field ( f )
critical := fType . Tag . Get ( "critical" ) == "true"
if ! fVal . CanSet ( ) || fType . Tag . Get ( "json" ) == "-" {
// field is unexported and isn't read from the JSON file
continue
}
if checker [ fType . Name ] != nil {
// field is in the JSON file
continue
}
2021-07-16 19:12:25 -07:00
if defaults [ fType . Name ] != nil {
2021-03-02 17:42:07 -08:00
// the field isn't in the JSON file but has a default value that we can use
2021-07-16 19:12:25 -07:00
fVal . Set ( reflect . ValueOf ( defaults [ fType . Name ] ) )
2021-03-02 17:42:07 -08:00
continue
}
if critical {
// the field isn't in the JSON file and has no default value
missing = append ( missing , MissingField {
Name : fType . Name ,
Description : fType . Tag . Get ( "description" ) ,
Critical : critical ,
} )
}
}
return cfg , missing , err
}
// InitConfig loads and parses gochan.json on startup and verifies its contents
func InitConfig ( versionStr string ) {
2022-08-30 11:30:59 -07:00
if flag . Lookup ( "test.v" ) != nil {
// create a dummy config for testing if we're using go test
cfg = & GochanConfig {
testing : true ,
SystemCriticalConfig : SystemCriticalConfig {
ListenIP : "127.0.0.1" ,
Port : 8080 ,
UseFastCGI : true ,
DebugMode : true ,
DocumentRoot : "html" ,
TemplateDir : "templates" ,
LogDir : "" ,
2022-09-01 22:42:06 -07:00
DBtype : "sqlite3" ,
DBhost : "./testdata/gochantest.db" ,
2022-08-30 11:30:59 -07:00
DBname : "gochan" ,
DBusername : "gochan" ,
DBpassword : "" ,
DBprefix : "gc_" ,
SiteDomain : "127.0.0.1" ,
WebRoot : "/" ,
RandomSeed : "abcd" ,
Version : ParseVersion ( versionStr ) ,
} ,
SiteConfig : SiteConfig {
Username : "gochan" ,
FirstPage : [ ] string { "index.html" , "firstrun.html" , "1.html" } ,
Lockdown : false ,
LockdownMessage : "This imageboard has temporarily disabled posting. We apologize for the inconvenience" ,
SiteName : "Gochan" ,
SiteSlogan : "Gochan testing" ,
MinifyHTML : true ,
MinifyJS : true ,
EnableAppeals : true ,
MaxLogDays : 14 ,
Verbosity : 1 ,
MaxRecentPosts : 3 ,
RecentPostsWithNoFile : false ,
} ,
BoardConfig : BoardConfig {
Sillytags : [ ] string { "Admin" , "Mod" , "Janitor" , "Dweeb" , "Kick me" , "Troll" , "worst pony" } ,
UseSillytags : false ,
Styles : [ ] Style {
{ Name : "Pipes" , Filename : "pipes.css" } ,
{ Name : "BunkerChan" , Filename : "bunkerchan.css" } ,
{ Name : "Burichan" , Filename : "burichan.css" } ,
{ Name : "Clear" , Filename : "clear.css" } ,
{ Name : "Dark" , Filename : "dark.css" } ,
{ Name : "Photon" , Filename : "photon.css" } ,
{ Name : "Yotsuba" , Filename : "yotsuba.css" } ,
{ Name : "Yotsuba B" , Filename : "yotsubab.css" } ,
{ Name : "Windows 9x" , Filename : "win9x.css" } ,
} ,
DefaultStyle : "pipes.css" ,
PostConfig : PostConfig {
NewThreadDelay : 30 ,
ReplyDelay : 7 ,
ThreadsPerPage : 15 ,
PostsPerThreadPage : 50 ,
RepliesOnBoardPage : 3 ,
StickyRepliesOnBoardPage : 1 ,
BanColors : [ ] string {
"admin:#0000A0" ,
"somemod:blue" ,
} ,
BanMessage : "USER WAS BANNED FOR THIS POST" ,
EnableEmbeds : true ,
EmbedWidth : 200 ,
EmbedHeight : 164 ,
ImagesOpenNewTab : true ,
NewTabOnOutlinks : true ,
} ,
UploadConfig : UploadConfig {
ThumbWidth : 200 ,
ThumbHeight : 200 ,
ThumbWidthReply : 125 ,
ThumbHeightReply : 125 ,
ThumbWidthCatalog : 50 ,
ThumbHeightCatalog : 50 ,
} ,
DateTimeFormat : "Mon, January 02, 2006 3:04 PM" ,
} ,
}
return
}
2022-04-03 16:02:07 -07:00
cfgPath = gcutil . FindResource (
"gochan.json" ,
"/usr/local/etc/gochan/gochan.json" ,
"/etc/gochan/gochan.json" )
2021-03-02 17:42:07 -08:00
if cfgPath == "" {
fmt . Println ( "gochan.json not found" )
os . Exit ( 1 )
}
2022-09-08 15:45:29 -07:00
cfgBytes , err := os . ReadFile ( cfgPath )
2021-03-02 17:42:07 -08:00
if err != nil {
fmt . Printf ( "Error reading %s: %s\n" , cfgPath , err . Error ( ) )
os . Exit ( 1 )
}
var fields [ ] MissingField
2022-09-08 15:45:29 -07:00
cfg , fields , err = ParseJSON ( cfgBytes )
2021-03-02 17:42:07 -08:00
if err != nil {
fmt . Printf ( "Error parsing %s: %s" , cfgPath , err . Error ( ) )
}
2021-07-11 11:51:29 -07:00
cfg . jsonLocation = cfgPath
2021-03-02 17:42:07 -08:00
numMissing := 0
for _ , missing := range fields {
fmt . Println ( "Missing field:" , missing . Name )
if missing . Description != "" {
fmt . Println ( "Description:" , missing . Description )
}
numMissing ++
}
if numMissing > 0 {
fmt . Println ( "gochan failed to load the configuration file because there are fields missing.\nSee gochan.example.json in sample-configs for an example configuration file" )
os . Exit ( 1 )
}
2021-07-11 11:51:29 -07:00
if err = cfg . ValidateValues ( ) ; err != nil {
2021-03-02 17:42:07 -08:00
fmt . Println ( err . Error ( ) )
os . Exit ( 1 )
}
2021-07-11 11:51:29 -07:00
if _ , err = os . Stat ( cfg . DocumentRoot ) ; err != nil {
2021-03-02 17:42:07 -08:00
fmt . Println ( err . Error ( ) )
os . Exit ( 1 )
}
2021-07-11 11:51:29 -07:00
if _ , err = os . Stat ( cfg . TemplateDir ) ; err != nil {
2021-03-02 17:42:07 -08:00
fmt . Println ( err . Error ( ) )
os . Exit ( 1 )
}
2021-07-11 11:51:29 -07:00
if _ , err = os . Stat ( cfg . LogDir ) ; err != nil {
2021-03-02 17:42:07 -08:00
fmt . Println ( err . Error ( ) )
os . Exit ( 1 )
}
2021-07-11 11:51:29 -07:00
cfg . LogDir = gcutil . FindResource ( cfg . LogDir , "log" , "/var/log/gochan/" )
2022-11-10 12:27:57 -08:00
if err = gcutil . InitLog ( path . Join ( cfg . LogDir , "gochan.log" ) , cfg . DebugMode ) ; err != nil {
2021-03-02 17:42:07 -08:00
fmt . Println ( err . Error ( ) )
os . Exit ( 1 )
}
2022-09-18 15:32:07 -07:00
if err = gcutil . InitAccessLog ( path . Join ( cfg . LogDir , "gochan_access.log" ) ) ; err != nil {
fmt . Println ( err . Error ( ) )
os . Exit ( 1 )
}
2021-03-02 17:42:07 -08:00
2021-07-11 11:51:29 -07:00
if cfg . Port == 0 {
cfg . Port = 80
2021-03-02 17:42:07 -08:00
}
2021-07-11 11:51:29 -07:00
if len ( cfg . FirstPage ) == 0 {
cfg . FirstPage = [ ] string { "index.html" , "1.html" , "firstrun.html" }
2021-03-02 17:42:07 -08:00
}
2021-07-11 11:51:29 -07:00
if cfg . WebRoot == "" {
cfg . WebRoot = "/"
2021-03-02 17:42:07 -08:00
}
2021-07-11 11:51:29 -07:00
if cfg . WebRoot [ 0 ] != '/' {
cfg . WebRoot = "/" + cfg . WebRoot
2021-03-02 17:42:07 -08:00
}
2021-07-11 11:51:29 -07:00
if cfg . WebRoot [ len ( cfg . WebRoot ) - 1 ] != '/' {
cfg . WebRoot += "/"
2021-03-02 17:42:07 -08:00
}
2021-07-11 11:51:29 -07:00
if cfg . EnableGeoIP {
if _ , err = os . Stat ( cfg . GeoIPDBlocation ) ; err != nil {
2022-09-04 14:27:14 -07:00
gcutil . LogError ( err ) .
Str ( "location" , cfg . GeoIPDBlocation ) .
Msg ( "Unable to load GeoIP file location set in gochan.json, disabling GeoIP" )
2021-03-02 17:42:07 -08:00
}
2021-07-11 11:51:29 -07:00
cfg . EnableGeoIP = false
2021-03-02 17:42:07 -08:00
}
_ , zoneOffset := time . Now ( ) . Zone ( )
2021-07-11 11:51:29 -07:00
cfg . TimeZone = zoneOffset / 60 / 60
2021-03-02 17:42:07 -08:00
2021-07-11 11:51:29 -07:00
cfg . Version = ParseVersion ( versionStr )
cfg . Version . Normalize ( )
2021-03-02 17:42:07 -08:00
}
2021-07-16 19:12:25 -07:00
// TODO: use reflect to check if the field exists in SystemCriticalConfig
func fieldIsCritical ( field string ) bool {
for _ , cF := range criticalFields {
if field == cF {
return true
}
}
return false
}
2022-07-18 12:34:43 -07:00
// WebPath returns an absolute path, starting at the web root (which is "/" by default)
func WebPath ( part ... string ) string {
return path . Join ( cfg . WebRoot , path . Join ( part ... ) )
}
2021-07-16 19:12:25 -07:00
// UpdateFromMap updates the configuration with the given key->values for use in things like the
// config editor page and possibly others
func UpdateFromMap ( m map [ string ] interface { } , validate bool ) error {
for key , val := range m {
if fieldIsCritical ( key ) {
// don't mess with critical/read-only fields (ListenIP, DocumentRoot, etc)
// after the server has started
continue
}
cfg . setField ( key , val )
}
if validate {
return cfg . ValidateValues ( )
}
return nil
}