diff --git a/irc/accounts.go b/irc/accounts.go new file mode 100644 index 00000000..76adf9c1 --- /dev/null +++ b/irc/accounts.go @@ -0,0 +1,16 @@ +// Copyright (c) 2016- Daniel Oaks +// released under the MIT license + +package irc + +import "time" + +// Account represents a user account. +type Account struct { + // Name of the account. + Name string + // RegisteredAt represents the time that the account was registered. + RegisteredAt time.Time + // Clients that are currently logged into this account (useful for notifications). + Clients []Client +} diff --git a/irc/commands.go b/irc/commands.go index ed8bc4cc..aae04dad 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -152,6 +152,10 @@ var Commands = map[string]Command{ usablePreReg: true, minParams: 0, }, + "REG": { + handler: regHandler, + minParams: 3, + }, /*TODO(dan): Add this back in "THEATRE": Command{ handler: theatreHandler, diff --git a/irc/config.go b/irc/config.go index 72894408..5ada6740 100644 --- a/irc/config.go +++ b/irc/config.go @@ -44,16 +44,32 @@ func (conf *PassConfig) PasswordBytes() []byte { return bytes } +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"` + } + } +} + type Config struct { Network struct { Name string } - Datastore struct { - Path string - SQLitePath string `yaml:"sqlite-path"` - } - Server struct { PassConfig Password string @@ -67,6 +83,15 @@ type Config struct { ProxyAllowedFrom []string `yaml:"proxy-allowed-from"` } + Datastore struct { + Path string + SQLitePath string `yaml:"sqlite-path"` + } + + Registration struct { + Accounts AccountRegistrationConfig + } + Operator map[string]*PassConfig Theater map[string]*PassConfig diff --git a/irc/numerics.go b/irc/numerics.go index 934216c0..3596e4ee 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -6,145 +6,155 @@ package irc const ( - RPL_WELCOME = "001" - RPL_YOURHOST = "002" - RPL_CREATED = "003" - RPL_MYINFO = "004" - RPL_ISUPPORT = "005" - RPL_BOUNCE = "010" - RPL_TRACELINK = "200" - RPL_TRACECONNECTING = "201" - RPL_TRACEHANDSHAKE = "202" - RPL_TRACEUNKNOWN = "203" - RPL_TRACEOPERATOR = "204" - RPL_TRACEUSER = "205" - RPL_TRACESERVER = "206" - RPL_TRACESERVICE = "207" - RPL_TRACENEWTYPE = "208" - RPL_TRACECLASS = "209" - RPL_TRACERECONNECT = "210" - RPL_STATSLINKINFO = "211" - RPL_STATSCOMMANDS = "212" - RPL_ENDOFSTATS = "219" - RPL_UMODEIS = "221" - RPL_SERVLIST = "234" - RPL_SERVLISTEND = "235" - RPL_STATSUPTIME = "242" - RPL_STATSOLINE = "243" - RPL_LUSERCLIENT = "251" - RPL_LUSEROP = "252" - RPL_LUSERUNKNOWN = "253" - RPL_LUSERCHANNELS = "254" - RPL_LUSERME = "255" - RPL_ADMINME = "256" - RPL_ADMINLOC1 = "257" - RPL_ADMINLOC2 = "258" - RPL_ADMINEMAIL = "259" - RPL_TRACELOG = "261" - RPL_TRACEEND = "262" - RPL_TRYAGAIN = "263" - RPL_AWAY = "301" - RPL_USERHOST = "302" - RPL_ISON = "303" - RPL_UNAWAY = "305" - RPL_NOWAWAY = "306" - RPL_WHOISUSER = "311" - RPL_WHOISSERVER = "312" - RPL_WHOISOPERATOR = "313" - RPL_WHOWASUSER = "314" - RPL_ENDOFWHO = "315" - RPL_WHOISIDLE = "317" - RPL_ENDOFWHOIS = "318" - RPL_WHOISCHANNELS = "319" - RPL_LIST = "322" - RPL_LISTEND = "323" - RPL_CHANNELMODEIS = "324" - RPL_UNIQOPIS = "325" - RPL_CHANNELCREATED = "329" - RPL_NOTOPIC = "331" - RPL_TOPIC = "332" - RPL_TOPICTIME = "333" - RPL_INVITING = "341" - RPL_SUMMONING = "342" - RPL_INVITELIST = "346" - RPL_ENDOFINVITELIST = "347" - RPL_EXCEPTLIST = "348" - RPL_ENDOFEXCEPTLIST = "349" - RPL_VERSION = "351" - RPL_WHOREPLY = "352" - RPL_NAMREPLY = "353" - RPL_LINKS = "364" - RPL_ENDOFLINKS = "365" - RPL_ENDOFNAMES = "366" - RPL_BANLIST = "367" - RPL_ENDOFBANLIST = "368" - RPL_ENDOFWHOWAS = "369" - RPL_INFO = "371" - RPL_MOTD = "372" - RPL_ENDOFINFO = "374" - RPL_MOTDSTART = "375" - RPL_ENDOFMOTD = "376" - RPL_YOUREOPER = "381" - RPL_REHASHING = "382" - RPL_YOURESERVICE = "383" - RPL_TIME = "391" - RPL_USERSSTART = "392" - RPL_USERS = "393" - RPL_ENDOFUSERS = "394" - RPL_NOUSERS = "395" - ERR_UNKNOWNERROR = "400" - ERR_NOSUCHNICK = "401" - ERR_NOSUCHSERVER = "402" - ERR_NOSUCHCHANNEL = "403" - ERR_CANNOTSENDTOCHAN = "404" - ERR_TOOMANYCHANNELS = "405" - ERR_WASNOSUCHNICK = "406" - ERR_TOOMANYTARGETS = "407" - ERR_NOSUCHSERVICE = "408" - ERR_NOORIGIN = "409" - ERR_INVALIDCAPCMD = "410" - ERR_NORECIPIENT = "411" - ERR_NOTEXTTOSEND = "412" - ERR_NOTOPLEVEL = "413" - ERR_WILDTOPLEVEL = "414" - ERR_BADMASK = "415" - ERR_UNKNOWNCOMMAND = "421" - ERR_NOMOTD = "422" - ERR_NOADMININFO = "423" - ERR_FILEERROR = "424" - ERR_NONICKNAMEGIVEN = "431" - ERR_ERRONEUSNICKNAME = "432" - ERR_NICKNAMEINUSE = "433" - ERR_NICKCOLLISION = "436" - ERR_UNAVAILRESOURCE = "437" - ERR_USERNOTINCHANNEL = "441" - ERR_NOTONCHANNEL = "442" - ERR_USERONCHANNEL = "443" - ERR_NOLOGIN = "444" - ERR_SUMMONDISABLED = "445" - ERR_USERSDISABLED = "446" - ERR_NOTREGISTERED = "451" - ERR_NEEDMOREPARAMS = "461" - ERR_ALREADYREGISTRED = "462" - ERR_NOPERMFORHOST = "463" - ERR_PASSWDMISMATCH = "464" - ERR_YOUREBANNEDCREEP = "465" - ERR_YOUWILLBEBANNED = "466" - ERR_KEYSET = "467" - ERR_CHANNELISFULL = "471" - ERR_UNKNOWNMODE = "472" - ERR_INVITEONLYCHAN = "473" - ERR_BANNEDFROMCHAN = "474" - ERR_BADCHANNELKEY = "475" - ERR_BADCHANMASK = "476" - ERR_NOCHANMODES = "477" - ERR_BANLISTFULL = "478" - ERR_NOPRIVILEGES = "481" - ERR_CHANOPRIVSNEEDED = "482" - ERR_CANTKILLSERVER = "483" - ERR_RESTRICTED = "484" - ERR_UNIQOPPRIVSNEEDED = "485" - ERR_NOOPERHOST = "491" - ERR_UMODEUNKNOWNFLAG = "501" - ERR_USERSDONTMATCH = "502" + RPL_WELCOME = "001" + RPL_YOURHOST = "002" + RPL_CREATED = "003" + RPL_MYINFO = "004" + RPL_ISUPPORT = "005" + RPL_BOUNCE = "010" + RPL_TRACELINK = "200" + RPL_TRACECONNECTING = "201" + RPL_TRACEHANDSHAKE = "202" + RPL_TRACEUNKNOWN = "203" + RPL_TRACEOPERATOR = "204" + RPL_TRACEUSER = "205" + RPL_TRACESERVER = "206" + RPL_TRACESERVICE = "207" + RPL_TRACENEWTYPE = "208" + RPL_TRACECLASS = "209" + RPL_TRACERECONNECT = "210" + RPL_STATSLINKINFO = "211" + RPL_STATSCOMMANDS = "212" + RPL_ENDOFSTATS = "219" + RPL_UMODEIS = "221" + RPL_SERVLIST = "234" + RPL_SERVLISTEND = "235" + RPL_STATSUPTIME = "242" + RPL_STATSOLINE = "243" + RPL_LUSERCLIENT = "251" + RPL_LUSEROP = "252" + RPL_LUSERUNKNOWN = "253" + RPL_LUSERCHANNELS = "254" + RPL_LUSERME = "255" + RPL_ADMINME = "256" + RPL_ADMINLOC1 = "257" + RPL_ADMINLOC2 = "258" + RPL_ADMINEMAIL = "259" + RPL_TRACELOG = "261" + RPL_TRACEEND = "262" + RPL_TRYAGAIN = "263" + RPL_AWAY = "301" + RPL_USERHOST = "302" + RPL_ISON = "303" + RPL_UNAWAY = "305" + RPL_NOWAWAY = "306" + RPL_WHOISUSER = "311" + RPL_WHOISSERVER = "312" + RPL_WHOISOPERATOR = "313" + RPL_WHOWASUSER = "314" + RPL_ENDOFWHO = "315" + RPL_WHOISIDLE = "317" + RPL_ENDOFWHOIS = "318" + RPL_WHOISCHANNELS = "319" + RPL_LIST = "322" + RPL_LISTEND = "323" + RPL_CHANNELMODEIS = "324" + RPL_UNIQOPIS = "325" + RPL_CHANNELCREATED = "329" + RPL_NOTOPIC = "331" + RPL_TOPIC = "332" + RPL_TOPICTIME = "333" + RPL_INVITING = "341" + RPL_SUMMONING = "342" + RPL_INVITELIST = "346" + RPL_ENDOFINVITELIST = "347" + RPL_EXCEPTLIST = "348" + RPL_ENDOFEXCEPTLIST = "349" + RPL_VERSION = "351" + RPL_WHOREPLY = "352" + RPL_NAMREPLY = "353" + RPL_LINKS = "364" + RPL_ENDOFLINKS = "365" + RPL_ENDOFNAMES = "366" + RPL_BANLIST = "367" + RPL_ENDOFBANLIST = "368" + RPL_ENDOFWHOWAS = "369" + RPL_INFO = "371" + RPL_MOTD = "372" + RPL_ENDOFINFO = "374" + RPL_MOTDSTART = "375" + RPL_ENDOFMOTD = "376" + RPL_YOUREOPER = "381" + RPL_REHASHING = "382" + RPL_YOURESERVICE = "383" + RPL_TIME = "391" + RPL_USERSSTART = "392" + RPL_USERS = "393" + RPL_ENDOFUSERS = "394" + RPL_NOUSERS = "395" + ERR_UNKNOWNERROR = "400" + ERR_NOSUCHNICK = "401" + ERR_NOSUCHSERVER = "402" + ERR_NOSUCHCHANNEL = "403" + ERR_CANNOTSENDTOCHAN = "404" + ERR_TOOMANYCHANNELS = "405" + ERR_WASNOSUCHNICK = "406" + ERR_TOOMANYTARGETS = "407" + ERR_NOSUCHSERVICE = "408" + ERR_NOORIGIN = "409" + ERR_INVALIDCAPCMD = "410" + ERR_NORECIPIENT = "411" + ERR_NOTEXTTOSEND = "412" + ERR_NOTOPLEVEL = "413" + ERR_WILDTOPLEVEL = "414" + ERR_BADMASK = "415" + ERR_UNKNOWNCOMMAND = "421" + ERR_NOMOTD = "422" + ERR_NOADMININFO = "423" + ERR_FILEERROR = "424" + ERR_NONICKNAMEGIVEN = "431" + ERR_ERRONEUSNICKNAME = "432" + ERR_NICKNAMEINUSE = "433" + ERR_NICKCOLLISION = "436" + ERR_UNAVAILRESOURCE = "437" + ERR_REG_UNAVAILABLE = "440" + ERR_USERNOTINCHANNEL = "441" + ERR_NOTONCHANNEL = "442" + ERR_USERONCHANNEL = "443" + ERR_NOLOGIN = "444" + ERR_SUMMONDISABLED = "445" + ERR_USERSDISABLED = "446" + ERR_NOTREGISTERED = "451" + ERR_NEEDMOREPARAMS = "461" + ERR_ALREADYREGISTRED = "462" + ERR_NOPERMFORHOST = "463" + ERR_PASSWDMISMATCH = "464" + ERR_YOUREBANNEDCREEP = "465" + ERR_YOUWILLBEBANNED = "466" + ERR_KEYSET = "467" + ERR_CHANNELISFULL = "471" + ERR_UNKNOWNMODE = "472" + ERR_INVITEONLYCHAN = "473" + ERR_BANNEDFROMCHAN = "474" + ERR_BADCHANNELKEY = "475" + ERR_BADCHANMASK = "476" + ERR_NOCHANMODES = "477" + ERR_BANLISTFULL = "478" + ERR_NOPRIVILEGES = "481" + ERR_CHANOPRIVSNEEDED = "482" + ERR_CANTKILLSERVER = "483" + ERR_RESTRICTED = "484" + ERR_UNIQOPPRIVSNEEDED = "485" + ERR_NOOPERHOST = "491" + ERR_UMODEUNKNOWNFLAG = "501" + ERR_USERSDONTMATCH = "502" + RPL_REGISTRATION_SUCCESS = "920" + ERR_ACCOUNT_ALREADY_EXISTS = "921" + ERR_REG_UNSPECIFIED_ERROR = "922" + RPL_VERIFYSUCCESS = "923" + ERR_ACCOUNT_ALREADY_VERIFIED = "924" + ERR_ACCOUNT_INVALID_VERIFY_CODE = "925" + RPL_REG_VERIFICATION_REQUIRED = "927" + ERR_REG_INVALID_CALLBACK = "929" + ERR_REG_INVALID_CRED_TYPE = "982" ) diff --git a/irc/registration.go b/irc/registration.go new file mode 100644 index 00000000..3d2069f3 --- /dev/null +++ b/irc/registration.go @@ -0,0 +1,84 @@ +// Copyright (c) 2016- Daniel Oaks +// released under the MIT license + +package irc + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/DanielOaks/girc-go/ircmsg" + "github.com/tidwall/buntdb" +) + +var ( + errAccountCreation = errors.New("Account could not be created") +) + +// AccountRegistration manages the registration of accounts. +type AccountRegistration struct { + Enabled bool + EnabledRegistrationCallbackTypes []string +} + +// NewAccountRegistration returns a new AccountRegistration, configured correctly. +func NewAccountRegistration(config AccountRegistrationConfig) (accountReg AccountRegistration) { + if config.Enabled { + accountReg.Enabled = true + accountReg.EnabledRegistrationCallbackTypes = config.EnabledCallbacks + } + return accountReg +} + +// regHandler parses the REG command. +func regHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { + subcommand := strings.ToLower(msg.Params[0]) + + if subcommand == "create" { + client.Notice("Parsing CREATE") + + // get and sanitise account name + account := NewName(msg.Params[1]) + if !account.IsNickname() || msg.Params[1] == "*" { + client.Send(nil, server.nameString, ERR_REG_UNSPECIFIED_ERROR, client.nickString, msg.Params[1], "Account name is not valid") + return false + } + accountString := account.String() + + // check whether account exists + // do it all in one write tx to prevent races + err := server.store.Update(func(tx *buntdb.Tx) error { + accountKey := fmt.Sprintf("account %s exists", accountString) + + _, err := tx.Get(accountKey) + if err != buntdb.ErrNotFound { + //TODO(dan): if account verified key doesn't exist account is not verified, calc the maximum time without verification and expire and continue if need be + client.Send(nil, server.nameString, ERR_ACCOUNT_ALREADY_EXISTS, client.nickString, msg.Params[1], "Account already exists") + return errAccountCreation + } + + registeredTimeKey := fmt.Sprintf("account %s registered.time", accountString) + + tx.Set(accountKey, "1", nil) + tx.Set(registeredTimeKey, strconv.FormatInt(time.Now().Unix(), 10), nil) + return nil + }) + + // account could not be created and relevant numerics have been dispatched, abort + if err != nil { + return false + } + + // account didn't already exist, continue with account creation and dispatching verification (if required) + + } else if subcommand == "verify" { + client.Notice("Parsing VERIFY") + } else { + client.Send(nil, server.nameString, ERR_UNKNOWNERROR, client.nickString, "REG", msg.Params[0], "Unknown subcommand") + } + + return false +} diff --git a/irc/server.go b/irc/server.go index 65e6006b..c4d5dd37 100644 --- a/irc/server.go +++ b/irc/server.go @@ -26,25 +26,27 @@ import ( ) type Server struct { - channels ChannelNameMap - clients *ClientLookupSet - commands chan Command - ctime time.Time - db *sql.DB - store buntdb.DB - idle chan *Client - motdLines []string - name Name - nameString string // cache for server name string since it's used with almost every reply - newConns chan clientConn - operators map[Name][]byte - password []byte - signals chan os.Signal - proxyAllowedFrom []string - whoWas *WhoWasList - theaters map[Name][]byte - isupport *ISupportList - checkIdent bool + accounts map[string]Account + channels ChannelNameMap + clients *ClientLookupSet + commands chan Command + ctime time.Time + db *sql.DB + store buntdb.DB + idle chan *Client + motdLines []string + name Name + nameString string // cache for server name string since it's used with almost every reply + newConns chan clientConn + operators map[Name][]byte + password []byte + accountRegistration *AccountRegistration + signals chan os.Signal + proxyAllowedFrom []string + whoWas *WhoWasList + theaters map[Name][]byte + isupport *ISupportList + checkIdent bool } var ( @@ -63,6 +65,7 @@ type clientConn struct { func NewServer(config *Config) *Server { server := &Server{ + accounts: make(map[string]Account), channels: make(ChannelNameMap), clients: NewClientLookupSet(), commands: make(chan Command), @@ -127,6 +130,10 @@ func NewServer(config *Config) *Server { server.wslisten(config.Server.Wslisten, config.Server.TLSListeners) } + // registration + accountReg := NewAccountRegistration(config.Registration.Accounts) + server.accountRegistration = &accountReg + // Attempt to clean up when receiving these signals. signal.Notify(server.signals, SERVER_SIGNALS...) @@ -144,9 +151,25 @@ func NewServer(config *Config) *Server { server.isupport.Add("NETWORK", config.Network.Name) server.isupport.Add("NICKLEN", strconv.Itoa(config.Limits.NickLen)) server.isupport.Add("PREFIX", "(qaohv)~&@%+") - // server.isupport.Add("STATUSMSG", "@+") //TODO(dan): Autogenerate based on PREFIXes, support STATUSMSG + // server.isupport.Add("STATUSMSG", "@+") //TODO(dan): Support STATUSMSG // server.isupport.Add("TARGMAX", "") //TODO(dan): Support this // server.isupport.Add("TOPICLEN", "") //TODO(dan): Support topic length + + // account registration + if server.accountRegistration.Enabled { + // 'none' isn't shown in the REGCALLBACKS vars + var enabledCallbackTypes []string + for _, name := range server.accountRegistration.EnabledRegistrationCallbackTypes { + if name != "none" { + enabledCallbackTypes = append(enabledCallbackTypes, name) + } + } + + server.isupport.Add("REGCOMMANDS", "CREATE,VERIFY") + server.isupport.Add("REGCALLBACKS", strings.Join(enabledCallbackTypes, ",")) + server.isupport.Add("REGCREDTYPES", "passphrase,certfp") + } + server.isupport.RegenerateCachedReply() return server diff --git a/oragono.yaml b/oragono.yaml index e8eb5938..860d4e2b 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -5,17 +5,6 @@ network: # name of the network name: OragonoTest -# datastore configuration -datastore: - # path to the datastore - # this can also be ":memory:" for an in-memory-only db - path: ircd.db - - # path to our sqlite db - # currently used to lookup masks and store persistent chan data - # but planned to be deprecated in a future release - sqlite-path: ircd-sqlite.db - # server configuration server: # server name @@ -65,6 +54,17 @@ operator: # generated using "oragono genpasswd" password: JDJhJDA0JE1vZmwxZC9YTXBhZ3RWT2xBbkNwZnV3R2N6VFUwQUI0RUJRVXRBRHliZVVoa0VYMnlIaGsu +# datastore configuration +datastore: + # path to the datastore + # this can also be ":memory:" for an in-memory-only db + path: ircd.db + + # path to our sqlite db + # currently used to lookup masks and store persistent chan data + # but planned to be deprecated in a future release + sqlite-path: ircd-sqlite.db + # limits - these need to be the same across the network limits: # nicklen is the max nick length allowed