diff --git a/channel.go b/channel.go index f1f2fbf..d54fac6 100644 --- a/channel.go +++ b/channel.go @@ -36,9 +36,9 @@ func (c *Channel) Join(client *Client, key string) { if c.HasMember(client) { // client is already in this channel return } - if len(c.Key) != 0 { //if key is required, verify that client provided matching key + if c.HasMode(ChannelModeKey) { //if key is required, verify that client provided matching key if c.Key != key { - m := irc.Message{Command: irc.ERR_BADCHANNELKEY} + m := irc.Message{Prefix: c.Server.Prefix, Command: irc.ERR_BADCHANNELKEY, Params: []string{client.Nickname, c.Name}, Trailing: "Cannot join channel (+k)"} err := client.Encode(&m) if err != nil { println(err.Error()) @@ -46,6 +46,11 @@ func (c *Channel) Join(client *Client, key string) { return } } + if c.HasMode(ChannelModeLimit) && c.GetMemberCount() >= c.GetLimit() { // Limit flag is set and limit is met + m := irc.Message{Prefix: c.Server.Prefix, Command: irc.ERR_CHANNELISFULL, Params: []string{client.Nickname, c.Name}, Trailing: "Cannot join channel (+l)"} + client.Encode(&m) + } + creator := c.GetMemberCount() == 0 c.AddMember(client) @@ -55,6 +60,10 @@ func (c *Channel) Join(client *Client, key string) { // Creator should be a channel operator - Maybe check if it is a "safe" channel c.AddMemberMode(client, ChannelModeOperator) + if len(key) != 0 { + c.SetKey(key) + + } } var m irc.Message @@ -66,6 +75,37 @@ func (c *Channel) Join(client *Client, key string) { } //Notify existing members that new member is joining + m = irc.Message{Prefix: client.Prefix, Command: irc.JOIN, Params: []string{c.Name}} + c.SendMessage(&m) + + c.Names(client) + +} + +// SetKey sets the channel key +func (c *Channel) SetKey(key string) { + c.AddModeWithValue(ChannelModeKey, key) +} + +// GetKey gets the key for the channel +func (c *Channel) GetKey() string { + val := c.GetMode(ChannelModeKey) + if val == nil { + return "" + } + return val.(string) + +} + +// Names responds to to IRC NAMES command for the channel +func (c *Channel) Names(client *Client) { + + if c.HasMode(ChannelModeSecret) && !c.HasMember(client) { // If channel is secret and client isn't a member, don't reveal it + return + } + if c.HasMode(ChannelModePrivate) && !c.HasMember(client) { // If channel is private and client isn't a member, don't reply + return + } allMembers := make([]string, len(c.members)) i := 0 @@ -73,36 +113,46 @@ func (c *Channel) Join(client *Client, key string) { allMembers[i] = member i++ } - // send list of users in channel + + //channelPrefix := "" + // RFC 2812 defines other channel prefixes + channelPrefix := "=" + if c.HasMode(ChannelModeSecret) { + channelPrefix = "@" + } + if c.HasMode(ChannelModePrivate) { + channelPrefix = "*" + } + for i := 0; i < (len(c.members)/20)+1; i++ { - memberStr := "= " + c.Name + " :" + memberStr := "" end := (i + 1) * 20 if end > len(c.members) { end = len(c.members) } - m := irc.Message{Prefix: client.Prefix, Command: irc.JOIN, Params: []string{c.Name}} + for _, member := range allMembers[i*20 : end] { mClient, _ := client.Server.GetClientByNick(member) if mClient != nil { if c.MemberHasMode(mClient, ChannelModeOperator) { memberStr += "@" + } else if c.MemberHasMode(mClient, ChannelModeVoice) { + memberStr += "+" } memberStr += mClient.Nickname + " " - mClient.Encode(&m) } } - m = irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_NAMREPLY, Params: []string{client.Nickname, memberStr}} + m := irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_NAMREPLY, Params: []string{client.Nickname, channelPrefix, c.Name}, Trailing: memberStr} client.Encode(&m) } - m = irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_ENDOFNAMES, Params: []string{client.Nickname, c.Name}, Trailing: "End of NAMES list"} + m := irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_ENDOFNAMES, Params: []string{client.Nickname, c.Name}, Trailing: "End of NAMES list"} client.Encode(&m) - } // Part handles when a client leaves a channel @@ -134,7 +184,7 @@ func (c *Channel) Quit(client *Client, message string) { return } - m := irc.Message{Prefix: client.Prefix, Command: irc.QUIT} + m := irc.Message{Prefix: client.Prefix, Command: irc.QUIT, Params: []string{c.Name}} if len(message) != 0 { m.Trailing = message } @@ -241,6 +291,7 @@ func (c *Channel) RemoveMemberMode(client *Client, mode ChannelMode) { } +// GetMemberModes returns the Channel Modes active for a member func (c *Channel) GetMemberModes(client *Client) *ChannelModeSet { c.membersMutex.RLock() defer c.membersMutex.RUnlock() @@ -332,3 +383,69 @@ func (c *Channel) TopicCommand(client *Client, topic string) { return } + +// AddBanMask sets the channel ban mask +func (c *Channel) AddBanMask(mask string) { + masks := c.GetBanMasks() + masks[mask] = nil + c.AddModeWithValue(ChannelModeBan, masks) +} + +// GetBanMasks gets the ban masks for the channel +func (c *Channel) GetBanMasks() map[string]interface{} { + val := c.GetMode(ChannelModeBan) + if val == nil { + return make(map[string]interface{}, 0) + } + return val.(map[string]interface{}) + +} + +// AddExceptionMask sets the channel exception mask +func (c *Channel) AddExceptionMask(mask string) { + masks := c.GetExceptionMasks() + masks[mask] = nil + c.AddModeWithValue(ChannelModeExceptionMask, masks) +} + +// GetExceptionMasks gets the exception masks for the channel +func (c *Channel) GetExceptionMasks() map[string]interface{} { + val := c.GetMode(ChannelModeExceptionMask) + if val == nil { + return make(map[string]interface{}, 0) + } + return val.(map[string]interface{}) + +} + +// AddInvitationMask sets the channel invitation mask +func (c *Channel) AddInvitationMask(mask string) { + masks := c.GetInvitationMasks() + masks[mask] = nil + c.AddModeWithValue(ChannelModeInvitationMask, masks) +} + +// GetInvitationMasks gets the invitation masks for the channel +func (c *Channel) GetInvitationMasks() map[string]interface{} { + val := c.GetMode(ChannelModeInvitationMask) + if val == nil { + return make(map[string]interface{}, 0) + } + return val.(map[string]interface{}) + +} + +// SetLimit sets the channel member limit +func (c *Channel) SetLimit(limit int) { + c.AddModeWithValue(ChannelModeLimit, limit) +} + +// GetLimit gets the member limit for the channel +func (c *Channel) GetLimit() int { + val := c.GetMode(ChannelModeLimit) + if val == nil { + return 0 + } + return val.(int) + +} diff --git a/client.go b/client.go index 522dc4c..4cd4e0b 100644 --- a/client.go +++ b/client.go @@ -126,7 +126,7 @@ func (c *Client) Welcome() { c.Prefix = &irc.Prefix{Name: c.Nickname, User: c.Name, Host: c.Host} m := irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_WELCOME, - Params: []string{c.Nickname, c.Server.Config.Welcome}} + Params: []string{c.Nickname, "Welcome to the Internet Relay Network", c.Prefix.String()}} err := c.Encode(&m) if err != nil { @@ -134,7 +134,7 @@ func (c *Client) Welcome() { } m = irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_YOURHOST, - Params: []string{c.Nickname, fmt.Sprintf("Your host is %s", c.Server.Config.Name)}} + Params: []string{c.Nickname, fmt.Sprintf("Your host is %s, running version %s", c.Server.Config.Name, c.Server.Config.Version)}} err = c.Encode(&m) if err != nil { @@ -157,10 +157,23 @@ func (c *Client) Welcome() { return } - m = irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_MOTDSTART, + // Send MOTD + c.MOTD() + +} + +// MOTD returns the Message of the Day of the server to the client +func (c *Client) MOTD() { + + if len(c.Server.Config.MOTD) == 0 { + m := irc.Message{Prefix: c.Server.Prefix, Command: irc.ERR_NOMOTD, Params: []string{c.Nickname}, Trailing: "MOTD File is missing"} + c.Encode(&m) + } + + m := irc.Message{Prefix: c.Server.Prefix, Command: irc.RPL_MOTDSTART, Params: []string{c.Nickname, fmt.Sprintf("%s - Message of the day", c.Server.Config.Name)}} - err = c.Encode(&m) + err := c.Encode(&m) if err != nil { return } diff --git a/commands.go b/commands.go index ccb9fdc..86ab570 100644 --- a/commands.go +++ b/commands.go @@ -2,6 +2,7 @@ package irc import ( "fmt" + "strconv" "strings" "github.com/sorcix/irc" @@ -351,7 +352,7 @@ func whoLine(client *Client, channel *Channel, recipientClient string) string { // Implemented according to RFC 1459 Section 4.2.4 and RFC 2812 Section 3.2.4 func TopicHandler(message *irc.Message, client *Client) { if len(message.Params) == 0 { - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NEEDMOREPARAMS, Trailing: "Not enough parameters"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NEEDMOREPARAMS, Params: []string{client.Nickname}, Trailing: "Not enough parameters"} client.Encode(&m) return } @@ -359,7 +360,7 @@ func TopicHandler(message *irc.Message, client *Client) { channelName := message.Params[0] channel, ok := client.Server.GetChannel(channelName) if !ok { - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NOSUCHCHANNEL, Params: []string{channelName}, Trailing: "No such channel"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NOSUCHCHANNEL, Params: []string{client.Nickname, channelName}, Trailing: "No such channel"} client.Encode(&m) return } @@ -380,7 +381,7 @@ func AwayHandler(message *irc.Message, client *Client) { if len(message.Params) == 0 && len(message.Trailing) == 0 { client.AwayMessage = "" client.RemoveMode(UserModeAway) - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_UNAWAY, Trailing: "You are no longer marked as being away"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_UNAWAY, Params: []string{client.Nickname}, Trailing: "You are no longer marked as being away"} client.Encode(&m) return } @@ -390,7 +391,7 @@ func AwayHandler(message *irc.Message, client *Client) { client.AwayMessage = strings.Join(message.Params, " ") } client.AddMode(UserModeAway) - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_NOWAWAY, Trailing: "You have been marked as being away"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_NOWAWAY, Params: []string{client.Nickname}, Trailing: "You have been marked as being away"} client.Encode(&m) return @@ -401,7 +402,7 @@ func AwayHandler(message *irc.Message, client *Client) { // Implemented according to RFC 1459 Section 4.2.3 and RFC 2812 Section 3.1.5 and RFC 2811 func ModeHandler(message *irc.Message, client *Client) { if len(message.Params) == 0 { - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NEEDMOREPARAMS, Trailing: "Not enough parameters"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NEEDMOREPARAMS, Params: []string{client.Nickname}, Trailing: "Not enough parameters"} client.Encode(&m) return } @@ -420,21 +421,21 @@ func ModeHandler(message *irc.Message, client *Client) { // Implemented according to RFC 1459 Section 4.2.3.2 and RFC 2812 Section 3.1.5 func UserModeHandler(message *irc.Message, client *Client) { if len(message.Params) == 0 { - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NEEDMOREPARAMS, Trailing: "Not enough parameters"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_NEEDMOREPARAMS, Params: []string{client.Nickname}, Trailing: "Not enough parameters"} client.Encode(&m) return } username := message.Params[0] if username != client.Nickname { - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_USERSDONTMATCH, Trailing: "Cannot change mode for other users"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_USERSDONTMATCH, Params: []string{client.Nickname}, Trailing: "Cannot change mode for other users"} client.Encode(&m) return } if len(message.Params) == 1 { // just nickname is provided // return current settings for this user - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_UMODEIS, Params: []string{client.UserModeSet.String()}} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_UMODEIS, Params: []string{client.Nickname, client.UserModeSet.String()}} client.Encode(&m) return } @@ -445,7 +446,7 @@ func UserModeHandler(message *irc.Message, client *Client) { case ModeModifierAdd: case ModeModifierRemove: default: - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UMODEUNKNOWNFLAG, Trailing: "Unknown MODE flag"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UMODEUNKNOWNFLAG, Params: []string{client.Nickname}, Trailing: "Unknown MODE flag"} client.Encode(&m) return } @@ -454,7 +455,7 @@ func UserModeHandler(message *irc.Message, client *Client) { mode := UserMode(modeFlag) _, ok := UserModes[mode] if !ok || mode == UserModeAway { // Away flag should only be set with AWAY command - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UMODEUNKNOWNFLAG, Trailing: "Unknown MODE flag"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UMODEUNKNOWNFLAG, Params: []string{client.Nickname}, Trailing: "Unknown MODE flag"} client.Encode(&m) return } @@ -501,90 +502,173 @@ func ChannelModeHandler(message *irc.Message, client *Client) { } if len(message.Params) == 1 { // just channel name is provided - // return current settings for this user - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_UMODEIS, Params: []string{channel.GetMemberModes(client).String()}} + // return current settings for this channel + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_CHANNELMODEIS, Params: []string{client.Nickname, channel.Name, channel.ChannelModeSet.String()}} client.Encode(&m) return } if !channel.MemberHasMode(client, ChannelModeOperator) { // Only channel operators can make these changes - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_CHANOPRIVSNEEDED, Params: []string{channel.Name}, Trailing: "You're not channel operator"} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_CHANOPRIVSNEEDED, Params: []string{client.Nickname, channel.Name}, Trailing: "You're not channel operator"} client.Encode(&m) return } - setLimit := false - getUsers := false - needMask := false - currentPlace := 1 - for _, modeFlags := range message.Params[1:] { - currentPlace++ - modifier := ModeModifier(modeFlags[0]) - switch modifier { - case ModeModifierAdd: - case ModeModifierRemove: - default: - //modifier := ModeModifierQuery - /* - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UMODEUNKNOWNFLAG, Trailing: "Unknown MODE flag"} - client.Encode(&m) - return - */ - } + type fullFlag struct { + ModeModifier + ChannelMode + Param string + } - for _, modeFlag := range modeFlags[1:] { - mode := ChannelMode(modeFlag) - _, ok := ChannelModes[mode] + needsArgs := []fullFlag{} + changes := []fullFlag{} + argsCount := 0 - if !ok { - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UMODEUNKNOWNFLAG, Trailing: "Unknown MODE flag"} - client.Encode(&m) - return + for _, param := range message.Params[1:] { + if len(needsArgs) != 0 { // This param will be an arg + mode := needsArgs[0] + needsArgs = needsArgs[1:] + argsCount++ + if argsCount > 3 { //Only allow 3 argument based flags per mode command + break } - - if modifier == ModeModifierAdd { - - switch mode { - case ChannelModeCreator: // Can't make oneself a creator - case ChannelModeLimit: - setLimit = true - case ChannelModeOperator, ChannelModeVoice: - getUsers = true - - default: - channel.AddMemberMode(client, mode) + mode.Param = param + switch mode.ChannelMode { + case ChannelModeOperator, ChannelModeVoice: + n, ok := client.Server.GetClientByNick(param) + if ok { + found := channel.MemberHasMode(client, mode.ChannelMode) + if mode.ModeModifier == ModeModifierAdd { + channel.AddMemberMode(n, mode.ChannelMode) + if !found { + changes = append(changes, mode) + } + } else { + channel.RemoveMemberMode(n, mode.ChannelMode) + if found { + changes = append(changes, mode) + } + } } + case ChannelModeLimit: + l, err := strconv.Atoi(param) + if err == nil { + channel.SetLimit(l) + changes = append(changes, mode) + } + case ChannelModeKey: + channel.SetKey(param) + changes = append(changes, mode) + case ChannelModeBan, ChannelModeExceptionMask, ChannelModeInvitationMask: - } else if modifier == ModeModifierRemove { - switch mode { - case ChannelModeCreator: // Can't remove oneself as creator - //case ChannelModeBan: // Can't remove oneself from being restricted - default: - channel.RemoveMemberMode(client, mode) + fillMask := func(mask string) string { + p := irc.ParsePrefix(param) + if len(p.Name) == 0 { + p.Name = "*" + } + if len(p.User) == 0 { + p.User = "*" + } + if len(p.Host) == 0 { + p.Host = "*" + } + + return p.String() + } + + mask := fillMask(param) + switch mode.ChannelMode { + case ChannelModeBan: + masks := channel.GetBanMasks() + _, ok := masks[mask] + if !ok { + channel.AddBanMask(mask) + changes = append(changes, mode) + } + + case ChannelModeExceptionMask: + masks := channel.GetExceptionMasks() + _, ok := masks[mask] + if !ok { + channel.AddExceptionMask(mask) + changes = append(changes, mode) + } + + case ChannelModeInvitationMask: + masks := channel.GetInvitationMasks() + _, ok := masks[mask] + if !ok { + channel.AddInvitationMask(mask) + changes = append(changes, mode) + } } } - } - - if setLimit { // Get the number for setting the member limit cap - /* - limit, err := strconv.Atoi(message.Params[currentPlace]) - if err != nil { + } else { // This should be a mode flag or series of mode flags + modifier := ModeModifierAdd + for _, char := range param { + mod := ModeModifier(char) + switch mod { // Set if flag is adding a removing a mode + case ModeModifierAdd: + modifier = ModeModifierAdd + case ModeModifierRemove: + modifier = ModeModifierRemove } - */ - } - if getUsers { // get users for either creating operators or adding voice - - } - if needMask { // Set the ban mask + flag := ChannelMode(char) + switch flag { + case ChannelModeVoice, ChannelModeOperator, ChannelModeExceptionMask, ChannelModeInvitationMask, ChannelModeBan, ChannelModeLimit: + needsArgs = append(needsArgs, fullFlag{modifier, flag, ""}) + case ChannelModeKey: + if modifier == ModeModifierAdd { + needsArgs = append(needsArgs, fullFlag{modifier, flag, ""}) + } + } + } } } - m := irc.Message{Prefix: client.Server.Prefix, Command: irc.RPL_UMODEIS, Params: []string{client.Nickname, client.UserModeSet.String()}} - client.Encode(&m) + changeString := "" + paramsChanged := []string{} + previousMode := ModeModifier(' ') + for _, change := range changes { + if len(change.Param) != 0 { + paramsChanged = append(paramsChanged, change.Param) + } + if change.ModeModifier != previousMode { + changeString += string(change.ModeModifier) + } + changeString += string(change.ChannelMode) + previousMode = change.ModeModifier + } + + params := []string{channel.Name, changeString} + params = append(params, paramsChanged...) + m := irc.Message{Prefix: client.Prefix, Command: irc.MODE, Params: params} + + // Notify channel members of channel changes + channel.SendMessage(&m) return } + +// NamesHandler is a specialized CommandHandler to respond to channel IRC NAMES commands from a client +// Implemented according to RFC 1459 Section 4.2.5 and RFC 2812 Section 3.2.5 +func NamesHandler(message *irc.Message, client *Client) { + if len(message.Params) == 0 { // Send NAMES response for all channels + for _, ch := range client.Server.channels { + ch.Names(client) + } + } else { + channelNames := strings.Split(message.Params[0], ",") + for _, channelName := range channelNames { + ch, ok := client.Server.GetChannel(channelName) + if ok { + ch.Names(client) + } + + } + } +} diff --git a/modes.go b/modes.go index 7a832f7..d308c4e 100644 --- a/modes.go +++ b/modes.go @@ -1,6 +1,9 @@ package irc -import "sync" +import ( + "fmt" + "sync" +) // UserMode - RFC 1459 Section 4.2.3.2 and RFC 2812 Section 3.1.5 type UserMode rune @@ -153,6 +156,13 @@ func (c *ChannelModeSet) AddMode(mode ChannelMode) { } +// AddModeWithValue adds a ChannelMode as active with a value +func (c *Channel) AddModeWithValue(mode ChannelMode, value interface{}) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.modes[mode] = value +} + // RemoveMode removes the given mode from the active set func (c *ChannelModeSet) RemoveMode(mode ChannelMode) { c.mutex.Lock() @@ -170,14 +180,30 @@ func (c *ChannelModeSet) HasMode(mode ChannelMode) bool { } +// GetMode determines if the ChannelModeSet contains the given ChannelMode +func (c *ChannelModeSet) GetMode(mode ChannelMode) interface{} { + c.mutex.RLock() + defer c.mutex.RUnlock() + return c.modes[mode] + +} + // String returns the ChannelModeSet formatted for the MODE queries func (c *ChannelModeSet) String() string { - s := "" - if len(c.modes) > 0 { - s = "+" - } - for m := range c.modes { + s := "+" + + params := []interface{}{} + for m, param := range c.modes { + switch m { + case ChannelModeKey, ChannelModeLimit, ChannelModeModerated, ChannelModeAnonymous, ChannelModeInviteOnly, ChannelModePrivate, ChannelModeSecret, ChannelModeTopic: + default: + continue + } s += string(m) + params = append(params, param) + } + for _, param := range params { + s += fmt.Sprintf(" %v", param) } return s diff --git a/mux.go b/mux.go index afeb331..7e296a0 100644 --- a/mux.go +++ b/mux.go @@ -26,7 +26,7 @@ func (c *CommandsMux) HandleFunc(command string, handler CommandHandlerFunc) { func (c *CommandsMux) ServeIRC(message *irc.Message, client *Client) { h, ok := c.commands[message.Command] if !ok { - m := irc.Message{Command: irc.ERR_UNKNOWNCOMMAND} + m := irc.Message{Prefix: client.Server.Prefix, Command: irc.ERR_UNKNOWNCOMMAND, Params: []string{client.Nickname}} client.Encode(&m) return } diff --git a/server.go b/server.go index 9721904..f1c5978 100644 --- a/server.go +++ b/server.go @@ -32,7 +32,7 @@ type Server struct { type ServerConfig struct { Name string MOTD string - Welcome string + Version string TLSConfig *tls.Config Addr string @@ -49,6 +49,12 @@ func NewServer(config ServerConfig) *Server { s.clientsByNick = map[string]*Client{} s.Prefix = &irc.Prefix{Name: config.Name} s.channels = map[string]*Channel{} + if len(s.Config.Name) == 0 { + s.Config.Name = "localhost" + } + if len(s.Config.Version) == 0 { + s.Config.Version = "1.0" + } return &s }