After the week I've had, I deserve to make a commit like this lmao
This commit is contained in:
		
							parent
							
								
									6f55ee1b45
								
							
						
					
					
						commit
						68961f8ad8
					
				| @ -6,7 +6,7 @@ import "context" | ||||
| // Conn is a HOPP connection. | ||||
| type Conn interface { | ||||
| 	io.Closer | ||||
| 	OpenTrans(ctx context.Context) (Trans, error) | ||||
| 	OpenTrans() (Trans, error) | ||||
| 	AcceptTrans(ctx context.Context) (Trans, error) | ||||
| } | ||||
| 
 | ||||
| @ -14,8 +14,8 @@ type Conn interface { | ||||
| type Trans interface { | ||||
| 	io.Closer | ||||
| 	ID() int64 | ||||
| 	Send(ctx context.Context, method uint16, data []byte) error | ||||
| 	Receive(ctx context.Context) (method uint16, data []byte, err error) | ||||
| 	SendDatagram(ctx context.Context, method uint16, data []byte) error | ||||
| 	ReceiveDatagram(ctx context.Context) (method uint16, data []byte, err error) | ||||
| 	// Send sends a message. | ||||
| 	Send(method uint16, data []byte) error | ||||
| 	// Receive receives a message. | ||||
| 	Receive() (method uint16, data []byte, err error) | ||||
| } | ||||
|  | ||||
| @ -22,8 +22,8 @@ guaranteed. | ||||
| 
 | ||||
| The message payload must be 65,535 (unsigned 16-bit integer limit) octets or | ||||
| smaller in length. This does not include the method code. Applications are free | ||||
| to send whatever data they wish as the payload, but it should be encoded using | ||||
| TAPE. | ||||
| to send whatever data they wish as the payload, but TAPE is recommended for | ||||
| encoding it. | ||||
| 
 | ||||
| Method codes should be written in upper-case base 16 with the prefix "M" in | ||||
| logs, error messages, documentation, etc. For example, the method code 62,206 in | ||||
| @ -31,7 +31,7 @@ decimal would be written as MF4CE. The application may choose any method codes, | ||||
| but groups of similar methods should be placed at consecutive intervals of | ||||
| M0100. Method codes MFF00-MFFFF are reserved for use by HOPP and its constituent | ||||
| protocols. Individuals or entities with the SWAG (secret wheel access group) | ||||
| pass are also permitted to define their own methods in this range. I'm just | ||||
| pass are also permitted to define their own methods within this range. I'm just | ||||
| fucking with you. | ||||
| 
 | ||||
| ## Table Pair Encoding (TAPE) | ||||
| @ -45,12 +45,12 @@ enables backwards compatibile application protocol changes. | ||||
| ### Table Structure | ||||
| A table is divided into two sections: the header, and the values. The header | ||||
| begins with the number (U16) of pairs in the table, which is then followed by | ||||
| that many tag-offset pairs. A tag-offset pair consists of a numerical (U16) | ||||
| tag, followed the position (U16) of the value relative to the start of the | ||||
| values section. The values section contains the value data for each pair, | ||||
| where the start of each value is determined by its offset, and the end is | ||||
| determined by the offset of the next value, or the end of the message if there | ||||
| is no value after it. | ||||
| that many tag-offset pairs. A tag-offset pair consists of a numerical (U16) tag, | ||||
| followed the position (U16) of the value relative to the start of the values | ||||
| section. The values section contains the value data for each pair, where the | ||||
| start of each value is determined by its offset, and the end is determined by | ||||
| the offset of the next value, or the end of the message if there is no value | ||||
| after it. | ||||
| 
 | ||||
| Both sections must be in the same order, and because of this, each value offset | ||||
| must be greater than or equal to the last. If a message has erratic structure | ||||
| @ -60,19 +60,22 @@ only the erratic pairs, as well as the pairs directly before those. | ||||
| ### Data Value Types | ||||
| The table below lists all data value types supported by TAPE. | ||||
| 
 | ||||
| | Name        | Size             | Description                 | Encoding Method | ||||
| | ------      | ---------------: | --------------------------- | --------------- | ||||
| | I8          |                1 | A signed 8-bit integer      | BETC | ||||
| | I16         |                2 | A signed 16-bit integer     | BETC | ||||
| | I32         |                4 | A signed 32-bit integer     | BETC | ||||
| | I64         |                8 | A signed 64-bit integer     | BETC | ||||
| | U8          |                1 | An unsigned 8-bit integer   | BEU | ||||
| | U16         |                2 | An unsigned 16-bit integer  | BEU | ||||
| | U32         |                4 | An unsigned 32-bit integer  | BEU | ||||
| | U64         |                8 | An unsigned 64-bit integer  | BEU | ||||
| | Array       |         Part-sum | An array of any above type  | PASTA | ||||
| | String      |              N/A | A UTF-8 string              | UTF-8 | ||||
| | StringArray | N * 2 + Part-sum | An array the String type    | VILA | ||||
| | Name        | Size            | Description                 | Encoding Method | ||||
| | ----------- | --------------: | --------------------------- | --------------- | ||||
| | I8          |               1 | A signed 8-bit integer      | BETC | ||||
| | I16         |               2 | A signed 16-bit integer     | BETC | ||||
| | I32         |               4 | A signed 32-bit integer     | BETC | ||||
| | I64         |               8 | A signed 64-bit integer     | BETC | ||||
| | U8          |               1 | An unsigned 8-bit integer   | BEU | ||||
| | U16         |               2 | An unsigned 16-bit integer  | BEU | ||||
| | U32         |               4 | An unsigned 32-bit integer  | BEU | ||||
| | U64         |               8 | An unsigned 64-bit integer  | BEU | ||||
| | Array       |         SOP[^1] | An array of any above type  | PASTA | ||||
| | String      |             N/A | A UTF-8 string              | UTF-8 | ||||
| | StringArray | n * 2 + SOP[^1] | An array the String type    | VILA | ||||
| 
 | ||||
| [^1]: SOP (sum of parts) refers to the sum of the size of every item in a data | ||||
| structure. | ||||
| 
 | ||||
| ### Encoding Methods | ||||
| Below are all encoding methods supported by TAPE. | ||||
| @ -88,9 +91,9 @@ octets which can fit all bits in the integer, regardless if the bits are on or | ||||
| off. Therefore, the size cannot change at runtime. | ||||
| 
 | ||||
| #### PASTA | ||||
| Packed Single-Type Array. The size is defined as at the size of an individual | ||||
| item times the number of items. Items are placed one after the other with no | ||||
| gaps in-between them, except as required to align the start of each item to the | ||||
| Packed Single-Type Array. The size is defined as the size of an individual item | ||||
| times the number of items. Items are placed one after the other with no gaps | ||||
| in-between them, except as required to align the start of each item to the | ||||
| nearest whole octet. Items should be of the same type and must be of the same | ||||
| size. | ||||
| 
 | ||||
| @ -133,7 +136,7 @@ Internet. | ||||
| ### METADAPT-A | ||||
| METADAPT-A requires a transport which offers a single full-duplex data stream | ||||
| that persists for the duration of the connection. All transactions are | ||||
| multiplexed onto this single stream. Each MMB contains a 12-octet long header, | ||||
| multiplexed onto this single stream. Each MMB contains a 8-octet long header, | ||||
| with the transaction ID, then the method, and then the payload size (in octets). | ||||
| The transaction ID is encoded as an I64, and the method and payload size are | ||||
| both encoded as U16s. The remainder of the message is the payload. Since each | ||||
| @ -143,6 +146,15 @@ Transactions "open" when the first message with a given transaction ID is sent. | ||||
| They "close" when a closing message is sent by either side. A closing message | ||||
| has method MFFFF and should not have a payload. | ||||
| 
 | ||||
| The ID of a given transaction is counted differently depending on from which end | ||||
| of the connection the transaction in question initiated from. The client (the | ||||
| party which initiated the connection) uses positive transaction IDs, while the | ||||
| server (the party which accepted the connection) uses negative transaction IDs. | ||||
| Transaction IDs must be unique within the connection, and if all IDs have been | ||||
| used up, the connection must fail. Don't worry about this though, because the | ||||
| sun will have expanded to swallow earth by then. Your connection will not last | ||||
| that long. | ||||
| 
 | ||||
| ### METADAPT-B | ||||
| METADAPT-B requires a transport which offers multiple multiplexed full-duplex | ||||
| data streams per connection that can be created and destroyed on-demand. Each | ||||
|  | ||||
							
								
								
									
										50
									
								
								dial.go
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								dial.go
									
									
									
									
									
								
							| @ -1,7 +1,9 @@ | ||||
| package hopp | ||||
| 
 | ||||
| import "net" | ||||
| import "context" | ||||
| import "crypto/tls" | ||||
| import "github.com/quic-go/quic-go" | ||||
| 
 | ||||
| // TODO: dial should be super simple like it is now, and there should be a | ||||
| // "dialer" which the dial function dial configures automaticaly, but the dialer | ||||
| @ -29,24 +31,52 @@ func (diale Dialer) Dial(ctx context.Context, network, address string) (Conn, er | ||||
| } | ||||
| 
 | ||||
| func (diale Dialer) dialQUIC(ctx context.Context, network, address string) (Conn, error) { | ||||
| 	// TODO: dial a QUIC connection and return METADAPT-B wrapping it | ||||
| 	udpNetwork, err := quicNetworkToUDPNetwork(network) | ||||
| 	if err != nil { return nil, err } | ||||
| 	addr, err := net.ResolveUDPAddr(udpNetwork, address) | ||||
| 	if err != nil { return nil, err } | ||||
| 	udpConn, err := net.DialUDP(udpNetwork, nil, addr) | ||||
| 	if err != nil { return nil, err } | ||||
| 	conn, err := quic.Dial(ctx, udpConn, addr, diale.tlsConfig(), diale.quicConfig()) | ||||
| 	if err != nil { return nil, err } | ||||
| 	return AdaptB(quicMultiConn { underlying: conn }), nil | ||||
| } | ||||
| 
 | ||||
| func (diale Dialer) dialUnix(ctx context.Context, network, address string) (Conn, error) { | ||||
| 	if network != "unix" { return nil, ErrUnknownNetwork } | ||||
| 	// TODO: dial a unix stream connection and return METADAPT-A wrapping it | ||||
| 	addr, err := net.ResolveUnixAddr(network, address) | ||||
| 	if err != nil { return nil, err } | ||||
| 	conn, err := net.DialUnix(network, nil, addr) | ||||
| 	if err != nil { return nil, err } | ||||
| 	// REMEMBER - THIS IS VERY IMPORTANT: | ||||
| 	// WHEN YOU INEVITABLY COPY PASTE THIS FOR THE SERVER-SIDE, CHANGE THE | ||||
| 	// PARTY CONSTANT TO ServerSide! OTHERWISE THERE WILL BE COLLISIONS! | ||||
| 	return AdaptA(conn, ClientSide), nil | ||||
| } | ||||
| 
 | ||||
| // addrStrs implements net.Addr | ||||
| type addrStrs struct { | ||||
| 	net  string | ||||
| 	addr string | ||||
| func (diale Dialer) tlsConfig() *tls.Config { | ||||
| 	conf := diale.TLSConfig.Clone() | ||||
| 	conf.NextProtos = []string { | ||||
| 		"HOPP/0", | ||||
| 	} | ||||
| 	return conf | ||||
| } | ||||
| 
 | ||||
| func (addr addrStrs) Network() string { | ||||
| 	return addr.net | ||||
| func (diale Dialer) quicConfig() *quic.Config { | ||||
| 	return &quic.Config { | ||||
| 		// TODO: perhaps we might want to put something here | ||||
| 		// the quic config shouldn't be exported, just set up | ||||
| 		// automatically. we can't have that strangely built quic-go | ||||
| 		// package be part of the API, or any third-party packages for | ||||
| 		// that matter. it must all be abstracted away. | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (addr addrStrs) String() string { | ||||
| 	return addr.addr | ||||
| func quicNetworkToUDPNetwork(network string) (string, error) { | ||||
| 	switch network { | ||||
| 	case "quic4": return "udp4", nil | ||||
| 	case "quic6": return "udp6", nil | ||||
| 	case "quic":  return "udp",  nil | ||||
| 	default:      return "", ErrUnknownNetwork | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										2
									
								
								error.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								error.go
									
									
									
									
									
								
							| @ -6,6 +6,8 @@ type Error string; const ( | ||||
| 	ErrUnknownMethod      Error = "unknown method" | ||||
| 	ErrPayloadTooLarge    Error = "payload too large" | ||||
| 	ErrUnknownNetwork     Error = "unknown network" | ||||
| 	ErrIntegerOverflow    Error = "integer overflow" | ||||
| 	ErrMessageMalformed   Error = "message is malformed" | ||||
| ) | ||||
| 
 | ||||
| // Error implements the error interface. | ||||
|  | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -3,6 +3,7 @@ module git.tebibyte.media/sashakoshka/hopp | ||||
| go 1.23.0 | ||||
| 
 | ||||
| require ( | ||||
| 	git.tebibyte.media/sashakoshka/go-util v0.8.0 | ||||
| 	github.com/gomarkdown/markdown v0.0.0-20241205020045-f7e15b2f3e62 | ||||
| 	github.com/quic-go/quic-go v0.48.2 | ||||
| ) | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,3 +1,5 @@ | ||||
| git.tebibyte.media/sashakoshka/go-util v0.8.0 h1:XFuZ8HQkrnibrV016rso00geCFPatKpX4jxkIVhZPaQ= | ||||
| git.tebibyte.media/sashakoshka/go-util v0.8.0/go.mod h1:0Q1t+PePdx6tFYkRuJNcpM1Mru7wE6X+it1kwuOH+6Y= | ||||
| github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= | ||||
| github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | ||||
| github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | ||||
|  | ||||
| @ -1,21 +0,0 @@ | ||||
| // Package metadapt implements the Message and Transaction Demarcation Protocol. | ||||
| package metadapt | ||||
| 
 | ||||
| // TODO: create interfaces for underlying connections for A and B, also have | ||||
| // A and B fulfill hopp.Conn. | ||||
| 
 | ||||
| // A implements METADAPT-A over a singular stream-oriented transport such as TCP | ||||
| // or UNIX domain stream sockets. | ||||
| type A struct { | ||||
| 	// Underlying specifies the underlying connection. It must be set before | ||||
| 	// calling methods on this object. | ||||
| 	Underlying ATransport | ||||
| } | ||||
| 
 | ||||
| // B implements METADAPT-B over a multiplexed stream-oriented transport such as | ||||
| // QUIC. | ||||
| type B struct { | ||||
| 	// Underlying specifies the underlying connection. It must be set before | ||||
| 	// calling methods on this object. | ||||
| 	Underlying BTransport | ||||
| } | ||||
							
								
								
									
										203
									
								
								metadapta.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								metadapta.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,203 @@ | ||||
| package hopp | ||||
| 
 | ||||
| import "io" | ||||
| import "fmt" | ||||
| import "net" | ||||
| import "sync" | ||||
| import "context" | ||||
| import "git.tebibyte.media/sashakoshka/hopp/tape" | ||||
| import "git.tebibyte.media/sashakoshka/go-util/sync" | ||||
| 
 | ||||
| const int64Max = int64((^uint64(0)) >> 1) | ||||
| 
 | ||||
| // Party represents a side of a connection. | ||||
| type Party bool; const ( | ||||
| 	ServerSide Party = false | ||||
| 	ClientSide Party = true | ||||
| ) | ||||
| 
 | ||||
| type a struct { | ||||
| 	underlying   net.Conn | ||||
| 	party        Party | ||||
| 	transID      int64 | ||||
| 	transLock    sync.RWMutex | ||||
| 	sendLock     sync.Mutex | ||||
| 	transMap     map[int64] *transA | ||||
| 	transChan    chan *transA | ||||
| 	err          error | ||||
| } | ||||
| 
 | ||||
| // AdaptA returns a connection implementing METADAPT-A over a singular stream- | ||||
| // oriented transport such as TCP or UNIX domain stream sockets. | ||||
| func AdaptA(underlying net.Conn, party Party) Conn { | ||||
| 	conn := &a { | ||||
| 		underlying: underlying, | ||||
| 		party:      party, | ||||
| 		transMap:   make(map[int64] *transA), | ||||
| 		transChan:  make(chan *transA), | ||||
| 	} | ||||
| 	go conn.receive() | ||||
| 	return conn | ||||
| } | ||||
| 
 | ||||
| func (this *a) Close() error { | ||||
| 	return this.underlying.Close() | ||||
| } | ||||
| 
 | ||||
| func (this *a) OpenTrans() (Trans, error) { | ||||
| 	this.transLock.Lock() | ||||
| 	defer this.transLock.Unlock() | ||||
| 	id := this.transID | ||||
| 	this.transID ++ | ||||
| 	trans := &transA { | ||||
| 		id:       id, | ||||
| 		incoming: usync.NewGate[incomingMessage](), | ||||
| 	} | ||||
| 	this.transMap[id] = trans | ||||
| 	if this.transID == int64Max { | ||||
| 		return nil, fmt.Errorf("could not open transaction: %w", ErrIntegerOverflow) | ||||
| 	} | ||||
| 	this.transID ++ | ||||
| 	return trans, nil | ||||
| } | ||||
| 
 | ||||
| func (this *a) AcceptTrans(ctx context.Context) (Trans, error) { | ||||
| 	select { | ||||
| 	case trans := <- this.transChan: | ||||
| 		return trans, nil | ||||
| 	case <- ctx.Done(): | ||||
| 		return nil, ctx.Err()	 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (this *a) unlistTransactionSafe(id int64) { | ||||
| 	this.transLock.Lock() | ||||
| 	defer this.transLock.Unlock() | ||||
| 	delete(this.transMap, id) | ||||
| } | ||||
| 
 | ||||
| func (this *a) sendMessageSafe(trans int64, method uint16, data []byte) error { | ||||
| 	this.sendLock.Lock() | ||||
| 	defer this.sendLock.Lock() | ||||
| 	 | ||||
| 	buffer := make([]byte, 8 + len(data)) | ||||
| 	tape.EncodeI64(buffer[:4], trans) | ||||
| 	tape.EncodeI16(buffer[4:6], method) | ||||
| 	length, ok := tape.U16CastSafe(len(data)) | ||||
| 	if !ok { return ErrPayloadTooLarge } | ||||
| 	tape.EncodeI16(data[6:8], length) | ||||
| 	copy(buffer[8:], data) | ||||
| 	_, err := this.underlying.Write(buffer) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (this *a) receive() { | ||||
| 	// TODO: multiplex receiving | ||||
| 	// if a received transaction has a malformed ID, reject it here and | ||||
| 	// cause the connection to fail | ||||
| 	// at the end of this function, close all incoming channels | ||||
| 	defer func() { | ||||
| 		this.underlying.Close() | ||||
| 		this.transLock.Lock() | ||||
| 		defer this.transLock.Lock() | ||||
| 		for _, trans := range this.transMap { | ||||
| 			trans.Close() | ||||
| 		} | ||||
| 		clear(this.transMap) | ||||
| 	}() | ||||
| 	for { | ||||
| 		transID, method, payload, err := decodeMessageA(this.underlying) | ||||
| 		if err != nil { | ||||
| 			this.err = fmt.Errorf("could not receive message: %w", err) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		err = this.receiveMultiplex(transID, method, payload) | ||||
| 		if err != nil { | ||||
| 			this.err = fmt.Errorf("could not receive message: %w", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (this *a) receiveMultiplex(transID int64, method uint16, payload []byte) error { | ||||
| 	if transID == 0 || this.party == partyFromTransID(transID) { | ||||
| 		return ErrMessageMalformed | ||||
| 	} | ||||
| 	 | ||||
| 	this.transLock.Lock() | ||||
| 	defer this.transLock.Unlock() | ||||
| 
 | ||||
| 	trans, ok := this.transMap[transID] | ||||
| 	if !ok { | ||||
| 		trans = &transA { | ||||
| 			parent:   this, | ||||
| 			id:       transID, | ||||
| 			incoming: usync.NewGate[incomingMessage](), | ||||
| 		} | ||||
| 		this.transChan <- trans | ||||
| 	} | ||||
| 
 | ||||
| 	trans.incoming.Send(incomingMessage { | ||||
| 		method:  method, | ||||
| 		payload: payload, | ||||
| 	}) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type transA struct { | ||||
| 	parent   *a | ||||
| 	id       int64 | ||||
| 	incoming usync.Gate[incomingMessage] | ||||
| } | ||||
| 
 | ||||
| func (this *transA) Close() error { | ||||
| 	this.incoming.Close() | ||||
| 	this.parent.unlistTransactionSafe(this.ID()) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (this *transA) ID() int64 { | ||||
| 	return this.id | ||||
| } | ||||
| 
 | ||||
| func (this *transA) Send(method uint16, data []byte) error { | ||||
| 	return this.parent.sendMessageSafe(this.id, method, data) | ||||
| } | ||||
| 
 | ||||
| func (this *transA) Receive() (method uint16, data []byte, err error) { | ||||
| 	message, ok := <- this.incoming.Receive() | ||||
| 	if !ok { | ||||
| 		if this.parent.err == nil { | ||||
| 			return 0, nil, fmt.Errorf("could not receive message: %w", io.EOF) | ||||
| 		} else { | ||||
| 			return 0, nil, this.parent.err | ||||
| 		} | ||||
| 	} | ||||
| 	return message.method, message.payload, nil | ||||
| } | ||||
| 
 | ||||
| type incomingMessage struct { | ||||
| 	method  uint16 | ||||
| 	payload []byte | ||||
| } | ||||
| 
 | ||||
| func decodeMessageA(reader io.Reader) (int64, uint16, []byte, error) { | ||||
| 	headerBuffer := [8]byte { } | ||||
| 	_, err := io.ReadFull(reader, headerBuffer[:]) | ||||
| 	if err != nil { return 0, 0, nil, err } | ||||
| 	transID, err := tape.DecodeI64[int64](headerBuffer[:4]) | ||||
| 	if err != nil { return 0, 0, nil, err } | ||||
| 	method, err := tape.DecodeI16[uint16](headerBuffer[4:6]) | ||||
| 	if err != nil { return 0, 0, nil, err } | ||||
| 	length, err := tape.DecodeI16[uint16](headerBuffer[6:8]) | ||||
| 	if err != nil { return 0, 0, nil, err } | ||||
| 	payloadBuffer := make([]byte, int(length)) | ||||
| 	_, err = io.ReadFull(reader, payloadBuffer) | ||||
| 	if err != nil { return 0, 0, nil, err } | ||||
| 	return transID, method, payloadBuffer, nil | ||||
| } | ||||
| 
 | ||||
| func partyFromTransID(id int64) Party { | ||||
| 	return id > 0 | ||||
| } | ||||
							
								
								
									
										93
									
								
								metadaptb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								metadaptb.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | ||||
| package hopp | ||||
| 
 | ||||
| import "io" | ||||
| import "net" | ||||
| import "context" | ||||
| import "git.tebibyte.media/sashakoshka/hopp/tape" | ||||
| 
 | ||||
| // B implements METADAPT-B over a multiplexed stream-oriented transport such as | ||||
| // QUIC. | ||||
| type b struct { | ||||
| 	underlying MultiConn | ||||
| } | ||||
| 
 | ||||
| // AdaptB returns a connection implementing METADAPT-B over a singular stream- | ||||
| // oriented transport such as TCP or UNIX domain stream sockets. | ||||
| func AdaptB(underlying MultiConn) Conn { | ||||
| 	return &b { | ||||
| 		underlying: underlying, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (this *b) Close() error { | ||||
| 	return this.underlying.Close() | ||||
| } | ||||
| 
 | ||||
| func (this *b) OpenTrans() (Trans, error) { | ||||
| 	stream, err := this.underlying.OpenStream() | ||||
| 	if err != nil { return nil, err } | ||||
| 	return transB { underlying: stream }, nil | ||||
| } | ||||
| 
 | ||||
| func (this *b) AcceptTrans(ctx context.Context) (Trans, error) { | ||||
| 	stream, err := this.underlying.AcceptStream(ctx) | ||||
| 	if err != nil { return nil, err } | ||||
| 	return transB { underlying: stream }, nil | ||||
| } | ||||
| 
 | ||||
| type transB struct { | ||||
| 	underlying Stream | ||||
| } | ||||
| 
 | ||||
| func (trans transB) Close() error { | ||||
| 	return trans.underlying.Close() | ||||
| } | ||||
| 
 | ||||
| func (trans transB) ID() int64 { | ||||
| 	return trans.underlying.ID() | ||||
| } | ||||
| 
 | ||||
| func (trans transB) Send(method uint16, data []byte) error { | ||||
| 	buffer := make([]byte, 4 + len(data)) | ||||
| 	tape.EncodeI16(buffer[:2], method) | ||||
| 	length, ok := tape.U16CastSafe(len(data)) | ||||
| 	if !ok { return ErrPayloadTooLarge } | ||||
| 	tape.EncodeI16(data[2:4], length) | ||||
| 	copy(buffer[4:], data) | ||||
| 	_, err := trans.underlying.Write(buffer) | ||||
| 	return err | ||||
| } | ||||
| 
 | ||||
| func (trans transB) Receive() (uint16, []byte, error) { | ||||
| 	headerBuffer := [4]byte { } | ||||
| 	_, err := io.ReadFull(trans.underlying, headerBuffer[:]) | ||||
| 	if err != nil { return 0, nil, err } | ||||
| 	method, err := tape.DecodeI16[uint16](headerBuffer[:2]) | ||||
| 	if err != nil { return 0, nil, err } | ||||
| 	length, err := tape.DecodeI16[uint16](headerBuffer[2:4]) | ||||
| 	if err != nil { return 0, nil, err } | ||||
| 	payloadBuffer := make([]byte, int(length)) | ||||
| 	_, err = io.ReadFull(trans.underlying, payloadBuffer) | ||||
| 	if err != nil { return 0, nil, err } | ||||
| 	return method, payloadBuffer, nil | ||||
| } | ||||
| 
 | ||||
| // MultiConn represens a multiplexed stream-oriented transport for use in [B]. | ||||
| type MultiConn interface { | ||||
| 	// See documentation for [net.Conn]. | ||||
| 	io.Closer | ||||
| 	LocalAddr() net.Addr | ||||
| 	RemoteAddr() net.Addr | ||||
| 	// AcceptStream accepts the next incoming stream from the other party. | ||||
| 	AcceptStream(context.Context) (Stream, error) | ||||
| 	// OpenStream opens a new stream. | ||||
| 	OpenStream() (Stream, error) | ||||
| } | ||||
| 
 | ||||
| // Stream represents a single stream returned by a [MultiConn]. | ||||
| type Stream interface { | ||||
| 	// See documentation for [net.Conn]. | ||||
| 	io.ReadWriteCloser | ||||
| 	// ID returns the stream ID | ||||
| 	ID() int64 | ||||
| } | ||||
							
								
								
									
										54
									
								
								quicwrap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								quicwrap.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| package hopp | ||||
| 
 | ||||
| import "net" | ||||
| import "context" | ||||
| import "github.com/quic-go/quic-go" | ||||
| 
 | ||||
| var _ MultiConn = quicMultiConn { } | ||||
| type quicMultiConn struct { | ||||
| 	underlying quic.Connection | ||||
| } | ||||
| 
 | ||||
| func (conn quicMultiConn) Close() error { | ||||
| 	return conn.underlying.CloseWithError(0, "good bye") | ||||
| } | ||||
| 
 | ||||
| func (conn quicMultiConn) LocalAddr() net.Addr { | ||||
| 	return conn.underlying.LocalAddr() | ||||
| } | ||||
| 
 | ||||
| func (conn quicMultiConn) RemoteAddr() net.Addr { | ||||
| 	return conn.underlying.RemoteAddr() | ||||
| } | ||||
| 
 | ||||
| func (conn quicMultiConn) AcceptStream(ctx context.Context) (Stream, error) { | ||||
| 	strea, err := conn.underlying.AcceptStream(ctx) | ||||
| 	if err != nil { return nil, err } | ||||
| 	return quicStream { underlying: strea }, nil | ||||
| } | ||||
| 
 | ||||
| func (conn quicMultiConn) OpenStream() (Stream, error) { | ||||
| 	strea, err := conn.underlying.OpenStream() | ||||
| 	if err != nil { return nil, err } | ||||
| 	return quicStream { underlying: strea }, nil | ||||
| } | ||||
| 
 | ||||
| type quicStream struct { | ||||
| 	underlying quic.Stream | ||||
| } | ||||
| 
 | ||||
| func (strea quicStream) Read(buffer []byte) (n int, err error) { | ||||
| 	return strea.underlying.Read(buffer) | ||||
| } | ||||
| 
 | ||||
| func (strea quicStream) Write(buffer []byte) (n int, err error) { | ||||
| 	return strea.underlying.Read(buffer) | ||||
| } | ||||
| 
 | ||||
| func (strea quicStream) Close() error { | ||||
| 	return strea.underlying.Close() | ||||
| } | ||||
| 
 | ||||
| func (strea quicStream) ID() int64 { | ||||
| 	return int64(strea.underlying.StreamID()) | ||||
| } | ||||
| @ -8,8 +8,8 @@ const uint16Max = 0xFFFF | ||||
| 
 | ||||
| // Error enumerates common errors in this package. | ||||
| type Error string; const ( | ||||
| 	ErrWrongBufferLength  Error = "wrong buffer length" | ||||
| 	ErrDataTooLarge       Error = "data too large" | ||||
| 	ErrWrongBufferLength Error = "wrong buffer length" | ||||
| 	ErrDataTooLarge      Error = "data too large" | ||||
| ) | ||||
| 
 | ||||
| // Error implements the error interface. | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user