Initial commit
This commit is contained in:
104
service/handle.go
Normal file
104
service/handle.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package service
|
||||
|
||||
import "io"
|
||||
import "log"
|
||||
import "net/http"
|
||||
import "hnakra/protocol"
|
||||
|
||||
func (mount *HTTP) handle (request protocol.MessageHTTPRequest) {
|
||||
log.Println("->?", request.Address, "requests", request.URL)
|
||||
bodyReader := &bodyReader {
|
||||
id: request.ID,
|
||||
reader: mount.requests.readerFor(request.ID),
|
||||
send: mount.send,
|
||||
close: func () { mount.requests.end(request.ID) },
|
||||
}
|
||||
defer mount.requests.remove(request.ID)
|
||||
|
||||
handler := mount.Handler
|
||||
if handler == nil {
|
||||
handler = http.DefaultServeMux
|
||||
}
|
||||
|
||||
// TODO possibly populate the http version and ContentLength
|
||||
// TODO: if there is an error, send an error status to the server
|
||||
httpRequest, _ := http.NewRequest(request.Method, request.URL, bodyReader)
|
||||
httpRequest.Header = http.Header(request.Header)
|
||||
|
||||
handler.ServeHTTP(&responseWriter {
|
||||
id: request.ID,
|
||||
header: make(http.Header),
|
||||
send: mount.send,
|
||||
}, httpRequest)
|
||||
|
||||
mount.send(protocol.MessageHTTPBodyEnd { ID: request.ID })
|
||||
}
|
||||
|
||||
type bodyReader struct {
|
||||
reader io.Reader
|
||||
requested bool
|
||||
id protocol.ID
|
||||
send func (protocol.Message) error
|
||||
close func ()
|
||||
}
|
||||
|
||||
func (reader *bodyReader) Read (buffer []byte) (amount int, err error) {
|
||||
if !reader.requested {
|
||||
err = reader.send(protocol.MessageHTTPBodyRequest {
|
||||
ID: reader.id,
|
||||
})
|
||||
if err != nil { return }
|
||||
reader.requested = true
|
||||
}
|
||||
return reader.reader.Read(buffer)
|
||||
}
|
||||
|
||||
func (reader *bodyReader) Close () error {
|
||||
// TODO: possibly make this mthd ask the server to stop sending body
|
||||
// segments
|
||||
reader.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
header http.Header
|
||||
wroteHeader bool
|
||||
id protocol.ID
|
||||
send func (protocol.Message) error
|
||||
}
|
||||
|
||||
func (writer *responseWriter) Header () http.Header {
|
||||
return writer.header
|
||||
}
|
||||
|
||||
func (writer *responseWriter) Write (buffer []byte) (amount int, err error) {
|
||||
if !writer.wroteHeader {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
for len(buffer) > 0 {
|
||||
sent := int(protocol.MaxIntOfSize(2) - 1)
|
||||
if len(buffer) < sent {
|
||||
sent = len(buffer)
|
||||
}
|
||||
err = writer.send(protocol.MessageHTTPBodySegment {
|
||||
ID: writer.id,
|
||||
Data: buffer[:sent],
|
||||
})
|
||||
if err != nil { return }
|
||||
buffer = buffer[sent:]
|
||||
amount += sent
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (writer *responseWriter) WriteHeader (statusCode int) {
|
||||
if writer.wroteHeader { return }
|
||||
|
||||
writer.wroteHeader = true
|
||||
writer.send(protocol.MessageHTTPResponse {
|
||||
ID: writer.id,
|
||||
Status: uint16(statusCode),
|
||||
Header: protocol.Map(writer.header),
|
||||
})
|
||||
}
|
||||
100
service/http.go
Normal file
100
service/http.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package service
|
||||
|
||||
import "log"
|
||||
import "net"
|
||||
import "sync"
|
||||
import "bufio"
|
||||
import "errors"
|
||||
import "net/http"
|
||||
import "hnakra/protocol"
|
||||
|
||||
// HTTP is an https:// mount.
|
||||
type HTTP struct {
|
||||
Mount
|
||||
|
||||
// AllowInsecure allows this mount to respond to plain-text HTTP
|
||||
// requests. You can get a TLS cert for free nowadays so there are very
|
||||
// few cases where you'd want this on.
|
||||
AllowInsecure bool
|
||||
|
||||
// Handler specifies the handler to invoke. If it is nil,
|
||||
// http.DefaultServeMux is used.
|
||||
Handler http.Handler
|
||||
|
||||
conn net.Conn
|
||||
connLock sync.Mutex
|
||||
connReadWriter *bufio.ReadWriter
|
||||
requests requestManager
|
||||
}
|
||||
|
||||
// Close closes the mount abruptly, interrupting any active connections.
|
||||
func (mount *HTTP) Close () error {
|
||||
mount.connLock.Lock()
|
||||
defer mount.connLock.Unlock()
|
||||
return mount.conn.Close()
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the service without interrupting any active
|
||||
// connections.
|
||||
func (mount *HTTP) Shutdown () error {
|
||||
// TODO
|
||||
return mount.Close()
|
||||
}
|
||||
|
||||
// Run connects to the router, and blocks while fulfilling requests. This method
|
||||
// will only return when the connection to the router has been closed.
|
||||
func (mount *HTTP) Run () (err error) {
|
||||
if mount.AllowInsecure {
|
||||
mount.conn, err = mount.connect("http")
|
||||
} else {
|
||||
mount.conn, err = mount.connect("https")
|
||||
}
|
||||
if err != nil { return }
|
||||
|
||||
mount.connReadWriter = bufio.NewReadWriter (
|
||||
bufio.NewReader(mount.conn),
|
||||
bufio.NewWriter(mount.conn))
|
||||
mount.requests.init()
|
||||
|
||||
for {
|
||||
message, err := protocol.ReadMessage(mount.connReadWriter)
|
||||
if err != nil { return err }
|
||||
|
||||
switch message.(type) {
|
||||
case protocol.MessageHTTPRequest:
|
||||
request := message.(protocol.MessageHTTPRequest)
|
||||
mount.requests.add(request.ID)
|
||||
go mount.handle(request)
|
||||
|
||||
case protocol.MessageHTTPBodySegment:
|
||||
segment := message.(protocol.MessageHTTPBodySegment)
|
||||
mount.requests.feed(segment.ID, segment.Data)
|
||||
|
||||
case protocol.MessageHTTPBodyEnd:
|
||||
end := message.(protocol.MessageHTTPBodyEnd)
|
||||
mount.requests.end(end.ID)
|
||||
|
||||
case protocol.MessageStatus:
|
||||
status := message.(protocol.MessageStatus)
|
||||
log.Println("router says:", status.Status)
|
||||
|
||||
default:
|
||||
mount.Close()
|
||||
return errors.New("router sent unknown type code")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewHTTP creates a very basic https:// mount with the specified name and
|
||||
// description.
|
||||
func NewHTTP (name, description string) HTTP {
|
||||
return HTTP { Mount: M(name, description) }
|
||||
}
|
||||
|
||||
func (mount *HTTP) send (message protocol.Message) (err error) {
|
||||
mount.connLock.Lock()
|
||||
defer mount.connLock.Unlock()
|
||||
err = message.Send(mount.connReadWriter)
|
||||
if err != nil { return }
|
||||
return mount.connReadWriter.Flush()
|
||||
}
|
||||
75
service/requestmanager.go
Normal file
75
service/requestmanager.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package service
|
||||
|
||||
import "io"
|
||||
import "sync"
|
||||
import "hnakra/protocol"
|
||||
|
||||
const HTTP_BODY_CHANNEL_BUFFER_SIZE = 32
|
||||
|
||||
type requestManager struct {
|
||||
requests map[protocol.ID] chan []byte
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
func (manager *requestManager) init () {
|
||||
manager.requests = make(map[protocol.ID] chan []byte)
|
||||
}
|
||||
|
||||
func (manager *requestManager) add (id protocol.ID) {
|
||||
manager.lock.Lock()
|
||||
manager.requests[id] = make(chan []byte)
|
||||
manager.lock.Unlock()
|
||||
}
|
||||
|
||||
func (manager *requestManager) feed (id protocol.ID, buffer []byte) {
|
||||
manager.lock.RLock()
|
||||
if manager.requests[id] != nil {
|
||||
manager.requests[id] <- buffer
|
||||
}
|
||||
manager.lock.RUnlock()
|
||||
}
|
||||
|
||||
func (manager *requestManager) end (id protocol.ID) {
|
||||
manager.lock.RLock()
|
||||
if manager.requests[id] != nil {
|
||||
close(manager.requests[id])
|
||||
manager.requests[id] = nil
|
||||
}
|
||||
manager.lock.RUnlock()
|
||||
}
|
||||
|
||||
func (manager *requestManager) remove (id protocol.ID) {
|
||||
manager.lock.Lock()
|
||||
delete(manager.requests, id)
|
||||
manager.lock.Unlock()
|
||||
}
|
||||
|
||||
func (manager *requestManager) readerFor (id protocol.ID) io.Reader {
|
||||
manager.lock.RLock()
|
||||
channel := manager.requests[id]
|
||||
manager.lock.RUnlock()
|
||||
return byteChannelReader { channel: channel }
|
||||
}
|
||||
|
||||
type byteChannelReader struct {
|
||||
channel <- chan []byte
|
||||
leftover []byte
|
||||
}
|
||||
|
||||
func (reader byteChannelReader) Read (buffer []byte) (amount int, err error) {
|
||||
// first, try to fill the buffer with leftover data
|
||||
amount = copy(buffer, reader.leftover)
|
||||
if amount > 0 {
|
||||
reader.leftover = reader.leftover[amount:]
|
||||
return
|
||||
}
|
||||
|
||||
// if there is no leftover data, read from the channel
|
||||
incoming, open := <- reader.channel
|
||||
if !open { return 0, io.EOF }
|
||||
amount = copy(buffer, incoming)
|
||||
|
||||
// save any data we could not fit into the buffer
|
||||
reader.leftover = incoming[amount:]
|
||||
return
|
||||
}
|
||||
187
service/service.go
Normal file
187
service/service.go
Normal file
@@ -0,0 +1,187 @@
|
||||
package service
|
||||
|
||||
import "os"
|
||||
import "log"
|
||||
import "fmt"
|
||||
import "net"
|
||||
import "errors"
|
||||
import "strings"
|
||||
import "crypto/tls"
|
||||
import "encoding/base64"
|
||||
import "hnakra/protocol"
|
||||
|
||||
// M creates a very basic mount with the specified name and description.
|
||||
func M (name, description string) Mount {
|
||||
return Mount {
|
||||
Name: name,
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// Mount contains generic information common to all mounts.
|
||||
type Mount struct {
|
||||
// Host specifies the host to mount on. If the host is left empty, it
|
||||
// will default to @ (meaning default/any host). The port is entirely up
|
||||
// to the router. Maximum length for host portion: 255 bytes
|
||||
Host string
|
||||
|
||||
// Path specifies the path to mount on. If the path ends with a /, then
|
||||
// all requests under the path will be sent to this service. If there is
|
||||
// no trailing /, this service will only recieve requests that match the
|
||||
// path exactly (when normalized).
|
||||
// Maximum length: 2^16-1 bytes
|
||||
Path string
|
||||
|
||||
// Router specifies the host:port of the router to connect to. This
|
||||
// defaults to $HNAKRA_ROUTER_HOST:$HNAKRA_ROUTER_PORT if left empty.
|
||||
// The default value of these environment variables (if not set) is
|
||||
// localhost:2048. If you are giving this service to other people, it is
|
||||
// a good idea to leave this empty.
|
||||
Router string
|
||||
|
||||
// Name should be set to a human-readable name of the service.
|
||||
// Maximum length: 255 bytes
|
||||
Name string
|
||||
|
||||
// Description should be set to a human-readable description of what the
|
||||
// service does.
|
||||
// Maximum length: 255 bytes
|
||||
Description string
|
||||
|
||||
// User is an optional string that specifies who is connecting to the
|
||||
// router. This can be used by routers in conjunction with the key
|
||||
// bytes in order to enforce rules about which service can do what. This
|
||||
// defaults to $HNAKRA_USER if left empty. If you are giving this
|
||||
// service to other people, it is a good idea to leave this empty.
|
||||
// Maximum length: 255 bytes
|
||||
User string
|
||||
|
||||
// Key is an optional byte slice that is sent to the router in order for
|
||||
// it to authorize the connection. If nil, this defaults to the contents
|
||||
// of $HNAKRA_KEY interpreted as base64. Do not embed a key in your
|
||||
// code - you should only use this if your service reads the key from a
|
||||
// safe source upon startup. In addition to this, If you choose to set
|
||||
// the key via the aforementioned enviroment variable, ensure that it
|
||||
// can only be read by the service.
|
||||
// Maximum length: 255 bytes
|
||||
Key []byte
|
||||
|
||||
// TLSConfig is an optional TLS configuration.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
idFactory *protocol.IDFactory
|
||||
}
|
||||
|
||||
func (mount *Mount) connect (scheme string) (conn net.Conn, err error) {
|
||||
log.Println("(i) service", mount.Name)
|
||||
mount.idFactory = protocol.NewServiceIDFactory()
|
||||
|
||||
defaultRouterHost := os.Getenv("HNAKRA_ROUTER_HOST")
|
||||
if defaultRouterHost == "" {
|
||||
defaultRouterHost = "localhost"
|
||||
}
|
||||
defaultRouterPort := os.Getenv("HNAKRA_ROUTER_PORT")
|
||||
if defaultRouterPort == "" {
|
||||
defaultRouterPort = "2048"
|
||||
}
|
||||
|
||||
// parse router host/port
|
||||
routerHost, routerPort, _ := strings.Cut(mount.Router, ":")
|
||||
if routerHost == "" {
|
||||
routerHost = defaultRouterHost
|
||||
}
|
||||
if routerPort == "" {
|
||||
routerPort = defaultRouterPort
|
||||
}
|
||||
|
||||
// get mount point
|
||||
host := mount.Host
|
||||
if host == "" {
|
||||
host = "@"
|
||||
}
|
||||
if len(host) > 255 {
|
||||
return nil, errors.New("mount point host cannot be longer than 255 bytes")
|
||||
}
|
||||
path := mount.Path
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
if len(path) > int(protocol.MaxIntOfSize(2)) {
|
||||
return nil, errors.New(fmt.Sprint (
|
||||
"mount point path cannot be longer than ",
|
||||
protocol.MaxIntOfSize(2), " bytes"))
|
||||
}
|
||||
|
||||
// get user
|
||||
user := mount.User
|
||||
if user == "" {
|
||||
user = os.Getenv("HNAKRA_USER")
|
||||
}
|
||||
if len(user) > 255 {
|
||||
return nil, errors.New("user cannot be longer than 255 bytes")
|
||||
}
|
||||
|
||||
// get key
|
||||
key := mount.Key
|
||||
if key == nil {
|
||||
key, err = base64.StdEncoding.DecodeString(os.Getenv("HNAKRA_KEY"))
|
||||
if err != nil { return nil, err }
|
||||
}
|
||||
if len(key) > 255 {
|
||||
return nil, errors.New("key cannot be longer than 255 bytes")
|
||||
}
|
||||
|
||||
// ensure name/description aren't too big
|
||||
if len(mount.Name) > 255 {
|
||||
return nil, errors.New("service name cannot be longer than 255 bytes")
|
||||
}
|
||||
if len(mount.Description) > 255 {
|
||||
return nil, errors.New("service description cannot be longer than 255 bytes")
|
||||
}
|
||||
|
||||
// connect to router
|
||||
routerAddr := fmt.Sprint(routerHost, ":", routerPort)
|
||||
log.Println("... dialing", routerAddr)
|
||||
conn, err = tls.Dial("tcp", routerAddr, mount.TLSConfig)
|
||||
if err != nil { return nil, err }
|
||||
|
||||
// log in
|
||||
log.Println (
|
||||
"... logging in as", user,
|
||||
"on", scheme + "://" + host + path)
|
||||
err = protocol.MessageLogin {
|
||||
ID: mount.idFactory.Next(),
|
||||
Version: protocol.Version { Major: 0, Minor: 0 },
|
||||
User: user,
|
||||
Key: key,
|
||||
Name: mount.Name,
|
||||
Description: mount.Description,
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: path,
|
||||
}.Send(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// read status
|
||||
message, err := protocol.ReadMessage(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
status, ok := message.(protocol.MessageStatus)
|
||||
if !ok {
|
||||
conn.Close()
|
||||
return nil, errors.New(fmt.Sprint("router sent unknown type, expecting", protocol.TypeStatus))
|
||||
}
|
||||
if status.Status != protocol.StatusOk {
|
||||
return nil, status
|
||||
}
|
||||
|
||||
log.Println(".// logged in")
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// TODO: other protocols like gemini, websocket, ftp, etc
|
||||
Reference in New Issue
Block a user