2016-06-15 13:50:56 +02:00
// Copyright (c) 2012-2014 Jeremy Latt
// Copyright (c) 2014-2015 Edmund Huber
2017-03-27 14:15:02 +02:00
// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net>
2016-06-15 13:50:56 +02:00
// released under the MIT license
2014-02-09 16:53:42 +01:00
package irc
import (
2016-04-13 12:45:09 +02:00
"crypto/tls"
2014-03-01 23:34:51 +01:00
"errors"
2016-10-23 02:47:11 +02:00
"fmt"
2016-04-12 15:00:09 +02:00
"io/ioutil"
2014-02-24 07:21:39 +01:00
"log"
2018-01-22 08:30:31 +01:00
"path/filepath"
2016-10-23 03:01:05 +02:00
"strings"
2017-01-12 08:40:01 +01:00
"time"
2016-04-12 15:00:09 +02:00
2017-10-05 16:03:53 +02:00
"code.cloudfoundry.org/bytefmt"
2017-10-09 23:37:13 +02:00
"github.com/oragono/oragono/irc/connection_limits"
2017-06-14 20:00:53 +02:00
"github.com/oragono/oragono/irc/custime"
"github.com/oragono/oragono/irc/logger"
2017-10-05 16:03:53 +02:00
"github.com/oragono/oragono/irc/passwd"
2017-10-05 15:47:43 +02:00
"github.com/oragono/oragono/irc/utils"
2016-04-12 15:00:09 +02:00
"gopkg.in/yaml.v2"
2014-02-09 16:53:42 +01:00
)
2017-04-16 03:31:33 +02:00
// PassConfig holds the connection password.
2014-03-01 23:34:51 +01:00
type PassConfig struct {
Password string
}
2017-04-16 03:31:33 +02:00
// TLSListenConfig defines configuration options for listening on TLS.
2016-04-28 12:12:23 +02:00
type TLSListenConfig struct {
2016-04-13 12:45:09 +02:00
Cert string
Key string
}
2017-04-16 03:31:33 +02:00
// Config returns the TLS contiguration assicated with this TLSListenConfig.
2016-04-28 12:12:23 +02:00
func ( conf * TLSListenConfig ) Config ( ) ( * tls . Config , error ) {
2016-04-13 12:45:09 +02:00
cert , err := tls . LoadX509KeyPair ( conf . Cert , conf . Key )
if err != nil {
2016-04-28 12:12:23 +02:00
return nil , errors . New ( "tls cert+key: invalid pair" )
2016-04-13 12:45:09 +02:00
}
return & tls . Config {
Certificates : [ ] tls . Certificate { cert } ,
} , err
}
2017-04-16 03:31:33 +02:00
// PasswordBytes returns the bytes represented by the password hash.
2014-03-01 23:34:51 +01:00
func ( conf * PassConfig ) PasswordBytes ( ) [ ] byte {
2017-10-05 16:03:53 +02:00
bytes , err := passwd . DecodePasswordHash ( conf . Password )
2014-02-24 07:21:39 +01:00
if err != nil {
2014-03-06 08:07:55 +01:00
log . Fatal ( "decode password error: " , err )
2014-02-24 07:21:39 +01:00
}
return bytes
}
2017-04-16 03:31:33 +02:00
// AccountRegistrationConfig controls account registration.
2016-09-04 11:25:33 +02:00
type AccountRegistrationConfig struct {
Enabled bool
EnabledCallbacks [ ] string ` yaml:"enabled-callbacks" `
Callbacks struct {
Mailto struct {
Server string
Port int
TLS struct {
Enabled bool
InsecureSkipVerify bool ` yaml:"insecure_skip_verify" `
ServerName string ` yaml:"servername" `
}
Username string
Password string
Sender string
VerifyMessageSubject string ` yaml:"verify-message-subject" `
VerifyMessage string ` yaml:"verify-message" `
}
}
2017-09-11 01:16:13 +02:00
AllowMultiplePerConnection bool ` yaml:"allow-multiple-per-connection" `
2016-09-04 11:25:33 +02:00
}
2017-04-16 03:31:33 +02:00
// ChannelRegistrationConfig controls channel registration.
2017-03-24 03:52:38 +01:00
type ChannelRegistrationConfig struct {
Enabled bool
}
2017-04-16 03:31:33 +02:00
// OperClassConfig defines a specific operator class.
2016-10-23 02:47:11 +02:00
type OperClassConfig struct {
Title string
2016-10-23 03:01:05 +02:00
WhoisLine string
2016-10-23 02:47:11 +02:00
Extends string
Capabilities [ ] string
}
2017-04-16 03:31:33 +02:00
// OperConfig defines a specific operator's configuration.
2016-10-23 02:47:11 +02:00
type OperConfig struct {
2016-10-23 03:01:05 +02:00
Class string
Vhost string
WhoisLine string ` yaml:"whois-line" `
Password string
2017-05-08 01:15:16 +02:00
Modes string
2016-10-23 02:47:11 +02:00
}
2017-06-19 22:53:16 +02:00
// PasswordBytes returns the bytes represented by the password hash.
2016-10-23 02:47:11 +02:00
func ( conf * OperConfig ) PasswordBytes ( ) [ ] byte {
2017-10-05 16:03:53 +02:00
bytes , err := passwd . DecodePasswordHash ( conf . Password )
2016-10-23 02:47:11 +02:00
if err != nil {
log . Fatal ( "decode password error: " , err )
}
return bytes
}
2017-04-16 03:31:33 +02:00
// LineLenConfig controls line lengths.
2017-01-13 15:22:42 +01:00
type LineLenConfig struct {
Tags int
Rest int
}
2017-04-16 03:31:33 +02:00
// STSConfig controls the STS configuration/
2017-03-09 10:07:35 +01:00
type STSConfig struct {
Enabled bool
Duration time . Duration ` yaml:"duration-real" `
DurationString string ` yaml:"duration" `
Port int
Preload bool
}
// Value returns the STS value to advertise in CAP
func ( sts * STSConfig ) Value ( ) string {
val := fmt . Sprintf ( "duration=%d," , int ( sts . Duration . Seconds ( ) ) )
if sts . Enabled && sts . Port > 0 {
val += fmt . Sprintf ( ",port=%d" , sts . Port )
}
if sts . Enabled && sts . Preload {
val += ",preload"
}
return val
}
2017-04-30 04:35:07 +02:00
// StackImpactConfig is the config used for StackImpact's profiling.
type StackImpactConfig struct {
Enabled bool
AgentKey string ` yaml:"agent-key" `
AppName string ` yaml:"app-name" `
}
2018-01-22 08:30:31 +01:00
// LangData is the data contained in a language file.
type LangData struct {
Name string
Code string
Maintainers string
Incomplete bool
Translations map [ string ] string
}
2017-04-16 03:31:33 +02:00
// Config defines the overall configuration.
2014-02-09 16:53:42 +01:00
type Config struct {
2016-04-12 07:44:00 +02:00
Network struct {
Name string
}
2014-03-01 23:34:51 +01:00
Server struct {
PassConfig
2017-10-09 23:37:13 +02:00
Password string
Name string
Listen [ ] string
TLSListeners map [ string ] * TLSListenConfig ` yaml:"tls-listeners" `
STS STSConfig
CheckIdent bool ` yaml:"check-ident" `
MOTD string
2017-10-15 08:18:14 +02:00
MOTDFormatting bool ` yaml:"motd-formatting" `
ProxyAllowedFrom [ ] string ` yaml:"proxy-allowed-from" `
WebIRC [ ] webircConfig ` yaml:"webirc" `
MaxSendQString string ` yaml:"max-sendq" `
2017-10-09 23:37:13 +02:00
MaxSendQBytes uint64
ConnectionLimiter connection_limits . LimiterConfig ` yaml:"connection-limits" `
ConnectionThrottler connection_limits . ThrottlerConfig ` yaml:"connection-throttling" `
2014-03-01 23:34:51 +01:00
}
2014-02-25 20:11:34 +01:00
2018-01-21 07:11:16 +01:00
Languages struct {
Enabled bool
Path string
2018-01-22 08:30:31 +01:00
Default string
Data map [ string ] LangData
2018-01-21 07:11:16 +01:00
}
2016-09-04 11:25:33 +02:00
Datastore struct {
2016-09-17 13:23:04 +02:00
Path string
2016-09-04 11:25:33 +02:00
}
2017-03-06 00:43:52 +01:00
Accounts struct {
Registration AccountRegistrationConfig
AuthenticationEnabled bool ` yaml:"authentication-enabled" `
2016-09-04 11:25:33 +02:00
}
2017-03-24 03:52:38 +01:00
Channels struct {
2017-09-06 23:34:38 +02:00
DefaultModes * string ` yaml:"default-modes" `
2017-03-24 03:52:38 +01:00
Registration ChannelRegistrationConfig
}
2016-10-23 02:47:11 +02:00
OperClasses map [ string ] * OperClassConfig ` yaml:"oper-classes" `
Opers map [ string ] * OperConfig
2014-03-13 09:55:46 +01:00
2017-10-02 05:31:40 +02:00
Logging [ ] logger . LoggingConfig
2017-03-06 04:31:10 +01:00
2017-04-30 04:35:07 +02:00
Debug struct {
2017-10-26 10:19:01 +02:00
RecoverFromErrors * bool ` yaml:"recover-from-errors" `
StackImpact StackImpactConfig
2017-04-30 04:35:07 +02:00
}
2016-08-12 14:20:32 +02:00
Limits struct {
2017-01-13 15:22:42 +01:00
AwayLen uint ` yaml:"awaylen" `
ChanListModes uint ` yaml:"chan-list-modes" `
ChannelLen uint ` yaml:"channellen" `
KickLen uint ` yaml:"kicklen" `
MonitorEntries uint ` yaml:"monitor-entries" `
NickLen uint ` yaml:"nicklen" `
TopicLen uint ` yaml:"topiclen" `
WhowasEntries uint ` yaml:"whowas-entries" `
LineLen LineLenConfig ` yaml:"linelen" `
2016-08-12 14:20:32 +02:00
}
2017-09-28 07:30:53 +02:00
Filename string
2014-02-24 07:21:39 +01:00
}
2017-04-16 03:31:33 +02:00
// OperClass defines an assembled operator class.
2016-10-23 02:47:11 +02:00
type OperClass struct {
Title string
2016-10-23 03:01:05 +02:00
WhoisLine string ` yaml:"whois-line" `
2016-10-23 02:47:11 +02:00
Capabilities map [ string ] bool // map to make lookups much easier
}
2017-04-16 03:31:33 +02:00
// OperatorClasses returns a map of assembled operator classes from the given config.
2016-10-23 02:47:11 +02:00
func ( conf * Config ) OperatorClasses ( ) ( * map [ string ] OperClass , error ) {
ocs := make ( map [ string ] OperClass )
// loop from no extends to most extended, breaking if we can't add any more
lenOfLastOcs := - 1
for {
if lenOfLastOcs == len ( ocs ) {
return nil , errors . New ( "OperClasses contains a looping dependency, or a class extends from a class that doesn't exist" )
}
lenOfLastOcs = len ( ocs )
var anyMissing bool
for name , info := range conf . OperClasses {
_ , exists := ocs [ name ]
_ , extendsExists := ocs [ info . Extends ]
if exists {
// class already exists
continue
} else if len ( info . Extends ) > 0 && ! extendsExists {
// class we extend on doesn't exist
_ , exists := conf . OperClasses [ info . Extends ]
if ! exists {
return nil , fmt . Errorf ( "Operclass [%s] extends [%s], which doesn't exist" , name , info . Extends )
}
anyMissing = true
continue
}
// create new operclass
var oc OperClass
oc . Capabilities = make ( map [ string ] bool )
// get inhereted info from other operclasses
if len ( info . Extends ) > 0 {
einfo , _ := ocs [ info . Extends ]
for capab := range einfo . Capabilities {
oc . Capabilities [ capab ] = true
}
}
// add our own info
oc . Title = info . Title
for _ , capab := range info . Capabilities {
oc . Capabilities [ capab ] = true
}
2016-10-23 03:01:05 +02:00
if len ( info . WhoisLine ) > 0 {
oc . WhoisLine = info . WhoisLine
} else {
oc . WhoisLine = "is a"
if strings . Contains ( strings . ToLower ( string ( oc . Title [ 0 ] ) ) , "aeiou" ) {
oc . WhoisLine += "n"
}
oc . WhoisLine += " "
oc . WhoisLine += oc . Title
}
2016-10-23 02:47:11 +02:00
ocs [ name ] = oc
}
if ! anyMissing {
// we've got every operclass!
break
}
}
return & ocs , nil
}
2017-04-16 03:31:33 +02:00
// Oper represents a single assembled operator's config.
2016-10-23 02:47:11 +02:00
type Oper struct {
2016-10-23 03:01:05 +02:00
Class * OperClass
WhoisLine string
2016-10-23 03:28:31 +02:00
Vhost string
2016-10-23 03:01:05 +02:00
Pass [ ] byte
2017-05-08 01:15:16 +02:00
Modes string
2016-10-23 02:47:11 +02:00
}
2017-04-16 03:31:33 +02:00
// Operators returns a map of operator configs from the given OperClass and config.
2016-10-23 02:47:11 +02:00
func ( conf * Config ) Operators ( oc * map [ string ] OperClass ) ( map [ string ] Oper , error ) {
operators := make ( map [ string ] Oper )
for name , opConf := range conf . Opers {
var oper Oper
// oper name
2016-10-11 15:51:46 +02:00
name , err := CasefoldName ( name )
2016-10-23 02:47:11 +02:00
if err != nil {
return nil , fmt . Errorf ( "Could not casefold oper name: %s" , err . Error ( ) )
}
oper . Pass = opConf . PasswordBytes ( )
2016-10-23 03:28:31 +02:00
oper . Vhost = opConf . Vhost
2016-10-23 02:47:11 +02:00
class , exists := ( * oc ) [ opConf . Class ]
if ! exists {
return nil , fmt . Errorf ( "Could not load operator [%s] - they use operclass [%s] which does not exist" , name , opConf . Class )
2016-10-11 15:51:46 +02:00
}
2016-10-23 02:47:11 +02:00
oper . Class = & class
2016-10-23 03:01:05 +02:00
if len ( opConf . WhoisLine ) > 0 {
oper . WhoisLine = opConf . WhoisLine
} else {
oper . WhoisLine = class . WhoisLine
}
2017-05-08 01:15:16 +02:00
oper . Modes = strings . TrimSpace ( opConf . Modes )
2016-10-23 02:47:11 +02:00
// successful, attach to list of opers
operators [ name ] = oper
2014-02-24 18:41:09 +01:00
}
2016-10-23 02:47:11 +02:00
return operators , nil
2014-02-24 18:41:09 +01:00
}
2017-04-16 03:31:33 +02:00
// TLSListeners returns a list of TLS listeners and their configs.
2016-10-11 15:51:46 +02:00
func ( conf * Config ) TLSListeners ( ) map [ string ] * tls . Config {
tlsListeners := make ( map [ string ] * tls . Config )
2016-04-28 12:12:23 +02:00
for s , tlsListenersConf := range conf . Server . TLSListeners {
config , err := tlsListenersConf . Config ( )
2017-09-12 00:40:15 +02:00
config . ClientAuth = tls . RequestClientCert
2016-04-13 12:45:09 +02:00
if err != nil {
log . Fatal ( err )
}
2017-07-26 08:27:11 +02:00
tlsListeners [ s ] = config
2016-04-13 12:45:09 +02:00
}
2016-04-28 12:12:23 +02:00
return tlsListeners
2016-04-13 12:45:09 +02:00
}
2017-04-16 03:31:33 +02:00
// LoadConfig loads the given YAML configuration file.
2014-02-24 07:21:39 +01:00
func LoadConfig ( filename string ) ( config * Config , err error ) {
2016-04-12 15:00:09 +02:00
data , err := ioutil . ReadFile ( filename )
2014-02-09 16:53:42 +01:00
if err != nil {
2016-04-12 15:00:09 +02:00
return nil , err
2014-02-09 16:53:42 +01:00
}
2016-04-12 15:00:09 +02:00
err = yaml . Unmarshal ( data , & config )
if err != nil {
return nil , err
}
2017-09-28 07:30:53 +02:00
config . Filename = filename
2016-06-28 08:06:17 +02:00
// we need this so PasswordBytes returns the correct info
if config . Server . Password != "" {
config . Server . PassConfig . Password = config . Server . Password
}
2016-04-12 07:44:00 +02:00
if config . Network . Name == "" {
return nil , errors . New ( "Network name missing" )
}
2014-03-01 23:34:51 +01:00
if config . Server . Name == "" {
2016-04-12 15:00:09 +02:00
return nil , errors . New ( "Server name missing" )
2014-03-01 23:34:51 +01:00
}
2017-10-05 15:47:43 +02:00
if ! utils . IsHostname ( config . Server . Name ) {
2016-04-21 02:48:15 +02:00
return nil , errors . New ( "Server name must match the format of a hostname" )
}
2016-08-19 15:21:52 +02:00
if config . Datastore . Path == "" {
return nil , errors . New ( "Datastore path missing" )
}
2014-03-01 23:34:51 +01:00
if len ( config . Server . Listen ) == 0 {
2016-04-12 15:00:09 +02:00
return nil , errors . New ( "Server listening addresses missing" )
2014-02-10 22:52:28 +01:00
}
2016-10-16 12:14:56 +02:00
if config . Limits . NickLen < 1 || config . Limits . ChannelLen < 2 || config . Limits . AwayLen < 1 || config . Limits . KickLen < 1 || config . Limits . TopicLen < 1 {
2016-08-12 14:20:32 +02:00
return nil , errors . New ( "Limits aren't setup properly, check them and make them sane" )
}
2017-03-09 10:07:35 +01:00
if config . Server . STS . Enabled {
config . Server . STS . Duration , err = custime . ParseDuration ( config . Server . STS . DurationString )
if err != nil {
return nil , fmt . Errorf ( "Could not parse STS duration: %s" , err . Error ( ) )
}
if config . Server . STS . Port < 0 || config . Server . STS . Port > 65535 {
return nil , fmt . Errorf ( "STS port is incorrect, should be 0 if disabled: %d" , config . Server . STS . Port )
}
}
2017-10-09 23:37:13 +02:00
if config . Server . ConnectionThrottler . Enabled {
config . Server . ConnectionThrottler . Duration , err = time . ParseDuration ( config . Server . ConnectionThrottler . DurationString )
2017-01-12 08:40:01 +01:00
if err != nil {
return nil , fmt . Errorf ( "Could not parse connection-throttle duration: %s" , err . Error ( ) )
}
2017-10-09 23:37:13 +02:00
config . Server . ConnectionThrottler . BanDuration , err = time . ParseDuration ( config . Server . ConnectionThrottler . BanDurationString )
2017-01-12 08:40:01 +01:00
if err != nil {
return nil , fmt . Errorf ( "Could not parse connection-throttle ban-duration: %s" , err . Error ( ) )
}
}
2017-10-15 08:18:14 +02:00
// process webirc blocks
var newWebIRC [ ] webircConfig
for _ , webirc := range config . Server . WebIRC {
// skip webirc blocks with no hosts (such as the example one)
2017-10-15 10:15:18 +02:00
if len ( webirc . Hosts ) == 0 {
2017-10-15 08:18:14 +02:00
continue
}
2017-10-16 00:47:49 +02:00
err = webirc . Populate ( )
2017-10-15 08:18:14 +02:00
if err != nil {
return nil , fmt . Errorf ( "Could not parse WebIRC config: %s" , err . Error ( ) )
}
newWebIRC = append ( newWebIRC , webirc )
}
config . Server . WebIRC = newWebIRC
// process limits
2017-01-13 15:22:42 +01:00
if config . Limits . LineLen . Tags < 512 || config . Limits . LineLen . Rest < 512 {
2017-01-18 00:46:30 +01:00
return nil , errors . New ( "Line lengths must be 512 or greater (check the linelen section under server->limits)" )
2016-11-29 09:38:04 +01:00
}
2017-10-02 05:31:40 +02:00
var newLogConfigs [ ] logger . LoggingConfig
2017-03-06 04:31:10 +01:00
for _ , logConfig := range config . Logging {
// methods
2017-03-10 13:02:08 +01:00
methods := make ( map [ string ] bool )
2017-03-06 04:31:10 +01:00
for _ , method := range strings . Split ( logConfig . Method , " " ) {
if len ( method ) > 0 {
2017-03-10 13:02:08 +01:00
methods [ strings . ToLower ( method ) ] = true
2017-03-06 04:31:10 +01:00
}
}
2017-03-10 13:02:08 +01:00
if methods [ "file" ] && logConfig . Filename == "" {
2017-03-06 04:31:10 +01:00
return nil , errors . New ( "Logging configuration specifies 'file' method but 'filename' is empty" )
}
2017-03-10 13:02:08 +01:00
logConfig . MethodFile = methods [ "file" ]
2017-05-01 10:51:37 +02:00
logConfig . MethodStdout = methods [ "stdout" ]
2017-03-10 13:02:08 +01:00
logConfig . MethodStderr = methods [ "stderr" ]
2017-03-06 04:31:10 +01:00
// levels
2017-03-10 13:02:08 +01:00
level , exists := logger . LogLevelNames [ strings . ToLower ( logConfig . LevelString ) ]
2017-03-06 04:31:10 +01:00
if ! exists {
return nil , fmt . Errorf ( "Could not translate log leve [%s]" , logConfig . LevelString )
}
logConfig . Level = level
// types
for _ , typeStr := range strings . Split ( logConfig . TypeString , " " ) {
if len ( typeStr ) == 0 {
continue
}
if typeStr == "-" {
return nil , errors . New ( "Encountered logging type '-' with no type to exclude" )
}
if typeStr [ 0 ] == '-' {
typeStr = typeStr [ 1 : ]
2017-03-10 13:02:08 +01:00
logConfig . ExcludedTypes = append ( logConfig . ExcludedTypes , typeStr )
2017-03-06 04:31:10 +01:00
} else {
2017-03-10 13:02:08 +01:00
logConfig . Types = append ( logConfig . Types , typeStr )
2017-03-06 04:31:10 +01:00
}
}
if len ( logConfig . Types ) < 1 {
return nil , errors . New ( "Logger has no types to log" )
}
2017-03-06 06:50:23 +01:00
newLogConfigs = append ( newLogConfigs , logConfig )
2017-03-06 04:31:10 +01:00
}
2017-03-06 06:50:23 +01:00
config . Logging = newLogConfigs
2016-10-23 02:47:11 +02:00
2017-03-13 23:12:39 +01:00
config . Server . MaxSendQBytes , err = bytefmt . ToBytes ( config . Server . MaxSendQString )
if err != nil {
return nil , fmt . Errorf ( "Could not parse maximum SendQ size (make sure it only contains whole numbers): %s" , err . Error ( ) )
}
2018-01-22 08:30:31 +01:00
// get language files
config . Languages . Data = make ( map [ string ] LangData )
if config . Languages . Enabled {
files , err := ioutil . ReadDir ( config . Languages . Path )
if err != nil {
return nil , fmt . Errorf ( "Could not load language files: %s" , err . Error ( ) )
}
for _ , f := range files {
// skip dirs
if f . IsDir ( ) {
continue
}
// only load .lang.yaml files
name := f . Name ( )
if ! strings . HasSuffix ( strings . ToLower ( name ) , ".lang.yaml" ) {
continue
}
data , err = ioutil . ReadFile ( filepath . Join ( config . Languages . Path , name ) )
if err != nil {
return nil , fmt . Errorf ( "Could not load language file [%s]: %s" , name , err . Error ( ) )
}
var langInfo LangData
err = yaml . Unmarshal ( data , & langInfo )
if err != nil {
return nil , fmt . Errorf ( "Could not parse language file [%s]: %s" , name , err . Error ( ) )
}
// confirm that values are correct
if langInfo . Code == "en" {
return nil , fmt . Errorf ( "Cannot have language file with code 'en' (this is the default language using strings inside the server code). If you're making an English variant, name it with a more specific code" )
}
if langInfo . Code == "" || langInfo . Name == "" || langInfo . Maintainers == "" {
return nil , fmt . Errorf ( "Code, name or maintainers is empty in language file [%s]" , name )
}
if len ( langInfo . Translations ) == 0 {
return nil , fmt . Errorf ( "Language file [%s] contains no translations" , name )
}
// check for duplicate languages
_ , exists := config . Languages . Data [ strings . ToLower ( langInfo . Code ) ]
if exists {
return nil , fmt . Errorf ( "Language code [%s] defined twice" , langInfo . Code )
}
// and insert into lang info
config . Languages . Data [ strings . ToLower ( langInfo . Code ) ] = langInfo
}
// confirm that default language exists
if config . Languages . Default == "" {
config . Languages . Default = "en"
} else {
config . Languages . Default = strings . ToLower ( config . Languages . Default )
}
_ , exists := config . Languages . Data [ config . Languages . Default ]
if config . Languages . Default != "en" && ! exists {
return nil , fmt . Errorf ( "Cannot find default language [%s]" , config . Languages . Default )
}
}
2016-04-12 15:00:09 +02:00
return config , nil
2014-02-09 16:53:42 +01:00
}