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:
parent
b448f7c8c4
commit
0dad762255
1
.github/workflows/orion.yml
vendored
1
.github/workflows/orion.yml
vendored
|
@ -18,3 +18,4 @@ jobs:
|
|||
go-version: '1.14'
|
||||
- name: Compile binaries
|
||||
run: make
|
||||
run: make test
|
||||
|
|
3
Makefile
3
Makefile
|
@ -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
|
||||
|
|
|
@ -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
111
cmd/orion/orion_test.go
Normal 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
|
||||
}
|
Loading…
Reference in a new issue