mirror of
https://github.com/ChronosX88/yans.git
synced 2024-11-08 22:51:00 +00:00
212 lines
4.5 KiB
Go
212 lines
4.5 KiB
Go
|
package server
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"context"
|
||
|
"fmt"
|
||
|
"github.com/ChronosX88/yans/internal"
|
||
|
"github.com/ChronosX88/yans/internal/config"
|
||
|
"github.com/ChronosX88/yans/internal/models"
|
||
|
"github.com/ChronosX88/yans/internal/protocol"
|
||
|
"github.com/jmoiron/sqlx"
|
||
|
_ "github.com/mattn/go-sqlite3"
|
||
|
"github.com/pressly/goose/v3"
|
||
|
"io"
|
||
|
"log"
|
||
|
"net"
|
||
|
"strings"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
type NNTPServer struct {
|
||
|
ctx context.Context
|
||
|
cancelFunc context.CancelFunc
|
||
|
|
||
|
ln net.Listener
|
||
|
port int
|
||
|
|
||
|
db *sqlx.DB
|
||
|
}
|
||
|
|
||
|
func NewNNTPServer(cfg config.Config) (*NNTPServer, error) {
|
||
|
db, err := sqlx.Open("sqlite3", cfg.DatabasePath)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
goose.SetBaseFS(internal.Migrations)
|
||
|
|
||
|
if err := goose.SetDialect("sqlite3"); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
if err := goose.Up(db.DB, "migrations"); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
ctx, cancel := context.WithCancel(context.Background())
|
||
|
ns := &NNTPServer{
|
||
|
ctx: ctx,
|
||
|
cancelFunc: cancel,
|
||
|
port: cfg.Port,
|
||
|
db: db,
|
||
|
}
|
||
|
return ns, nil
|
||
|
}
|
||
|
|
||
|
func (ns *NNTPServer) Start() error {
|
||
|
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", ns.port))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
go func(ctx context.Context) {
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
break
|
||
|
default:
|
||
|
{
|
||
|
conn, err := ln.Accept()
|
||
|
if err != nil {
|
||
|
log.Println(err)
|
||
|
}
|
||
|
log.Printf("Client %s has connected!", conn.RemoteAddr().String())
|
||
|
go ns.handleNewConnection(ctx, conn)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}(ns.ctx)
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (ns *NNTPServer) handleNewConnection(ctx context.Context, conn net.Conn) {
|
||
|
_, err := conn.Write([]byte(protocol.MessageNNTPServiceReadyPostingProhibited))
|
||
|
if err != nil {
|
||
|
log.Print(err)
|
||
|
conn.Close()
|
||
|
return
|
||
|
}
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
break
|
||
|
default:
|
||
|
{
|
||
|
message, err := bufio.NewReader(conn).ReadString('\n')
|
||
|
if err != nil {
|
||
|
if err == io.EOF || err.(*net.OpError).Unwrap() == net.ErrClosed {
|
||
|
log.Printf("Client %s has diconnected!", conn.RemoteAddr().String())
|
||
|
} else {
|
||
|
log.Print(err)
|
||
|
conn.Close()
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
log.Printf("Received message from %s: %s", conn.RemoteAddr().String(), string(message))
|
||
|
err = ns.handleMessage(conn, message)
|
||
|
if err != nil {
|
||
|
log.Print(err)
|
||
|
conn.Close()
|
||
|
return
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (ns *NNTPServer) handleMessage(conn net.Conn, msg string) error {
|
||
|
msg = strings.TrimSuffix(msg, "\r\n")
|
||
|
splittedMessage := strings.Split(msg, " ")
|
||
|
command := splittedMessage[0]
|
||
|
|
||
|
reply := ""
|
||
|
quit := false
|
||
|
|
||
|
switch command {
|
||
|
case protocol.CommandCapabilities:
|
||
|
{
|
||
|
reply = "101 Capability list:\r\nVERSION 2\r\nIMPLEMENTATION\r\n."
|
||
|
break
|
||
|
}
|
||
|
case protocol.CommandDate:
|
||
|
{
|
||
|
reply = fmt.Sprintf("111 %s", time.Now().UTC().Format("20060102150405"))
|
||
|
break
|
||
|
}
|
||
|
case protocol.CommandQuit:
|
||
|
{
|
||
|
reply = protocol.MessageNNTPServiceExitsNormally
|
||
|
quit = true
|
||
|
break
|
||
|
}
|
||
|
case protocol.CommandMode:
|
||
|
{
|
||
|
if splittedMessage[1] == "READER" {
|
||
|
// TODO actually switch current conn to reader mode
|
||
|
reply = protocol.MessageReaderModePostingProhibited
|
||
|
} else {
|
||
|
reply = protocol.MessageUnknownCommand
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
case protocol.CommandList:
|
||
|
{
|
||
|
groups, err := ns.listGroups()
|
||
|
if err != nil {
|
||
|
reply = protocol.MessageErrorHappened + err.Error()
|
||
|
log.Println(err)
|
||
|
}
|
||
|
sb := strings.Builder{}
|
||
|
sb.Write([]byte("215 list of newsgroups follows\n"))
|
||
|
if len(splittedMessage) == 1 || splittedMessage[1] == "ACTIVE" {
|
||
|
for _, v := range groups {
|
||
|
// TODO set high/low mark and posting status to actual values
|
||
|
sb.Write([]byte(fmt.Sprintf("%s 0 0 n\r\n", v.GroupName)))
|
||
|
}
|
||
|
} else if splittedMessage[1] == "NEWSGROUPS" {
|
||
|
for _, v := range groups {
|
||
|
desc := ""
|
||
|
if v.Description == nil {
|
||
|
desc = "No description"
|
||
|
} else {
|
||
|
desc = *v.Description
|
||
|
}
|
||
|
sb.Write([]byte(fmt.Sprintf("%s %s\r\n", v.GroupName, desc)))
|
||
|
}
|
||
|
} else {
|
||
|
reply = protocol.MessageUnknownCommand
|
||
|
break
|
||
|
}
|
||
|
|
||
|
sb.Write([]byte("."))
|
||
|
reply = sb.String()
|
||
|
}
|
||
|
default:
|
||
|
{
|
||
|
reply = protocol.MessageUnknownCommand
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
_, err := conn.Write([]byte(reply + "\r\n"))
|
||
|
if quit {
|
||
|
conn.Close()
|
||
|
}
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func (ns *NNTPServer) listGroups() ([]models.Group, error) {
|
||
|
var groups []models.Group
|
||
|
return groups, ns.db.Select(&groups, "SELECT * FROM groups")
|
||
|
}
|
||
|
|
||
|
func (ns *NNTPServer) getArticlesCount(g models.Group) (int, error) {
|
||
|
var count int
|
||
|
return count, ns.db.Select(&count, "SELECT COUNT(*) FROM articles_to_groups WHERE group_id = ?", g.ID)
|
||
|
}
|
||
|
|
||
|
func (ns *NNTPServer) Stop() {
|
||
|
ns.cancelFunc()
|
||
|
}
|