aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2024-12-07 21:13:28 +0800
committerRunxi Yu <me@runxiyu.org>2024-12-07 21:13:28 +0800
commit8c18330aa3976aa981bbe4f135960a92b5923fb6 (patch)
tree6b1548fce387c3d86f110732f971d85f64990c9a
downloadmeseircd-8c18330aa3976aa981bbe4f135960a92b5923fb6.tar.gz
meseircd-8c18330aa3976aa981bbe4f135960a92b5923fb6.tar.zst
meseircd-8c18330aa3976aa981bbe4f135960a92b5923fb6.zip
Initial commit
-rw-r--r--.gitignore1
-rw-r--r--LICENSE.CC0124
-rw-r--r--LICENSE.MIT16
-rw-r--r--README.md3
-rw-r--r--const.go7
-rw-r--r--errors.go13
-rw-r--r--go.mod3
-rw-r--r--main.go53
-rw-r--r--msg.go125
-rw-r--r--tags.go119
-rw-r--r--util.go8
11 files changed, 472 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc08677
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/meseircd
diff --git a/LICENSE.CC0 b/LICENSE.CC0
new file mode 100644
index 0000000..7c5febc
--- /dev/null
+++ b/LICENSE.CC0
@@ -0,0 +1,124 @@
+This license applies to code written for the MeseIRCd project.
+
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
+
diff --git a/LICENSE.MIT b/LICENSE.MIT
new file mode 100644
index 0000000..6c8954c
--- /dev/null
+++ b/LICENSE.MIT
@@ -0,0 +1,16 @@
+This license applies to code copied from Ergo IRCd.
+
+Copyright (c) 2016-2021 Daniel Oaks
+Copyright (c) 2018-2021 Shivaram Lingamneni
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..551a422
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# MeseIRCd
+
+A primitive internet relay chat daemon.
diff --git a/const.go b/const.go
new file mode 100644
index 0000000..962db26
--- /dev/null
+++ b/const.go
@@ -0,0 +1,7 @@
+package main
+
+const (
+ MaxlenTags = 8191
+ MaxlenTagData = MaxlenTags - 2
+ MaxlenBody = 510
+)
diff --git a/errors.go b/errors.go
new file mode 100644
index 0000000..2d4ed02
--- /dev/null
+++ b/errors.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "errors"
+)
+
+var (
+ ErrEmptyMessage = errors.New("empty message")
+ ErrIllegalByte = errors.New("illegal byte")
+ ErrTagsTooLong = errors.New("tags too long")
+ ErrInvalidTagContent = errors.New("invalid tag content")
+ ErrBodyTooLong = errors.New("body too long")
+)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..5447113
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module git.sr.ht/~runxiyu/meseircd
+
+go 1.23.3
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..0898bd5
--- /dev/null
+++ b/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "bufio"
+ "log"
+ "log/slog"
+ "net"
+)
+
+type Client struct {
+ conn net.Conn
+}
+
+func main() {
+ listener, err := net.Listen("tcp", ":6667")
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer listener.Close()
+
+ for {
+ conn, err := listener.Accept()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ client := &Client{
+ conn: conn,
+ }
+ go func() {
+ defer func() {
+ raised := recover()
+ if raised != nil {
+ slog.Error("connection routine panicked", "raised", raised)
+ }
+ }()
+ client.handleConnection()
+ }()
+ }
+}
+
+func (client *Client) handleConnection() {
+ reader := bufio.NewReader(client.conn)
+ for {
+ line, err := reader.ReadString('\n')
+ if err != nil {
+ slog.Error("error while reading from connection", "error", err)
+ client.conn.Close()
+ return
+ }
+ _, _ = parseIRCMsg(line)
+ }
+}
diff --git a/msg.go b/msg.go
new file mode 100644
index 0000000..ff4b3a4
--- /dev/null
+++ b/msg.go
@@ -0,0 +1,125 @@
+package main
+
+import (
+ "strings"
+)
+
+type Msg struct {
+ RawMessage string
+ RawSource string
+ Command string
+ Tags map[string]string
+ Params []string
+}
+
+// Partially adapted from https://github.com/ergochat/irc-go.git
+func parseIRCMsg(line string) (msg Msg, err error) {
+ msg = Msg{
+ RawMessage: line,
+ }
+
+ line = strings.TrimSuffix(line, "\n")
+ line = strings.TrimSuffix(line, "\r")
+
+ if len(line) == 0 {
+ err = ErrEmptyMessage
+ return
+ }
+
+ for _, v := range line {
+ if v == '\x00' || v == '\r' || v == '\n' {
+ err = ErrIllegalByte
+ return
+ }
+ }
+
+ // IRCv3 tags
+ if line[0] == '@' {
+ tagEnd := strings.IndexByte(line, ' ')
+ if tagEnd == -1 {
+ err = ErrEmptyMessage
+ return
+ }
+ tagsString := line[1:tagEnd]
+ if 0 < MaxlenTagData && MaxlenTagData < len(tagsString) {
+ err = ErrTagsTooLong
+ return
+ }
+ msg.Tags, err = parseTags(tagsString)
+ if err != nil {
+ return
+ }
+ // Skip over the tags and the separating space
+ line = line[tagEnd+1:]
+ }
+
+ if len(line) > MaxlenBody {
+ err = ErrBodyTooLong
+ line = line[:MaxlenBody]
+ }
+
+ line = trimInitialSpaces(line)
+
+ // Source
+ if 0 < len(line) && line[0] == ':' {
+ sourceEnd := strings.IndexByte(line, ' ')
+ if sourceEnd == -1 {
+ err = ErrEmptyMessage
+ return
+ }
+ msg.RawSource = line[1:sourceEnd]
+ // Skip over the source and the separating space
+ line = line[sourceEnd+1:]
+ }
+
+ // Command
+ commandEnd := strings.IndexByte(line, ' ')
+ paramStart := commandEnd + 1
+ if commandEnd == -1 {
+ commandEnd = len(line)
+ paramStart = len(line)
+ }
+ baseCommand := line[:commandEnd]
+ if len(baseCommand) == 0 {
+ err = ErrEmptyMessage
+ return
+ }
+ // TODO: Actually must be either letters or a 3-digit numeric
+ if !isASCII(baseCommand) {
+ err = ErrIllegalByte
+ return
+ }
+ msg.Command = strings.ToUpper(baseCommand)
+ line = line[paramStart:]
+
+ // Other arguments
+ for {
+ line = trimInitialSpaces(line)
+ if len(line) == 0 {
+ break
+ }
+ // Trailing
+ if line[0] == ':' {
+ msg.Params = append(msg.Params, line[1:])
+ break
+ }
+ paramEnd := strings.IndexByte(line, ' ')
+ if paramEnd == -1 {
+ msg.Params = append(msg.Params, line)
+ break
+ }
+ msg.Params = append(msg.Params, line[:paramEnd])
+ line = line[paramEnd+1:]
+ }
+
+ return
+}
+
+func isASCII(str string) bool {
+ for i := 0; i < len(str); i++ {
+ if str[i] > 127 {
+ return false
+ }
+ }
+ return true
+}
diff --git a/tags.go b/tags.go
new file mode 100644
index 0000000..43a2833
--- /dev/null
+++ b/tags.go
@@ -0,0 +1,119 @@
+// Almost everything in this file is adapted from Ergo IRCd
+// This is probably considered a derived work for copyright purposes
+//
+// SPDX-License-Identifier: MIT
+
+package main
+
+import (
+ "strings"
+ "unicode/utf8"
+)
+
+func parseTags(tagsString string) (tags map[string]string, err error) {
+ tags = make(map[string]string)
+ for 0 < len(tagsString) {
+ tagEnd := strings.IndexByte(tagsString, ';')
+ endPos := tagEnd
+ nextPos := tagEnd + 1
+ if tagEnd == -1 {
+ endPos = len(tagsString)
+ nextPos = len(tagsString)
+ }
+ tagPair := tagsString[:endPos]
+ equalsIndex := strings.IndexByte(tagPair, '=')
+ var tagName, tagValue string
+ if equalsIndex == -1 {
+ // Tag with no value
+ tagName = tagPair
+ } else {
+ tagName, tagValue = tagPair[:equalsIndex], tagPair[equalsIndex+1:]
+ }
+ // "Implementations [...] MUST NOT perform any validation that would
+ // reject the message if an invalid tag key name is used."
+ if validateTagName(tagName) {
+ if !validateTagValue(tagValue) {
+ err = ErrInvalidTagContent
+ return
+ }
+ tags[tagName] = UnescapeTagValue(tagValue)
+ }
+ tagsString = tagsString[nextPos:]
+ }
+ return
+}
+
+func UnescapeTagValue(inString string) string {
+ // buf.Len() == 0 is the fastpath where we have not needed to unescape any chars
+ var buf strings.Builder
+ remainder := inString
+ for {
+ backslashPos := strings.IndexByte(remainder, '\\')
+
+ if backslashPos == -1 {
+ if buf.Len() == 0 {
+ return inString
+ } else {
+ buf.WriteString(remainder)
+ break
+ }
+ } else if backslashPos == len(remainder)-1 {
+ // trailing backslash, which we strip
+ if buf.Len() == 0 {
+ return inString[:len(inString)-1]
+ } else {
+ buf.WriteString(remainder[:len(remainder)-1])
+ break
+ }
+ }
+
+ // Non-trailing backslash detected; we're now on the slowpath
+ // where we modify the string
+ if buf.Len() < len(inString) {
+ buf.Grow(len(inString))
+ }
+ buf.WriteString(remainder[:backslashPos])
+ buf.WriteByte(escapedCharLookupTable[remainder[backslashPos+1]])
+ remainder = remainder[backslashPos+2:]
+ }
+
+ return buf.String()
+}
+
+var escapedCharLookupTable [256]byte
+
+func init() {
+ for i := 0; i < 256; i += 1 {
+ escapedCharLookupTable[i] = byte(i)
+ }
+ escapedCharLookupTable[':'] = ';'
+ escapedCharLookupTable['s'] = ' '
+ escapedCharLookupTable['r'] = '\r'
+ escapedCharLookupTable['n'] = '\n'
+}
+
+// https://ircv3.net/specs/extensions/message-tags.html#rules-for-naming-message-tags
+func validateTagName(name string) bool {
+ if len(name) == 0 {
+ return false
+ }
+ if name[0] == '+' {
+ name = name[1:]
+ }
+ if len(name) == 0 {
+ return false
+ }
+ // Let's err on the side of leniency here; allow -./ (45-47) in any position
+ for i := 0; i < len(name); i++ {
+ c := name[i]
+ if !(('-' <= c && c <= '/') || ('0' <= c && c <= '9') || ('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) {
+ return false
+ }
+ }
+ return true
+}
+
+// "Tag values MUST be encoded as UTF8."
+func validateTagValue(value string) bool {
+ return utf8.ValidString(value)
+}
diff --git a/util.go b/util.go
new file mode 100644
index 0000000..0037330
--- /dev/null
+++ b/util.go
@@ -0,0 +1,8 @@
+package main
+
+func trimInitialSpaces(line string) string {
+ var i int
+ for i = 0; i < len(line) && line[i] == ' '; i++ {
+ }
+ return line[i:]
+}