Browse Source

Merge pull request #247 from slingamn/vhosts.3

initial vhosts implementation, #183
tags/v0.12.0
Daniel Oaks 6 years ago
parent
commit
de7b679fc5
No account linked to committer's email address
16 changed files with 1069 additions and 375 deletions
  1. 316
    17
      irc/accounts.go
  2. 6
    115
      irc/chanserv.go
  3. 62
    22
      irc/client.go
  4. 2
    16
      irc/commands.go
  5. 38
    9
      irc/config.go
  6. 1
    0
      irc/errors.go
  7. 5
    2
      irc/gateways.go
  8. 22
    0
      irc/getters.go
  9. 29
    52
      irc/handlers.go
  10. 12
    0
      irc/help.go
  11. 314
    0
      irc/hostserv.go
  12. 1
    4
      irc/nickname.go
  13. 24
    133
      irc/nickserv.go
  14. 12
    5
      irc/server.go
  15. 194
    0
      irc/services.go
  16. 31
    0
      oragono.yaml

+ 316
- 17
irc/accounts.go View File

@@ -14,6 +14,7 @@ import (
14 14
 	"strconv"
15 15
 	"strings"
16 16
 	"sync"
17
+	"sync/atomic"
17 18
 	"time"
18 19
 
19 20
 	"github.com/oragono/oragono/irc/caps"
@@ -30,14 +31,24 @@ const (
30 31
 	keyAccountRegTime          = "account.registered.time %s"
31 32
 	keyAccountCredentials      = "account.credentials %s"
32 33
 	keyAccountAdditionalNicks  = "account.additionalnicks %s"
34
+	keyAccountVHost            = "account.vhost %s"
33 35
 	keyCertToAccount           = "account.creds.certfp %s"
36
+
37
+	keyVHostQueueAcctToId = "vhostQueue %s"
38
+	vhostRequestIdx       = "vhostQueue"
34 39
 )
35 40
 
36 41
 // everything about accounts is persistent; therefore, the database is the authoritative
37 42
 // source of truth for all account information. anything on the heap is just a cache
38 43
 type AccountManager struct {
44
+	// XXX these are up here so they can be aligned to a 64-bit boundary, please forgive me
45
+	// autoincrementing ID for vhost requests:
46
+	vhostRequestID           uint64
47
+	vhostRequestPendingCount uint64
48
+
39 49
 	sync.RWMutex                      // tier 2
40 50
 	serialCacheUpdateMutex sync.Mutex // tier 3
51
+	vHostUpdateMutex       sync.Mutex // tier 3
41 52
 
42 53
 	server *Server
43 54
 	// track clients logged in to accounts
@@ -53,6 +64,7 @@ func NewAccountManager(server *Server) *AccountManager {
53 64
 	}
54 65
 
55 66
 	am.buildNickToAccountIndex()
67
+	am.initVHostRequestQueue()
56 68
 	return &am
57 69
 }
58 70
 
@@ -94,8 +106,44 @@ func (am *AccountManager) buildNickToAccountIndex() {
94 106
 		am.nickToAccount = result
95 107
 		am.Unlock()
96 108
 	}
109
+}
97 110
 
98
-	return
111
+func (am *AccountManager) initVHostRequestQueue() {
112
+	if !am.server.AccountConfig().VHosts.Enabled {
113
+		return
114
+	}
115
+
116
+	am.vHostUpdateMutex.Lock()
117
+	defer am.vHostUpdateMutex.Unlock()
118
+
119
+	// the db maps the account name to the autoincrementing integer ID of its request
120
+	// create an numerically ordered index on ID, so we can list the oldest requests
121
+	// finally, collect the integer id of the newest request and the total request count
122
+	var total uint64
123
+	var lastIDStr string
124
+	err := am.server.store.Update(func(tx *buntdb.Tx) error {
125
+		err := tx.CreateIndex(vhostRequestIdx, fmt.Sprintf(keyVHostQueueAcctToId, "*"), buntdb.IndexInt)
126
+		if err != nil {
127
+			return err
128
+		}
129
+		return tx.Descend(vhostRequestIdx, func(key, value string) bool {
130
+			if lastIDStr == "" {
131
+				lastIDStr = value
132
+			}
133
+			total++
134
+			return true
135
+		})
136
+	})
137
+
138
+	if err != nil {
139
+		am.server.logger.Error("internal", "could not create vhost queue index", err.Error())
140
+	}
141
+
142
+	lastID, _ := strconv.ParseUint(lastIDStr, 10, 64)
143
+	am.server.logger.Debug("services", fmt.Sprintf("vhost queue length is %d, autoincrementing id is %d", total, lastID))
144
+
145
+	atomic.StoreUint64(&am.vhostRequestID, lastID)
146
+	atomic.StoreUint64(&am.vhostRequestPendingCount, total)
99 147
 }
100 148
 
101 149
 func (am *AccountManager) NickToAccount(nick string) string {
@@ -109,6 +157,17 @@ func (am *AccountManager) NickToAccount(nick string) string {
109 157
 	return am.nickToAccount[cfnick]
110 158
 }
111 159
 
160
+func (am *AccountManager) AccountToClients(account string) (result []*Client) {
161
+	cfaccount, err := CasefoldName(account)
162
+	if err != nil {
163
+		return
164
+	}
165
+
166
+	am.RLock()
167
+	defer am.RUnlock()
168
+	return am.accountToClients[cfaccount]
169
+}
170
+
112 171
 func (am *AccountManager) Register(client *Client, account string, callbackNamespace string, callbackValue string, passphrase string, certfp string) error {
113 172
 	casefoldedAccount, err := CasefoldName(account)
114 173
 	if err != nil || account == "" || account == "*" {
@@ -342,7 +401,12 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
342 401
 		return err
343 402
 	}
344 403
 
345
-	am.Login(client, raw.Name)
404
+	raw.Verified = true
405
+	clientAccount, err := am.deserializeRawAccount(raw)
406
+	if err != nil {
407
+		return err
408
+	}
409
+	am.Login(client, clientAccount)
346 410
 	return nil
347 411
 }
348 412
 
@@ -464,7 +528,7 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
464 528
 		return errAccountInvalidCredentials
465 529
 	}
466 530
 
467
-	am.Login(client, account.Name)
531
+	am.Login(client, account)
468 532
 	return nil
469 533
 }
470 534
 
@@ -484,6 +548,11 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
484 548
 		return
485 549
 	}
486 550
 
551
+	result, err = am.deserializeRawAccount(raw)
552
+	return
553
+}
554
+
555
+func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result ClientAccount, err error) {
487 556
 	result.Name = raw.Name
488 557
 	regTimeInt, _ := strconv.ParseInt(raw.RegisteredAt, 10, 64)
489 558
 	result.RegisteredAt = time.Unix(regTimeInt, 0)
@@ -495,6 +564,13 @@ func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount,
495 564
 	}
496 565
 	result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
497 566
 	result.Verified = raw.Verified
567
+	if raw.VHost != "" {
568
+		e := json.Unmarshal([]byte(raw.VHost), &result.VHost)
569
+		if e != nil {
570
+			am.server.logger.Warning("internal", fmt.Sprintf("could not unmarshal vhost for account %s: %v", result.Name, e))
571
+			// pretend they have no vhost and move on
572
+		}
573
+	}
498 574
 	return
499 575
 }
500 576
 
@@ -506,6 +582,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
506 582
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
507 583
 	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
508 584
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
585
+	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
509 586
 
510 587
 	_, e := tx.Get(accountKey)
511 588
 	if e == buntdb.ErrNotFound {
@@ -518,6 +595,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
518 595
 	result.Credentials, _ = tx.Get(credentialsKey)
519 596
 	result.Callback, _ = tx.Get(callbackKey)
520 597
 	result.AdditionalNicks, _ = tx.Get(nicksKey)
598
+	result.VHost, _ = tx.Get(vhostKey)
521 599
 
522 600
 	if _, e = tx.Get(verifiedKey); e == nil {
523 601
 		result.Verified = true
@@ -540,6 +618,8 @@ func (am *AccountManager) Unregister(account string) error {
540 618
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
541 619
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
542 620
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
621
+	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
622
+	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
543 623
 
544 624
 	var clients []*Client
545 625
 
@@ -560,6 +640,9 @@ func (am *AccountManager) Unregister(account string) error {
560 640
 		tx.Delete(nicksKey)
561 641
 		credText, err = tx.Get(credentialsKey)
562 642
 		tx.Delete(credentialsKey)
643
+		tx.Delete(vhostKey)
644
+		_, err := tx.Delete(vhostQueueKey)
645
+		am.decrementVHostQueueCount(casefoldedAccount, err)
563 646
 		return nil
564 647
 	})
565 648
 
@@ -624,17 +707,239 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
624 707
 	}
625 708
 
626 709
 	// ok, we found an account corresponding to their certificate
627
-
628
-	am.Login(client, rawAccount.Name)
710
+	clientAccount, err := am.deserializeRawAccount(rawAccount)
711
+	if err != nil {
712
+		return err
713
+	}
714
+	am.Login(client, clientAccount)
629 715
 	return nil
630 716
 }
631 717
 
632
-func (am *AccountManager) Login(client *Client, account string) {
633
-	am.Lock()
634
-	defer am.Unlock()
718
+// represents someone's status in hostserv
719
+type VHostInfo struct {
720
+	ApprovedVHost   string
721
+	Enabled         bool
722
+	RequestedVHost  string
723
+	RejectedVHost   string
724
+	RejectionReason string
725
+	LastRequestTime time.Time
726
+}
727
+
728
+// pair type, <VHostInfo, accountName>
729
+type PendingVHostRequest struct {
730
+	VHostInfo
731
+	Account string
732
+}
733
+
734
+// callback type implementing the actual business logic of vhost operations
735
+type vhostMunger func(input VHostInfo) (output VHostInfo, err error)
736
+
737
+func (am *AccountManager) VHostSet(account string, vhost string) (result VHostInfo, err error) {
738
+	munger := func(input VHostInfo) (output VHostInfo, err error) {
739
+		output = input
740
+		output.Enabled = true
741
+		output.ApprovedVHost = vhost
742
+		return
743
+	}
744
+
745
+	return am.performVHostChange(account, munger)
746
+}
747
+
748
+func (am *AccountManager) VHostRequest(account string, vhost string) (result VHostInfo, err error) {
749
+	munger := func(input VHostInfo) (output VHostInfo, err error) {
750
+		output = input
751
+		output.RequestedVHost = vhost
752
+		output.RejectedVHost = ""
753
+		output.RejectionReason = ""
754
+		output.LastRequestTime = time.Now().UTC()
755
+		return
756
+	}
757
+
758
+	return am.performVHostChange(account, munger)
759
+}
760
+
761
+func (am *AccountManager) VHostApprove(account string) (result VHostInfo, err error) {
762
+	munger := func(input VHostInfo) (output VHostInfo, err error) {
763
+		output = input
764
+		output.Enabled = true
765
+		output.ApprovedVHost = input.RequestedVHost
766
+		output.RequestedVHost = ""
767
+		output.RejectionReason = ""
768
+		return
769
+	}
770
+
771
+	return am.performVHostChange(account, munger)
772
+}
773
+
774
+func (am *AccountManager) VHostReject(account string, reason string) (result VHostInfo, err error) {
775
+	munger := func(input VHostInfo) (output VHostInfo, err error) {
776
+		output = input
777
+		output.RejectedVHost = output.RequestedVHost
778
+		output.RequestedVHost = ""
779
+		output.RejectionReason = reason
780
+		return
781
+	}
782
+
783
+	return am.performVHostChange(account, munger)
784
+}
785
+
786
+func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) {
787
+	munger := func(input VHostInfo) (output VHostInfo, err error) {
788
+		output = input
789
+		output.Enabled = enabled
790
+		return
791
+	}
792
+
793
+	return am.performVHostChange(client.Account(), munger)
794
+}
795
+
796
+func (am *AccountManager) performVHostChange(account string, munger vhostMunger) (result VHostInfo, err error) {
797
+	account, err = CasefoldName(account)
798
+	if err != nil || account == "" {
799
+		err = errAccountDoesNotExist
800
+		return
801
+	}
802
+
803
+	am.vHostUpdateMutex.Lock()
804
+	defer am.vHostUpdateMutex.Unlock()
805
+
806
+	clientAccount, err := am.LoadAccount(account)
807
+	if err != nil {
808
+		err = errAccountDoesNotExist
809
+		return
810
+	} else if !clientAccount.Verified {
811
+		err = errAccountUnverified
812
+		return
813
+	}
814
+
815
+	result, err = munger(clientAccount.VHost)
816
+	if err != nil {
817
+		return
818
+	}
819
+
820
+	vhtext, err := json.Marshal(result)
821
+	if err != nil {
822
+		err = errAccountUpdateFailed
823
+		return
824
+	}
825
+	vhstr := string(vhtext)
826
+
827
+	key := fmt.Sprintf(keyAccountVHost, account)
828
+	queueKey := fmt.Sprintf(keyVHostQueueAcctToId, account)
829
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
830
+		if _, _, err := tx.Set(key, vhstr, nil); err != nil {
831
+			return err
832
+		}
833
+
834
+		// update request queue
835
+		if clientAccount.VHost.RequestedVHost == "" && result.RequestedVHost != "" {
836
+			id := atomic.AddUint64(&am.vhostRequestID, 1)
837
+			if _, _, err = tx.Set(queueKey, strconv.FormatUint(id, 10), nil); err != nil {
838
+				return err
839
+			}
840
+			atomic.AddUint64(&am.vhostRequestPendingCount, 1)
841
+		} else if clientAccount.VHost.RequestedVHost != "" && result.RequestedVHost == "" {
842
+			_, err = tx.Delete(queueKey)
843
+			am.decrementVHostQueueCount(account, err)
844
+		}
845
+
846
+		return nil
847
+	})
848
+
849
+	if err != nil {
850
+		err = errAccountUpdateFailed
851
+		return
852
+	}
853
+
854
+	am.applyVhostToClients(account, result)
855
+	return result, nil
856
+}
857
+
858
+// XXX annoying helper method for keeping the queue count in sync with the DB
859
+// `err` is the buntdb error returned from deleting the queue key
860
+func (am *AccountManager) decrementVHostQueueCount(account string, err error) {
861
+	if err == nil {
862
+		// successfully deleted a queue entry, do a 2's complement decrement:
863
+		atomic.AddUint64(&am.vhostRequestPendingCount, ^uint64(0))
864
+	} else if err != buntdb.ErrNotFound {
865
+		am.server.logger.Error("internal", "buntdb dequeue error", account, err.Error())
866
+	}
867
+}
868
+
869
+func (am *AccountManager) VHostListRequests(limit int) (requests []PendingVHostRequest, total int) {
870
+	am.vHostUpdateMutex.Lock()
871
+	defer am.vHostUpdateMutex.Unlock()
872
+
873
+	total = int(atomic.LoadUint64(&am.vhostRequestPendingCount))
874
+
875
+	prefix := fmt.Sprintf(keyVHostQueueAcctToId, "")
876
+	accounts := make([]string, 0, limit)
877
+	err := am.server.store.View(func(tx *buntdb.Tx) error {
878
+		return tx.Ascend(vhostRequestIdx, func(key, value string) bool {
879
+			accounts = append(accounts, strings.TrimPrefix(key, prefix))
880
+			return len(accounts) < limit
881
+		})
882
+	})
883
+
884
+	if err != nil {
885
+		am.server.logger.Error("internal", "couldn't traverse vhost queue", err.Error())
886
+		return
887
+	}
888
+
889
+	for _, account := range accounts {
890
+		accountInfo, err := am.LoadAccount(account)
891
+		if err == nil {
892
+			requests = append(requests, PendingVHostRequest{
893
+				Account:   account,
894
+				VHostInfo: accountInfo.VHost,
895
+			})
896
+		} else {
897
+			am.server.logger.Error("internal", "corrupt account", account, err.Error())
898
+		}
899
+	}
900
+	return
901
+}
902
+
903
+func (am *AccountManager) applyVHostInfo(client *Client, info VHostInfo) {
904
+	// if hostserv is disabled in config, then don't grant vhosts
905
+	// that were previously approved while it was enabled
906
+	if !am.server.AccountConfig().VHosts.Enabled {
907
+		return
908
+	}
909
+
910
+	vhost := ""
911
+	if info.Enabled {
912
+		vhost = info.ApprovedVHost
913
+	}
914
+	oldNickmask := client.NickMaskString()
915
+	updated := client.SetVHost(vhost)
916
+	if updated {
917
+		// TODO: doing I/O here is kind of a kludge
918
+		go client.sendChghost(oldNickmask, vhost)
919
+	}
920
+}
921
+
922
+func (am *AccountManager) applyVhostToClients(account string, result VHostInfo) {
923
+	am.RLock()
924
+	clients := am.accountToClients[account]
925
+	am.RUnlock()
926
+
927
+	for _, client := range clients {
928
+		am.applyVHostInfo(client, result)
929
+	}
930
+}
931
+
932
+func (am *AccountManager) Login(client *Client, account ClientAccount) {
933
+	changed := client.SetAccountName(account.Name)
934
+	if changed {
935
+		go client.nickTimer.Touch()
936
+	}
937
+
938
+	am.applyVHostInfo(client, account.VHost)
635 939
 
636
-	am.loginToAccount(client, account)
637 940
 	casefoldedAccount := client.Account()
941
+	am.Lock()
942
+	defer am.Unlock()
638 943
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
639 944
 }
640 945
 
@@ -691,6 +996,7 @@ type ClientAccount struct {
691 996
 	Credentials     AccountCredentials
692 997
 	Verified        bool
693 998
 	AdditionalNicks []string
999
+	VHost           VHostInfo
694 1000
 }
695 1001
 
696 1002
 // convenience for passing around raw serialized account data
@@ -701,14 +1007,7 @@ type rawClientAccount struct {
701 1007
 	Callback        string
702 1008
 	Verified        bool
703 1009
 	AdditionalNicks string
704
-}
705
-
706
-// loginToAccount logs the client into the given account.
707
-func (am *AccountManager) loginToAccount(client *Client, account string) {
708
-	changed := client.SetAccountName(account)
709
-	if changed {
710
-		go client.nickTimer.Touch()
711
-	}
1010
+	VHost           string
712 1011
 }
713 1012
 
714 1013
 // logoutOfAccount logs the client out of their current account.

+ 6
- 115
irc/chanserv.go View File

@@ -5,7 +5,6 @@ package irc
5 5
 
6 6
 import (
7 7
 	"fmt"
8
-	"sort"
9 8
 	"strings"
10 9
 
11 10
 	"github.com/goshuirc/irc-go/ircfmt"
@@ -22,29 +21,16 @@ To see in-depth help for a specific ChanServ command, try:
22 21
 Here are the commands you can use:
23 22
 %s`
24 23
 
25
-type csCommand struct {
26
-	capabs    []string // oper capabs the given user has to have to access this command
27
-	handler   func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
28
-	help      string
29
-	helpShort string
30
-	oper      bool // true if the user has to be an oper to use this command
31
-}
32
-
33 24
 var (
34
-	chanservCommands = map[string]*csCommand{
35
-		"help": {
36
-			help: `Syntax: $bHELP [command]$b
37
-
38
-HELP returns information on the given command.`,
39
-			helpShort: `$bHELP$b shows in-depth information about commands.`,
40
-		},
25
+	chanservCommands = map[string]*serviceCommand{
41 26
 		"op": {
42 27
 			handler: csOpHandler,
43 28
 			help: `Syntax: $bOP #channel [nickname]$b
44 29
 
45 30
 OP makes the given nickname, or yourself, a channel admin. You can only use
46 31
 this command if you're the founder of the channel.`,
47
-			helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`,
32
+			helpShort:    `$bOP$b makes the given user (or yourself) a channel admin.`,
33
+			authRequired: true,
48 34
 		},
49 35
 		"register": {
50 36
 			handler: csRegisterHandler,
@@ -53,7 +39,8 @@ this command if you're the founder of the channel.`,
53 39
 REGISTER lets you own the given channel. If you rejoin this channel, you'll be
54 40
 given admin privs on it. Modes set on the channel and the topic will also be
55 41
 remembered.`,
56
-			helpShort: `$bREGISTER$b lets you own a given channel.`,
42
+			helpShort:    `$bREGISTER$b lets you own a given channel.`,
43
+			authRequired: true,
57 44
 		},
58 45
 	}
59 46
 )
@@ -63,91 +50,6 @@ func csNotice(rb *ResponseBuffer, text string) {
63 50
 	rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
64 51
 }
65 52
 
66
-// chanservReceiveNotice handles NOTICEs that ChanServ receives.
67
-func (server *Server) chanservNoticeHandler(client *Client, message string, rb *ResponseBuffer) {
68
-	// do nothing
69
-}
70
-
71
-// chanservReceiveNotice handles NOTICEs that ChanServ receives.
72
-func (server *Server) chanservPrivmsgHandler(client *Client, message string, rb *ResponseBuffer) {
73
-	commandName, params := utils.ExtractParam(message)
74
-	commandName = strings.ToLower(commandName)
75
-
76
-	commandInfo := chanservCommands[commandName]
77
-	if commandInfo == nil {
78
-		csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP"))
79
-		return
80
-	}
81
-
82
-	if commandInfo.oper && !client.HasMode(modes.Operator) {
83
-		csNotice(rb, client.t("Command restricted"))
84
-		return
85
-	}
86
-
87
-	if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
88
-		csNotice(rb, client.t("Command restricted"))
89
-		return
90
-	}
91
-
92
-	// custom help handling here to prevent recursive init loop
93
-	if commandName == "help" {
94
-		csHelpHandler(server, client, commandName, params, rb)
95
-		return
96
-	}
97
-
98
-	if commandInfo.handler == nil {
99
-		csNotice(rb, client.t("Command error. Please report this to the developers"))
100
-		return
101
-	}
102
-
103
-	server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName))
104
-
105
-	commandInfo.handler(server, client, commandName, params, rb)
106
-}
107
-
108
-func csHelpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
109
-	csNotice(rb, ircfmt.Unescape(client.t("*** $bChanServ HELP$b ***")))
110
-
111
-	if params == "" {
112
-		// show general help
113
-		var shownHelpLines sort.StringSlice
114
-		for _, commandInfo := range chanservCommands {
115
-			// skip commands user can't access
116
-			if commandInfo.oper && !client.HasMode(modes.Operator) {
117
-				continue
118
-			}
119
-			if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
120
-				continue
121
-			}
122
-
123
-			shownHelpLines = append(shownHelpLines, "    "+client.t(commandInfo.helpShort))
124
-		}
125
-
126
-		// sort help lines
127
-		sort.Sort(shownHelpLines)
128
-
129
-		// assemble help text
130
-		assembledHelpLines := strings.Join(shownHelpLines, "\n")
131
-		fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(chanservHelp), assembledHelpLines))
132
-
133
-		// push out help text
134
-		for _, line := range strings.Split(fullHelp, "\n") {
135
-			csNotice(rb, line)
136
-		}
137
-	} else {
138
-		commandInfo := chanservCommands[strings.ToLower(strings.TrimSpace(params))]
139
-		if commandInfo == nil {
140
-			csNotice(rb, client.t("Unknown command. To see available commands, run /CS HELP"))
141
-		} else {
142
-			for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
143
-				csNotice(rb, line)
144
-			}
145
-		}
146
-	}
147
-
148
-	csNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of ChanServ HELP$b ***")))
149
-}
150
-
151 53
 func csOpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
152 54
 	channelName, clientToOp := utils.ExtractParam(params)
153 55
 
@@ -171,13 +73,7 @@ func csOpHandler(server *Server, client *Client, command, params string, rb *Res
171 73
 	}
172 74
 
173 75
 	clientAccount := client.Account()
174
-
175
-	if clientAccount == "" {
176
-		csNotice(rb, client.t("You must be logged in to op on a channel"))
177
-		return
178
-	}
179
-
180
-	if clientAccount != channelInfo.Founder() {
76
+	if clientAccount == "" || clientAccount != channelInfo.Founder() {
181 77
 		csNotice(rb, client.t("You must be the channel founder to op"))
182 78
 		return
183 79
 	}
@@ -239,11 +135,6 @@ func csRegisterHandler(server *Server, client *Client, command, params string, r
239 135
 		return
240 136
 	}
241 137
 
242
-	if client.Account() == "" {
243
-		csNotice(rb, client.t("You must be logged in to register a channel"))
244
-		return
245
-	}
246
-
247 138
 	// this provides the synchronization that allows exactly one registration of the channel:
248 139
 	err = channelInfo.SetRegistered(client.Account())
249 140
 	if err != nil {

+ 62
- 22
irc/client.go View File

@@ -46,7 +46,6 @@ type Client struct {
46 46
 	capVersion         caps.Version
47 47
 	certfp             string
48 48
 	channels           ChannelSet
49
-	class              *OperClass
50 49
 	ctime              time.Time
51 50
 	exitedSnomaskSent  bool
52 51
 	fakelag            *Fakelag
@@ -65,7 +64,7 @@ type Client struct {
65 64
 	nickMaskCasefolded string
66 65
 	nickMaskString     string // cache for nickmask string since it's used with lots of replies
67 66
 	nickTimer          *NickTimer
68
-	operName           string
67
+	oper               *Oper
69 68
 	preregNick         string
70 69
 	proxiedIP          net.IP // actual remote IP if using the PROXY protocol
71 70
 	quitMessage        string
@@ -81,7 +80,6 @@ type Client struct {
81 80
 	stateMutex         sync.RWMutex // tier 1
82 81
 	username           string
83 82
 	vhost              string
84
-	whoisLine          string
85 83
 }
86 84
 
87 85
 // NewClient returns a client with all the appropriate info setup.
@@ -312,10 +310,6 @@ func (client *Client) Ping() {
312 310
 
313 311
 }
314 312
 
315
-//
316
-// server goroutine
317
-//
318
-
319 313
 // Register sets the client details as appropriate when entering the network.
320 314
 func (client *Client) Register() {
321 315
 	client.stateMutex.Lock()
@@ -495,12 +489,13 @@ func (client *Client) HasUsername() bool {
495 489
 
496 490
 // HasRoleCapabs returns true if client has the given (role) capabilities.
497 491
 func (client *Client) HasRoleCapabs(capabs ...string) bool {
498
-	if client.class == nil {
492
+	oper := client.Oper()
493
+	if oper == nil {
499 494
 		return false
500 495
 	}
501 496
 
502 497
 	for _, capab := range capabs {
503
-		if !client.class.Capabilities[capab] {
498
+		if !oper.Class.Capabilities[capab] {
504 499
 			return false
505 500
 		}
506 501
 	}
@@ -547,12 +542,45 @@ func (client *Client) Friends(capabs ...caps.Capability) ClientSet {
547 542
 	return friends
548 543
 }
549 544
 
545
+// XXX: CHGHOST requires prefix nickmask to have original hostname,
546
+// this is annoying to do correctly
547
+func (client *Client) sendChghost(oldNickMask string, vhost string) {
548
+	username := client.Username()
549
+	for fClient := range client.Friends(caps.ChgHost) {
550
+		fClient.sendFromClientInternal("", client, oldNickMask, nil, "CHGHOST", username, vhost)
551
+	}
552
+}
553
+
554
+// choose the correct vhost to display
555
+func (client *Client) getVHostNoMutex() string {
556
+	// hostserv vhost OR operclass vhost OR nothing (i.e., normal rdns hostmask)
557
+	if client.vhost != "" {
558
+		return client.vhost
559
+	} else if client.oper != nil {
560
+		return client.oper.Vhost
561
+	} else {
562
+		return ""
563
+	}
564
+}
565
+
566
+// SetVHost updates the client's hostserv-based vhost
567
+func (client *Client) SetVHost(vhost string) (updated bool) {
568
+	client.stateMutex.Lock()
569
+	defer client.stateMutex.Unlock()
570
+	updated = (client.vhost != vhost)
571
+	client.vhost = vhost
572
+	if updated {
573
+		client.updateNickMaskNoMutex()
574
+	}
575
+	return
576
+}
577
+
550 578
 // updateNick updates `nick` and `nickCasefolded`.
551 579
 func (client *Client) updateNick(nick string) {
552 580
 	casefoldedName, err := CasefoldName(nick)
553 581
 	if err != nil {
554
-		log.Println(fmt.Sprintf("ERROR: Nick [%s] couldn't be casefolded... this should never happen. Printing stacktrace.", client.nick))
555
-		debug.PrintStack()
582
+		client.server.logger.Error("internal", "nick couldn't be casefolded", nick, err.Error())
583
+		return
556 584
 	}
557 585
 	client.stateMutex.Lock()
558 586
 	client.nick = nick
@@ -573,19 +601,18 @@ func (client *Client) updateNickMask(nick string) {
573 601
 	client.updateNickMaskNoMutex()
574 602
 }
575 603
 
576
-// updateNickMask updates the casefolded nickname and nickmask, not holding any mutexes.
604
+// updateNickMask updates the casefolded nickname and nickmask, not acquiring any mutexes.
577 605
 func (client *Client) updateNickMaskNoMutex() {
578
-	if len(client.vhost) > 0 {
579
-		client.hostname = client.vhost
580
-	} else {
606
+	client.hostname = client.getVHostNoMutex()
607
+	if client.hostname == "" {
581 608
 		client.hostname = client.rawHostname
582 609
 	}
583 610
 
584 611
 	nickMaskString := fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.hostname)
585 612
 	nickMaskCasefolded, err := Casefold(nickMaskString)
586 613
 	if err != nil {
587
-		log.Println(fmt.Sprintf("ERROR: Nickmask [%s] couldn't be casefolded... this should never happen. Printing stacktrace.", client.nickMaskString))
588
-		debug.PrintStack()
614
+		client.server.logger.Error("internal", "nickmask couldn't be casefolded", nickMaskString, err.Error())
615
+		return
589 616
 	}
590 617
 
591 618
 	client.nickMaskString = nickMaskString
@@ -598,19 +625,26 @@ func (client *Client) AllNickmasks() []string {
598 625
 	var mask string
599 626
 	var err error
600 627
 
601
-	if len(client.vhost) > 0 {
602
-		mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.vhost))
628
+	client.stateMutex.RLock()
629
+	nick := client.nick
630
+	username := client.username
631
+	rawHostname := client.rawHostname
632
+	vhost := client.getVHostNoMutex()
633
+	client.stateMutex.RUnlock()
634
+
635
+	if len(vhost) > 0 {
636
+		mask, err = Casefold(fmt.Sprintf("%s!%s@%s", nick, username, vhost))
603 637
 		if err == nil {
604 638
 			masks = append(masks, mask)
605 639
 		}
606 640
 	}
607 641
 
608
-	mask, err = Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.rawHostname))
642
+	mask, err = Casefold(fmt.Sprintf("%s!%s@%s", nick, username, rawHostname))
609 643
 	if err == nil {
610 644
 		masks = append(masks, mask)
611 645
 	}
612 646
 
613
-	mask2, err := Casefold(fmt.Sprintf("%s!%s@%s", client.nick, client.username, client.IPString()))
647
+	mask2, err := Casefold(fmt.Sprintf("%s!%s@%s", nick, username, client.IPString()))
614 648
 	if err == nil && mask2 != mask {
615 649
 		masks = append(masks, mask2)
616 650
 	}
@@ -772,6 +806,12 @@ func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags *m
772 806
 // SendFromClient sends an IRC line coming from a specific client.
773 807
 // Adds account-tag to the line as well.
774 808
 func (client *Client) SendFromClient(msgid string, from *Client, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
809
+	return client.sendFromClientInternal(msgid, from, from.NickMaskString(), tags, command, params...)
810
+}
811
+
812
+// XXX this is a hack where we allow overriding the client's nickmask
813
+// this is to support CHGHOST, which requires that we send the *original* nickmask with the response
814
+func (client *Client) sendFromClientInternal(msgid string, from *Client, nickmask string, tags *map[string]ircmsg.TagValue, command string, params ...string) error {
775 815
 	// attach account-tag
776 816
 	if client.capabilities.Has(caps.AccountTag) && from.LoggedIntoAccount() {
777 817
 		if tags == nil {
@@ -789,7 +829,7 @@ func (client *Client) SendFromClient(msgid string, from *Client, tags *map[strin
789 829
 		}
790 830
 	}
791 831
 
792
-	return client.Send(tags, from.nickMaskString, command, params...)
832
+	return client.Send(tags, nickmask, command, params...)
793 833
 }
794 834
 
795 835
 var (

+ 2
- 16
irc/commands.go View File

@@ -92,14 +92,6 @@ func init() {
92 92
 			usablePreReg: true,
93 93
 			minParams:    1,
94 94
 		},
95
-		"CHANSERV": {
96
-			handler:   csHandler,
97
-			minParams: 1,
98
-		},
99
-		"CS": {
100
-			handler:   csHandler,
101
-			minParams: 1,
102
-		},
103 95
 		"DEBUG": {
104 96
 			handler:   debugHandler,
105 97
 			minParams: 1,
@@ -182,10 +174,6 @@ func init() {
182 174
 			usablePreReg: true,
183 175
 			minParams:    1,
184 176
 		},
185
-		"NICKSERV": {
186
-			handler:   nsHandler,
187
-			minParams: 1,
188
-		},
189 177
 		"NOTICE": {
190 178
 			handler:   noticeHandler,
191 179
 			minParams: 2,
@@ -198,10 +186,6 @@ func init() {
198 186
 			handler:   npcaHandler,
199 187
 			minParams: 3,
200 188
 		},
201
-		"NS": {
202
-			handler:   nsHandler,
203
-			minParams: 1,
204
-		},
205 189
 		"OPER": {
206 190
 			handler:   operHandler,
207 191
 			minParams: 2,
@@ -323,4 +307,6 @@ func init() {
323 307
 			minParams: 1,
324 308
 		},
325 309
 	}
310
+
311
+	initializeServices()
326 312
 }

+ 38
- 9
irc/config.go View File

@@ -13,6 +13,7 @@ import (
13 13
 	"io/ioutil"
14 14
 	"log"
15 15
 	"path/filepath"
16
+	"regexp"
16 17
 	"strings"
17 18
 	"time"
18 19
 
@@ -64,6 +65,7 @@ type AccountConfig struct {
64 65
 	AuthenticationEnabled bool                  `yaml:"authentication-enabled"`
65 66
 	SkipServerPassword    bool                  `yaml:"skip-server-password"`
66 67
 	NickReservation       NickReservationConfig `yaml:"nick-reservation"`
68
+	VHosts                VHostConfig
67 69
 }
68 70
 
69 71
 // AccountRegistrationConfig controls account registration.
@@ -91,6 +93,18 @@ type AccountRegistrationConfig struct {
91 93
 	AllowMultiplePerConnection bool `yaml:"allow-multiple-per-connection"`
92 94
 }
93 95
 
96
+type VHostConfig struct {
97
+	Enabled        bool
98
+	MaxLength      int    `yaml:"max-length"`
99
+	ValidRegexpRaw string `yaml:"valid-regexp"`
100
+	ValidRegexp    *regexp.Regexp
101
+	UserRequests   struct {
102
+		Enabled  bool
103
+		Channel  string
104
+		Cooldown time.Duration
105
+	} `yaml:"user-requests"`
106
+}
107
+
94 108
 type NickReservationMethod int
95 109
 
96 110
 const (
@@ -278,8 +292,8 @@ type OperClass struct {
278 292
 }
279 293
 
280 294
 // OperatorClasses returns a map of assembled operator classes from the given config.
281
-func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
282
-	ocs := make(map[string]OperClass)
295
+func (conf *Config) OperatorClasses() (map[string]*OperClass, error) {
296
+	ocs := make(map[string]*OperClass)
283 297
 
284 298
 	// loop from no extends to most extended, breaking if we can't add any more
285 299
 	lenOfLastOcs := -1
@@ -335,7 +349,7 @@ func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
335 349
 				oc.WhoisLine += oc.Title
336 350
 			}
337 351
 
338
-			ocs[name] = oc
352
+			ocs[name] = &oc
339 353
 		}
340 354
 
341 355
 		if !anyMissing {
@@ -344,11 +358,12 @@ func (conf *Config) OperatorClasses() (*map[string]OperClass, error) {
344 358
 		}
345 359
 	}
346 360
 
347
-	return &ocs, nil
361
+	return ocs, nil
348 362
 }
349 363
 
350 364
 // Oper represents a single assembled operator's config.
351 365
 type Oper struct {
366
+	Name      string
352 367
 	Class     *OperClass
353 368
 	WhoisLine string
354 369
 	Vhost     string
@@ -357,8 +372,8 @@ type Oper struct {
357 372
 }
358 373
 
359 374
 // Operators returns a map of operator configs from the given OperClass and config.
360
-func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error) {
361
-	operators := make(map[string]Oper)
375
+func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error) {
376
+	operators := make(map[string]*Oper)
362 377
 	for name, opConf := range conf.Opers {
363 378
 		var oper Oper
364 379
 
@@ -367,14 +382,15 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error)
367 382
 		if err != nil {
368 383
 			return nil, fmt.Errorf("Could not casefold oper name: %s", err.Error())
369 384
 		}
385
+		oper.Name = name
370 386
 
371 387
 		oper.Pass = opConf.PasswordBytes()
372 388
 		oper.Vhost = opConf.Vhost
373
-		class, exists := (*oc)[opConf.Class]
389
+		class, exists := oc[opConf.Class]
374 390
 		if !exists {
375 391
 			return nil, fmt.Errorf("Could not load operator [%s] - they use operclass [%s] which does not exist", name, opConf.Class)
376 392
 		}
377
-		oper.Class = &class
393
+		oper.Class = class
378 394
 		if len(opConf.WhoisLine) > 0 {
379 395
 			oper.WhoisLine = opConf.WhoisLine
380 396
 		} else {
@@ -388,7 +404,7 @@ func (conf *Config) Operators(oc *map[string]OperClass) (map[string]Oper, error)
388 404
 		oper.Modes = modeChanges
389 405
 
390 406
 		// successful, attach to list of opers
391
-		operators[name] = oper
407
+		operators[name] = &oper
392 408
 	}
393 409
 	return operators, nil
394 410
 }
@@ -537,6 +553,19 @@ func LoadConfig(filename string) (config *Config, err error) {
537 553
 		}
538 554
 	}
539 555
 
556
+	rawRegexp := config.Accounts.VHosts.ValidRegexpRaw
557
+	if rawRegexp != "" {
558
+		regexp, err := regexp.Compile(rawRegexp)
559
+		if err == nil {
560
+			config.Accounts.VHosts.ValidRegexp = regexp
561
+		} else {
562
+			log.Printf("invalid vhost regexp: %s\n", err.Error())
563
+		}
564
+	}
565
+	if config.Accounts.VHosts.ValidRegexp == nil {
566
+		config.Accounts.VHosts.ValidRegexp = defaultValidVhostRegex
567
+	}
568
+
540 569
 	maxSendQBytes, err := bytefmt.ToBytes(config.Server.MaxSendQString)
541 570
 	if err != nil {
542 571
 		return nil, fmt.Errorf("Could not parse maximum SendQ size (make sure it only contains whole numbers): %s", err.Error())

+ 1
- 0
irc/errors.go View File

@@ -21,6 +21,7 @@ var (
21 21
 	errAccountTooManyNicks            = errors.New("Account has too many reserved nicks")
22 22
 	errAccountNickReservationFailed   = errors.New("Could not (un)reserve nick")
23 23
 	errAccountCantDropPrimaryNick     = errors.New("Can't unreserve primary nickname")
24
+	errAccountUpdateFailed            = errors.New("Error while updating your account information")
24 25
 	errCallbackFailed                 = errors.New("Account verification could not be sent")
25 26
 	errCertfpAlreadyExists            = errors.New("An account already exists with your certificate")
26 27
 	errChannelAlreadyRegistered       = errors.New("Channel is already registered")

+ 5
- 2
irc/gateways.go View File

@@ -75,9 +75,12 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (exiting bool)
75 75
 	}
76 76
 
77 77
 	// given IP is sane! override the client's current IP
78
+	rawHostname := utils.LookupHostname(proxiedIP)
79
+	client.stateMutex.Lock()
78 80
 	client.proxiedIP = parsedProxiedIP
79
-	client.rawHostname = utils.LookupHostname(proxiedIP)
80
-	client.hostname = client.rawHostname
81
+	client.rawHostname = rawHostname
82
+	client.stateMutex.Unlock()
83
+	// nickmask will be updated when the client completes registration
81 84
 
82 85
 	// set tls info
83 86
 	client.certfp = ""

+ 22
- 0
irc/getters.go View File

@@ -83,6 +83,16 @@ func (server *Server) FakelagConfig() *FakelagConfig {
83 83
 	return &server.config.Fakelag
84 84
 }
85 85
 
86
+func (server *Server) GetOperator(name string) (oper *Oper) {
87
+	name, err := CasefoldName(name)
88
+	if err != nil {
89
+		return
90
+	}
91
+	server.configurableStateMutex.RLock()
92
+	defer server.configurableStateMutex.RUnlock()
93
+	return server.operators[name]
94
+}
95
+
86 96
 func (client *Client) Nick() string {
87 97
 	client.stateMutex.RLock()
88 98
 	defer client.stateMutex.RUnlock()
@@ -119,6 +129,18 @@ func (client *Client) Realname() string {
119 129
 	return client.realname
120 130
 }
121 131
 
132
+func (client *Client) Oper() *Oper {
133
+	client.stateMutex.RLock()
134
+	defer client.stateMutex.RUnlock()
135
+	return client.oper
136
+}
137
+
138
+func (client *Client) SetOper(oper *Oper) {
139
+	client.stateMutex.Lock()
140
+	defer client.stateMutex.Unlock()
141
+	client.oper = oper
142
+}
143
+
122 144
 func (client *Client) Registered() bool {
123 145
 	client.stateMutex.RLock()
124 146
 	defer client.stateMutex.RUnlock()

+ 29
- 52
irc/handlers.go View File

@@ -31,6 +31,7 @@ import (
31 31
 	"github.com/oragono/oragono/irc/sno"
32 32
 	"github.com/oragono/oragono/irc/utils"
33 33
 	"github.com/tidwall/buntdb"
34
+	"golang.org/x/crypto/bcrypt"
34 35
 )
35 36
 
36 37
 // ACC [REGISTER|VERIFY] ...
@@ -494,12 +495,6 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo
494 495
 	return false
495 496
 }
496 497
 
497
-// CHANSERV [...]
498
-func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
499
-	server.chanservPrivmsgHandler(client, strings.Join(msg.Params, " "), rb)
500
-	return false
501
-}
502
-
503 498
 // DEBUG <subcmd>
504 499
 func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
505 500
 	param := strings.ToUpper(msg.Params[0])
@@ -562,7 +557,8 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
562 557
 // DLINE LIST
563 558
 func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
564 559
 	// check oper permissions
565
-	if !client.class.Capabilities["oper:local_ban"] {
560
+	oper := client.Oper()
561
+	if oper == nil || !oper.Class.Capabilities["oper:local_ban"] {
566 562
 		rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
567 563
 		return false
568 564
 	}
@@ -665,7 +661,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
665 661
 			}
666 662
 		}
667 663
 	}
668
-	operName := client.operName
664
+	operName := oper.Name
669 665
 	if operName == "" {
670 666
 		operName = server.name
671 667
 	}
@@ -977,7 +973,8 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
977 973
 // KLINE LIST
978 974
 func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
979 975
 	// check oper permissions
980
-	if !client.class.Capabilities["oper:local_ban"] {
976
+	oper := client.Oper()
977
+	if oper == nil || !oper.Class.Capabilities["oper:local_ban"] {
981 978
 		rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
982 979
 		return false
983 980
 	}
@@ -1052,7 +1049,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
1052 1049
 	}
1053 1050
 
1054 1051
 	// get oper name
1055
-	operName := client.operName
1052
+	operName := oper.Name
1056 1053
 	if operName == "" {
1057 1054
 		operName = server.name
1058 1055
 	}
@@ -1648,11 +1645,9 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
1648 1645
 			if err != nil {
1649 1646
 				continue
1650 1647
 			}
1651
-			if target == "chanserv" {
1652
-				server.chanservNoticeHandler(client, message, rb)
1653
-				continue
1654
-			} else if target == "nickserv" {
1655
-				server.nickservNoticeHandler(client, message, rb)
1648
+
1649
+			// NOTICEs sent to services are ignored
1650
+			if _, isService := OragonoServices[target]; isService {
1656 1651
 				continue
1657 1652
 			}
1658 1653
 
@@ -1715,46 +1710,29 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1715 1710
 	return false
1716 1711
 }
1717 1712
 
1718
-// NICKSERV [params...]
1719
-func nsHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1720
-	server.nickservPrivmsgHandler(client, strings.Join(msg.Params, " "), rb)
1721
-	return false
1722
-}
1723
-
1724 1713
 // OPER <name> <password>
1725 1714
 func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
1726
-	name, err := CasefoldName(msg.Params[0])
1727
-	if err != nil {
1728
-		rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
1729
-		return true
1730
-	}
1731 1715
 	if client.HasMode(modes.Operator) == true {
1732 1716
 		rb.Add(nil, server.name, ERR_UNKNOWNERROR, "OPER", client.t("You're already opered-up!"))
1733 1717
 		return false
1734 1718
 	}
1735
-	server.configurableStateMutex.RLock()
1736
-	oper := server.operators[name]
1737
-	server.configurableStateMutex.RUnlock()
1738 1719
 
1739
-	password := []byte(msg.Params[1])
1740
-	err = passwd.ComparePassword(oper.Pass, password)
1741
-	if (oper.Pass == nil) || (err != nil) {
1720
+	authorized := false
1721
+	oper := server.GetOperator(msg.Params[0])
1722
+	if oper != nil {
1723
+		password := []byte(msg.Params[1])
1724
+		authorized = (bcrypt.CompareHashAndPassword(oper.Pass, password) == nil)
1725
+	}
1726
+	if !authorized {
1742 1727
 		rb.Add(nil, server.name, ERR_PASSWDMISMATCH, client.nick, client.t("Password incorrect"))
1743 1728
 		return true
1744 1729
 	}
1745 1730
 
1746
-	client.operName = name
1747
-	client.class = oper.Class
1748
-	client.whoisLine = oper.WhoisLine
1749
-
1750
-	// push new vhost if one is set
1751
-	if len(oper.Vhost) > 0 {
1752
-		for fClient := range client.Friends(caps.ChgHost) {
1753
-			fClient.SendFromClient("", client, nil, "CHGHOST", client.username, oper.Vhost)
1754
-		}
1755
-		// CHGHOST requires prefix nickmask to have original hostname, so do that before updating nickmask
1756
-		client.vhost = oper.Vhost
1757
-		client.updateNickMask("")
1731
+	oldNickmask := client.NickMaskString()
1732
+	client.SetOper(oper)
1733
+	client.updateNickMask("")
1734
+	if client.NickMaskString() != oldNickmask {
1735
+		client.sendChghost(oldNickmask, oper.Vhost)
1758 1736
 	}
1759 1737
 
1760 1738
 	// set new modes: modes.Operator, plus anything specified in the config
@@ -1769,7 +1747,7 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
1769 1747
 	rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator"))
1770 1748
 	rb.Add(nil, server.name, "MODE", client.nick, applied.String())
1771 1749
 
1772
-	server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, client.operName))
1750
+	server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name))
1773 1751
 
1774 1752
 	// client may now be unthrottled by the fakelag system
1775 1753
 	client.resetFakelag()
@@ -1868,11 +1846,8 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
1868 1846
 			channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
1869 1847
 		} else {
1870 1848
 			target, err = CasefoldName(targetString)
1871
-			if target == "chanserv" {
1872
-				server.chanservPrivmsgHandler(client, message, rb)
1873
-				continue
1874
-			} else if target == "nickserv" {
1875
-				server.nickservPrivmsgHandler(client, message, rb)
1849
+			if service, isService := OragonoServices[target]; isService {
1850
+				servicePrivmsgHandler(service, server, client, message, rb)
1876 1851
 				continue
1877 1852
 			}
1878 1853
 			user := server.clients.Get(target)
@@ -2179,7 +2154,8 @@ func topicHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
2179 2154
 // UNDLINE <ip>|<net>
2180 2155
 func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2181 2156
 	// check oper permissions
2182
-	if !client.class.Capabilities["oper:local_unban"] {
2157
+	oper := client.Oper()
2158
+	if oper == nil || !oper.Class.Capabilities["oper:local_unban"] {
2183 2159
 		rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
2184 2160
 		return false
2185 2161
 	}
@@ -2242,7 +2218,8 @@ func unDLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
2242 2218
 // UNKLINE <mask>
2243 2219
 func unKLineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2244 2220
 	// check oper permissions
2245
-	if !client.class.Capabilities["oper:local_unban"] {
2221
+	oper := client.Oper()
2222
+	if oper == nil || !oper.Class.Capabilities["oper:local_unban"] {
2246 2223
 		rb.Add(nil, server.name, ERR_NOPRIVS, client.nick, msg.Command, client.t("Insufficient oper privs"))
2247 2224
 		return false
2248 2225
 	}

+ 12
- 0
irc/help.go View File

@@ -187,6 +187,18 @@ Get an explanation of <argument>, or "index" for a list of help topics.`,
187 187
 		text: `HELPOP <argument>
188 188
 
189 189
 Get an explanation of <argument>, or "index" for a list of help topics.`,
190
+	},
191
+	"hostserv": {
192
+		text: `HOSTSERV <command> [params]
193
+
194
+HostServ lets you manage your vhost (a string displayed in place of your
195
+real hostname).`,
196
+	},
197
+	"hs": {
198
+		text: `HS <command> [params]
199
+
200
+HostServ lets you manage your vhost (a string displayed in place of your
201
+real hostname).`,
190 202
 	},
191 203
 	"info": {
192 204
 		text: `INFO

+ 314
- 0
irc/hostserv.go View File

@@ -0,0 +1,314 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"errors"
8
+	"fmt"
9
+	"regexp"
10
+	"strings"
11
+	"time"
12
+
13
+	"github.com/oragono/oragono/irc/utils"
14
+)
15
+
16
+const hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed
17
+in place of your client's hostname/IP).
18
+
19
+To see in-depth help for a specific HostServ command, try:
20
+    $b/HS HELP <command>$b
21
+
22
+Here are the commands you can use:
23
+%s`
24
+
25
+var (
26
+	errVHostBadCharacters = errors.New("Vhost contains prohibited characters")
27
+	errVHostTooLong       = errors.New("Vhost is too long")
28
+	// ascii only for now
29
+	defaultValidVhostRegex = regexp.MustCompile(`^[0-9A-Za-z.\-_/]+$`)
30
+)
31
+
32
+func hostservEnabled(server *Server) bool {
33
+	return server.AccountConfig().VHosts.Enabled
34
+}
35
+
36
+func hostservRequestsEnabled(server *Server) bool {
37
+	ac := server.AccountConfig()
38
+	return ac.VHosts.Enabled && ac.VHosts.UserRequests.Enabled
39
+}
40
+
41
+var (
42
+	hostservCommands = map[string]*serviceCommand{
43
+		"on": {
44
+			handler: hsOnOffHandler,
45
+			help: `Syntax: $bON$b
46
+
47
+ON enables your vhost, if you have one approved.`,
48
+			helpShort:    `$bON$b enables your vhost, if you have one approved.`,
49
+			authRequired: true,
50
+			enabled:      hostservEnabled,
51
+		},
52
+		"off": {
53
+			handler: hsOnOffHandler,
54
+			help: `Syntax: $bOFF$b
55
+
56
+OFF disables your vhost, if you have one approved.`,
57
+			helpShort:    `$bOFF$b disables your vhost, if you have one approved.`,
58
+			authRequired: true,
59
+			enabled:      hostservEnabled,
60
+		},
61
+		"request": {
62
+			handler: hsRequestHandler,
63
+			help: `Syntax: $bREQUEST <vhost>$b
64
+
65
+REQUEST requests that a new vhost by assigned to your account. The request must
66
+then be approved by a server operator.`,
67
+			helpShort:    `$bREQUEST$b requests a new vhost, pending operator approval.`,
68
+			authRequired: true,
69
+			enabled:      hostservRequestsEnabled,
70
+		},
71
+		"status": {
72
+			handler: hsStatusHandler,
73
+			help: `Syntax: $bSTATUS$b
74
+
75
+STATUS displays your current vhost, if any, and the status of your most recent
76
+request for a new one.`,
77
+			helpShort:    `$bSTATUS$b shows your vhost and request status.`,
78
+			authRequired: true,
79
+			enabled:      hostservEnabled,
80
+		},
81
+		"set": {
82
+			handler: hsSetHandler,
83
+			help: `Syntax: $bSET <user> <vhost>$b
84
+
85
+SET sets a user's vhost, bypassing the request system.`,
86
+			helpShort: `$bSET$b sets a user's vhost.`,
87
+			capabs:    []string{"vhosts"},
88
+			enabled:   hostservEnabled,
89
+		},
90
+		"del": {
91
+			handler: hsSetHandler,
92
+			help: `Syntax: $bDEL <user>$b
93
+
94
+DEL sets a user's vhost, bypassing the request system.`,
95
+			helpShort: `$bDEL$b deletes a user's vhost.`,
96
+			capabs:    []string{"vhosts"},
97
+			enabled:   hostservEnabled,
98
+		},
99
+		"waiting": {
100
+			handler: hsWaitingHandler,
101
+			help: `Syntax: $bWAITING$b
102
+
103
+WAITING shows a list of pending vhost requests, which can then be approved
104
+or rejected.`,
105
+			helpShort: `$bWAITING$b shows a list of pending vhost requests.`,
106
+			capabs:    []string{"vhosts"},
107
+			enabled:   hostservEnabled,
108
+		},
109
+		"approve": {
110
+			handler: hsApproveHandler,
111
+			help: `Syntax: $bAPPROVE <user>$b
112
+
113
+APPROVE approves a user's vhost request.`,
114
+			helpShort: `$bAPPROVE$b approves a user's vhost request.`,
115
+			capabs:    []string{"vhosts"},
116
+			enabled:   hostservEnabled,
117
+		},
118
+		"reject": {
119
+			handler: hsRejectHandler,
120
+			help: `Syntax: $bREJECT <user> [<reason>]$b
121
+
122
+REJECT rejects a user's vhost request, optionally giving them a reason
123
+for the rejection.`,
124
+			helpShort: `$bREJECT$b rejects a user's vhost request.`,
125
+			capabs:    []string{"vhosts"},
126
+			enabled:   hostservEnabled,
127
+		},
128
+	}
129
+)
130
+
131
+// hsNotice sends the client a notice from HostServ
132
+func hsNotice(rb *ResponseBuffer, text string) {
133
+	rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text)
134
+}
135
+
136
+// hsNotifyChannel notifies the designated channel of new vhost activity
137
+func hsNotifyChannel(server *Server, message string) {
138
+	chname := server.AccountConfig().VHosts.UserRequests.Channel
139
+	channel := server.channels.Get(chname)
140
+	if channel == nil {
141
+		return
142
+	}
143
+	chname = channel.Name()
144
+	for _, client := range channel.Members() {
145
+		client.Send(nil, "HostServ", "PRIVMSG", chname, message)
146
+	}
147
+}
148
+
149
+func hsOnOffHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
150
+	enable := false
151
+	if command == "on" {
152
+		enable = true
153
+	}
154
+
155
+	_, err := server.accounts.VHostSetEnabled(client, enable)
156
+	if err != nil {
157
+		hsNotice(rb, client.t("An error occurred"))
158
+	} else if enable {
159
+		hsNotice(rb, client.t("Successfully enabled your vhost"))
160
+	} else {
161
+		hsNotice(rb, client.t("Successfully disabled your vhost"))
162
+	}
163
+}
164
+
165
+func hsRequestHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
166
+	vhost, _ := utils.ExtractParam(params)
167
+	if validateVhost(server, vhost, false) != nil {
168
+		hsNotice(rb, client.t("Invalid vhost"))
169
+		return
170
+	}
171
+
172
+	accountName := client.Account()
173
+	account, err := server.accounts.LoadAccount(client.Account())
174
+	if err != nil {
175
+		hsNotice(rb, client.t("An error occurred"))
176
+		return
177
+	}
178
+	elapsed := time.Now().Sub(account.VHost.LastRequestTime)
179
+	remainingTime := server.AccountConfig().VHosts.UserRequests.Cooldown - elapsed
180
+	// you can update your existing request, but if you were rejected,
181
+	// you can't spam a replacement request
182
+	if account.VHost.RequestedVHost == "" && remainingTime > 0 {
183
+		hsNotice(rb, fmt.Sprintf(client.t("You must wait an additional %v before making another request"), remainingTime))
184
+		return
185
+	}
186
+
187
+	_, err = server.accounts.VHostRequest(accountName, vhost)
188
+	if err != nil {
189
+		hsNotice(rb, client.t("An error occurred"))
190
+	} else {
191
+		hsNotice(rb, fmt.Sprintf(client.t("Your vhost request will be reviewed by an administrator")))
192
+		chanMsg := fmt.Sprintf("Account %s requests vhost %s", accountName, vhost)
193
+		hsNotifyChannel(server, chanMsg)
194
+		// TODO send admins a snomask of some kind
195
+	}
196
+}
197
+
198
+func hsStatusHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
199
+	accountName := client.Account()
200
+	account, err := server.accounts.LoadAccount(accountName)
201
+	if err != nil {
202
+		server.logger.Warning("internal", "error loading account info", accountName, err.Error())
203
+		hsNotice(rb, client.t("An error occurred"))
204
+		return
205
+	}
206
+
207
+	if account.VHost.ApprovedVHost != "" {
208
+		hsNotice(rb, fmt.Sprintf(client.t("Account %s has vhost: %s"), accountName, account.VHost.ApprovedVHost))
209
+		if !account.VHost.Enabled {
210
+			hsNotice(rb, fmt.Sprintf(client.t("This vhost is currently disabled, but can be enabled with /HS ON")))
211
+		}
212
+	} else {
213
+		hsNotice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
214
+	}
215
+	if account.VHost.RequestedVHost != "" {
216
+		hsNotice(rb, fmt.Sprintf(client.t("A request is pending for vhost: %s"), account.VHost.RequestedVHost))
217
+	}
218
+	if account.VHost.RejectedVHost != "" {
219
+		hsNotice(rb, fmt.Sprintf(client.t("A request was previously made for vhost: %s"), account.VHost.RejectedVHost))
220
+		hsNotice(rb, fmt.Sprintf(client.t("It was rejected for reason: %s"), account.VHost.RejectionReason))
221
+	}
222
+}
223
+
224
+func validateVhost(server *Server, vhost string, oper bool) error {
225
+	ac := server.AccountConfig()
226
+	if len(vhost) > ac.VHosts.MaxLength {
227
+		return errVHostTooLong
228
+	}
229
+	if !ac.VHosts.ValidRegexp.MatchString(vhost) {
230
+		return errVHostBadCharacters
231
+	}
232
+	return nil
233
+}
234
+
235
+func hsSetHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
236
+	var user, vhost string
237
+	user, params = utils.ExtractParam(params)
238
+	if user == "" {
239
+		hsNotice(rb, client.t("A user is required"))
240
+		return
241
+	}
242
+	if command == "set" {
243
+		vhost, _ = utils.ExtractParam(params)
244
+		if validateVhost(server, vhost, true) != nil {
245
+			hsNotice(rb, client.t("Invalid vhost"))
246
+			return
247
+		}
248
+	} else if command != "del" {
249
+		server.logger.Warning("internal", "invalid hostserv set command", command)
250
+		return
251
+	}
252
+
253
+	_, err := server.accounts.VHostSet(user, vhost)
254
+	if err != nil {
255
+		hsNotice(rb, client.t("An error occurred"))
256
+	} else if vhost != "" {
257
+		hsNotice(rb, client.t("Successfully set vhost"))
258
+	} else {
259
+		hsNotice(rb, client.t("Successfully cleared vhost"))
260
+	}
261
+}
262
+
263
+func hsWaitingHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
264
+	requests, total := server.accounts.VHostListRequests(10)
265
+	hsNotice(rb, fmt.Sprintf(client.t("There are %d pending requests for vhosts (%d displayed)"), total, len(requests)))
266
+	for i, request := range requests {
267
+		hsNotice(rb, fmt.Sprintf(client.t("%d. User %s requests vhost: %s"), i+1, request.Account, request.RequestedVHost))
268
+	}
269
+}
270
+
271
+func hsApproveHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
272
+	user, _ := utils.ExtractParam(params)
273
+	if user == "" {
274
+		hsNotice(rb, client.t("A user is required"))
275
+		return
276
+	}
277
+
278
+	vhostInfo, err := server.accounts.VHostApprove(user)
279
+	if err != nil {
280
+		hsNotice(rb, client.t("An error occurred"))
281
+	} else {
282
+		hsNotice(rb, fmt.Sprintf(client.t("Successfully approved vhost request for %s"), user))
283
+		chanMsg := fmt.Sprintf("Oper %s approved vhost %s for account %s", client.Nick(), vhostInfo.ApprovedVHost, user)
284
+		hsNotifyChannel(server, chanMsg)
285
+		for _, client := range server.accounts.AccountToClients(user) {
286
+			client.Notice(client.t("Your vhost request was approved by an administrator"))
287
+		}
288
+	}
289
+}
290
+
291
+func hsRejectHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
292
+	user, params := utils.ExtractParam(params)
293
+	if user == "" {
294
+		hsNotice(rb, client.t("A user is required"))
295
+		return
296
+	}
297
+	reason := strings.TrimSpace(params)
298
+
299
+	vhostInfo, err := server.accounts.VHostReject(user, reason)
300
+	if err != nil {
301
+		hsNotice(rb, client.t("An error occurred"))
302
+	} else {
303
+		hsNotice(rb, fmt.Sprintf(client.t("Successfully rejected vhost request for %s"), user))
304
+		chanMsg := fmt.Sprintf("Oper %s rejected vhost %s for account %s, with the reason: %v", client.Nick(), vhostInfo.RejectedVHost, user, reason)
305
+		hsNotifyChannel(server, chanMsg)
306
+		for _, client := range server.accounts.AccountToClients(user) {
307
+			if reason == "" {
308
+				client.Notice("Your vhost request was rejected by an administrator")
309
+			} else {
310
+				client.Notice(fmt.Sprintf(client.t("Your vhost request was rejected by an administrator. The reason given was: %s"), reason))
311
+			}
312
+		}
313
+	}
314
+}

+ 1
- 4
irc/nickname.go View File

@@ -16,10 +16,7 @@ import (
16 16
 
17 17
 var (
18 18
 	restrictedNicknames = map[string]bool{
19
-		"=scene=":  true, // used for rp commands
20
-		"chanserv": true,
21
-		"nickserv": true,
22
-		"hostserv": true,
19
+		"=scene=": true, // used for rp commands
23 20
 	}
24 21
 )
25 22
 

+ 24
- 133
irc/nickserv.go View File

@@ -5,15 +5,20 @@ package irc
5 5
 
6 6
 import (
7 7
 	"fmt"
8
-	"sort"
9 8
 	"strings"
10 9
 
11
-	"github.com/goshuirc/irc-go/ircfmt"
12
-
13
-	"github.com/oragono/oragono/irc/modes"
14 10
 	"github.com/oragono/oragono/irc/utils"
15 11
 )
16 12
 
13
+// "enabled" callbacks for specific nickserv commands
14
+func servCmdRequiresAccreg(server *Server) bool {
15
+	return server.AccountConfig().Registration.Enabled
16
+}
17
+
18
+func servCmdRequiresAuthEnabled(server *Server) bool {
19
+	return server.AccountConfig().AuthenticationEnabled
20
+}
21
+
17 22
 const nickservHelp = `NickServ lets you register and login to an account.
18 23
 
19 24
 To see in-depth help for a specific NickServ command, try:
@@ -22,24 +27,16 @@ To see in-depth help for a specific NickServ command, try:
22 27
 Here are the commands you can use:
23 28
 %s`
24 29
 
25
-type nsCommand struct {
26
-	capabs          []string // oper capabs the given user has to have to access this command
27
-	handler         func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
28
-	help            string
29
-	helpShort       string
30
-	nickReservation bool // nick reservation must be enabled to use this command
31
-	oper            bool // true if the user has to be an oper to use this command
32
-}
33
-
34 30
 var (
35
-	nickservCommands = map[string]*nsCommand{
31
+	nickservCommands = map[string]*serviceCommand{
36 32
 		"drop": {
37 33
 			handler: nsDropHandler,
38 34
 			help: `Syntax: $bDROP [nickname]$b
39 35
 
40 36
 DROP de-links the given (or your current) nickname from your user account.`,
41
-			helpShort:       `$bDROP$b de-links your current (or the given) nickname from your user account.`,
42
-			nickReservation: true,
37
+			helpShort:    `$bDROP$b de-links your current (or the given) nickname from your user account.`,
38
+			enabled:      servCmdRequiresAccreg,
39
+			authRequired: true,
43 40
 		},
44 41
 		"ghost": {
45 42
 			handler: nsGhostHandler,
@@ -47,7 +44,8 @@ DROP de-links the given (or your current) nickname from your user account.`,
47 44
 
48 45
 GHOST disconnects the given user from the network if they're logged in with the
49 46
 same user account, letting you reclaim your nickname.`,
50
-			helpShort: `$bGHOST$b reclaims your nickname.`,
47
+			helpShort:    `$bGHOST$b reclaims your nickname.`,
48
+			authRequired: true,
51 49
 		},
52 50
 		"group": {
53 51
 			handler: nsGroupHandler,
@@ -55,15 +53,11 @@ same user account, letting you reclaim your nickname.`,
55 53
 
56 54
 GROUP links your current nickname with your logged-in account, preventing other
57 55
 users from changing to it (or forcing them to rename).`,
58
-			helpShort:       `$bGROUP$b links your current nickname to your user account.`,
59
-			nickReservation: true,
56
+			helpShort:    `$bGROUP$b links your current nickname to your user account.`,
57
+			enabled:      servCmdRequiresAccreg,
58
+			authRequired: true,
60 59
 		},
61
-		"help": {
62
-			help: `Syntax: $bHELP [command]$b
63 60
 
64
-HELP returns information on the given command.`,
65
-			helpShort: `$bHELP$b shows in-depth information about commands.`,
66
-		},
67 61
 		"identify": {
68 62
 			handler: nsIdentifyHandler,
69 63
 			help: `Syntax: $bIDENTIFY <username> [password]$b
@@ -91,15 +85,16 @@ registration, you can send an asterisk (*) as the email address.
91 85
 If the password is left out, your account will be registered to your TLS client
92 86
 certificate (and you will need to use that certificate to login in future).`,
93 87
 			helpShort: `$bREGISTER$b lets you register a user account.`,
88
+			enabled:   servCmdRequiresAccreg,
94 89
 		},
95 90
 		"sadrop": {
96 91
 			handler: nsDropHandler,
97 92
 			help: `Syntax: $bSADROP <nickname>$b
98 93
 
99
-SADROP foribly de-links the given nickname from the attached user account.`,
100
-			helpShort:       `$bSADROP$b forcibly de-links the given nickname from its user account.`,
101
-			nickReservation: true,
102
-			capabs:          []string{"unregister"},
94
+SADROP forcibly de-links the given nickname from the attached user account.`,
95
+			helpShort: `$bSADROP$b forcibly de-links the given nickname from its user account.`,
96
+			capabs:    []string{"unregister"},
97
+			enabled:   servCmdRequiresAccreg,
103 98
 		},
104 99
 		"unregister": {
105 100
 			handler: nsUnregisterHandler,
@@ -116,6 +111,7 @@ IRC operator with the correct permissions).`,
116 111
 VERIFY lets you complete an account registration, if the server requires email
117 112
 or other verification.`,
118 113
 			helpShort: `$bVERIFY$b lets you complete account registration.`,
114
+			enabled:   servCmdRequiresAccreg,
119 115
 		},
120 116
 	}
121 117
 )
@@ -125,53 +121,6 @@ func nsNotice(rb *ResponseBuffer, text string) {
125 121
 	rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text)
126 122
 }
127 123
 
128
-// nickservNoticeHandler handles NOTICEs that NickServ receives.
129
-func (server *Server) nickservNoticeHandler(client *Client, message string, rb *ResponseBuffer) {
130
-	// do nothing
131
-}
132
-
133
-// nickservPrivmsgHandler handles PRIVMSGs that NickServ receives.
134
-func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb *ResponseBuffer) {
135
-	commandName, params := utils.ExtractParam(message)
136
-	commandName = strings.ToLower(commandName)
137
-
138
-	commandInfo := nickservCommands[commandName]
139
-	if commandInfo == nil {
140
-		nsNotice(rb, client.t("Unknown command. To see available commands, run /NS HELP"))
141
-		return
142
-	}
143
-
144
-	if commandInfo.oper && !client.HasMode(modes.Operator) {
145
-		nsNotice(rb, client.t("Command restricted"))
146
-		return
147
-	}
148
-
149
-	if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
150
-		nsNotice(rb, client.t("Command restricted"))
151
-		return
152
-	}
153
-
154
-	if commandInfo.nickReservation && !server.AccountConfig().Registration.Enabled {
155
-		nsNotice(rb, client.t("Account registration has been disabled"))
156
-		return
157
-	}
158
-
159
-	// custom help handling here to prevent recursive init loop
160
-	if commandName == "help" {
161
-		nsHelpHandler(server, client, commandName, params, rb)
162
-		return
163
-	}
164
-
165
-	if commandInfo.handler == nil {
166
-		nsNotice(rb, client.t("Command error. Please report this to the developers"))
167
-		return
168
-	}
169
-
170
-	server.logger.Debug("nickserv", fmt.Sprintf("Client %s ran command %s", client.Nick(), commandName))
171
-
172
-	commandInfo.handler(server, client, commandName, params, rb)
173
-}
174
-
175 124
 func nsDropHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
176 125
 	sadrop := command == "sadrop"
177 126
 	nick, _ := utils.ExtractParam(params)
@@ -218,12 +167,6 @@ func nsGhostHandler(server *Server, client *Client, command, params string, rb *
218 167
 }
219 168
 
220 169
 func nsGroupHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
221
-	account := client.Account()
222
-	if account == "" {
223
-		nsNotice(rb, client.t("You're not logged into an account"))
224
-		return
225
-	}
226
-
227 170
 	nick := client.NickCasefolded()
228 171
 	err := server.accounts.SetNickReserved(client, nick, false, true)
229 172
 	if err == nil {
@@ -237,59 +180,7 @@ func nsGroupHandler(server *Server, client *Client, command, params string, rb *
237 180
 	}
238 181
 }
239 182
 
240
-func nsHelpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
241
-	nsNotice(rb, ircfmt.Unescape(client.t("*** $bNickServ HELP$b ***")))
242
-
243
-	if params == "" {
244
-		// show general help
245
-		var shownHelpLines sort.StringSlice
246
-		for _, commandInfo := range nickservCommands {
247
-			// skip commands user can't access
248
-			if commandInfo.oper && !client.HasMode(modes.Operator) {
249
-				continue
250
-			}
251
-			if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
252
-				continue
253
-			}
254
-			if commandInfo.nickReservation && !server.AccountConfig().Registration.Enabled {
255
-				continue
256
-			}
257
-
258
-			shownHelpLines = append(shownHelpLines, "    "+client.t(commandInfo.helpShort))
259
-		}
260
-
261
-		// sort help lines
262
-		sort.Sort(shownHelpLines)
263
-
264
-		// assemble help text
265
-		assembledHelpLines := strings.Join(shownHelpLines, "\n")
266
-		fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(nickservHelp), assembledHelpLines))
267
-
268
-		// push out help text
269
-		for _, line := range strings.Split(fullHelp, "\n") {
270
-			nsNotice(rb, line)
271
-		}
272
-	} else {
273
-		commandInfo := nickservCommands[strings.ToLower(strings.TrimSpace(params))]
274
-		if commandInfo == nil {
275
-			nsNotice(rb, client.t("Unknown command. To see available commands, run /NS HELP"))
276
-		} else {
277
-			for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
278
-				nsNotice(rb, line)
279
-			}
280
-		}
281
-	}
282
-
283
-	nsNotice(rb, ircfmt.Unescape(client.t("*** $bEnd of NickServ HELP$b ***")))
284
-}
285
-
286 183
 func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) {
287
-	// fail out if we need to
288
-	if !server.AccountConfig().AuthenticationEnabled {
289
-		nsNotice(rb, client.t("Login has been disabled"))
290
-		return
291
-	}
292
-
293 184
 	loginSuccessful := false
294 185
 
295 186
 	username, passphrase := utils.ExtractParam(params)

+ 12
- 5
irc/server.go View File

@@ -115,8 +115,8 @@ type Server struct {
115 115
 	name                       string
116 116
 	nameCasefolded             string
117 117
 	networkName                string
118
-	operators                  map[string]Oper
119
-	operclasses                map[string]OperClass
118
+	operators                  map[string]*Oper
119
+	operclasses                map[string]*OperClass
120 120
 	password                   []byte
121 121
 	passwords                  *passwd.SaltedManager
122 122
 	recoverFromErrors          bool
@@ -659,8 +659,9 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
659 659
 	if whoischannels != nil {
660 660
 		rb.Add(nil, client.server.name, RPL_WHOISCHANNELS, client.nick, target.nick, strings.Join(whoischannels, " "))
661 661
 	}
662
-	if target.class != nil {
663
-		rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, target.whoisLine)
662
+	tOper := target.Oper()
663
+	if tOper != nil {
664
+		rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, tOper.WhoisLine)
664 665
 	}
665 666
 	if client.HasMode(modes.Operator) || client == target {
666 667
 		rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP"))
@@ -863,6 +864,12 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
863 864
 		server.accounts.buildNickToAccountIndex()
864 865
 	}
865 866
 
867
+	hsPreviouslyDisabled := oldAccountConfig != nil && !oldAccountConfig.VHosts.Enabled
868
+	hsNowEnabled := config.Accounts.VHosts.Enabled
869
+	if hsPreviouslyDisabled && hsNowEnabled {
870
+		server.accounts.initVHostRequestQueue()
871
+	}
872
+
866 873
 	// STS
867 874
 	stsValue := config.Server.STS.Value()
868 875
 	var stsDisabled bool
@@ -944,7 +951,7 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
944 951
 		ChanListModes:  int(config.Limits.ChanListModes),
945 952
 		LineLen:        lineLenConfig,
946 953
 	}
947
-	server.operclasses = *operclasses
954
+	server.operclasses = operclasses
948 955
 	server.operators = opers
949 956
 	server.checkIdent = config.Server.CheckIdent
950 957
 

+ 194
- 0
irc/services.go View File

@@ -0,0 +1,194 @@
1
+// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"fmt"
8
+	"sort"
9
+	"strings"
10
+
11
+	"github.com/goshuirc/irc-go/ircfmt"
12
+	"github.com/goshuirc/irc-go/ircmsg"
13
+
14
+	"github.com/oragono/oragono/irc/utils"
15
+)
16
+
17
+// defines an IRC service, e.g., NICKSERV
18
+type ircService struct {
19
+	Name           string
20
+	ShortName      string
21
+	CommandAliases []string
22
+	Commands       map[string]*serviceCommand
23
+	HelpBanner     string
24
+}
25
+
26
+// defines a command associated with a service, e.g., NICKSERV IDENTIFY
27
+type serviceCommand struct {
28
+	capabs       []string // oper capabs the given user has to have to access this command
29
+	handler      func(server *Server, client *Client, command, params string, rb *ResponseBuffer)
30
+	help         string
31
+	helpShort    string
32
+	authRequired bool
33
+	enabled      func(*Server) bool // is this command enabled in the server config?
34
+}
35
+
36
+// all services, by lowercase name
37
+var OragonoServices = map[string]*ircService{
38
+	"nickserv": {
39
+		Name:           "NickServ",
40
+		ShortName:      "NS",
41
+		CommandAliases: []string{"NICKSERV", "NS"},
42
+		Commands:       nickservCommands,
43
+		HelpBanner:     nickservHelp,
44
+	},
45
+	"chanserv": {
46
+		Name:           "ChanServ",
47
+		ShortName:      "CS",
48
+		CommandAliases: []string{"CHANSERV", "CS"},
49
+		Commands:       chanservCommands,
50
+		HelpBanner:     chanservHelp,
51
+	},
52
+	"hostserv": {
53
+		Name:           "HostServ",
54
+		ShortName:      "HS",
55
+		CommandAliases: []string{"HOSTSERV", "HS"},
56
+		Commands:       hostservCommands,
57
+		HelpBanner:     hostservHelp,
58
+	},
59
+}
60
+
61
+// all service commands at the protocol level, by uppercase command name
62
+// e.g., NICKSERV, NS
63
+var oragonoServicesByCommandAlias map[string]*ircService
64
+
65
+// special-cased command shared by all services
66
+var servHelpCmd serviceCommand = serviceCommand{
67
+	help: `Syntax: $bHELP [command]$b
68
+
69
+HELP returns information on the given command.`,
70
+	helpShort: `$bHELP$b shows in-depth information about commands.`,
71
+}
72
+
73
+// this handles IRC commands like `/NICKSERV INFO`, translating into `/MSG NICKSERV INFO`
74
+func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
75
+	service, ok := oragonoServicesByCommandAlias[msg.Command]
76
+	if !ok {
77
+		server.logger.Warning("internal", "can't handle unrecognized service", msg.Command)
78
+		return false
79
+	}
80
+
81
+	fakePrivmsg := strings.Join(msg.Params, " ")
82
+	servicePrivmsgHandler(service, server, client, fakePrivmsg, rb)
83
+	return false
84
+}
85
+
86
+// generic handler for service PRIVMSG
87
+func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) {
88
+	commandName, params := utils.ExtractParam(message)
89
+	commandName = strings.ToLower(commandName)
90
+
91
+	nick := rb.target.Nick()
92
+	sendNotice := func(notice string) {
93
+		rb.Add(nil, service.Name, "NOTICE", nick, notice)
94
+	}
95
+
96
+	cmd := service.Commands[commandName]
97
+	if cmd == nil {
98
+		sendNotice(fmt.Sprintf("%s /%s HELP", client.t("Unknown command. To see available commands, run"), service.ShortName))
99
+		return
100
+	}
101
+
102
+	if cmd.enabled != nil && !cmd.enabled(server) {
103
+		sendNotice(client.t("This command has been disabled by the server administrators"))
104
+		return
105
+	}
106
+
107
+	if 0 < len(cmd.capabs) && !client.HasRoleCapabs(cmd.capabs...) {
108
+		sendNotice(client.t("Command restricted"))
109
+		return
110
+	}
111
+
112
+	if cmd.authRequired && client.Account() == "" {
113
+		sendNotice(client.t("You're not logged into an account"))
114
+		return
115
+	}
116
+
117
+	server.logger.Debug("services", fmt.Sprintf("Client %s ran %s command %s", client.Nick(), service.Name, commandName))
118
+	if commandName == "help" {
119
+		serviceHelpHandler(service, server, client, params, rb)
120
+	} else {
121
+		cmd.handler(server, client, commandName, params, rb)
122
+	}
123
+}
124
+
125
+// generic handler that displays help for service commands
126
+func serviceHelpHandler(service *ircService, server *Server, client *Client, params string, rb *ResponseBuffer) {
127
+	nick := rb.target.Nick()
128
+	sendNotice := func(notice string) {
129
+		rb.Add(nil, service.Name, "NOTICE", nick, notice)
130
+	}
131
+
132
+	sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
133
+
134
+	if params == "" {
135
+		// show general help
136
+		var shownHelpLines sort.StringSlice
137
+		for _, commandInfo := range service.Commands {
138
+			// skip commands user can't access
139
+			if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
140
+				continue
141
+			}
142
+			if commandInfo.enabled != nil && !commandInfo.enabled(server) {
143
+				continue
144
+			}
145
+
146
+			shownHelpLines = append(shownHelpLines, "    "+client.t(commandInfo.helpShort))
147
+		}
148
+
149
+		// sort help lines
150
+		sort.Sort(shownHelpLines)
151
+
152
+		// assemble help text
153
+		assembledHelpLines := strings.Join(shownHelpLines, "\n")
154
+		fullHelp := ircfmt.Unescape(fmt.Sprintf(client.t(service.HelpBanner), assembledHelpLines))
155
+
156
+		// push out help text
157
+		for _, line := range strings.Split(fullHelp, "\n") {
158
+			sendNotice(line)
159
+		}
160
+	} else {
161
+		commandInfo := service.Commands[strings.ToLower(strings.TrimSpace(params))]
162
+		if commandInfo == nil {
163
+			sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName)))
164
+		} else {
165
+			for _, line := range strings.Split(ircfmt.Unescape(client.t(commandInfo.help)), "\n") {
166
+				sendNotice(line)
167
+			}
168
+		}
169
+	}
170
+
171
+	sendNotice(ircfmt.Unescape(fmt.Sprintf(client.t("*** $bEnd of %s HELP$b ***"), service.Name)))
172
+}
173
+
174
+func initializeServices() {
175
+	// this modifies the global Commands map,
176
+	// so it must be called from irc/commands.go's init()
177
+	oragonoServicesByCommandAlias = make(map[string]*ircService)
178
+
179
+	for serviceName, service := range OragonoServices {
180
+		// make `/MSG ServiceName HELP` work correctly
181
+		service.Commands["help"] = &servHelpCmd
182
+
183
+		// reserve the nickname
184
+		restrictedNicknames[serviceName] = true
185
+
186
+		// register the protocol-level commands (NICKSERV, NS) that talk to the service
187
+		var ircCmdDef Command
188
+		ircCmdDef.handler = serviceCmdHandler
189
+		for _, ircCmd := range service.CommandAliases {
190
+			Commands[ircCmd] = ircCmdDef
191
+			oragonoServicesByCommandAlias[ircCmd] = service
192
+		}
193
+	}
194
+}

+ 31
- 0
oragono.yaml View File

@@ -198,6 +198,36 @@ accounts:
198 198
         # rename-prefix - this is the prefix to use when renaming clients (e.g. Guest-AB54U31)
199 199
         rename-prefix: Guest-
200 200
 
201
+    # vhosts controls the assignment of vhosts (strings displayed in place of the user's
202
+    # hostname/IP) by the HostServ service
203
+    vhosts:
204
+        # are vhosts enabled at all?
205
+        enabled: true
206
+
207
+        # maximum length of a vhost
208
+        max-length: 64
209
+
210
+        # regexp for testing the validity of a vhost
211
+        # (make sure any changes you make here are RFC-compliant)
212
+        valid-regexp: '^[0-9A-Za-z.\-_/]+$'
213
+
214
+        # options controlling users requesting vhosts:
215
+        user-requests:
216
+            # can users request vhosts at all? if this is false, operators with the
217
+            # 'vhosts' capability can still assign vhosts manually
218
+            enabled: false
219
+
220
+            # if uncommented, all new vhost requests will be dumped into the given
221
+            # channel, so opers can review them as they are sent in. ensure that you
222
+            # have registered and restricted the channel appropriately before you
223
+            # uncomment this.
224
+            #channel: "#vhosts"
225
+
226
+            # after a user's vhost has been approved or rejected, they need to wait
227
+            # this long (starting from the time of their original request)
228
+            # before they can request a new one.
229
+            cooldown: 168h
230
+
201 231
 # channel options
202 232
 channels:
203 233
     # modes that are set when new channels are created
@@ -252,6 +282,7 @@ oper-classes:
252 282
             - "oper:die"
253 283
             - "unregister"
254 284
             - "samode"
285
+            - "vhosts"
255 286
 
256 287
 # ircd operators
257 288
 opers:

Loading…
Cancel
Save