Add unit test

Add the first unit test to check the basic functionality of the Gemini
server. Add also a simple client that is able to perform actions against
this server as required for the test.
This commit is contained in:
Felix Niederwanger 2022-02-20 18:27:41 +01:00
parent b448f7c8c4
commit 0dad762255
Signed by: phoenix
GPG key ID: 6E77A590E3F6D71C
4 changed files with 201 additions and 8 deletions

View file

@ -18,3 +18,4 @@ jobs:
go-version: '1.14'
- name: Compile binaries
run: make
run: make test

View file

@ -12,6 +12,9 @@ cert:
openssl genrsa -out orion.key 2048
openssl req -x509 -nodes -days 3650 -key orion.key -out orion.crt
test:
go test ./...
# Container recipies
docker:
docker build . -t feldspaten.org/orion

View file

@ -8,6 +8,7 @@ import (
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
)
@ -25,25 +26,42 @@ type GeminiServer struct {
server net.Listener
}
type GeminiRequest struct {
conn *tls.Conn
Status int
Meta string
}
func (srv *GeminiServer) Close() error {
return srv.server.Close()
}
func (srv *GeminiServer) Loop(handler GeminiHandler) error {
for {
conn, err := srv.server.Accept()
if err != nil {
if err == io.EOF {
return nil
} else {
fmt.Fprintf(os.Stderr, "accept error: %s\n", err)
continue
if err := srv.SingleLoop(handler); err != nil {
if err != nil {
if err != io.EOF {
fmt.Fprintf(os.Stderr, "accept error: %s\n", err)
return nil
}
return err
}
}
go handleConnection(conn, handler)
}
}
func (srv *GeminiServer) SingleLoop(handler GeminiHandler) error {
conn, err := srv.server.Accept()
if err != nil {
if err == io.EOF {
return nil
}
return err
}
go handleConnection(conn, handler)
return nil
}
func CreateGeminiServer(hostname string, bindAddr string, cert tls.Certificate) (GeminiServer, error) {
var srv GeminiServer
var err error
@ -110,3 +128,63 @@ func handleConnection(conn io.ReadWriteCloser, handler GeminiHandler) error {
return handler(cleanPath, conn)
}
func Gemini(remote string, path string) (GeminiRequest, error) {
req := GeminiRequest{conn: nil, Status: 0, Meta: ""}
// Accepts self-signing requests for now. A better solution would be to implement TOFU
config := tls.Config{InsecureSkipVerify: true}
conn, err := tls.Dial("tcp", remote, &config)
if err != nil {
return req, err
}
req.conn = conn
req.conn.Write([]byte(path))
return req, nil
}
func (req *GeminiRequest) Close() {
if req.conn != nil {
req.conn.Close()
}
}
func (req *GeminiRequest) readLine() (string, error) {
buf := make([]byte, 1025)
for i := 0; i < 1024; i++ {
if _, err := req.conn.Read(buf[i : i+1]); err != nil {
return "", err
}
if buf[i] == '\n' {
line := string(buf[:i])
return strings.TrimSpace(line), nil // TrimSpace necessary for the \r character
}
}
return "", fmt.Errorf("Response too long")
}
// Do performs the request and sets the internal Status and Meta fields
func (req *GeminiRequest) Do() error {
line, err := req.readLine()
if err != nil {
return err
}
// Get STATUS and META
i := strings.Index(line, " ")
if i < 0 {
return fmt.Errorf("Invalid response")
}
req.Status, err = strconv.Atoi(line[:i])
if err != nil {
return fmt.Errorf("Invalid response code")
}
if i < len(line)-1 {
req.Meta = line[i+1:]
}
return nil
}
// Read reads from the buffer
func (req *GeminiRequest) Read(buf []byte) (int, error) {
return req.conn.Read(buf)
}

111
cmd/orion/orion_test.go Normal file
View file

@ -0,0 +1,111 @@
package main
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io"
"math/big"
"strings"
"testing"
"time"
)
func generateCerts(size int) tls.Certificate {
key, err := rsa.GenerateKey(rand.Reader, size)
// Generate a pem block with the private key
keyPem := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
})
serial, err := rand.Int(rand.Reader, big.NewInt(65535))
if err != nil {
panic(err)
}
tml := x509.Certificate{
// you can add any attr that you need
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(5, 0, 0),
// you have to generate a different serial number each execution
SerialNumber: serial,
Subject: pkix.Name{
CommonName: "localhost",
Organization: []string{"Internet Widgets Corporation"},
},
BasicConstraintsValid: true,
}
cert, err := x509.CreateCertificate(rand.Reader, &tml, &tml, &key.PublicKey, key)
if err != nil {
panic(err)
}
// Generate a pem block with the certificate
certPem := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert,
})
tlsCert, err := tls.X509KeyPair(certPem, keyPem)
if err != nil {
panic(err)
}
return tlsCert
}
func TestServer(t *testing.T) {
t.Logf("Generating TLS certificate ... ")
cert := generateCerts(2048)
server, err := CreateGeminiServer("localhost", ":1965", cert)
if err != nil {
t.Errorf("Error creating gemini server: %s", err)
return
}
defer server.Close()
go func() {
if err := server.Loop(testHandler); err != nil {
t.Errorf("server loop error: %s\n", err)
}
}()
// Do server request
req, err := Gemini("localhost:1965", "/")
if err != nil {
t.Errorf("Error initializing gemini request: %s", err)
return
}
if req.Do() != nil {
t.Errorf("Error performing gemini request: %s", err)
return
}
if req.Status != StatusSuccess {
t.Errorf("gemini request returned status %d", req.Status)
return
}
buf := make([]byte, 1500)
n, err := req.Read(buf)
if err != nil {
t.Errorf("Error reading from gemini server: %s", err)
return
} else {
// Check response
resp := strings.TrimSpace(string(buf[:n]))
if resp != "test OK" {
fmt.Errorf("Invalid response from gemini server")
return
}
fmt.Println("Response OK")
}
defer req.Close()
}
func testHandler(path string, conn io.ReadWriteCloser) error {
SendResponse(conn, StatusSuccess, "text/txt")
conn.Write([]byte("test OK"))
return nil
}