2020-03-13 17:13:03 +00:00
|
|
|
package soju
|
2020-02-04 09:46:22 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2020-02-07 10:36:42 +00:00
|
|
|
"log"
|
2021-03-18 11:08:25 +00:00
|
|
|
"mime"
|
2020-02-04 09:46:22 +00:00
|
|
|
"net"
|
2020-04-23 20:25:43 +00:00
|
|
|
"net/http"
|
2021-02-09 16:34:46 +00:00
|
|
|
"strings"
|
2020-02-06 20:30:44 +00:00
|
|
|
"sync"
|
2020-04-23 20:25:43 +00:00
|
|
|
"sync/atomic"
|
2020-02-18 16:26:17 +00:00
|
|
|
"time"
|
2020-02-04 09:46:22 +00:00
|
|
|
|
|
|
|
"gopkg.in/irc.v3"
|
2020-04-23 20:25:43 +00:00
|
|
|
"nhooyr.io/websocket"
|
2020-07-22 15:03:01 +00:00
|
|
|
|
|
|
|
"git.sr.ht/~emersion/soju/config"
|
2020-02-04 09:46:22 +00:00
|
|
|
)
|
|
|
|
|
2020-02-18 16:26:17 +00:00
|
|
|
// TODO: make configurable
|
2020-08-19 17:28:29 +00:00
|
|
|
var retryConnectDelay = time.Minute
|
2020-04-01 14:41:17 +00:00
|
|
|
var connectTimeout = 15 * time.Second
|
2020-04-01 14:25:03 +00:00
|
|
|
var writeTimeout = 10 * time.Second
|
2020-08-19 17:28:29 +00:00
|
|
|
var upstreamMessageDelay = 2 * time.Second
|
|
|
|
var upstreamMessageBurst = 10
|
2020-02-18 16:26:17 +00:00
|
|
|
|
2020-02-06 14:50:46 +00:00
|
|
|
type Logger interface {
|
|
|
|
Print(v ...interface{})
|
|
|
|
Printf(format string, v ...interface{})
|
|
|
|
}
|
|
|
|
|
2020-02-06 19:25:37 +00:00
|
|
|
type prefixLogger struct {
|
|
|
|
logger Logger
|
|
|
|
prefix string
|
|
|
|
}
|
|
|
|
|
|
|
|
var _ Logger = (*prefixLogger)(nil)
|
|
|
|
|
|
|
|
func (l *prefixLogger) Print(v ...interface{}) {
|
|
|
|
v = append([]interface{}{l.prefix}, v...)
|
|
|
|
l.logger.Print(v...)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *prefixLogger) Printf(format string, v ...interface{}) {
|
|
|
|
v = append([]interface{}{l.prefix}, v...)
|
|
|
|
l.logger.Printf("%v"+format, v...)
|
|
|
|
}
|
|
|
|
|
2020-02-06 15:03:07 +00:00
|
|
|
type Server struct {
|
2020-07-22 15:03:01 +00:00
|
|
|
Hostname string
|
|
|
|
Logger Logger
|
|
|
|
HistoryLimit int
|
|
|
|
LogPath string
|
|
|
|
Debug bool
|
|
|
|
HTTPOrigins []string
|
|
|
|
AcceptProxyIPs config.IPSet
|
2020-08-11 08:36:14 +00:00
|
|
|
Identd *Identd // can be nil
|
2020-03-04 17:22:58 +00:00
|
|
|
|
2021-05-24 19:13:31 +00:00
|
|
|
db Database
|
2021-02-09 16:34:46 +00:00
|
|
|
stopWG sync.WaitGroup
|
2020-02-06 20:11:35 +00:00
|
|
|
|
2021-02-09 16:34:46 +00:00
|
|
|
lock sync.Mutex
|
|
|
|
listeners map[net.Listener]struct{}
|
|
|
|
users map[string]*user
|
2020-02-07 10:36:42 +00:00
|
|
|
}
|
|
|
|
|
2021-05-24 19:13:31 +00:00
|
|
|
func NewServer(db Database) *Server {
|
2020-02-07 10:36:42 +00:00
|
|
|
return &Server{
|
Add support for downstream CHATHISTORY
This adds support for the WIP (at the time of this commit)
draft/chathistory extension, based on the draft at [1] and the
additional comments at [2].
This gets the history by parsing the chat logs, and is therefore only
enabled when the logs are enabled and the log path is configured.
Getting the history only from the logs adds some restrictions:
- we cannot get history by msgid (those are not logged)
- we cannot get the users masks (maybe they could be inferred from the
JOIN etc, but it is not worth the effort and would not work every
time)
The regular soju network history is not sent to clients that support
draft/chathistory, so that they can fetch what they need by manually
calling CHATHISTORY.
The only supported command is BEFORE for now, because that is the only
required command for an app that offers an "infinite history scrollback"
feature.
Regarding implementation, rather than reading the file from the end in
reverse, we simply start from the beginning of each log file, store each
PRIVMSG into a ring, then add the last lines of that ring into the
history we'll return later. The message parsing implementation must be
kept somewhat fast because an app could potentially request thousands of
messages in several files. Here we are using simple sscanf and indexOf
rather than regexps.
In case some log files do not contain any message (for example because
the user had not joined a channel at that time), we try up to a 100 days
of empty log files before giving up.
[1]: https://github.com/prawnsalad/ircv3-specifications/pull/3/files
[2]: https://github.com/ircv3/ircv3-specifications/pull/393/files#r350210018
2020-05-21 22:59:57 +00:00
|
|
|
Logger: log.New(log.Writer(), "", log.LstdFlags),
|
|
|
|
HistoryLimit: 1000,
|
|
|
|
db: db,
|
2021-02-09 16:34:46 +00:00
|
|
|
listeners: make(map[net.Listener]struct{}),
|
|
|
|
users: make(map[string]*user),
|
2020-02-07 10:36:42 +00:00
|
|
|
}
|
2020-02-04 17:56:07 +00:00
|
|
|
}
|
2020-02-04 09:46:22 +00:00
|
|
|
|
2020-02-04 17:56:07 +00:00
|
|
|
func (s *Server) prefix() *irc.Prefix {
|
|
|
|
return &irc.Prefix{Name: s.Hostname}
|
|
|
|
}
|
2020-02-04 10:25:53 +00:00
|
|
|
|
2021-02-09 16:34:46 +00:00
|
|
|
func (s *Server) Start() error {
|
2020-03-04 17:22:58 +00:00
|
|
|
users, err := s.db.ListUsers()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2020-03-03 14:26:19 +00:00
|
|
|
}
|
2020-02-07 10:36:42 +00:00
|
|
|
|
|
|
|
s.lock.Lock()
|
2020-08-07 13:31:07 +00:00
|
|
|
for i := range users {
|
|
|
|
s.addUserLocked(&users[i])
|
2020-02-06 15:03:07 +00:00
|
|
|
}
|
2020-03-04 17:22:58 +00:00
|
|
|
s.lock.Unlock()
|
|
|
|
|
2021-02-09 16:34:46 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) Shutdown() {
|
|
|
|
s.lock.Lock()
|
|
|
|
for ln := range s.listeners {
|
|
|
|
if err := ln.Close(); err != nil {
|
|
|
|
s.Logger.Printf("failed to stop listener: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, u := range s.users {
|
|
|
|
u.events <- eventStop{}
|
|
|
|
}
|
|
|
|
s.lock.Unlock()
|
|
|
|
|
|
|
|
s.stopWG.Wait()
|
2020-02-06 15:03:07 +00:00
|
|
|
}
|
|
|
|
|
2020-06-06 23:30:27 +00:00
|
|
|
func (s *Server) createUser(user *User) (*user, error) {
|
|
|
|
s.lock.Lock()
|
|
|
|
defer s.lock.Unlock()
|
|
|
|
|
|
|
|
if _, ok := s.users[user.Username]; ok {
|
|
|
|
return nil, fmt.Errorf("user %q already exists", user.Username)
|
|
|
|
}
|
|
|
|
|
|
|
|
err := s.db.StoreUser(user)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("could not create user in db: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-08-07 13:31:07 +00:00
|
|
|
return s.addUserLocked(user), nil
|
2020-06-06 23:30:27 +00:00
|
|
|
}
|
|
|
|
|
2021-06-23 17:21:18 +00:00
|
|
|
func (s *Server) forEachUser(f func(*user)) {
|
|
|
|
s.lock.Lock()
|
|
|
|
for _, u := range s.users {
|
|
|
|
f(u)
|
|
|
|
}
|
|
|
|
s.lock.Unlock()
|
|
|
|
}
|
|
|
|
|
2020-02-07 10:39:56 +00:00
|
|
|
func (s *Server) getUser(name string) *user {
|
|
|
|
s.lock.Lock()
|
|
|
|
u := s.users[name]
|
|
|
|
s.lock.Unlock()
|
|
|
|
return u
|
|
|
|
}
|
|
|
|
|
2020-08-07 13:31:07 +00:00
|
|
|
func (s *Server) addUserLocked(user *User) *user {
|
|
|
|
s.Logger.Printf("starting bouncer for user %q", user.Username)
|
|
|
|
u := newUser(s, user)
|
|
|
|
s.users[u.Username] = u
|
|
|
|
|
2021-02-09 16:34:46 +00:00
|
|
|
s.stopWG.Add(1)
|
|
|
|
|
2020-08-07 13:31:07 +00:00
|
|
|
go func() {
|
|
|
|
u.run()
|
|
|
|
|
|
|
|
s.lock.Lock()
|
|
|
|
delete(s.users, u.Username)
|
|
|
|
s.lock.Unlock()
|
2021-02-09 16:34:46 +00:00
|
|
|
|
|
|
|
s.stopWG.Done()
|
2020-08-07 13:31:07 +00:00
|
|
|
}()
|
|
|
|
|
|
|
|
return u
|
|
|
|
}
|
|
|
|
|
2020-04-23 20:25:43 +00:00
|
|
|
var lastDownstreamID uint64 = 0
|
|
|
|
|
2020-07-01 15:02:37 +00:00
|
|
|
func (s *Server) handle(ic ircConn) {
|
2020-04-23 20:25:43 +00:00
|
|
|
id := atomic.AddUint64(&lastDownstreamID, 1)
|
2020-07-01 15:02:37 +00:00
|
|
|
dc := newDownstreamConn(s, ic, id)
|
2020-04-23 20:25:43 +00:00
|
|
|
if err := dc.runUntilRegistered(); err != nil {
|
|
|
|
dc.logger.Print(err)
|
|
|
|
} else {
|
|
|
|
dc.user.events <- eventDownstreamConnected{dc}
|
|
|
|
if err := dc.readMessages(dc.user.events); err != nil {
|
|
|
|
dc.logger.Print(err)
|
|
|
|
}
|
|
|
|
dc.user.events <- eventDownstreamDisconnected{dc}
|
|
|
|
}
|
|
|
|
dc.Close()
|
|
|
|
}
|
|
|
|
|
2020-02-04 10:25:53 +00:00
|
|
|
func (s *Server) Serve(ln net.Listener) error {
|
2021-02-09 16:34:46 +00:00
|
|
|
s.lock.Lock()
|
|
|
|
s.listeners[ln] = struct{}{}
|
|
|
|
s.lock.Unlock()
|
|
|
|
|
|
|
|
s.stopWG.Add(1)
|
|
|
|
|
|
|
|
defer func() {
|
|
|
|
s.lock.Lock()
|
|
|
|
delete(s.listeners, ln)
|
|
|
|
s.lock.Unlock()
|
|
|
|
|
|
|
|
s.stopWG.Done()
|
|
|
|
}()
|
|
|
|
|
2020-02-04 09:46:22 +00:00
|
|
|
for {
|
2020-04-23 20:25:43 +00:00
|
|
|
conn, err := ln.Accept()
|
2021-02-09 16:34:46 +00:00
|
|
|
// TODO: use net.ErrClosed when available
|
|
|
|
if err != nil && strings.Contains(err.Error(), "use of closed network connection") {
|
|
|
|
return nil
|
|
|
|
} else if err != nil {
|
2020-02-04 09:46:22 +00:00
|
|
|
return fmt.Errorf("failed to accept connection: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-07-01 15:02:37 +00:00
|
|
|
go s.handle(newNetIRCConn(conn))
|
2020-04-23 20:25:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|
|
|
conn, err := websocket.Accept(w, req, &websocket.AcceptOptions{
|
|
|
|
OriginPatterns: s.HTTPOrigins,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
s.Logger.Printf("failed to serve HTTP connection: %v", err)
|
|
|
|
return
|
2020-02-04 09:46:22 +00:00
|
|
|
}
|
2020-06-29 16:33:23 +00:00
|
|
|
|
2020-07-22 15:03:01 +00:00
|
|
|
isProxy := false
|
2020-06-29 16:33:23 +00:00
|
|
|
if host, _, err := net.SplitHostPort(req.RemoteAddr); err == nil {
|
|
|
|
if ip := net.ParseIP(host); ip != nil {
|
2020-07-22 15:03:01 +00:00
|
|
|
isProxy = s.AcceptProxyIPs.Contains(ip)
|
2020-06-29 16:33:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-18 12:22:28 +00:00
|
|
|
// Only trust the Forwarded header field if this is a trusted proxy IP
|
2020-06-29 16:33:23 +00:00
|
|
|
// to prevent users from spoofing the remote address
|
2020-06-29 16:27:43 +00:00
|
|
|
remoteAddr := req.RemoteAddr
|
2021-03-18 11:08:25 +00:00
|
|
|
if isProxy {
|
|
|
|
forwarded := parseForwarded(req.Header)
|
2021-03-18 12:21:38 +00:00
|
|
|
if forwarded["for"] != "" {
|
|
|
|
remoteAddr = forwarded["for"]
|
2021-03-18 11:08:25 +00:00
|
|
|
}
|
2020-06-29 16:27:43 +00:00
|
|
|
}
|
2020-06-29 16:33:23 +00:00
|
|
|
|
2020-07-01 15:02:37 +00:00
|
|
|
s.handle(newWebsocketIRCConn(conn, remoteAddr))
|
2020-02-04 09:46:22 +00:00
|
|
|
}
|
2021-03-18 11:08:25 +00:00
|
|
|
|
|
|
|
func parseForwarded(h http.Header) map[string]string {
|
|
|
|
forwarded := h.Get("Forwarded")
|
|
|
|
if forwarded == "" {
|
2021-03-18 12:22:28 +00:00
|
|
|
return map[string]string{
|
|
|
|
"for": h.Get("X-Forwarded-For"),
|
|
|
|
"proto": h.Get("X-Forwarded-Proto"),
|
|
|
|
"host": h.Get("X-Forwarded-Host"),
|
|
|
|
}
|
2021-03-18 11:08:25 +00:00
|
|
|
}
|
|
|
|
// Hack to easily parse header parameters
|
|
|
|
_, params, _ := mime.ParseMediaType("hack; " + forwarded)
|
|
|
|
return params
|
|
|
|
}
|