1// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2// released under the MIT license
3
4package irc
5
6import (
7	"errors"
8	"fmt"
9	"regexp"
10
11	"github.com/ergochat/irc-go/ircfmt"
12
13	"github.com/ergochat/ergo/irc/utils"
14)
15
16const (
17	hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed
18in place of your client's hostname/IP).`
19)
20
21var (
22	errVHostBadCharacters = errors.New("Vhost contains prohibited characters")
23	errVHostTooLong       = errors.New("Vhost is too long")
24	// ascii only for now
25	defaultValidVhostRegex = regexp.MustCompile(`^[0-9A-Za-z.\-_/]+$`)
26)
27
28func hostservEnabled(config *Config) bool {
29	return config.Accounts.VHosts.Enabled
30}
31
32var (
33	hostservCommands = map[string]*serviceCommand{
34		"on": {
35			handler: hsOnOffHandler,
36			help: `Syntax: $bON$b
37
38ON enables your vhost, if you have one approved.`,
39			helpShort:    `$bON$b enables your vhost, if you have one approved.`,
40			authRequired: true,
41			enabled:      hostservEnabled,
42		},
43		"off": {
44			handler: hsOnOffHandler,
45			help: `Syntax: $bOFF$b
46
47OFF disables your vhost, if you have one approved.`,
48			helpShort:    `$bOFF$b disables your vhost, if you have one approved.`,
49			authRequired: true,
50			enabled:      hostservEnabled,
51		},
52		"status": {
53			handler: hsStatusHandler,
54			help: `Syntax: $bSTATUS [user]$b
55
56STATUS displays your current vhost, if any, and whether it is enabled or
57disabled. A server operator can view someone else's status.`,
58			helpShort: `$bSTATUS$b shows your vhost status.`,
59			enabled:   hostservEnabled,
60		},
61		"set": {
62			handler: hsSetHandler,
63			help: `Syntax: $bSET <user> <vhost>$b
64
65SET sets a user's vhost, bypassing the request system.`,
66			helpShort: `$bSET$b sets a user's vhost.`,
67			capabs:    []string{"vhosts"},
68			enabled:   hostservEnabled,
69			minParams: 2,
70		},
71		"del": {
72			handler: hsSetHandler,
73			help: `Syntax: $bDEL <user>$b
74
75DEL deletes a user's vhost.`,
76			helpShort: `$bDEL$b deletes a user's vhost.`,
77			capabs:    []string{"vhosts"},
78			enabled:   hostservEnabled,
79			minParams: 1,
80		},
81		"setcloaksecret": {
82			handler: hsSetCloakSecretHandler,
83			help: `Syntax: $bSETCLOAKSECRET$b <secret> [code]
84
85SETCLOAKSECRET can be used to set or rotate the cloak secret. You should use
86a cryptographically strong secret. To prevent accidental modification, a
87verification code is required; invoking the command without a code will
88display the necessary code.`,
89			helpShort: `$bSETCLOAKSECRET$b modifies the IP cloaking secret.`,
90			capabs:    []string{"vhosts", "rehash"},
91			minParams: 1,
92			maxParams: 2,
93		},
94	}
95)
96
97func hsOnOffHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
98	enable := false
99	if command == "on" {
100		enable = true
101	}
102
103	_, err := server.accounts.VHostSetEnabled(client, enable)
104	if err == errNoVhost {
105		service.Notice(rb, client.t(err.Error()))
106	} else if err != nil {
107		service.Notice(rb, client.t("An error occurred"))
108	} else if enable {
109		service.Notice(rb, client.t("Successfully enabled your vhost"))
110	} else {
111		service.Notice(rb, client.t("Successfully disabled your vhost"))
112	}
113}
114
115func hsStatusHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
116	var accountName string
117	if len(params) > 0 {
118		if !client.HasRoleCapabs("vhosts") {
119			service.Notice(rb, client.t("Command restricted"))
120			return
121		}
122		accountName = params[0]
123	} else {
124		accountName = client.Account()
125		if accountName == "" {
126			service.Notice(rb, client.t("You're not logged into an account"))
127			return
128		}
129	}
130
131	account, err := server.accounts.LoadAccount(accountName)
132	if err != nil {
133		if err != errAccountDoesNotExist {
134			server.logger.Warning("internal", "error loading account info", accountName, err.Error())
135		}
136		service.Notice(rb, client.t("No such account"))
137		return
138	}
139
140	if account.VHost.ApprovedVHost != "" {
141		service.Notice(rb, fmt.Sprintf(client.t("Account %[1]s has vhost: %[2]s"), accountName, account.VHost.ApprovedVHost))
142		if !account.VHost.Enabled {
143			service.Notice(rb, client.t("This vhost is currently disabled, but can be enabled with /HS ON"))
144		}
145	} else {
146		service.Notice(rb, fmt.Sprintf(client.t("Account %s has no vhost"), accountName))
147	}
148}
149
150func validateVhost(server *Server, vhost string, oper bool) error {
151	config := server.Config()
152	if len(vhost) > config.Accounts.VHosts.MaxLength {
153		return errVHostTooLong
154	}
155	if !config.Accounts.VHosts.validRegexp.MatchString(vhost) {
156		return errVHostBadCharacters
157	}
158	return nil
159}
160
161func hsSetHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
162	user := params[0]
163	var vhost string
164
165	if command == "set" {
166		vhost = params[1]
167		if validateVhost(server, vhost, true) != nil {
168			service.Notice(rb, client.t("Invalid vhost"))
169			return
170		}
171	}
172	// else: command == "del", vhost == ""
173
174	_, err := server.accounts.VHostSet(user, vhost)
175	if err != nil {
176		service.Notice(rb, client.t("An error occurred"))
177	} else if vhost != "" {
178		service.Notice(rb, client.t("Successfully set vhost"))
179	} else {
180		service.Notice(rb, client.t("Successfully cleared vhost"))
181	}
182}
183
184func hsSetCloakSecretHandler(service *ircService, server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
185	secret := params[0]
186	expectedCode := utils.ConfirmationCode(secret, server.ctime)
187	if len(params) == 1 || params[1] != expectedCode {
188		service.Notice(rb, ircfmt.Unescape(client.t("$bWarning: changing the cloak secret will invalidate stored ban/invite/exception lists.$b")))
189		service.Notice(rb, fmt.Sprintf(client.t("To confirm, run this command: %s"), fmt.Sprintf("/HS SETCLOAKSECRET %s %s", secret, expectedCode)))
190		return
191	}
192	StoreCloakSecret(server.store, secret)
193	service.Notice(rb, client.t("Rotated the cloak secret; you must rehash or restart the server for it to take effect"))
194}
195