123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- // Copyright (c) 2020 Shivaram Lingamneni
- // released under the MIT license
-
- package irc
-
- import (
- "bytes"
- "errors"
- "io"
- "net"
- "unicode/utf8"
-
- "github.com/gorilla/websocket"
- "github.com/goshuirc/irc-go/ircmsg"
-
- "github.com/oragono/oragono/irc/utils"
- )
-
- const (
- maxReadQBytes = ircmsg.MaxlenTagsFromClient + MaxLineLen + 1024
- initialBufferSize = 1024
- )
-
- var (
- crlf = []byte{'\r', '\n'}
- errReadQ = errors.New("ReadQ Exceeded")
- errWSBinaryMessage = errors.New("WebSocket binary messages are unsupported")
- )
-
- // IRCConn abstracts away the distinction between a regular
- // net.Conn (which includes both raw TCP and TLS) and a websocket.
- // it doesn't expose the net.Conn, io.Reader, or io.Writer interfaces
- // because websockets are message-oriented, not stream-oriented, and
- // therefore this abstraction is message-oriented as well.
- type IRCConn interface {
- UnderlyingConn() *utils.WrappedConn
-
- // these take an IRC line or lines, correctly terminated with CRLF:
- WriteLine([]byte) error
- WriteLines([][]byte) error
- // this returns an IRC line, possibly terminated with CRLF, LF, or nothing:
- ReadLine() (line []byte, err error)
-
- Close() error
- }
-
- // IRCStreamConn is an IRCConn over a regular stream connection.
- type IRCStreamConn struct {
- conn *utils.WrappedConn
-
- buf []byte
- start int // start of valid (i.e., read but not yet consumed) data in the buffer
- end int // end of valid data in the buffer
- searchFrom int // start of valid data in the buffer not yet searched for \n
- eof bool
- }
-
- func NewIRCStreamConn(conn *utils.WrappedConn) *IRCStreamConn {
- return &IRCStreamConn{
- conn: conn,
- }
- }
-
- func (cc *IRCStreamConn) UnderlyingConn() *utils.WrappedConn {
- return cc.conn
- }
-
- func (cc *IRCStreamConn) WriteLine(buf []byte) (err error) {
- _, err = cc.conn.Write(buf)
- return
- }
-
- func (cc *IRCStreamConn) WriteLines(buffers [][]byte) (err error) {
- // on Linux, with a plaintext TCP or Unix domain socket,
- // the Go runtime will optimize this into a single writev(2) call:
- _, err = (*net.Buffers)(&buffers).WriteTo(cc.conn)
- return
- }
-
- func (cc *IRCStreamConn) ReadLine() ([]byte, error) {
- for {
- // try to find a terminated line in the buffered data already read
- nlidx := bytes.IndexByte(cc.buf[cc.searchFrom:cc.end], '\n')
- if nlidx != -1 {
- // got a complete line
- line := cc.buf[cc.start : cc.searchFrom+nlidx]
- cc.start = cc.searchFrom + nlidx + 1
- cc.searchFrom = cc.start
- if globalUtf8EnforcementSetting && !utf8.Valid(line) {
- return line, errInvalidUtf8
- } else {
- return line, nil
- }
- }
-
- if cc.start == 0 && len(cc.buf) == maxReadQBytes {
- return nil, errReadQ // out of space, can't expand or slide
- }
-
- if cc.eof {
- return nil, io.EOF
- }
-
- if len(cc.buf) < maxReadQBytes && (len(cc.buf)-(cc.end-cc.start) < initialBufferSize/2) {
- // allocate a new buffer, copy any remaining data
- newLen := utils.RoundUpToPowerOfTwo(len(cc.buf) + 1)
- if newLen > maxReadQBytes {
- newLen = maxReadQBytes
- } else if newLen < initialBufferSize {
- newLen = initialBufferSize
- }
- newBuf := make([]byte, newLen)
- copy(newBuf, cc.buf[cc.start:cc.end])
- cc.buf = newBuf
- } else if cc.start != 0 {
- // slide remaining data back to the front of the buffer
- copy(cc.buf, cc.buf[cc.start:cc.end])
- }
- cc.end = cc.end - cc.start
- cc.start = 0
-
- cc.searchFrom = cc.end
- n, err := cc.conn.Read(cc.buf[cc.end:])
- cc.end += n
- if n != 0 && err == io.EOF {
- // we may have received new \n-terminated lines, try to parse them
- cc.eof = true
- } else if err != nil {
- return nil, err
- }
- }
- }
-
- func (cc *IRCStreamConn) Close() (err error) {
- return cc.conn.Close()
- }
-
- // IRCWSConn is an IRCConn over a websocket.
- type IRCWSConn struct {
- conn *websocket.Conn
- }
-
- func NewIRCWSConn(conn *websocket.Conn) IRCWSConn {
- return IRCWSConn{conn: conn}
- }
-
- func (wc IRCWSConn) UnderlyingConn() *utils.WrappedConn {
- // just assume that the type is OK
- wConn, _ := wc.conn.UnderlyingConn().(*utils.WrappedConn)
- return wConn
- }
-
- func (wc IRCWSConn) WriteLine(buf []byte) (err error) {
- buf = bytes.TrimSuffix(buf, crlf)
- // #1483: if we have websockets at all, then we're enforcing utf8
- return wc.conn.WriteMessage(websocket.TextMessage, buf)
- }
-
- func (wc IRCWSConn) WriteLines(buffers [][]byte) (err error) {
- for _, buf := range buffers {
- err = wc.WriteLine(buf)
- if err != nil {
- return
- }
- }
- return
- }
-
- func (wc IRCWSConn) ReadLine() (line []byte, err error) {
- messageType, line, err := wc.conn.ReadMessage()
- if err == nil {
- if messageType == websocket.TextMessage {
- return line, nil
- } else {
- return nil, errWSBinaryMessage
- }
- } else if err == websocket.ErrReadLimit {
- return line, errReadQ
- } else {
- return line, err
- }
- }
-
- func (wc IRCWSConn) Close() (err error) {
- return wc.conn.Close()
- }
|