yans/internal/server/handler.go

529 lines
14 KiB
Go
Raw Normal View History

package server
import (
2022-02-05 14:46:25 +00:00
"bufio"
2022-01-25 16:27:58 +00:00
"database/sql"
2022-02-03 16:44:08 +00:00
"encoding/json"
"fmt"
2022-01-19 19:51:08 +00:00
"github.com/ChronosX88/yans/internal/backend"
2022-01-20 21:29:58 +00:00
"github.com/ChronosX88/yans/internal/models"
"github.com/ChronosX88/yans/internal/protocol"
2022-02-03 17:39:08 +00:00
"github.com/ChronosX88/yans/internal/utils"
2022-02-03 16:44:08 +00:00
"github.com/google/uuid"
"io"
"net/mail"
2022-02-03 17:39:08 +00:00
"strconv"
"strings"
"time"
)
type Handler struct {
handlers map[string]func(s *Session, command string, arguments []string, id uint) error
2022-02-03 16:44:08 +00:00
backend backend.StorageBackend
serverDomain string
}
2022-02-03 16:44:08 +00:00
func NewHandler(b backend.StorageBackend, serverDomain string) *Handler {
h := &Handler{}
2022-01-19 19:51:08 +00:00
h.backend = b
h.handlers = map[string]func(s *Session, command string, arguments []string, id uint) error{
protocol.CommandCapabilities: h.handleCapabilities,
protocol.CommandDate: h.handleDate,
protocol.CommandQuit: h.handleQuit,
protocol.CommandList: h.handleList,
protocol.CommandMode: h.handleModeReader,
2022-01-25 16:27:58 +00:00
protocol.CommandGroup: h.handleGroup,
2022-01-25 21:29:30 +00:00
protocol.CommandNewGroups: h.handleNewGroups,
2022-02-03 16:44:08 +00:00
protocol.CommandPost: h.handlePost,
2022-02-03 17:39:08 +00:00
protocol.CommandListGroup: h.handleListgroup,
2022-02-05 10:51:50 +00:00
protocol.CommandArticle: h.handleArticle,
protocol.CommandHead: h.handleArticle,
protocol.CommandBody: h.handleArticle,
protocol.CommandStat: h.handleArticle,
}
2022-02-03 16:44:08 +00:00
h.serverDomain = serverDomain
return h
}
func (h *Handler) handleCapabilities(s *Session, command string, arguments []string, id uint) error {
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
return s.tconn.PrintfLine(s.capabilities.String())
}
func (h *Handler) handleDate(s *Session, command string, arguments []string, id uint) error {
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 111, Message: time.Now().UTC().Format("20060102150405")}.String())
}
func (h *Handler) handleQuit(s *Session, command string, arguments []string, id uint) error {
s.tconn.PrintfLine(protocol.NNTPResponse{Code: 205, Message: "NNTP Service exits normally, bye!"}.String())
s.conn.Close()
return nil
}
func (h *Handler) handleList(s *Session, command string, arguments []string, id uint) error {
sb := strings.Builder{}
listType := ""
if len(arguments) != 0 {
listType = arguments[0]
}
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
switch listType {
case "":
fallthrough
case "ACTIVE":
{
2022-01-20 21:29:58 +00:00
var groups []models.Group
var err error
if len(arguments) == 2 {
groups, err = h.backend.ListGroupsByPattern(arguments[1])
} else {
groups, err = h.backend.ListGroups()
}
if err != nil {
return err
}
sb.Write([]byte(protocol.NNTPResponse{Code: 215, Message: "list of newsgroups follows"}.String() + protocol.CRLF))
for _, v := range groups {
2022-01-20 21:29:58 +00:00
// TODO set actual post permission status
2022-02-03 17:39:08 +00:00
c, err := h.backend.GetArticlesCount(&v)
if err != nil {
return err
}
if c > 0 {
2022-02-03 17:39:08 +00:00
highWaterMark, err := h.backend.GetGroupHighWaterMark(&v)
if err != nil {
return err
}
2022-02-03 17:39:08 +00:00
lowWaterMark, err := h.backend.GetGroupLowWaterMark(&v)
if err != nil {
return err
}
sb.Write([]byte(fmt.Sprintf("%s %d %d n"+protocol.CRLF, v.GroupName, highWaterMark, lowWaterMark)))
} else {
2022-01-20 21:29:58 +00:00
sb.Write([]byte(fmt.Sprintf("%s 0 1 n"+protocol.CRLF, v.GroupName)))
}
}
}
case "NEWSGROUPS":
{
2022-01-20 21:29:58 +00:00
var groups []models.Group
var err error
if len(arguments) == 2 {
groups, err = h.backend.ListGroupsByPattern(arguments[1])
} else {
groups, err = h.backend.ListGroups()
}
if err != nil {
return err
}
2022-01-20 21:29:58 +00:00
sb.Write([]byte(protocol.NNTPResponse{Code: 215, Message: "list of newsgroups follows"}.String() + protocol.CRLF))
for _, v := range groups {
desc := ""
if v.Description == nil {
desc = "No description"
} else {
desc = *v.Description
}
sb.Write([]byte(fmt.Sprintf("%s %s"+protocol.CRLF, v.GroupName, desc)))
}
}
default:
{
2022-02-03 16:44:08 +00:00
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
}
}
sb.Write([]byte(protocol.MultilineEnding))
return s.tconn.PrintfLine(sb.String())
}
func (h *Handler) handleModeReader(s *Session, command string, arguments []string, id uint) error {
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
if len(arguments) == 0 || arguments[0] != "READER" {
2022-02-03 16:44:08 +00:00
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
}
(&s.capabilities).Remove(protocol.ModeReaderCapability)
(&s.capabilities).Remove(protocol.ListCapability)
(&s.capabilities).Add(protocol.Capability{Type: protocol.ReaderCapability})
(&s.capabilities).Add(protocol.Capability{Type: protocol.ListCapability, Params: "ACTIVE NEWSGROUPS"})
s.mode = SessionModeReader
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 201, Message: "Reader mode, posting prohibited"}.String()) // TODO vary on auth status
}
func (h *Handler) handleGroup(s *Session, command string, arguments []string, id uint) error {
2022-01-25 16:27:58 +00:00
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
if len(arguments) == 0 || len(arguments) > 1 {
2022-02-03 16:44:08 +00:00
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
2022-01-25 16:27:58 +00:00
}
g, err := h.backend.GetGroup(arguments[0])
if err != nil {
if err == sql.ErrNoRows {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 411, Message: "No such newsgroup"}.String())
2022-01-25 21:29:30 +00:00
} else {
return err
2022-01-25 16:27:58 +00:00
}
}
2022-02-03 17:39:08 +00:00
highWaterMark, err := h.backend.GetGroupHighWaterMark(&g)
2022-01-25 21:29:30 +00:00
if err != nil && err != sql.ErrNoRows {
2022-01-25 16:27:58 +00:00
return err
}
2022-02-03 17:39:08 +00:00
lowWaterMark, err := h.backend.GetGroupLowWaterMark(&g)
2022-01-25 21:29:30 +00:00
if err != nil && err != sql.ErrNoRows {
2022-01-25 16:27:58 +00:00
return err
}
2022-02-03 17:39:08 +00:00
articlesCount, err := h.backend.GetArticlesCount(&g)
2022-01-25 21:29:30 +00:00
if err != nil && err != sql.ErrNoRows {
2022-01-25 16:27:58 +00:00
return err
}
s.currentGroup = &g
2022-02-03 16:44:08 +00:00
return s.tconn.PrintfLine(protocol.NNTPResponse{
Code: 211,
Message: fmt.Sprintf("%d %d %d %s", articlesCount, lowWaterMark, highWaterMark, g.GroupName),
}.String())
2022-01-25 16:27:58 +00:00
}
func (h *Handler) handleNewGroups(s *Session, command string, arguments []string, id uint) error {
2022-01-25 21:29:30 +00:00
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
if len(arguments) < 2 || len(arguments) > 3 {
2022-02-03 16:44:08 +00:00
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
2022-01-25 21:29:30 +00:00
}
dateString := arguments[0] + " " + arguments[1]
//isGMT := false
//if len(arguments) == 3 {
// isGMT = true
//}
var date time.Time
var err error
if len(dateString) == 15 {
date, err = time.Parse("20060102 150405", dateString)
if err != nil {
return err
}
} else if len(dateString) == 13 {
date, err = time.Parse("060102 150405", dateString)
if err != nil {
return err
}
} else {
2022-02-03 16:44:08 +00:00
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
2022-01-25 21:29:30 +00:00
}
g, err := h.backend.GetNewGroupsSince(date.Unix())
if err != nil {
return err
}
2022-02-03 16:44:08 +00:00
dw := s.tconn.DotWriter()
dw.Write([]byte(protocol.NNTPResponse{Code: 231, Message: "list of new newsgroups follows"}.String() + protocol.CRLF))
2022-01-25 21:29:30 +00:00
for _, v := range g {
// TODO set actual post permission status
2022-02-03 17:39:08 +00:00
c, err := h.backend.GetArticlesCount(&v)
2022-01-25 21:29:30 +00:00
if err != nil {
return err
}
if c > 0 {
2022-02-03 17:39:08 +00:00
highWaterMark, err := h.backend.GetGroupHighWaterMark(&v)
2022-01-25 21:29:30 +00:00
if err != nil {
return err
}
2022-02-03 17:39:08 +00:00
lowWaterMark, err := h.backend.GetGroupLowWaterMark(&v)
2022-01-25 21:29:30 +00:00
if err != nil {
return err
}
2022-02-03 16:44:08 +00:00
dw.Write([]byte(fmt.Sprintf("%s %d %d n"+protocol.CRLF, v.GroupName, highWaterMark, lowWaterMark)))
2022-01-25 21:29:30 +00:00
} else {
2022-02-03 16:44:08 +00:00
dw.Write([]byte(fmt.Sprintf("%s 0 1 n"+protocol.CRLF, v.GroupName)))
2022-01-25 21:29:30 +00:00
}
}
2022-02-03 16:44:08 +00:00
return dw.Close()
}
func (h *Handler) handlePost(s *Session, command string, arguments []string, id uint) error {
2022-02-03 16:44:08 +00:00
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
if len(arguments) != 0 {
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
}
if err := s.tconn.PrintfLine(protocol.NNTPResponse{Code: 340, Message: "Input article; end with <CR-LF>.<CR-LF>"}.String()); err != nil {
2022-02-03 16:44:08 +00:00
return err
}
headers, err := s.tconn.ReadMIMEHeader()
if err != nil {
return err
}
// generate message id
messageID := fmt.Sprintf("<%s@%s>", uuid.New().String(), h.serverDomain)
2022-02-05 10:51:50 +00:00
headers.Set("Message-ID", messageID)
2022-02-03 16:44:08 +00:00
headerJson, err := json.Marshal(headers)
if err != nil {
return err
}
a := models.Article{}
a.HeaderRaw = string(headerJson)
a.Header = headers
dr := s.tconn.DotReader()
// TODO handle multipart message
body, err := io.ReadAll(dr)
if err != nil {
return err
}
a.Body = string(body)
// set thread property
if headers.Get("In-Reply-To") != "" {
parentMessage, err := h.backend.GetArticle(headers.Get("In-Reply-To"))
if err != nil {
if err == sql.ErrNoRows {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: "no such message you are replying to"}.String())
} else {
return err
}
}
if !parentMessage.Thread.Valid {
var parentHeader mail.Header
err = json.Unmarshal([]byte(parentMessage.HeaderRaw), &parentHeader)
parentMessageID := parentHeader["Message-ID"]
a.Thread = sql.NullString{String: parentMessageID[0], Valid: true}
} else {
a.Thread = parentMessage.Thread
}
}
err = h.backend.SaveArticle(a, strings.Split(a.Header.Get("Newsgroups"), ","))
if err != nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 441, Message: err.Error()}.String())
}
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 240, Message: "Article received OK"}.String())
2022-01-25 21:29:30 +00:00
}
func (h *Handler) handleListgroup(s *Session, command string, arguments []string, id uint) error {
2022-02-03 17:39:08 +00:00
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
currentGroup := s.currentGroup
var low, high int64
if len(arguments) == 1 {
g, err := h.backend.GetGroup(arguments[0])
if err != nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 411, Message: "No such newsgroup"}.String())
}
currentGroup = &g
} else if len(arguments) == 2 {
g, err := h.backend.GetGroup(arguments[0])
if err != nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 411, Message: "No such newsgroup"}.String())
}
currentGroup = &g
low, high, err = utils.ParseRange(arguments[1])
if err != nil {
low = 0
high = 0
}
if high != -1 && low > high {
low = -1
high = -1
}
}
if currentGroup == nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 412, Message: "No newsgroup selected"}.String())
}
highWaterMark, err := h.backend.GetGroupHighWaterMark(currentGroup)
if err != nil && err != sql.ErrNoRows {
return err
}
lowWaterMark, err := h.backend.GetGroupLowWaterMark(currentGroup)
if err != nil && err != sql.ErrNoRows {
return err
}
articlesCount, err := h.backend.GetArticlesCount(currentGroup)
if err != nil && err != sql.ErrNoRows {
return err
}
nums, err := h.backend.GetArticleNumbers(currentGroup, low, high)
if err != nil && err != sql.ErrNoRows {
return err
}
dw := s.tconn.DotWriter()
dw.Write([]byte(protocol.NNTPResponse{Code: 211, Message: fmt.Sprintf("%d %d %d %s list follows%s", articlesCount, lowWaterMark, highWaterMark, currentGroup.GroupName, protocol.CRLF)}.String()))
for _, v := range nums {
dw.Write([]byte(strconv.FormatInt(v, 10) + protocol.CRLF))
}
return dw.Close()
}
func (h *Handler) handleArticle(s *Session, command string, arguments []string, id uint) error {
2022-02-05 10:51:50 +00:00
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
if len(arguments) == 0 && s.currentArticle == nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 420, Message: "No current article selected"}.String())
}
if len(arguments) > 1 {
return s.tconn.PrintfLine(protocol.ErrSyntaxError.String())
}
getByArticleNum := true
num, err := strconv.Atoi(arguments[0])
if err != nil {
getByArticleNum = false
}
if getByArticleNum && s.currentGroup == nil {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 412, Message: "No newsgroup selected"}.String())
}
var a models.Article
if getByArticleNum {
a, err = h.backend.GetArticleByNumber(s.currentGroup, num)
if err != nil {
if err == sql.ErrNoRows {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 423, Message: "No article with that number"}.String())
} else {
return err
}
}
} else {
a, err = h.backend.GetArticle(arguments[0])
if err != nil {
if err == sql.ErrNoRows {
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 430, Message: "No Such Article Found"}.String())
} else {
return err
}
}
}
s.currentArticle = &a
switch command {
case protocol.CommandArticle:
{
m := utils.NewMessage()
for k, v := range a.Header {
m.SetHeader(k, v...)
}
2022-02-05 14:40:38 +00:00
m.SetBody("text/plain", a.Body) // FIXME currently only plain text is supported
dw := s.tconn.DotWriter()
_, err = dw.Write([]byte(protocol.NNTPResponse{Code: 220, Message: fmt.Sprintf("%d %s", num, a.Header.Get("Message-ID"))}.String() + protocol.CRLF))
if err != nil {
2022-02-05 14:40:38 +00:00
return err
}
_, err = m.WriteTo(dw)
if err != nil {
2022-02-05 14:40:38 +00:00
return err
}
2022-02-05 14:46:25 +00:00
return dw.Close()
}
case protocol.CommandHead:
{
m := utils.NewMessage()
for k, v := range a.Header {
m.SetHeader(k, v...)
}
2022-02-05 14:46:25 +00:00
dw := s.tconn.DotWriter()
_, err = dw.Write([]byte(protocol.NNTPResponse{Code: 221, Message: fmt.Sprintf("%d %s", num, a.Header.Get("Message-ID"))}.String() + protocol.CRLF))
if err != nil {
2022-02-05 14:46:25 +00:00
return err
}
_, err = m.WriteTo(dw)
if err != nil {
2022-02-05 14:46:25 +00:00
return err
}
return dw.Close()
2022-02-05 14:46:25 +00:00
}
case protocol.CommandBody:
{
m := utils.NewMessage()
for k, v := range a.Header {
m.SetHeader(k, v...)
}
2022-02-05 14:46:25 +00:00
m.SetBody("text/plain", a.Body) // FIXME currently only plain text is supported
dw := s.tconn.DotWriter()
2022-02-05 14:46:25 +00:00
_, err = dw.Write([]byte(protocol.NNTPResponse{Code: 222, Message: fmt.Sprintf("%d %s", num, a.Header.Get("Message-ID"))}.String() + protocol.CRLF))
if err != nil {
return err
}
2022-02-05 14:46:25 +00:00
w := bufio.NewWriter(dw)
_, err = w.Write([]byte(a.Body))
if err != nil {
return err
}
2022-02-05 14:46:25 +00:00
err = w.Flush()
if err != nil {
return err
}
return dw.Close()
}
case protocol.CommandStat:
{
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 223, Message: fmt.Sprintf("%d %s", num, a.Header.Get("Message-ID"))}.String())
}
2022-02-05 14:46:25 +00:00
}
return nil
2022-02-05 14:46:25 +00:00
}
func (h *Handler) Handle(s *Session, message string, id uint) error {
splittedMessage := strings.Split(message, " ")
for i, v := range splittedMessage {
splittedMessage[i] = strings.TrimSpace(v)
}
cmdName := splittedMessage[0]
handler, ok := h.handlers[cmdName]
if !ok {
s.tconn.StartResponse(id)
defer s.tconn.EndResponse(id)
2022-02-03 17:39:08 +00:00
return s.tconn.PrintfLine(protocol.NNTPResponse{Code: 500, Message: "Unknown command"}.String())
}
return handler(s, cmdName, splittedMessage[1:], id)
}