12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658 |
- // Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
- // released under the MIT license
-
- package irc
-
- import (
- "fmt"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "time"
-
- "github.com/ergochat/irc-go/ircfmt"
-
- "github.com/ergochat/ergo/irc/caps"
- "github.com/ergochat/ergo/irc/custime"
- "github.com/ergochat/ergo/irc/passwd"
- "github.com/ergochat/ergo/irc/sno"
- "github.com/ergochat/ergo/irc/utils"
- )
-
- // "enabled" callbacks for specific nickserv commands
- func servCmdRequiresAccreg(config *Config) bool {
- return config.Accounts.Registration.Enabled
- }
-
- func servCmdRequiresAuthEnabled(config *Config) bool {
- return config.Accounts.AuthenticationEnabled
- }
-
- func servCmdRequiresNickRes(config *Config) bool {
- return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled
- }
-
- func servCmdRequiresBouncerEnabled(config *Config) bool {
- return config.Accounts.Multiclient.Enabled
- }
-
- func servCmdRequiresEmailReset(config *Config) bool {
- return config.Accounts.Registration.EmailVerification.Enabled &&
- config.Accounts.Registration.EmailVerification.PasswordReset.Enabled
- }
-
- const nickservHelp = `NickServ lets you register, log in to, and manage an account.`
-
- var (
- nickservCommands = map[string]*serviceCommand{
- "clients": {
- handler: nsClientsHandler,
- help: `Syntax: $bCLIENTS LIST [nickname]$b
-
- CLIENTS LIST shows information about the clients currently attached, via
- the server's multiclient functionality, to your nickname. An administrator
- can use this command to list another user's clients.
-
- Syntax: $bCLIENTS LOGOUT [nickname] [client_id/all]$b
-
- CLIENTS LOGOUT detaches a single client, or all clients currently attached
- to your nickname. An administrator can use this command to logout another
- user's clients.`,
- helpShort: `$bCLIENTS$b can list and logout the sessions attached to a nickname.`,
- enabled: servCmdRequiresBouncerEnabled,
- minParams: 1,
- },
- "drop": {
- handler: nsDropHandler,
- help: `Syntax: $bDROP [nickname]$b
-
- DROP de-links the given (or your current) nickname from your user account.`,
- helpShort: `$bDROP$b de-links your current (or the given) nickname from your user account.`,
- enabled: servCmdRequiresNickRes,
- authRequired: true,
- },
- "enforce": {
- hidden: true,
- handler: nsEnforceHandler,
- help: `Syntax: $bENFORCE [method]$b
-
- ENFORCE is an alias for $bGET enforce$b and $bSET enforce$b. See the help
- entry for $bSET$b for more information.`,
- authRequired: true,
- enabled: servCmdRequiresNickRes,
- },
- "ghost": {
- handler: nsGhostHandler,
- help: `Syntax: $bGHOST <nickname>$b
-
- GHOST disconnects the given user from the network if they're logged in with the
- same user account, letting you reclaim your nickname.`,
- helpShort: `$bGHOST$b reclaims your nickname.`,
- enabled: servCmdRequiresNickRes,
- authRequired: true,
- minParams: 1,
- },
- "group": {
- handler: nsGroupHandler,
- help: `Syntax: $bGROUP$b
-
- GROUP links your current nickname with your logged-in account, so other people
- will not be able to use it.`,
- helpShort: `$bGROUP$b links your current nickname to your user account.`,
- enabled: servCmdRequiresNickRes,
- authRequired: true,
- },
- "identify": {
- handler: nsIdentifyHandler,
- help: `Syntax: $bIDENTIFY <username> [password]$b
-
- IDENTIFY lets you login to the given username using either password auth, or
- certfp (your client certificate) if a password is not given.`,
- helpShort: `$bIDENTIFY$b lets you login to your account.`,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 1,
- },
- "list": {
- handler: nsListHandler,
- help: `Syntax: $bLIST [regex]$b
-
- LIST returns the list of registered nicknames, which match the given regex.
- If no regex is provided, all registered nicknames are returned.`,
- helpShort: `$bLIST$b searches the list of registered nicknames.`,
- enabled: servCmdRequiresAuthEnabled,
- capabs: []string{"accreg"},
- minParams: 0,
- },
- "info": {
- handler: nsInfoHandler,
- help: `Syntax: $bINFO [username]$b
-
- INFO gives you information about the given (or your own) user account.`,
- helpShort: `$bINFO$b gives you information on a user account.`,
- },
- "register": {
- handler: nsRegisterHandler,
- // TODO: "email" is an oversimplification here; it's actually any callback, e.g.,
- // person@example.com, mailto:person@example.com, tel:16505551234.
- help: `Syntax: $bREGISTER <password> [email]$b
-
- REGISTER lets you register your current nickname as a user account. If the
- server allows anonymous registration, you can omit the e-mail address.
-
- If you are currently logged in with a TLS client certificate and wish to use
- it instead of a password to log in, send * as the password.`,
- helpShort: `$bREGISTER$b lets you register a user account.`,
- enabled: servCmdRequiresAccreg,
- minParams: 1,
- maxParams: 2,
- },
- "sadrop": {
- handler: nsDropHandler,
- help: `Syntax: $bSADROP <nickname>$b
-
- SADROP forcibly de-links the given nickname from the attached user account.`,
- helpShort: `$bSADROP$b forcibly de-links the given nickname from its user account.`,
- capabs: []string{"accreg"},
- enabled: servCmdRequiresNickRes,
- minParams: 1,
- },
- "saregister": {
- handler: nsSaregisterHandler,
- help: `Syntax: $bSAREGISTER <username> [password]$b
-
- SAREGISTER registers an account on someone else's behalf.
- This is for use in configurations that require SASL for all connections;
- an administrator can set use this command to set up user accounts.`,
- helpShort: `$bSAREGISTER$b registers an account on someone else's behalf.`,
- enabled: servCmdRequiresAuthEnabled, // deliberate
- capabs: []string{"accreg"},
- minParams: 1,
- },
- "saverify": {
- handler: nsSaverifyHandler,
- help: `Syntax: $bSAVERIFY <username>$b
-
- SAVERIFY manually verifies an account that is pending verification.`,
- helpShort: `$bSAVERIFY$b manually verifies an account that is pending verification.`,
- enabled: servCmdRequiresAuthEnabled, // deliberate
- capabs: []string{"accreg"},
- minParams: 1,
- },
- "sessions": {
- hidden: true,
- handler: nsClientsHandler,
- help: `Syntax: $bSESSIONS [nickname]$b
-
- SESSIONS is an alias for $bCLIENTS LIST$b. See the help entry for $bCLIENTS$b
- for more information.`,
- enabled: servCmdRequiresBouncerEnabled,
- },
- "unregister": {
- handler: nsUnregisterHandler,
- help: `Syntax: $bUNREGISTER <username> [code]$b
-
- UNREGISTER lets you delete your user account (or someone else's, if you're an
- IRC operator with the correct permissions). To prevent accidental
- unregistrations, a verification code is required; invoking the command without
- a code will display the necessary code.`,
- helpShort: `$bUNREGISTER$b lets you delete your user account.`,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 1,
- },
- "erase": {
- handler: nsUnregisterHandler,
- help: `Syntax: $bERASE <username> [code]$b
-
- ERASE deletes all records of an account, allowing it to be re-registered.
- This should be used with caution, because it violates an expectation that
- account names are permanent identifiers. Typically, UNREGISTER should be
- used instead. A confirmation code is required; invoking the command
- without a code will display the necessary code.`,
- helpShort: `$bERASE$b erases all records of an account, allowing reuse.`,
- enabled: servCmdRequiresAuthEnabled,
- capabs: []string{"accreg"},
- minParams: 1,
- },
- "verify": {
- handler: nsVerifyHandler,
- help: `Syntax: $bVERIFY <username> <code>$b
-
- VERIFY lets you complete an account registration, if the server requires email
- or other verification.`,
- helpShort: `$bVERIFY$b lets you complete account registration.`,
- enabled: servCmdRequiresAccreg,
- minParams: 2,
- },
- "passwd": {
- handler: nsPasswdHandler,
- help: `Syntax: $bPASSWD <current> <new> <new_again>$b
- Or: $bPASSWD <username> <new>$b
-
- PASSWD lets you change your account password. You must supply your current
- password and confirm the new one by typing it twice. If you're an IRC operator
- with the correct permissions, you can use PASSWD to reset someone else's
- password by supplying their username and then the desired password. To
- indicate an empty password, use * instead.`,
- helpShort: `$bPASSWD$b lets you change your password.`,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 2,
- },
- "password": {
- aliasOf: "passwd",
- },
- "get": {
- handler: nsGetHandler,
- help: `Syntax: $bGET <setting>$b
-
- GET queries the current values of your account settings. For more information
- on the settings and their possible values, see HELP SET.`,
- helpShort: `$bGET$b queries the current values of your account settings`,
- authRequired: true,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 1,
- },
- "saget": {
- handler: nsGetHandler,
- help: `Syntax: $bSAGET <account> <setting>$b
-
- SAGET queries the values of someone else's account settings. For more
- information on the settings and their possible values, see HELP SET.`,
- helpShort: `$bSAGET$b queries the current values of another user's account settings`,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 2,
- capabs: []string{"accreg"},
- },
- "set": {
- handler: nsSetHandler,
- helpShort: `$bSET$b modifies your account settings`,
- // these are broken out as separate strings so they can be translated separately
- helpStrings: []string{
- `Syntax $bSET <setting> <value>$b
-
- SET modifies your account settings. The following settings are available:`,
-
- `$bENFORCE$b
- 'enforce' lets you specify a custom enforcement mechanism for your registered
- nicknames. Your options are:
- 1. 'none' [no enforcement, overriding the server default]
- 2. 'strict' [you must already be authenticated to use the nick]
- 3. 'default' [use the server default]`,
-
- `$bMULTICLIENT$b
- If 'multiclient' is enabled and you are already logged in and using a nick, a
- second client of yours that authenticates with SASL and requests the same nick
- is allowed to attach to the nick as well (this is comparable to the behavior
- of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
- 'off' (disallow it), and 'default' (use the server default value).`,
-
- `$bAUTOREPLAY-LINES$b
- 'autoreplay-lines' controls the number of lines of channel history that will
- be replayed to you automatically when joining a channel. Your options are any
- positive number, 0 to disable the feature, and 'default' to use the server
- default.`,
-
- `$bREPLAY-JOINS$b
- 'replay-joins' controls whether replayed channel history will include
- lines for join and part. This provides more information about the context of
- messages, but may be spammy. Your options are 'always' and the default of
- 'commands-only' (the messages will be replayed in CHATHISTORY output, but not
- during autoreplay).`,
- `$bALWAYS-ON$b
- 'always-on' controls whether your nickname/identity will remain active
- even while you are disconnected from the server. Your options are 'true',
- 'false', and 'default' (use the server default value).`,
- `$bAUTOREPLAY-MISSED$b
- 'autoreplay-missed' is only effective for always-on clients. If enabled,
- if you have at most one active session, the server will remember the time
- you disconnect and then replay missed messages to you when you reconnect.
- Your options are 'on' and 'off'.`,
- `$bDM-HISTORY$b
- 'dm-history' is only effective for always-on clients. It lets you control
- how the history of your direct messages is stored. Your options are:
- 1. 'off' [no history]
- 2. 'ephemeral' [a limited amount of temporary history, not stored on disk]
- 3. 'on' [history stored in a permanent database, if available]
- 4. 'default' [use the server default]`,
- `$bAUTO-AWAY$b
- 'auto-away' is only effective for always-on clients. If enabled, you will
- automatically be marked away when all your sessions are disconnected, and
- automatically return from away when you connect again.`,
- `$bEMAIL$b
- 'email' controls the e-mail address associated with your account (if the
- server operator allows it, this address can be used for password resets).
- As an additional security measure, if you have a password set, you must
- provide it as an additional argument to $bSET$b, for example,
- SET EMAIL test@example.com hunter2`,
- },
- authRequired: true,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 2,
- },
- "saset": {
- handler: nsSetHandler,
- help: `Syntax: $bSASET <account> <setting> <value>$b
-
- SASET modifies the values of someone else's account settings. For more
- information on the settings and their possible values, see HELP SET.`,
- helpShort: `$bSASET$b modifies another user's account settings`,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 3,
- capabs: []string{"accreg"},
- },
- "sendpass": {
- handler: nsSendpassHandler,
- help: `Syntax: $bSENDPASS <account>$b
-
- SENDPASS sends a password reset email to the email address associated with
- the target account. The reset code in the email can then be used with the
- $bRESETPASS$b command.`,
- helpShort: `$bSENDPASS$b initiates an email-based password reset`,
- enabled: servCmdRequiresEmailReset,
- minParams: 1,
- },
- "resetpass": {
- handler: nsResetpassHandler,
- help: `Syntax: $bRESETPASS <account> <code> <password>$b
-
- RESETPASS resets an account password, using a reset code that was emailed as
- the result of a previous $bSENDPASS$b command.`,
- helpShort: `$bRESETPASS$b completes an email-based password reset`,
- enabled: servCmdRequiresEmailReset,
- minParams: 3,
- },
- "cert": {
- handler: nsCertHandler,
- help: `Syntax: $bCERT <LIST | ADD | DEL> [account] [certfp]$b
-
- CERT examines or modifies the SHA-256 TLS certificate fingerprints that can
- be used to log into an account. Specifically, $bCERT LIST$b lists the
- authorized fingerprints, $bCERT ADD <fingerprint>$b adds a new fingerprint, and
- $bCERT DEL <fingerprint>$b removes a fingerprint. If you're an IRC operator
- with the correct permissions, you can act on another user's account, for
- example with $bCERT ADD <account> <fingerprint>$b. See the operator manual
- for instructions on how to compute the fingerprint.`,
- helpShort: `$bCERT$b controls a user account's certificate fingerprints`,
- enabled: servCmdRequiresAuthEnabled,
- minParams: 1,
- },
- "suspend": {
- handler: nsSuspendHandler,
- help: `Syntax: $bSUSPEND ADD <nickname> [DURATION duration] [reason]$b
- $bSUSPEND DEL <nickname>$b
- $bSUSPEND LIST$b
-
- Suspending an account disables it (preventing new logins) and disconnects
- all associated clients. You can specify a time limit or a reason for
- the suspension. The $bDEL$b subcommand reverses a suspension, and the $bLIST$b
- command lists all current suspensions.`,
- helpShort: `$bSUSPEND$b manages account suspensions`,
- minParams: 1,
- capabs: []string{"ban"},
- },
- "rename": {
- handler: nsRenameHandler,
- help: `Syntax: $bRENAME <account> <newname>$b
-
- RENAME allows a server administrator to change the name of an account.
- Currently, you can only change the canonical casefolding of an account
- (e.g., you can change "Alice" to "alice", but not "Alice" to "Amanda").`,
- helpShort: `$bRENAME$b renames an account`,
- minParams: 2,
- capabs: []string{"accreg"},
- },
- "verifyemail": {
- handler: nsVerifyEmailHandler,
- authRequired: true,
- minParams: 1,
- hidden: true,
- },
- }
- )
-
- func nsGetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- var account string
- if command == "saget" {
- account = params[0]
- params = params[1:]
- } else {
- account = client.Account()
- }
-
- accountData, err := server.accounts.LoadAccount(account)
- if err == errAccountDoesNotExist {
- service.Notice(rb, client.t("No such account"))
- return
- } else if err != nil {
- service.Notice(rb, client.t("Error loading account data"))
- return
- }
-
- displaySetting(service, params[0], accountData.Settings, client, rb)
- }
-
- func displaySetting(service *ircService, settingName string, settings AccountSettings, client *Client, rb *ResponseBuffer) {
- config := client.server.Config()
- switch strings.ToLower(settingName) {
- case "enforce":
- storedValue := settings.NickEnforcement
- serializedStoredValue := nickReservationToString(storedValue)
- service.Notice(rb, fmt.Sprintf(client.t("Your stored nickname enforcement setting is: %s"), serializedStoredValue))
- serializedActualValue := nickReservationToString(configuredEnforcementMethod(config, storedValue))
- service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, your nickname is enforced with: %s"), serializedActualValue))
- case "autoreplay-lines":
- if settings.AutoreplayLines == nil {
- service.Notice(rb, fmt.Sprintf(client.t("You will receive the server default of %d lines of autoreplayed history"), config.History.AutoreplayOnJoin))
- } else {
- service.Notice(rb, fmt.Sprintf(client.t("You will receive %d lines of autoreplayed history"), *settings.AutoreplayLines))
- }
- case "replay-joins":
- switch settings.ReplayJoins {
- case ReplayJoinsCommandsOnly:
- service.Notice(rb, client.t("You will see JOINs and PARTs in /HISTORY output, but not in autoreplay"))
- case ReplayJoinsAlways:
- service.Notice(rb, client.t("You will see JOINs and PARTs in /HISTORY output and in autoreplay"))
- }
- case "multiclient":
- if !config.Accounts.Multiclient.Enabled {
- service.Notice(rb, client.t("This feature has been disabled by the server administrators"))
- } else {
- switch settings.AllowBouncer {
- case MulticlientAllowedServerDefault:
- if config.Accounts.Multiclient.AllowedByDefault {
- service.Notice(rb, client.t("Multiclient functionality is currently enabled for your account, but you can opt out"))
- } else {
- service.Notice(rb, client.t("Multiclient functionality is currently disabled for your account, but you can opt in"))
- }
- case MulticlientDisallowedByUser:
- service.Notice(rb, client.t("Multiclient functionality is currently disabled for your account"))
- case MulticlientAllowedByUser:
- service.Notice(rb, client.t("Multiclient functionality is currently enabled for your account"))
- }
- }
- case "always-on":
- stored := settings.AlwaysOn
- actual := persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, stored)
- service.Notice(rb, fmt.Sprintf(client.t("Your stored always-on setting is: %s"), userPersistentStatusToString(stored)))
- if actual {
- service.Notice(rb, client.t("Given current server settings, your client is always-on"))
- } else {
- service.Notice(rb, client.t("Given current server settings, your client is not always-on"))
- }
- case "autoreplay-missed":
- stored := settings.AutoreplayMissed
- if stored {
- alwaysOn := persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
- if alwaysOn {
- service.Notice(rb, client.t("Autoreplay of missed messages is enabled"))
- } else {
- service.Notice(rb, client.t("You have enabled autoreplay of missed messages, but you can't receive them because your client isn't set to always-on"))
- }
- } else {
- service.Notice(rb, client.t("Your account is not configured to receive autoreplayed missed messages"))
- }
- case "auto-away":
- stored := settings.AutoAway
- alwaysOn := persistenceEnabled(config.Accounts.Multiclient.AlwaysOn, settings.AlwaysOn)
- actual := persistenceEnabled(config.Accounts.Multiclient.AutoAway, settings.AutoAway)
- service.Notice(rb, fmt.Sprintf(client.t("Your stored auto-away setting is: %s"), userPersistentStatusToString(stored)))
- if actual && alwaysOn {
- service.Notice(rb, client.t("Given current server settings, auto-away is enabled for your client"))
- } else if actual && !alwaysOn {
- service.Notice(rb, client.t("Because your client is not always-on, auto-away is disabled"))
- } else if !actual {
- service.Notice(rb, client.t("Given current server settings, auto-away is disabled for your client"))
- }
- case "dm-history":
- effectiveValue := historyEnabled(config.History.Persistent.DirectMessages, settings.DMHistory)
- service.Notice(rb, fmt.Sprintf(client.t("Your stored direct message history setting is: %s"), historyStatusToString(settings.DMHistory)))
- service.Notice(rb, fmt.Sprintf(client.t("Given current server settings, your direct message history setting is: %s"), historyStatusToString(effectiveValue)))
- case "email":
- if settings.Email != "" {
- service.Notice(rb, fmt.Sprintf(client.t("Your stored e-mail address is: %s"), settings.Email))
- } else {
- service.Notice(rb, client.t("You have no stored e-mail address"))
- }
- default:
- service.Notice(rb, client.t("No such setting"))
- }
- }
-
- func userPersistentStatusToString(status PersistentStatus) string {
- // #1544: "mandatory" as a user setting should display as "enabled"
- result := persistentStatusToString(status)
- if result == "mandatory" {
- result = "enabled"
- }
- return result
- }
-
- func nsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- var privileged bool
- var account string
- if command == "saset" {
- privileged = true
- account = params[0]
- params = params[1:]
- } else {
- account = client.Account()
- }
-
- key := strings.ToLower(params[0])
- // unprivileged NS SET EMAIL is different because it requires a confirmation
- if !privileged && key == "email" {
- nsSetEmailHandler(service, client, params, rb)
- return
- }
-
- var munger settingsMunger
- var finalSettings AccountSettings
- var err error
- switch key {
- case "pass", "password":
- service.Notice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
- return
- case "enforce":
- var method NickEnforcementMethod
- method, err = nickReservationFromString(params[1])
- if err != nil {
- err = errInvalidParams
- break
- }
- // updating enforcement settings is special-cased, because it requires
- // an update to server.accounts.accountToMethod
- finalSettings, err = server.accounts.SetEnforcementStatus(account, method)
- if err == nil {
- finalSettings.NickEnforcement = method // success
- }
- case "autoreplay-lines":
- var newValue *int
- if strings.ToLower(params[1]) != "default" {
- val, err_ := strconv.Atoi(params[1])
- if err_ != nil || val < 0 {
- err = errInvalidParams
- break
- }
- newValue = new(int)
- *newValue = val
- }
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.AutoreplayLines = newValue
- return
- }
- case "multiclient":
- var newValue MulticlientAllowedSetting
- if strings.ToLower(params[1]) == "default" {
- newValue = MulticlientAllowedServerDefault
- } else {
- var enabled bool
- enabled, err = utils.StringToBool(params[1])
- if enabled {
- newValue = MulticlientAllowedByUser
- } else {
- newValue = MulticlientDisallowedByUser
- }
- }
- if err == nil {
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.AllowBouncer = newValue
- return
- }
- }
- case "replay-joins":
- var newValue ReplayJoinsSetting
- newValue, err = replayJoinsSettingFromString(params[1])
- if err == nil {
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.ReplayJoins = newValue
- return
- }
- }
- case "always-on":
- // #821: it's problematic to alter the value of always-on if you're not
- // the (actual or potential) always-on client yourself. make an exception
- // for `saset` to give operators an escape hatch (any consistency problems
- // can probably be fixed by restarting the server):
- if command != "saset" {
- details := client.Details()
- if details.nick != details.accountName {
- err = errNickAccountMismatch
- }
- }
- if err == nil {
- var newValue PersistentStatus
- newValue, err = persistentStatusFromString(params[1])
- // "opt-in" and "opt-out" don't make sense as user preferences
- if err == nil && newValue != PersistentOptIn && newValue != PersistentOptOut {
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.AlwaysOn = newValue
- return
- }
- }
- }
- case "autoreplay-missed":
- var newValue bool
- newValue, err = utils.StringToBool(params[1])
- if err == nil {
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.AutoreplayMissed = newValue
- return
- }
- }
- case "auto-away":
- var newValue PersistentStatus
- newValue, err = persistentStatusFromString(params[1])
- // "opt-in" and "opt-out" don't make sense as user preferences
- if err == nil && newValue != PersistentOptIn && newValue != PersistentOptOut {
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.AutoAway = newValue
- return
- }
- }
- case "dm-history":
- var newValue HistoryStatus
- newValue, err = historyStatusFromString(params[1])
- if err == nil {
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.DMHistory = newValue
- return
- }
- }
- case "email":
- newValue := params[1]
- munger = func(in AccountSettings) (out AccountSettings, err error) {
- out = in
- out.Email = newValue
- return
- }
- default:
- err = errInvalidParams
- }
-
- if munger != nil {
- finalSettings, err = server.accounts.ModifyAccountSettings(account, munger)
- }
-
- switch err {
- case nil:
- service.Notice(rb, client.t("Successfully changed your account settings"))
- displaySetting(service, key, finalSettings, client, rb)
- case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
- service.Notice(rb, client.t(err.Error()))
- case errNickAccountMismatch:
- service.Notice(rb, fmt.Sprintf(client.t("Your nickname must match your account name %s exactly to modify this setting. Try changing it with /NICK, or logging out and back in with the correct nickname."), client.AccountName()))
- default:
- // unknown error
- service.Notice(rb, client.t("An error occurred"))
- }
- }
-
- // handle unprivileged NS SET EMAIL, which sends a confirmation code
- func nsSetEmailHandler(service *ircService, client *Client, params []string, rb *ResponseBuffer) {
- config := client.server.Config()
- if !config.Accounts.Registration.EmailVerification.Enabled {
- rb.Notice(client.t("E-mail verification is disabled"))
- return
- }
- if !nsLoginThrottleCheck(service, client, rb) {
- return
- }
- var password string
- if len(params) > 2 {
- password = params[2]
- }
- account := client.Account()
- errorMessage := nsConfirmPassword(client.server, account, password)
- if errorMessage != "" {
- service.Notice(rb, client.t(errorMessage))
- return
- }
- err := client.server.accounts.NsSetEmail(client, params[1])
- switch err {
- case nil:
- service.Notice(rb, client.t("Check your e-mail for instructions on how to confirm your change of address"))
- case errLimitExceeded:
- service.Notice(rb, client.t("Try again later"))
- default:
- // if appropriate, show the client the error from the attempted email sending
- if rErr := registrationCallbackErrorText(config, client, err); rErr != "" {
- service.Notice(rb, rErr)
- } else {
- service.Notice(rb, client.t("An error occurred"))
- }
- }
- }
-
- func nsVerifyEmailHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- err := server.accounts.NsVerifyEmail(client, params[0])
- switch err {
- case nil:
- service.Notice(rb, client.t("Successfully changed your account settings"))
- displaySetting(service, "email", client.AccountSettings(), client, rb)
- case errAccountVerificationInvalidCode:
- service.Notice(rb, client.t(err.Error()))
- default:
- service.Notice(rb, client.t("An error occurred"))
- }
- }
-
- func nsDropHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- sadrop := command == "sadrop"
- var nick string
- if len(params) > 0 {
- nick = params[0]
- } else {
- nick = client.NickCasefolded()
- }
-
- err := server.accounts.SetNickReserved(client, nick, sadrop, false)
- if err == nil {
- service.Notice(rb, fmt.Sprintf(client.t("Successfully ungrouped nick %s with your account"), nick))
- } else if err == errAccountNotLoggedIn {
- service.Notice(rb, client.t("You're not logged into an account"))
- } else if err == errAccountCantDropPrimaryNick {
- service.Notice(rb, client.t("You can't ungroup your primary nickname (try unregistering your account instead)"))
- } else {
- service.Notice(rb, client.t("Could not ungroup nick"))
- }
- }
-
- func nsGhostHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- nick := params[0]
-
- ghost := server.clients.Get(nick)
- if ghost == nil {
- service.Notice(rb, client.t("No such nick"))
- return
- } else if ghost == client {
- service.Notice(rb, client.t("You can't GHOST yourself (try /QUIT instead)"))
- return
- } else if ghost.AlwaysOn() {
- service.Notice(rb, client.t("You can't GHOST an always-on client"))
- return
- }
-
- authorized := false
- account := client.Account()
- if account != "" {
- // the user must either own the nick, or the target client
- authorized = (server.accounts.NickToAccount(nick) == account) || (ghost.Account() == account)
- }
- if !authorized {
- service.Notice(rb, client.t("You don't own that nick"))
- return
- }
-
- ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil)
- ghost.destroy(nil)
- }
-
- func nsGroupHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- nick := client.Nick()
- err := server.accounts.SetNickReserved(client, nick, false, true)
- if err == nil {
- service.Notice(rb, fmt.Sprintf(client.t("Successfully grouped nick %s with your account"), nick))
- } else if err == errAccountTooManyNicks {
- service.Notice(rb, client.t("You have too many nicks reserved already (you can remove some with /NS DROP)"))
- } else if err == errNicknameReserved {
- service.Notice(rb, client.t("That nickname is already reserved by someone else"))
- } else {
- service.Notice(rb, client.t("Error reserving nickname"))
- }
- }
-
- func nsLoginThrottleCheck(service *ircService, client *Client, rb *ResponseBuffer) (success bool) {
- throttled, remainingTime := client.checkLoginThrottle()
- if throttled {
- service.Notice(rb, fmt.Sprintf(client.t("Please wait at least %v and try again"), remainingTime.Round(time.Millisecond)))
- }
- return !throttled
- }
-
- func nsIdentifyHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if client.LoggedIntoAccount() {
- service.Notice(rb, client.t("You're already logged into an account"))
- return
- }
-
- var err error
- loginSuccessful := false
-
- var username, passphrase string
- if len(params) == 1 {
- if rb.session.certfp != "" {
- username = params[0]
- } else {
- // XXX undocumented compatibility mode with other nickservs, allowing
- // /msg NickServ identify passphrase
- username = client.NickCasefolded()
- passphrase = params[0]
- }
- } else {
- username = params[0]
- passphrase = params[1]
- }
-
- // try passphrase
- if passphrase != "" {
- err = server.accounts.AuthenticateByPassphrase(client, username, passphrase)
- loginSuccessful = (err == nil)
- }
-
- // try certfp
- if !loginSuccessful && rb.session.certfp != "" {
- err = server.accounts.AuthenticateByCertificate(client, rb.session.certfp, rb.session.peerCerts, "")
- loginSuccessful = (err == nil)
- }
-
- nickFixupFailed := false
- if loginSuccessful {
- if !fixupNickEqualsAccount(client, rb, server.Config(), service.prefix) {
- loginSuccessful = false
- // fixupNickEqualsAccount sends its own error message, don't send another
- nickFixupFailed = true
- }
- }
-
- if loginSuccessful {
- sendSuccessfulAccountAuth(service, client, rb, true)
- } else if !nickFixupFailed {
- service.Notice(rb, fmt.Sprintf(client.t("Authentication failed: %s"), authErrorToMessage(server, err)))
- }
- }
-
- func nsListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if !client.HasRoleCapabs("accreg") {
- service.Notice(rb, client.t("Insufficient privileges"))
- return
- }
-
- var searchRegex *regexp.Regexp
- if len(params) > 0 {
- var err error
- searchRegex, err = regexp.Compile(params[0])
- if err != nil {
- service.Notice(rb, client.t("Invalid regex"))
- return
- }
- }
-
- service.Notice(rb, ircfmt.Unescape(client.t("*** $bNickServ LIST$b ***")))
-
- nicks := server.accounts.AllNicks()
- for _, nick := range nicks {
- if searchRegex == nil || searchRegex.MatchString(nick) {
- service.Notice(rb, fmt.Sprintf(" %s", nick))
- }
- }
-
- service.Notice(rb, ircfmt.Unescape(client.t("*** $bEnd of NickServ LIST$b ***")))
- }
-
- func nsInfoHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if !server.Config().Accounts.AuthenticationEnabled && !client.HasRoleCapabs("accreg") {
- service.Notice(rb, client.t("This command has been disabled by the server administrators"))
- return
- }
-
- var accountName string
- if len(params) > 0 {
- nick := params[0]
- if server.Config().Accounts.NickReservation.Enabled {
- accountName = server.accounts.NickToAccount(nick)
- if accountName == "" {
- service.Notice(rb, client.t("That nickname is not registered"))
- return
- }
- } else {
- accountName = nick
- }
- } else {
- accountName = client.Account()
- if accountName == "" {
- service.Notice(rb, client.t("You're not logged into an account"))
- return
- }
- }
-
- account, err := server.accounts.LoadAccount(accountName)
- if err != nil || !account.Verified {
- if server.accounts.accountWasUnregistered(accountName) {
- service.Notice(rb, client.t("Name reserved due to a prior registration"))
- } else {
- service.Notice(rb, client.t("Account does not exist"))
- }
- return
- }
-
- service.Notice(rb, fmt.Sprintf(client.t("Account: %s"), account.Name))
- registeredAt := account.RegisteredAt.Format(time.RFC1123)
- service.Notice(rb, fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
-
- if account.Name == client.AccountName() || client.HasRoleCapabs("accreg") {
- if account.Settings.Email != "" {
- service.Notice(rb, fmt.Sprintf(client.t("Email address: %s"), account.Settings.Email))
- }
- }
-
- // TODO nicer formatting for this
- for _, nick := range account.AdditionalNicks {
- service.Notice(rb, fmt.Sprintf(client.t("Additional grouped nick: %s"), nick))
- }
- listRegisteredChannels(service, accountName, rb)
- if account.Suspended != nil {
- service.Notice(rb, suspensionToString(client, *account.Suspended))
- }
- }
-
- func listRegisteredChannels(service *ircService, accountName string, rb *ResponseBuffer) {
- client := rb.session.client
- channels := client.server.channels.ChannelsForAccount(accountName)
- service.Notice(rb, fmt.Sprintf(client.t("Account %s has %d registered channel(s)."), accountName, len(channels)))
- for _, channel := range channels {
- service.Notice(rb, fmt.Sprintf(client.t("Registered channel: %s"), channel))
- }
- }
-
- func nsRegisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- details := client.Details()
- passphrase := params[0]
- var email string
- if 1 < len(params) {
- email = params[1]
- }
-
- certfp := rb.session.certfp
- if passphrase == "*" {
- if certfp == "" {
- service.Notice(rb, client.t("You must be connected with TLS and a client certificate to do this"))
- return
- } else {
- passphrase = ""
- }
- }
-
- if passphrase != "" {
- cfPassphrase, err := Casefold(passphrase)
- if err == nil && cfPassphrase == details.nickCasefolded {
- service.Notice(rb, client.t("Usage: REGISTER <passphrase> [email]")) // #1179
- return
- }
- }
-
- if !nsLoginThrottleCheck(service, client, rb) {
- return
- }
-
- config := server.Config()
- account := details.nick
- if config.Accounts.NickReservation.ForceGuestFormat {
- matches := config.Accounts.NickReservation.guestRegexp.FindStringSubmatch(account)
- if matches == nil || len(matches) < 2 {
- service.Notice(rb, client.t("Erroneous nickname"))
- return
- }
- account = matches[1]
- }
-
- callbackNamespace, callbackValue, validationErr := parseCallback(email, config)
- if validationErr != nil {
- service.Notice(rb, client.t("Registration requires a valid e-mail address"))
- return
- }
-
- err := server.accounts.Register(client, account, callbackNamespace, callbackValue, passphrase, rb.session.certfp)
- if err == nil {
- if callbackNamespace == "*" {
- err = server.accounts.Verify(client, account, "", true)
- if err == nil && fixupNickEqualsAccount(client, rb, config, service.prefix) {
- sendSuccessfulRegResponse(service, client, rb)
- }
- } else {
- messageTemplate := client.t("Account created, pending verification; verification code has been sent to %s")
- message := fmt.Sprintf(messageTemplate, callbackValue)
- service.Notice(rb, message)
- announcePendingReg(client, rb, account)
- }
- } else {
- // details could not be stored and relevant numerics have been dispatched, abort
- message := registrationErrorToMessage(config, client, err)
- service.Notice(rb, client.t(message))
- }
- }
-
- func nsSaregisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- registerCap := rb.session.capabilities.Has(caps.AccountRegistration)
-
- var account, passphrase string
- account = params[0]
- if 1 < len(params) && params[1] != "*" {
- passphrase = params[1]
- }
- err := server.accounts.SARegister(account, passphrase)
-
- if err != nil {
- var errMsg string
- var failCode string
- if err == errAccountAlreadyRegistered || err == errAccountAlreadyVerified {
- errMsg = client.t("Account already exists")
- failCode = "USERNAME_EXISTS"
- } else if err == errNameReserved {
- errMsg = client.t(err.Error())
- failCode = "USERNAME_EXISTS"
- } else if err == errAccountBadPassphrase {
- errMsg = client.t("Passphrase contains forbidden characters or is otherwise invalid")
- failCode = "INVALID_PASSWORD"
- } else {
- server.logger.Error("services", "unknown error from saregister", err.Error())
- errMsg = fmt.Sprintf(client.t("Could not register: %v"), err)
- failCode = "UNKNOWN_ERROR"
- }
- service.Notice(rb, errMsg)
- if registerCap {
- rb.Add(nil, server.name, "FAIL", "REGISTER", failCode, utils.SafeErrorParam(account), err.Error())
- }
- } else {
- service.Notice(rb, fmt.Sprintf(client.t("Successfully registered account %s"), account))
- if registerCap {
- rb.Add(nil, server.name, "REGISTER", "SUCCESS", account, client.t("Account successfully registered"))
- }
- server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Operator $c[grey][$r%s$c[grey]] registered account $c[grey][$r%s$c[grey]] with SAREGISTER"), client.Oper().Name, account))
- }
- }
-
- func nsSaverifyHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- account := params[0]
- err := server.accounts.Verify(nil, account, "", true)
- if err == nil {
- service.Notice(rb, fmt.Sprintf(client.t("Successfully verified account %s"), account))
- } else {
- service.Notice(rb, fmt.Sprintf(client.t("Failed to verify account %s: %v"), account, err.Error()))
- }
- }
-
- func nsUnregisterHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- erase := command == "erase"
-
- username := params[0]
- var verificationCode string
- if len(params) > 1 {
- verificationCode = params[1]
- }
-
- if username == "" {
- service.Notice(rb, client.t("You must specify an account"))
- return
- }
-
- var accountName string
- var registeredAt time.Time
- if erase {
- // account may not be in a loadable state, e.g., if it was unregistered
- accountName = username
- // make the confirmation code nondeterministic for ERASE
- registeredAt = server.ctime
- } else {
- account, err := server.accounts.LoadAccount(username)
- if err == errAccountDoesNotExist {
- service.Notice(rb, client.t("Invalid account name"))
- return
- } else if err != nil {
- service.Notice(rb, client.t("Internal error"))
- return
- }
- accountName = account.Name
- registeredAt = account.RegisteredAt
- }
-
- if !(accountName == client.AccountName() || client.HasRoleCapabs("accreg")) {
- service.Notice(rb, client.t("Insufficient oper privs"))
- return
- }
-
- expectedCode := utils.ConfirmationCode(accountName, registeredAt)
- if expectedCode != verificationCode {
- if erase {
- service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: erasing this account will allow it to be re-registered; consider UNREGISTER instead.$b")))
- } else {
- service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this account will remove its stored privileges.$b")))
- service.Notice(rb, ircfmt.Unescape(client.t("$bNote that an unregistered account name remains reserved and cannot be re-registered.$b")))
- service.Notice(rb, ircfmt.Unescape(client.t("$bIf you are having problems with your account, contact an administrator.$b")))
- service.Notice(rb, ircfmt.Unescape(client.t("$bUnregistering your account will unregister all channels you founded.$b")))
- service.Notice(rb, ircfmt.Unescape(client.t("$bTo prevent this, transfer your channels first with CS TRANSFER.$b")))
- }
- service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/NS %s %s %s", strings.ToUpper(command), accountName, expectedCode)))
- return
- }
-
- err := server.accounts.Unregister(accountName, erase)
- if err == errAccountDoesNotExist {
- service.Notice(rb, client.t(err.Error()))
- } else if err != nil {
- service.Notice(rb, client.t("Error while unregistering account"))
- } else {
- service.Notice(rb, fmt.Sprintf(client.t("Successfully unregistered account %s"), accountName))
- server.logger.Info("accounts", "client", client.Nick(), "unregistered account", accountName)
- client.server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] unregistered account $c[grey][$r%s$c[grey]]"), client.NickMaskString(), accountName))
- }
- }
-
- func nsVerifyHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- username, code := params[0], params[1]
- err := server.accounts.Verify(client, username, code, false)
-
- var errorMessage string
- if err != nil {
- switch err {
- case errAccountAlreadyLoggedIn, errAccountVerificationInvalidCode, errAccountAlreadyVerified:
- errorMessage = err.Error()
- default:
- errorMessage = errAccountVerificationFailed.Error()
- }
- }
-
- if errorMessage != "" {
- service.Notice(rb, client.t(errorMessage))
- return
- }
-
- if fixupNickEqualsAccount(client, rb, server.Config(), service.prefix) {
- sendSuccessfulRegResponse(service, client, rb)
- }
- }
-
- func nsConfirmPassword(server *Server, account, passphrase string) (errorMessage string) {
- accountData, err := server.accounts.LoadAccount(account)
- if err != nil {
- errorMessage = `You're not logged into an account`
- } else {
- hash := accountData.Credentials.PassphraseHash
- if hash != nil {
- if passphrase == "" {
- errorMessage = `You must supply a password`
- } else if passwd.CompareHashAndPassword(hash, []byte(passphrase)) != nil {
- errorMessage = `Password incorrect`
- }
- }
- }
- return
- }
-
- func nsPasswdHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- var target string
- var newPassword string
- var errorMessage string
-
- var oper *Oper
-
- switch len(params) {
- case 2:
- oper = client.Oper()
- if !oper.HasRoleCapab("accreg") {
- errorMessage = `Insufficient privileges`
- } else {
- target, newPassword = params[0], params[1]
- if newPassword == "*" {
- newPassword = ""
- }
- message := fmt.Sprintf("Operator %s ran NS PASSWD for account %s", oper.Name, target)
- server.snomasks.Send(sno.LocalOpers, message)
- server.logger.Info("opers", message)
- }
- case 3:
- target = client.Account()
- newPassword = params[1]
- if newPassword == "*" {
- newPassword = ""
- }
-
- checkPassword := params[2]
- if checkPassword == "*" {
- checkPassword = "" // #1883
- }
-
- if target == "" {
- errorMessage = `You're not logged into an account`
- } else if newPassword != checkPassword {
- errorMessage = `Passwords do not match`
- } else {
- if !nsLoginThrottleCheck(service, client, rb) {
- return
- }
- errorMessage = nsConfirmPassword(server, target, params[0])
- }
- default:
- errorMessage = `Invalid parameters`
- }
-
- if errorMessage != "" {
- service.Notice(rb, client.t(errorMessage))
- return
- }
-
- err := server.accounts.setPassword(target, newPassword, oper != nil)
- switch err {
- case nil:
- service.Notice(rb, client.t("Password changed"))
- case errEmptyCredentials:
- service.Notice(rb, client.t("You can't delete your password unless you add a certificate fingerprint"))
- case errCredsExternallyManaged:
- service.Notice(rb, client.t("Your account credentials are managed externally and cannot be changed here"))
- case errCASFailed:
- service.Notice(rb, client.t("Try again later"))
- case errAccountDoesNotExist:
- service.Notice(rb, client.t("Account does not exist"))
- default:
- server.logger.Error("internal", "could not upgrade user password:", err.Error())
- service.Notice(rb, client.t("Password could not be changed due to server error"))
- }
- }
-
- func nsEnforceHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- newParams := []string{"enforce"}
- if len(params) == 0 {
- nsGetHandler(service, server, client, "get", newParams, rb)
- } else {
- newParams = append(newParams, params[0])
- nsSetHandler(service, server, client, "set", newParams, rb)
- }
- }
-
- func nsClientsHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- var verb string
-
- if command == "sessions" {
- // Legacy "SESSIONS" command is an alias for CLIENTS LIST.
- verb = "list"
- } else if len(params) > 0 {
- verb = strings.ToLower(params[0])
- params = params[1:]
- }
-
- switch verb {
- case "list":
- nsClientsListHandler(service, server, client, params, rb)
- case "logout":
- nsClientsLogoutHandler(service, server, client, params, rb)
- default:
- service.Notice(rb, client.t("Invalid parameters"))
- }
- }
-
- func nsClientsListHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) {
- target := client
- hasPrivs := client.HasRoleCapabs("ban")
- if 0 < len(params) {
- target = server.clients.Get(params[0])
- if target == nil {
- service.Notice(rb, client.t("No such nick"))
- return
- }
- if target != client && !hasPrivs {
- service.Notice(rb, client.t("Command restricted"))
- return
- }
- }
-
- sessionData, currentIndex := target.AllSessionData(rb.session, hasPrivs)
- service.Notice(rb, fmt.Sprintf(client.t("Nickname %[1]s has %[2]d attached clients(s)"), target.Nick(), len(sessionData)))
- for i, session := range sessionData {
- if currentIndex == i {
- service.Notice(rb, fmt.Sprintf(client.t("Client %d (currently attached client):"), session.sessionID))
- } else {
- service.Notice(rb, fmt.Sprintf(client.t("Client %d:"), session.sessionID))
- }
- if session.deviceID != "" {
- service.Notice(rb, fmt.Sprintf(client.t("Device ID: %s"), session.deviceID))
- }
- service.Notice(rb, fmt.Sprintf(client.t("IP address: %s"), session.ip.String()))
- service.Notice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname))
- if hasPrivs {
- service.Notice(rb, fmt.Sprintf(client.t("Connection: %s"), session.connInfo))
- }
- service.Notice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(time.RFC1123)))
- service.Notice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(time.RFC1123)))
- if session.certfp != "" {
- service.Notice(rb, fmt.Sprintf(client.t("Certfp: %s"), session.certfp))
- }
- for _, capStr := range session.caps {
- if capStr != "" {
- service.Notice(rb, fmt.Sprintf(client.t("IRCv3 CAPs: %s"), capStr))
- }
- }
- }
- }
-
- func nsClientsLogoutHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) {
- if len(params) < 1 {
- service.Notice(rb, client.t("Missing client ID to logout (or \"all\")"))
- return
- }
-
- target := client
- if len(params) >= 2 {
- // CLIENTS LOGOUT [nickname] [client ID]
- target = server.clients.Get(params[0])
- if target == nil {
- service.Notice(rb, client.t("No such nick"))
- return
- }
- // User must have "kill" privileges to logout other user sessions.
- if target != client {
- oper := client.Oper()
- if !oper.HasRoleCapab("kill") {
- service.Notice(rb, client.t("Insufficient oper privs"))
- return
- }
- }
- params = params[1:]
- }
-
- var sessionToDestroy *Session // target.destroy(nil) will logout all sessions
- if strings.ToLower(params[0]) != "all" {
- sessionID, err := strconv.ParseInt(params[0], 10, 64)
- if err != nil {
- service.Notice(rb, client.t("Client ID to logout should be an integer (or \"all\")"))
- return
- }
- // Find the client ID that the user requested to logout.
- sessions := target.Sessions()
- for _, session := range sessions {
- if session.sessionID == sessionID {
- sessionToDestroy = session
- }
- }
- if sessionToDestroy == nil {
- service.Notice(rb, client.t("Specified client ID does not exist"))
- return
- }
- }
-
- target.destroy(sessionToDestroy)
- if (sessionToDestroy != nil && rb.session != sessionToDestroy) || client != target {
- if sessionToDestroy != nil {
- service.Notice(rb, client.t("Successfully logged out session"))
- } else {
- service.Notice(rb, client.t("Successfully logged out all sessions"))
- }
- }
- }
-
- func nsCertHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- verb := strings.ToLower(params[0])
- params = params[1:]
- var target, certfp string
-
- switch verb {
- case "list":
- if 1 <= len(params) {
- target = params[0]
- }
- case "add", "del":
- if 2 <= len(params) {
- target, certfp = params[0], params[1]
- if cftarget, err := CasefoldName(target); err == nil && client.Account() == cftarget {
- // If the target is equal to the account, then the user accidentally invoked operator
- // syntax (cert add mynick <fp>) instead of self syntax (cert add <fp>).
- target = ""
- }
- } else if len(params) == 1 {
- certfp = params[0]
- } else if len(params) == 0 && verb == "add" && rb.session.certfp != "" {
- certfp = rb.session.certfp // #1059
- } else {
- service.Notice(rb, client.t("Invalid parameters"))
- return
- }
- default:
- service.Notice(rb, client.t("Invalid parameters"))
- return
- }
-
- hasPrivs := client.HasRoleCapabs("accreg")
- if target != "" && !hasPrivs {
- service.Notice(rb, client.t("Insufficient privileges"))
- return
- } else if target == "" {
- target = client.Account()
- if target == "" {
- service.Notice(rb, client.t("You're not logged into an account"))
- return
- }
- }
-
- var err error
- switch verb {
- case "list":
- accountData, err := server.accounts.LoadAccount(target)
- if err == errAccountDoesNotExist {
- service.Notice(rb, client.t("Account does not exist"))
- return
- } else if err != nil {
- service.Notice(rb, client.t("An error occurred"))
- return
- }
- certfps := accountData.Credentials.Certfps
- service.Notice(rb, fmt.Sprintf(client.t("There are %[1]d certificate fingerprint(s) authorized for account %[2]s."), len(certfps), accountData.Name))
- for i, certfp := range certfps {
- service.Notice(rb, fmt.Sprintf("%d: %s", i+1, certfp))
- }
- return
- case "add":
- err = server.accounts.addRemoveCertfp(target, certfp, true, hasPrivs)
- case "del":
- err = server.accounts.addRemoveCertfp(target, certfp, false, hasPrivs)
- }
-
- switch err {
- case nil:
- if verb == "add" {
- service.Notice(rb, client.t("Certificate fingerprint successfully added"))
- } else {
- service.Notice(rb, client.t("Certificate fingerprint successfully removed"))
- }
- case errNoop:
- if verb == "add" {
- service.Notice(rb, client.t("That certificate fingerprint was already authorized"))
- } else {
- service.Notice(rb, client.t("Certificate fingerprint not found"))
- }
- case errAccountDoesNotExist:
- service.Notice(rb, client.t("Account does not exist"))
- case errLimitExceeded:
- service.Notice(rb, client.t("You already have too many certificate fingerprints"))
- case utils.ErrInvalidCertfp:
- service.Notice(rb, client.t("Invalid certificate fingerprint"))
- case errCertfpAlreadyExists:
- service.Notice(rb, client.t("That certificate fingerprint is already associated with another account"))
- case errEmptyCredentials:
- service.Notice(rb, client.t("You can't remove all your certificate fingerprints unless you add a password"))
- case errCredsExternallyManaged:
- service.Notice(rb, client.t("Your account credentials are managed externally and cannot be changed here"))
- case errCASFailed:
- service.Notice(rb, client.t("Try again later"))
- default:
- server.logger.Error("internal", "could not modify certificates:", err.Error())
- service.Notice(rb, client.t("An error occurred"))
- }
- }
-
- func nsSuspendHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- subCmd := strings.ToLower(params[0])
- params = params[1:]
- switch subCmd {
- case "add":
- nsSuspendAddHandler(service, server, client, command, params, rb)
- case "del", "delete", "remove":
- nsSuspendRemoveHandler(service, server, client, command, params, rb)
- case "list":
- nsSuspendListHandler(service, server, client, command, params, rb)
- default:
- service.Notice(rb, client.t("Invalid parameters"))
- }
- }
-
- func nsSuspendAddHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if len(params) == 0 {
- service.Notice(rb, client.t("Invalid parameters"))
- return
- }
-
- account := params[0]
- params = params[1:]
-
- var duration time.Duration
- if 2 <= len(params) && strings.ToLower(params[0]) == "duration" {
- var err error
- cDuration, err := custime.ParseDuration(params[1])
- if err != nil {
- service.Notice(rb, client.t("Invalid time duration for NS SUSPEND"))
- return
- }
- duration = time.Duration(cDuration)
- params = params[2:]
- }
-
- var reason string
- if len(params) != 0 {
- reason = strings.Join(params, " ")
- }
-
- name := client.Oper().Name
-
- err := server.accounts.Suspend(account, duration, name, reason)
- switch err {
- case nil:
- service.Notice(rb, fmt.Sprintf(client.t("Successfully suspended account %s"), account))
- case errAccountDoesNotExist:
- service.Notice(rb, client.t("No such account"))
- default:
- service.Notice(rb, client.t("An error occurred"))
- }
- }
-
- func nsSuspendRemoveHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if len(params) == 0 {
- service.Notice(rb, client.t("Invalid parameters"))
- return
- }
-
- err := server.accounts.Unsuspend(params[0])
- switch err {
- case nil:
- service.Notice(rb, fmt.Sprintf(client.t("Successfully un-suspended account %s"), params[0]))
- case errAccountDoesNotExist:
- service.Notice(rb, client.t("No such account"))
- case errNoop:
- service.Notice(rb, client.t("Account was not suspended"))
- default:
- service.Notice(rb, client.t("An error occurred"))
- }
- }
-
- // sort in reverse order of creation time
- type ByCreationTime []AccountSuspension
-
- func (a ByCreationTime) Len() int { return len(a) }
- func (a ByCreationTime) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
- func (a ByCreationTime) Less(i, j int) bool { return a[i].TimeCreated.After(a[j].TimeCreated) }
-
- func nsSuspendListHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- listAccountSuspensions(client, rb, service.prefix)
- }
-
- func listAccountSuspensions(client *Client, rb *ResponseBuffer, source string) {
- suspensions := client.server.accounts.ListSuspended()
- sort.Sort(ByCreationTime(suspensions))
- nick := client.Nick()
- rb.Add(nil, source, "NOTICE", nick, fmt.Sprintf(client.t("There are %d active account suspensions."), len(suspensions)))
- for _, suspension := range suspensions {
- rb.Add(nil, source, "NOTICE", nick, suspensionToString(client, suspension))
- }
- }
-
- func suspensionToString(client *Client, suspension AccountSuspension) (result string) {
- duration := client.t("indefinite")
- if suspension.Duration != time.Duration(0) {
- duration = suspension.Duration.String()
- }
- ts := suspension.TimeCreated.Format(time.RFC1123)
- reason := client.t("No reason given.")
- if suspension.Reason != "" {
- reason = fmt.Sprintf(client.t("Reason: %s"), suspension.Reason)
- }
- return fmt.Sprintf(client.t("Account %[1]s suspended at %[2]s. Duration: %[3]s. %[4]s"), suspension.AccountName, ts, duration, reason)
- }
-
- func nsSendpassHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if !nsLoginThrottleCheck(service, client, rb) {
- return
- }
-
- account := params[0]
- var message string
- err := server.accounts.NsSendpass(client, account)
- switch err {
- case nil:
- server.snomasks.Send(sno.LocalAccounts, fmt.Sprintf("Client %s sent a password reset for account %s", client.Nick(), account))
- message = `Successfully sent password reset email`
- case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended:
- message = err.Error()
- case errValidEmailRequired:
- message = `That account is not associated with an email address`
- case errLimitExceeded:
- message = `Try again later`
- default:
- server.logger.Error("services", "error in NS SENDPASS", err.Error())
- message = `An error occurred`
- }
- rb.Notice(client.t(message))
- }
-
- func nsResetpassHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- if !nsLoginThrottleCheck(service, client, rb) {
- return
- }
-
- var message string
- err := server.accounts.NsResetpass(client, params[0], params[1], params[2])
- switch err {
- case nil:
- message = `Successfully reset account password`
- case errAccountDoesNotExist, errAccountUnverified, errAccountSuspended, errAccountBadPassphrase:
- message = err.Error()
- case errAccountInvalidCredentials:
- message = `Code did not match`
- default:
- server.logger.Error("services", "error in NS RESETPASS", err.Error())
- message = `An error occurred`
- }
- rb.Notice(client.t(message))
- }
-
- func nsRenameHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
- oldName, newName := params[0], params[1]
- err := server.accounts.Rename(oldName, newName)
-
- if err != nil {
- service.Notice(rb, fmt.Sprintf(client.t("Couldn't rename account: %s"), client.t(err.Error())))
- return
- }
-
- service.Notice(rb, client.t("Successfully renamed account"))
- if server.Config().Accounts.NickReservation.ForceNickEqualsAccount {
- if curClient := server.clients.Get(oldName); curClient != nil {
- renameErr := performNickChange(client.server, client, curClient, nil, newName, rb)
- if renameErr != nil && renameErr != errNoop {
- service.Notice(rb, fmt.Sprintf(client.t("Warning: could not rename affected client: %v"), err))
- }
- }
- }
- }
|