Add SASL auth and CAP negotiation

This commit is contained in:
Ken-Håvard Lieng 2020-05-23 08:05:37 +02:00
parent be8b785813
commit 2f8dad2529
18 changed files with 563 additions and 127 deletions

File diff suppressed because one or more lines are too long

View File

@ -408,6 +408,17 @@ i[class*=' icon-']:before {
width: 100%;
}
.connect-section {
border-bottom: 1px solid #ddd;
padding-bottom: 15px;
margin: 15px 0;
margin-bottom: 10px;
}
.connect-section h2 {
margin-bottom: 10px;
}
input[type='number'] {
appearance: textfield;
}
@ -806,7 +817,6 @@ input.message-input-nick.invalid {
}
.settings h2 {
font-weight: 700;
color: #222;
}

View File

@ -1,13 +1,13 @@
import React, { Component } from 'react';
import { createSelector } from 'reselect';
import { Form, withFormik } from 'formik';
import { FiMoreHorizontal } from 'react-icons/fi';
import Navicon from 'components/ui/Navicon';
import Button from 'components/ui/Button';
import Checkbox from 'components/ui/formik/Checkbox';
import TextInput from 'components/ui/TextInput';
import Error from 'components/ui/formik/Error';
import { isValidNick, isValidChannel, isValidUsername, isInt } from 'utils';
import { FiMoreHorizontal } from 'react-icons/fi';
const getSortedDefaultChannels = createSelector(
defaults => defaults.channels,
@ -56,11 +56,21 @@ class Connect extends Component {
const { hexIP } = this.props;
return (
<div>
{!hexIP && <TextInput name="username" />}
<TextInput name="password" type="password" noTrim />
<TextInput name="realname" noTrim />
<>
<div className="connect-section">
<h2>SASL</h2>
<TextInput name="account" />
<TextInput name="password" type="password" />
</div>
{!hexIP && <TextInput name="username" />}
<TextInput
name="serverPassword"
label="Server Password"
type="password"
noTrim
/>
<TextInput name="realname" noTrim />
</>
);
};
@ -170,8 +180,10 @@ export default withFormik({
port,
nick: query.nick || localStorage.lastNick || '',
channels: channels || defaults.channels.join(','),
account: '',
password: '',
username: query.username || '',
password: defaults.password ? ' ' : '',
serverPassword: defaults.serverPassword ? ' ' : '',
realname: query.realname || localStorage.lastRealname || '',
tls: ssl
};
@ -219,7 +231,7 @@ export default withFormik({
const channels = values.channels ? values.channels.split(',') : [];
delete values.channels;
values.password = values.password.trim();
values.serverPassword = values.serverPassword.trim();
values.port = `${values.port}`;
connect(values);

View File

@ -13,7 +13,7 @@ port = 6697
channels = [
"#dispatch"
]
password = ""
server_password = ""
ssl = true
# Only allow a nick to be filled in
readonly = false

View File

@ -27,7 +27,7 @@ type Defaults struct {
Host string
Port string
Channels []string
Password string
ServerPassword string `mapstructure:"server_password"`
SSL bool
ReadOnly bool
ShowDetails bool `mapstructure:"show_details"`

129
pkg/irc/cap.go Normal file
View File

@ -0,0 +1,129 @@
package irc
import (
"strings"
)
func (c *Client) HasCapability(name string, values ...string) bool {
if capValues, ok := c.enabledCapabilities[name]; ok {
if len(values) == 0 || capValues == nil {
return true
}
for _, v := range values {
for _, vCap := range capValues {
if v == vCap {
return true
}
}
}
}
return false
}
var clientWantedCaps = []string{}
func (c *Client) writeCAP() {
c.write("CAP LS 302")
}
func (c *Client) handleCAP(msg *Message) {
if len(msg.Params) < 3 {
c.write("CAP END")
return
}
caps := parseCaps(msg.LastParam())
c.lock.Lock()
defer c.lock.Unlock()
switch msg.Params[1] {
case "LS":
for cap, values := range caps {
for _, wanted := range c.wantedCapabilities {
if cap == wanted {
c.requestedCapabilities[cap] = values
}
}
}
if len(msg.Params) == 3 {
if len(c.requestedCapabilities) == 0 {
c.write("CAP END")
return
}
reqCaps := []string{}
for cap := range c.requestedCapabilities {
reqCaps = append(reqCaps, cap)
}
c.write("CAP REQ :" + strings.Join(reqCaps, " "))
}
case "ACK":
for cap := range caps {
if v, ok := c.requestedCapabilities[cap]; ok {
c.enabledCapabilities[cap] = v
delete(c.requestedCapabilities, cap)
}
}
if len(c.requestedCapabilities) == 0 {
if c.SASL != nil && c.HasCapability("sasl", c.SASL.Name()) {
c.write("AUTHENTICATE " + c.SASL.Name())
} else {
c.write("CAP END")
}
}
case "NAK":
for cap := range caps {
delete(c.requestedCapabilities, cap)
}
if len(c.requestedCapabilities) == 0 {
c.write("CAP END")
}
case "NEW":
reqCaps := []string{}
for cap, values := range caps {
for _, wanted := range c.wantedCapabilities {
if cap == wanted && !c.HasCapability(cap) {
c.requestedCapabilities[cap] = values
reqCaps = append(reqCaps, cap)
}
}
}
if len(reqCaps) > 0 {
c.write("CAP REQ :" + strings.Join(reqCaps, " "))
}
case "DEL":
for cap := range caps {
delete(c.enabledCapabilities, cap)
}
}
}
func parseCaps(caps string) map[string][]string {
result := map[string][]string{}
parts := strings.Split(caps, " ")
for _, part := range parts {
capParts := strings.Split(part, "=")
name := capParts[0]
if len(capParts) > 1 {
result[name] = strings.Split(capParts[1], ",")
} else {
result[name] = nil
}
}
return result
}

58
pkg/irc/cap_test.go Normal file
View File

@ -0,0 +1,58 @@
package irc
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestParseCaps(t *testing.T) {
cases := []struct {
input string
expected map[string][]string
}{
{
"sasl",
map[string][]string{
"sasl": nil,
},
}, {
"sasl=PLAIN",
map[string][]string{
"sasl": {"PLAIN"},
},
}, {
"cake sasl=PLAIN",
map[string][]string{
"cake": nil,
"sasl": {"PLAIN"},
},
}, {
"cake sasl=PLAIN pie",
map[string][]string{
"cake": nil,
"sasl": {"PLAIN"},
"pie": nil,
},
}, {
"cake sasl=PLAIN pie=BLUEBERRY,RASPBERRY",
map[string][]string{
"cake": nil,
"sasl": {"PLAIN"},
"pie": {"BLUEBERRY", "RASPBERRY"},
},
}, {
"cake sasl=PLAIN pie=BLUEBERRY,RASPBERRY cheesecake",
map[string][]string{
"cake": nil,
"sasl": {"PLAIN"},
"pie": {"BLUEBERRY", "RASPBERRY"},
"cheesecake": nil,
},
},
}
for _, tc := range cases {
assert.Equal(t, tc.expected, parseCaps(tc.input))
}
}

View File

@ -19,6 +19,7 @@ type Client struct {
Password string
Username string
Realname string
SASL SASL
HandleNickInUse func(string) string
// Version is the reply to VERSION and FINGER CTCP messages
@ -32,6 +33,10 @@ type Client struct {
nick string
channels []string
wantedCapabilities []string
requestedCapabilities map[string][]string
enabledCapabilities map[string][]string
conn net.Conn
connected bool
registered bool
@ -58,6 +63,9 @@ func NewClient(nick, username string) *Client {
out: make(chan string, 32),
quit: make(chan struct{}),
reconnect: make(chan struct{}),
wantedCapabilities: clientWantedCaps,
enabledCapabilities: map[string][]string{},
requestedCapabilities: map[string][]string{},
dialer: &net.Dialer{Timeout: 10 * time.Second},
recvBuf: make([]byte, 0, 4096),
backoff: &backoff.Backoff{
@ -191,6 +199,11 @@ func (c *Client) writeUser(username, realname string) {
}
func (c *Client) register() {
if c.SASL != nil {
c.wantedCapabilities = append(c.wantedCapabilities, "sasl")
}
c.writeCAP()
if c.Password != "" {
c.writePass(c.Password)
}

View File

@ -152,11 +152,13 @@ func TestRegister(t *testing.T) {
c.Username = "user"
c.Realname = "rn"
c.register()
assert.Equal(t, "CAP LS 302\r\n", <-out)
assert.Equal(t, "NICK nick\r\n", <-out)
assert.Equal(t, "USER user 0 * :rn\r\n", <-out)
c.Password = "pass"
c.register()
assert.Equal(t, "CAP LS 302\r\n", <-out)
assert.Equal(t, "PASS pass\r\n", <-out)
assert.Equal(t, "NICK nick\r\n", <-out)
assert.Equal(t, "USER user 0 * :rn\r\n", <-out)

View File

@ -226,6 +226,9 @@ func (c *Client) recv() {
c.handleCTCP(ctcp, msg)
}
case CAP:
c.handleCAP(msg)
case RPL_WELCOME:
c.setNick(msg.Params[0])
c.setRegistered(true)
@ -251,6 +254,8 @@ func (c *Client) recv() {
return
}
c.handleSASL(msg)
c.Messages <- msg
}
}

64
pkg/irc/sasl.go Normal file
View File

@ -0,0 +1,64 @@
package irc
import (
"bytes"
"encoding/base64"
)
type SASL interface {
Name() string
Encode() string
}
type SASLPlain struct {
Username string
Password string
}
func (s *SASLPlain) Name() string {
return "PLAIN"
}
func (s *SASLPlain) Encode() string {
buf := bytes.Buffer{}
buf.WriteString(s.Username)
buf.WriteByte(0x0)
buf.WriteString(s.Username)
buf.WriteByte(0x0)
buf.WriteString(s.Password)
return base64.StdEncoding.EncodeToString(buf.Bytes())
}
type SASLExternal struct{}
func (s *SASLExternal) Name() string {
return "EXTERNAL"
}
func (s *SASLExternal) Encode() string {
return "+"
}
func (c *Client) handleSASL(msg *Message) {
switch msg.Command {
case AUTHENTICATE:
auth := c.SASL.Encode()
for len(auth) >= 400 {
c.write("AUTHENTICATE " + auth)
auth = auth[400:]
}
if len(auth) > 0 {
c.write("AUTHENTICATE " + auth)
} else {
c.write("AUTHENTICATE +")
}
case RPL_SASLSUCCESS:
c.write("CAP END")
case ERR_NICKLOCKED, ERR_SASLFAIL, ERR_SASLTOOLONG, ERR_SASLABORTED, RPL_SASLMECHS:
c.write("CAP END")
}
}

View File

@ -12,7 +12,7 @@ import (
type connectDefaults struct {
*config.Defaults
Password bool
ServerPassword bool
}
type dispatchVersion struct {
@ -51,7 +51,7 @@ func (d *Dispatch) getIndexData(r *http.Request, state *State) *indexData {
},
}
data.Defaults.Password = cfg.Defaults.Password != ""
data.Defaults.ServerPassword = cfg.Defaults.ServerPassword != ""
if state == nil {
data.Settings = storage.DefaultClientSettings()

View File

@ -553,8 +553,8 @@ func easyjson7e607aefDecodeGithubComKhliengDispatchServer2(in *jlexer.Lexer, out
continue
}
switch key {
case "password":
out.Password = bool(in.Bool())
case "serverPassword":
out.ServerPassword = bool(in.Bool())
case "name":
out.Name = string(in.String())
case "host":
@ -604,11 +604,11 @@ func easyjson7e607aefEncodeGithubComKhliengDispatchServer2(out *jwriter.Writer,
out.RawByte('{')
first := true
_ = first
if in.Password {
const prefix string = ",\"password\":"
if in.ServerPassword {
const prefix string = ",\"serverPassword\":"
first = false
out.RawString(prefix[1:])
out.Bool(bool(in.Password))
out.Bool(bool(in.ServerPassword))
}
if in.Name != "" {
const prefix string = ",\"name\":"

View File

@ -55,12 +55,19 @@ func connectIRC(server *storage.Server, state *State, srcIP []byte) *irc.Client
i.Realname = server.Nick
}
if server.Password == "" &&
cfg.Defaults.Password != "" &&
if server.ServerPassword == "" &&
cfg.Defaults.ServerPassword != "" &&
address == cfg.Defaults.Host {
i.Password = cfg.Defaults.Password
i.Password = cfg.Defaults.ServerPassword
} else {
i.Password = server.Password
i.Password = server.ServerPassword
}
if server.Account != "" && server.Password != "" {
i.SASL = &irc.SASLPlain{
Username: server.Account,
Password: server.Password,
}
}
if i.TLS {

View File

@ -866,14 +866,18 @@ func easyjson42239ddeDecodeGithubComKhliengDispatchServer8(in *jlexer.Lexer, out
out.Port = string(in.String())
case "tls":
out.TLS = bool(in.Bool())
case "password":
out.Password = string(in.String())
case "serverPassword":
out.ServerPassword = string(in.String())
case "nick":
out.Nick = string(in.String())
case "username":
out.Username = string(in.String())
case "realname":
out.Realname = string(in.String())
case "account":
out.Account = string(in.String())
case "password":
out.Password = string(in.String())
default:
in.SkipRecursive()
}
@ -964,15 +968,15 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer8(out *jwriter.Writer,
}
out.Bool(bool(in.TLS))
}
if in.Password != "" {
const prefix string = ",\"password\":"
if in.ServerPassword != "" {
const prefix string = ",\"serverPassword\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Password))
out.String(string(in.ServerPassword))
}
if in.Nick != "" {
const prefix string = ",\"nick\":"
@ -1004,6 +1008,26 @@ func easyjson42239ddeEncodeGithubComKhliengDispatchServer8(out *jwriter.Writer,
}
out.String(string(in.Realname))
}
if in.Account != "" {
const prefix string = ",\"account\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Account))
}
if in.Password != "" {
const prefix string = ",\"password\":"
if first {
first = false
out.RawString(prefix[1:])
} else {
out.RawString(prefix)
}
out.String(string(in.Password))
}
out.RawByte('}')
}

View File

@ -14,10 +14,12 @@ struct Server {
Host string
Port string
TLS bool
Password string
ServerPassword string
Nick string
Username string
Realname string
Account string
Password string
}
struct Channel {

View File

@ -286,7 +286,7 @@ func (d *Server) Size() (s uint64) {
s += l
}
{
l := uint64(len(d.Password))
l := uint64(len(d.ServerPassword))
{
@ -345,6 +345,36 @@ func (d *Server) Size() (s uint64) {
}
s += l
}
{
l := uint64(len(d.Account))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
{
l := uint64(len(d.Password))
{
t := l
for t >= 0x80 {
t >>= 7
s++
}
s++
}
s += l
}
s += 1
return
}
@ -424,7 +454,7 @@ func (d *Server) Marshal(buf []byte) ([]byte, error) {
}
}
{
l := uint64(len(d.Password))
l := uint64(len(d.ServerPassword))
{
@ -439,7 +469,7 @@ func (d *Server) Marshal(buf []byte) ([]byte, error) {
i++
}
copy(buf[i+1:], d.Password)
copy(buf[i+1:], d.ServerPassword)
i += l
}
{
@ -499,6 +529,44 @@ func (d *Server) Marshal(buf []byte) ([]byte, error) {
copy(buf[i+1:], d.Realname)
i += l
}
{
l := uint64(len(d.Account))
{
t := uint64(l)
for t >= 0x80 {
buf[i+1] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+1] = byte(t)
i++
}
copy(buf[i+1:], d.Account)
i += l
}
{
l := uint64(len(d.Password))
{
t := uint64(l)
for t >= 0x80 {
buf[i+1] = byte(t) | 0x80
t >>= 7
i++
}
buf[i+1] = byte(t)
i++
}
copy(buf[i+1:], d.Password)
i += l
}
return buf[:i+1], nil
}
@ -585,7 +653,7 @@ func (d *Server) Unmarshal(buf []byte) (uint64, error) {
l = t
}
d.Password = string(buf[i+1 : i+1+l])
d.ServerPassword = string(buf[i+1 : i+1+l])
i += l
}
{
@ -648,6 +716,46 @@ func (d *Server) Unmarshal(buf []byte) (uint64, error) {
d.Realname = string(buf[i+1 : i+1+l])
i += l
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+1] & 0x7F)
for buf[i+1]&0x80 == 0x80 {
i++
t |= uint64(buf[i+1]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Account = string(buf[i+1 : i+1+l])
i += l
}
{
l := uint64(0)
{
bs := uint8(7)
t := uint64(buf[i+1] & 0x7F)
for buf[i+1]&0x80 == 0x80 {
i++
t |= uint64(buf[i+1]&0x7F) << bs
bs += 7
}
i++
l = t
}
d.Password = string(buf[i+1 : i+1+l])
i += l
}
return i + 1, nil
}

View File

@ -136,10 +136,12 @@ type Server struct {
Host string
Port string
TLS bool
Password string
ServerPassword string
Nick string
Username string
Realname string
Account string
Password string
}
func (u *User) GetServer(address string) (*Server, error) {